Skip to content

Commit 7a3ae08

Browse files
authored
fix(next-task): invoke ship:ship via Skill tool instead of Task agent (#230) (#239)
* fix(next-task): invoke ship:ship via Skill tool instead of Task agent (#230) Phase 12 was calling Task({ subagent_type: "ship:ship" }) but ship:ship is a command/skill with no agent definition. The call silently failed, leaving the workflow stuck after delivery validation with no PR created. Replace with Skill({ skill: "ship:ship", args: "--state-file ..." }) and add Skill to allowed-tools in the command frontmatter. * test(next-task): add regression tests for ship:ship Skill invocation (#230) Add 4 tests to prevent regression of the Phase 12 fix that changed ship:ship from Task() to Skill() invocation: - allowed-tools frontmatter includes Skill - Phase 12 uses Skill() not Task() for ship:ship - --state-file argument is passed to ship:ship - codex adapter SKILL.md mirrors the Skill() fix * fix(next-task): add await to Skill() call and strengthen regression tests (#230) Add missing await to the Skill({ skill: "ship:ship", ... }) invocation in Phase 12 for consistency with codebase style (other Skill() calls use await). Improve regression tests in next-task-phase11.test.js: - Assert allowed-tools includes both Skill and Task (not just Skill) - Tie --state-file assertion to the Skill invocation line (not just anywhere) - Consolidate duplicate beforeAll file reads into shared describe block - Add --state-file assertion for Codex adapter parity - Strengthen negative regex to cover backtick-quoted variant of old bug - Add fs.existsSync guard on Codex adapter path for clear failure messages Regenerate Codex adapter to pick up await addition. * test(next-task): fix test regex to enforce same-line --state-file matching Replace dotall /s regex with [^\n]* to enforce that --state-file appears on the same line as the Skill invocation, matching the stated intent. Move codex adapter existence check from beforeAll to a named test for clearer Jest failure output when the file is missing. * fix(next-task): align Skill() key with codebase convention and quote state path Use name: instead of skill: in Skill() invocation to match the established convention used across all other Skill() calls in the codebase (learn-agent.md, learn/SKILL.md, etc.). Both forms are equivalent pseudo-code for the LLM, but consistency improves readability and avoids confusion. Quote the stateDir path in args to prevent argument splitting failures when the worktree path contains spaces: --state-file "${stateDir}/flow.json". Update regression tests to assert name: key and regenerate Codex adapter. * docs: add CHANGELOG entry for Phase 12 ship:ship fix (#230) * chore(next-task): add startPhase('ship') to Phase 12 for resume consistency Adds workflowState.startPhase('ship') at the start of Phase 12 so the --resume feature can detect that ship was reached if it fails mid-flight. Every other phase begins with startPhase(); Phase 12 was the only exception. Regenerate OpenCode and Codex adapters. * fix(next-task): use 'shipping' phase name in startPhase() call 'ship' is not a valid PHASES entry in workflow-state.js — the correct name is 'shipping'. Using 'ship' would throw "Invalid phase: ship" immediately before the Skill invocation, reintroducing the stuck workflow. Add regression test to prevent recurrence. Reported by Codex review on PR #239.
1 parent e2bf664 commit 7a3ae08

File tree

5 files changed

+62
-4
lines changed

5 files changed

+62
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
- **`/debate` command inline orchestration** — The `/debate` command now manages the full debate workflow directly (parse → resolve → execute → verdict), following the `/consult` pattern. The `debate-orchestrator` agent is now the programmatic entry point for other agents/workflows that need to spawn a debate via `Task()`. Fixes issue #231.
1717

18+
- **`/next-task` Phase 12 ship invocation** — Phase 12 now invokes `ship:ship` via `await Skill({ name: "ship:ship", args: ... })` instead of `Task({ subagent_type: "ship:ship", ... })`. `ship:ship` is a skill, not an agent; the previous `Task()` call silently failed, leaving the workflow stuck after delivery validation with no PR created. The Codex adapter is updated in parity and regression tests are added. Fixes issue #230.
19+
1820
## [5.1.0] - 2026-02-18
1921

2022
### Added

__tests__/next-task-phase11.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,59 @@ describe('next-task Phase 11 integration', () => {
119119
});
120120
});
121121

122+
describe('Phase 12 ship:ship invocation', () => {
123+
const cmdPath = path.join(nextTaskDir, 'commands', 'next-task.md');
124+
let cmdContent;
125+
126+
beforeAll(() => {
127+
cmdContent = fs.readFileSync(cmdPath, 'utf8');
128+
});
129+
130+
test('allowed-tools includes both Skill and Task', () => {
131+
const match = cmdContent.match(/^allowed-tools:\s*(.+)$/m);
132+
expect(match).not.toBeNull();
133+
expect(match[1]).toContain('Skill');
134+
expect(match[1]).toContain('Task');
135+
});
136+
137+
test('uses Skill() not Task() to invoke ship:ship', () => {
138+
expect(cmdContent).toContain('Skill({ name: "ship:ship"');
139+
expect(cmdContent).not.toMatch(/Task\(\s*\{\s*subagent_type:\s*["'`]ship:ship["'`]/);
140+
});
141+
142+
test('startPhase uses shipping not ship (valid PHASES entry)', () => {
143+
expect(cmdContent).toContain("startPhase('shipping')");
144+
expect(cmdContent).not.toContain("startPhase('ship')");
145+
});
146+
147+
test('passes --state-file argument on the same line as Skill invocation', () => {
148+
// Use [^\n]* to enforce same-line matching (no /s flag)
149+
expect(cmdContent).toMatch(/Skill\(\{[^\n]*ship:ship[^\n]*--state-file/);
150+
});
151+
});
152+
153+
describe('codex adapter ship:ship parity', () => {
154+
const codexSkillPath = path.join(__dirname, '..', 'adapters', 'codex', 'skills', 'next-task', 'SKILL.md');
155+
let codexContent;
156+
157+
beforeAll(() => {
158+
codexContent = fs.existsSync(codexSkillPath) ? fs.readFileSync(codexSkillPath, 'utf8') : null;
159+
});
160+
161+
test('codex adapter SKILL.md exists', () => {
162+
expect(codexContent).not.toBeNull();
163+
});
164+
165+
test('codex adapter SKILL.md uses Skill() for ship:ship', () => {
166+
expect(codexContent).toContain('Skill({ name: "ship:ship"');
167+
expect(codexContent).not.toMatch(/Task\(\s*\{\s*subagent_type:\s*["'`]ship:ship["'`]/);
168+
});
169+
170+
test('codex adapter passes --state-file on the same line as Skill invocation', () => {
171+
expect(codexContent).toMatch(/Skill\(\{[^\n]*ship:ship[^\n]*--state-file/);
172+
});
173+
});
174+
122175
describe('next-task agent count', () => {
123176
const agentsDir = path.join(nextTaskDir, 'agents');
124177

adapters/codex/skills/next-task/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,10 @@ workflowState.completePhase({ docsUpdated: true, fixesApplied: result.fixes?.len
519519
After docs update (sync-docs-agent) completes, invoke `ship:ship` explicitly:
520520
521521
```javascript
522+
workflowState.startPhase('shipping');
522523
console.log(`Task #${state.task.id} passed all validation. Invoking ship:ship...`);
523524
const stateDir = workflowState.getStateDir(); // Returns platform-aware state directory
524-
await Task({ subagent_type: "ship:ship", prompt: `Ship the task. State file: ${stateDir}/flow.json` });
525+
await Skill({ name: "ship:ship", args: `--state-file "${stateDir}/flow.json"` });
525526
```
526527
527528
**ship:ship responsibilities:**

adapters/opencode/commands/next-task.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ Uses the unified sync-docs agent from the sync-docs plugin with `before-pr` scop
365365

366366
After docs update (sync-docs-agent) completes, invoke `ship` explicitly:
367367

368-
*(JavaScript reference - not executable in OpenCode)*
368+
- Phase: shipping
369+
369370

370371
**ship responsibilities:**
371372
- Create PR, push branch

plugins/next-task/commands/next-task.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Master workflow orchestrator with autonomous task-to-production automation
33
codex-description: 'Use when user asks to "find next task", "what should I work on", "automate workflow", "implement and ship", "run next-task". Orchestrates complete task-to-production workflow: discovery, implementation, review, and delivery.'
44
argument-hint: "[filter] [--status] [--resume] [--abort] [--implement]"
5-
allowed-tools: Bash(git:*), Bash(gh:*), Bash(npm:*), Bash(node:*), Read, Write, Edit, Glob, Grep, Task, AskUserQuestion
5+
allowed-tools: Bash(git:*), Bash(gh:*), Bash(npm:*), Bash(node:*), Read, Write, Edit, Glob, Grep, Task, Skill, AskUserQuestion
66
---
77

88
# /next-task - Master Workflow Orchestrator
@@ -521,9 +521,10 @@ workflowState.completePhase({ docsUpdated: true, fixesApplied: result.fixes?.len
521521
After docs update (sync-docs-agent) completes, invoke `ship:ship` explicitly:
522522
523523
```javascript
524+
workflowState.startPhase('shipping');
524525
console.log(`Task #${state.task.id} passed all validation. Invoking ship:ship...`);
525526
const stateDir = workflowState.getStateDir(); // Returns platform-aware state directory
526-
await Task({ subagent_type: "ship:ship", prompt: `Ship the task. State file: ${stateDir}/flow.json` });
527+
await Skill({ name: "ship:ship", args: `--state-file "${stateDir}/flow.json"` });
527528
```
528529
529530
**ship:ship responsibilities:**

0 commit comments

Comments
 (0)