@snappedly-tools/shipyard 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +9 -6
  2. package/dist/{chunk-NQRFVKCU.js → chunk-2SEMX2NX.js} +2 -2
  3. package/dist/{chunk-NQRFVKCU.js.map → chunk-2SEMX2NX.js.map} +1 -1
  4. package/dist/{chunk-57EEKW3R.js → chunk-BAP7BWQ2.js} +17 -8
  5. package/dist/chunk-BAP7BWQ2.js.map +1 -0
  6. package/dist/{chunk-HXSZM52J.js → chunk-IVE7QDGC.js} +17 -11
  7. package/dist/chunk-IVE7QDGC.js.map +1 -0
  8. package/dist/{chunk-JZUBT4WG.js → chunk-KSTSHZNH.js} +3 -3
  9. package/dist/{chunk-JZUBT4WG.js.map → chunk-KSTSHZNH.js.map} +1 -1
  10. package/dist/{createSandbox-DmbnWAZv.d.ts → createSandbox-Bqopwday.d.ts} +6 -4
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +58 -13
  13. package/dist/index.js.map +1 -1
  14. package/dist/integrations/github.js +1 -1
  15. package/dist/main.js +26 -7
  16. package/dist/main.js.map +1 -1
  17. package/dist/sandboxes/docker.js +2 -2
  18. package/dist/templates/parallel-planner/block-scope.sh +6 -0
  19. package/dist/templates/parallel-planner/handoff.sh +7 -2
  20. package/dist/templates/parallel-planner/main.mts +41 -26
  21. package/dist/templates/parallel-planner/select-issues.mjs +20 -2
  22. package/dist/templates/parallel-planner/setup.sh +8 -1
  23. package/dist/templates/parallel-planner/triage-prompt.md +4 -2
  24. package/dist/templates/parallel-planner/verify-triage.sh +12 -3
  25. package/dist/templates/parallel-planner-with-review/block-scope.sh +6 -0
  26. package/dist/templates/parallel-planner-with-review/handoff.sh +7 -2
  27. package/dist/templates/parallel-planner-with-review/main.mts +42 -27
  28. package/dist/templates/parallel-planner-with-review/select-issues.mjs +20 -2
  29. package/dist/templates/parallel-planner-with-review/setup.sh +8 -1
  30. package/dist/templates/parallel-planner-with-review/triage-prompt.md +4 -2
  31. package/dist/templates/parallel-planner-with-review/verify-triage.sh +12 -3
  32. package/dist/templates/sequential-reviewer/block-scope.sh +6 -0
  33. package/dist/templates/sequential-reviewer/handoff.sh +7 -2
  34. package/dist/templates/sequential-reviewer/main.mts +35 -20
  35. package/dist/templates/sequential-reviewer/select-issues.mjs +20 -2
  36. package/dist/templates/sequential-reviewer/setup.sh +8 -1
  37. package/dist/templates/sequential-reviewer/triage-prompt.md +4 -2
  38. package/dist/templates/sequential-reviewer/verify-triage.sh +12 -3
  39. package/dist/templates/shared/block-scope.sh +6 -0
  40. package/dist/templates/shared/handoff.sh +7 -2
  41. package/dist/templates/shared/select-issues.mjs +20 -2
  42. package/dist/templates/shared/setup.sh +8 -1
  43. package/dist/templates/shared/triage-prompt.md +4 -2
  44. package/dist/templates/shared/verify-triage.sh +12 -3
  45. package/dist/templates/simple-loop/block-scope.sh +6 -0
  46. package/dist/templates/simple-loop/handoff.sh +7 -2
  47. package/dist/templates/simple-loop/main.mts +34 -19
  48. package/dist/templates/simple-loop/select-issues.mjs +20 -2
  49. package/dist/templates/simple-loop/setup.sh +8 -1
  50. package/dist/templates/simple-loop/triage-prompt.md +4 -2
  51. package/dist/templates/simple-loop/verify-triage.sh +12 -3
  52. package/dist/workflow.d.ts +1 -1
  53. package/dist/workflow.js +2 -2
  54. package/package.json +1 -1
  55. package/dist/chunk-57EEKW3R.js.map +0 -1
  56. package/dist/chunk-HXSZM52J.js.map +0 -1
@@ -1,4 +1,4 @@
1
- export { defaultImageName, docker } from '../chunk-JZUBT4WG.js';
2
- import '../chunk-57EEKW3R.js';
1
+ export { defaultImageName, docker } from '../chunk-KSTSHZNH.js';
2
+ import '../chunk-BAP7BWQ2.js';
3
3
  //# sourceMappingURL=docker.js.map
4
4
  //# sourceMappingURL=docker.js.map
@@ -108,6 +108,10 @@ Work was not handed off. To retry, resolve the problem, remove shipyard:blocked,
108
108
  fi
109
109
 
110
110
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
111
+ if ! remove_issue_status "$root" ready-for-human; then
112
+ echo "Warning: could not clear spec triage status" >&2
113
+ status_synced=false
114
+ fi
111
115
  if ! remove_issue_status "$root" shipyard:outstanding-tasks; then
112
116
  echo "Warning: could not clear spec status" >&2
113
117
  status_synced=false
@@ -122,9 +126,11 @@ else
122
126
  fi
123
127
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
124
128
  for affected_id in "${affected_ids[@]:1}"; do
129
+ remove_issue_status "$affected_id" ready-for-human || status=75
125
130
  remove_issue_status "$affected_id" shipyard:pending || status=75
126
131
  done
127
132
  else
133
+ remove_issue_status "$root" ready-for-human || status=75
128
134
  remove_issue_status "$root" shipyard:pending || status=75
129
135
  fi
130
136
 
@@ -103,6 +103,7 @@ for ((i=1; i<${#scope_ids[@]}; i++)); do
103
103
  echo "Could not mark ticket #${scope_ids[i]} complete; retry issue completion when GitHub is available" >&2
104
104
  exit 75
105
105
  fi
106
+ remove_issue_label "${scope_ids[i]}" ready-for-human
106
107
  remove_issue_label "${scope_ids[i]}" shipyard:blocked
107
108
  remove_issue_label "${scope_ids[i]}" shipyard:pending
108
109
  done
@@ -128,7 +129,11 @@ if ! gh label create "$status_label" --repo "$repo" --color "$status_color" --de
128
129
  echo "Warning: could not create $status_label" >&2
129
130
  status_synced=false
130
131
  fi
131
- if ! gh issue edit "$issue" --repo "$repo" --add-label "$status_label"; then
132
+ if gh issue edit "$issue" --repo "$repo" --add-label "$status_label"; then
133
+ if [[ "$status_label" == shipyard:complete ]]; then
134
+ remove_issue_label "$issue" ready-for-human
135
+ fi
136
+ else
132
137
  if [[ "$branch" == "shipyard/issue-$issue" ]]; then
133
138
  echo "Could not mark ticket #$issue complete; retry issue completion when GitHub is available" >&2
134
139
  exit 75
@@ -170,6 +175,6 @@ if [[ "$status_synced" == true ]]; then
170
175
  fi
171
176
  done
172
177
  else
173
- echo "Warning: status labels need reconciliation; retaining ticket activation" >&2
178
+ echo "Warning: status labels need reconciliation; add shipyard to the ticket to retry" >&2
174
179
  fi
175
180
  printf '%s\n' "$url"
@@ -27,24 +27,27 @@ const roleModels = {
27
27
  routine: readRoleModel("routine"),
28
28
  strong: readRoleModel("strong"),
29
29
  };
30
- const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
31
- type CodexReasoningEffort = shipyard.CodexReasoningEffort;
32
- const readCodexReasoningEffort = (
30
+ const REASONING_EFFORTS = shipyard.REASONING_EFFORTS;
31
+ type ReasoningEffort = shipyard.ReasoningEffort;
32
+ const readRoleReasoningEffort = (
33
33
  role: ModelRole,
34
- ): CodexReasoningEffort | undefined => {
35
- if (!CODEX_PROVIDER) return undefined;
36
- const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
37
- const effort = process.env[envName]?.trim();
34
+ ): ReasoningEffort | undefined => {
35
+ const envName = `SHIPYARD_${role.toUpperCase()}_REASONING_EFFORT`;
36
+ const legacyName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
37
+ const sharedEffort = process.env[envName]?.trim();
38
+ const effort =
39
+ sharedEffort ||
40
+ (CODEX_PROVIDER ? process.env[legacyName]?.trim() : undefined);
38
41
  if (!effort) return undefined;
39
- if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
42
+ if (!(REASONING_EFFORTS as readonly string[]).includes(effort))
40
43
  throw new Error(
41
- `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
44
+ `${sharedEffort ? envName : legacyName} must be one of ${REASONING_EFFORTS.join(", ")}; received "${effort}"`,
42
45
  );
43
- return effort as CodexReasoningEffort;
46
+ return effort as ReasoningEffort;
44
47
  };
45
48
  const roleEfforts = {
46
- routine: readCodexReasoningEffort("routine"),
47
- strong: readCodexReasoningEffort("strong"),
49
+ routine: readRoleReasoningEffort("routine"),
50
+ strong: readRoleReasoningEffort("strong"),
48
51
  };
49
52
  const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
50
53
  if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
@@ -112,12 +115,14 @@ type Ticket = {
112
115
  body: string;
113
116
  state: string;
114
117
  blockedBy: Array<{ id: string; title: string; state: string }>;
118
+ triageReady: boolean;
115
119
  };
116
120
  type Scope = {
117
121
  id: string;
118
122
  title: string;
119
123
  branch: string;
120
124
  kind: "standalone" | "spec";
125
+ triageReady?: boolean;
121
126
  body?: string;
122
127
  tickets?: Ticket[];
123
128
  completedTicketIds?: string[];
@@ -166,15 +171,17 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
166
171
  baseBranch: scope.branch,
167
172
  sandbox: sandboxProvider,
168
173
  hooks,
174
+ copyToWorktree: [".shipyard/setup.sh"],
169
175
  });
170
176
  try {
171
- await sandbox.run({
172
- name: `triage #${ticket.id}`,
173
- maxIterations: 1,
174
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
175
- promptFile: "./.shipyard/triage-prompt.md",
176
- promptArgs: { TASK_ID: ticket.id },
177
- });
177
+ if (!ticket.triageReady)
178
+ await sandbox.run({
179
+ name: `triage #${ticket.id}`,
180
+ maxIterations: 1,
181
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
182
+ promptFile: "./.shipyard/triage-prompt.md",
183
+ promptArgs: { TASK_ID: ticket.id, BASE_BRANCH: targetBranch },
184
+ });
178
185
  verifyTriage(ticket.id);
179
186
  const implementation = await sandbox.run({
180
187
  name: "implementer",
@@ -212,6 +219,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
212
219
  const plan = await shipyard.run({
213
220
  hooks,
214
221
  sandbox: sandboxProvider,
222
+ copyToWorktree: [".shipyard/setup.sh"],
215
223
  name: "planner",
216
224
  branchStrategy: { type: "branch", branch: plannerBranch },
217
225
  maxIterations: 1,
@@ -257,6 +265,8 @@ for (let iteration = 0; iteration < 10; iteration++) {
257
265
  repository,
258
266
  "--add-label",
259
267
  "shipyard:pending",
268
+ "--remove-label",
269
+ "shipyard",
260
270
  ]);
261
271
  if (
262
272
  scope.branch !==
@@ -271,6 +281,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
271
281
  branch: scope.branch,
272
282
  sandbox: sandboxProvider,
273
283
  hooks,
284
+ copyToWorktree: [".shipyard/setup.sh"],
274
285
  });
275
286
  await closeClean(seed);
276
287
  const tickets = scope.tickets ?? [];
@@ -305,6 +316,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
305
316
  branch: scope.branch,
306
317
  sandbox: sandboxProvider,
307
318
  hooks,
319
+ copyToWorktree: [".shipyard/setup.sh"],
308
320
  });
309
321
  try {
310
322
  for (const [index, outcome] of settled.entries()) {
@@ -430,17 +442,19 @@ for (let iteration = 0; iteration < 10; iteration++) {
430
442
  branch: scope.branch,
431
443
  sandbox: sandboxProvider,
432
444
  hooks,
445
+ copyToWorktree: [".shipyard/setup.sh"],
433
446
  });
434
447
  let handoffEvidence: string;
435
448
  try {
436
449
  if (scope.kind === "standalone") {
437
- await integration.run({
438
- name: `triage #${id}`,
439
- maxIterations: 1,
440
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
441
- promptFile: "./.shipyard/triage-prompt.md",
442
- promptArgs: { TASK_ID: id },
443
- });
450
+ if (!scope.triageReady)
451
+ await integration.run({
452
+ name: `triage #${id}`,
453
+ maxIterations: 1,
454
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
455
+ promptFile: "./.shipyard/triage-prompt.md",
456
+ promptArgs: { TASK_ID: id, BASE_BRANCH: targetBranch },
457
+ });
444
458
  verifyTriage(id);
445
459
  const standalone = await integration.run({
446
460
  name: "implementer",
@@ -477,6 +491,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
477
491
  const publication = await shipyard.createSandbox({
478
492
  branch: scope.branch,
479
493
  sandbox: sandboxProvider,
494
+ copyToWorktree: [".shipyard/handoff.sh"],
480
495
  });
481
496
  try {
482
497
  const scopeIds = [
@@ -124,6 +124,17 @@ const dependencies = (id) => {
124
124
  };
125
125
  const planningSpec = (body) =>
126
126
  /work item type:\*?\*?\s*planning spec/i.test(body ?? "");
127
+ const triageStates = [
128
+ "needs-triage",
129
+ "needs-info",
130
+ "ready-for-agent",
131
+ "ready-for-human",
132
+ "wontfix",
133
+ ];
134
+ const triageReady = (labels) =>
135
+ triageStates
136
+ .filter((state) => labels.some((label) => label.name === state))
137
+ .join() === "ready-for-agent";
127
138
  const textParent = (body, issueId) => {
128
139
  const match = /^##?\s*Parent\b[^\n]*\n/im.exec(body ?? "");
129
140
  if (!match) return undefined;
@@ -476,9 +487,10 @@ for (const candidate of activated) {
476
487
  ),
477
488
  );
478
489
  selectedTicketIds = new Set(selectableTickets.map((ticket) => ticket.id));
479
- const tickets = selectableTickets.map(({ labels: _labels, ...ticket }) => ({
490
+ const tickets = selectableTickets.map(({ labels, ...ticket }) => ({
480
491
  ...ticket,
481
492
  blockedBy: dependencies(ticket.id),
493
+ triageReady: triageReady(labels),
482
494
  }));
483
495
  const completedTicketIds = linkedTickets
484
496
  .filter((ticket) =>
@@ -756,7 +768,13 @@ for (const candidate of activated) {
756
768
  completedTicketIds,
757
769
  outstandingTicketIds,
758
770
  }
759
- : { id, title: candidate.title, branch, kind: "standalone" },
771
+ : {
772
+ id,
773
+ title: candidate.title,
774
+ branch,
775
+ kind: "standalone",
776
+ triageReady: triageReady(candidate.labels ?? []),
777
+ },
760
778
  );
761
779
  } catch (error) {
762
780
  if (!(error instanceof SelectionError)) throw error;
@@ -18,7 +18,14 @@ if [[ -f package.json ]]; then
18
18
  else manager=npm; fi
19
19
  fi
20
20
  case "$manager" in
21
- npm) npm install ;;
21
+ npm)
22
+ if [[ -f package-lock.json ]]; then
23
+ if ! npm ci; then
24
+ echo "npm ci failed; installing from package.json without changing package-lock.json" >&2
25
+ npm install --no-package-lock
26
+ fi
27
+ else npm install --no-package-lock
28
+ fi ;;
22
29
  pnpm) corepack pnpm install ;;
23
30
  yarn) corepack yarn install ;;
24
31
  bun)
@@ -1,5 +1,7 @@
1
1
  Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
- Follow `/triage` and the repository's `docs/agents/triage.md` label mapping. Read the full issue and comments, verify its claim against the codebase, then apply the appropriate category and state label and post the brief or triage notes required by the skill. The maintainer has authorized Shipyard to apply an evidence-based recommendation during this unattended intake. If a decision requires maintainer judgment, use `needs-triage` and record the question. Do not implement the issue or start a PR in this phase.
3
+ Follow `/triage` and the repository's triage label mapping. Read the full issue and comments. Verify the reported behavior against `origin/{{BASE_BRANCH}}`, the integration baseline. The checked-out task branch may contain unfinished changes from an earlier attempt. Do not close the issue as already complete based on the task branch or comments about that candidate.
4
4
 
5
- Report the applied state and evidence. Shipyard will independently re-read GitHub labels and implement only an open issue carrying both `shipyard` and the sole triage state `ready-for-agent`.
5
+ Apply the appropriate category and state label and post the brief or triage notes required by the skill. The maintainer has authorized Shipyard to apply an evidence-based recommendation during this unattended intake. Derive acceptance criteria from the stated outcome. For example, equal spacing between named header items is a testable visual goal without a pixel target. Choose a representative supported viewport if none is specified. An inaccessible attachment does not require `needs-info` when the text establishes the current and desired behavior; record the verification limit in the brief. Do not treat equal CSS gap values alone as proof that the rendered gaps are equal. Apply `needs-info` and ask one concrete question only when missing details change the intended result or prevent meaningful verification. Use `needs-triage` only when the available facts leave a product or policy decision for the maintainer, not for routine layout or implementation choices. Preserve the `shipyard` or `shipyard:pending` label. Do not implement the issue or start a PR in this phase.
6
+
7
+ Report the applied state and evidence. Shipyard will independently re-read GitHub labels and implement only an open issue carrying `shipyard` or `shipyard:pending` and the sole triage state `ready-for-agent`.
@@ -11,9 +11,18 @@ gh issue view "$issue" --repo "$repo" --json state,labels |
11
11
  const issue = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
12
12
  const labels = new Set(issue.labels.map((label) => label.name));
13
13
  const states = ["needs-triage", "needs-info", "ready-for-agent", "ready-for-human", "wontfix"];
14
- if (issue.state !== "OPEN" || !labels.has("shipyard") ||
15
- states.filter((state) => labels.has(state)).join() !== "ready-for-agent") {
16
- console.error("Issue cannot be implemented: requires an open issue with shipyard and only ready-for-agent as its triage state");
14
+ if (issue.state !== "OPEN" ||
15
+ !(labels.has("shipyard") || labels.has("shipyard:pending"))) {
16
+ console.error("Issue cannot be implemented: requires an open issue with shipyard or shipyard:pending");
17
+ process.exit(1);
18
+ }
19
+ const triageStates = states.filter((state) => labels.has(state));
20
+ if (triageStates.join() === "needs-info") {
21
+ console.error("Issue needs information from the reporter before implementation");
22
+ process.exit(1);
23
+ }
24
+ if (triageStates.join() !== "ready-for-agent") {
25
+ console.error(`Issue cannot be implemented: triage state is ${triageStates.join() || "missing"}; requires only ready-for-agent`);
17
26
  process.exit(1);
18
27
  }
19
28
  '
@@ -108,6 +108,10 @@ Work was not handed off. To retry, resolve the problem, remove shipyard:blocked,
108
108
  fi
109
109
 
110
110
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
111
+ if ! remove_issue_status "$root" ready-for-human; then
112
+ echo "Warning: could not clear spec triage status" >&2
113
+ status_synced=false
114
+ fi
111
115
  if ! remove_issue_status "$root" shipyard:outstanding-tasks; then
112
116
  echo "Warning: could not clear spec status" >&2
113
117
  status_synced=false
@@ -122,9 +126,11 @@ else
122
126
  fi
123
127
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
124
128
  for affected_id in "${affected_ids[@]:1}"; do
129
+ remove_issue_status "$affected_id" ready-for-human || status=75
125
130
  remove_issue_status "$affected_id" shipyard:pending || status=75
126
131
  done
127
132
  else
133
+ remove_issue_status "$root" ready-for-human || status=75
128
134
  remove_issue_status "$root" shipyard:pending || status=75
129
135
  fi
130
136
 
@@ -103,6 +103,7 @@ for ((i=1; i<${#scope_ids[@]}; i++)); do
103
103
  echo "Could not mark ticket #${scope_ids[i]} complete; retry issue completion when GitHub is available" >&2
104
104
  exit 75
105
105
  fi
106
+ remove_issue_label "${scope_ids[i]}" ready-for-human
106
107
  remove_issue_label "${scope_ids[i]}" shipyard:blocked
107
108
  remove_issue_label "${scope_ids[i]}" shipyard:pending
108
109
  done
@@ -128,7 +129,11 @@ if ! gh label create "$status_label" --repo "$repo" --color "$status_color" --de
128
129
  echo "Warning: could not create $status_label" >&2
129
130
  status_synced=false
130
131
  fi
131
- if ! gh issue edit "$issue" --repo "$repo" --add-label "$status_label"; then
132
+ if gh issue edit "$issue" --repo "$repo" --add-label "$status_label"; then
133
+ if [[ "$status_label" == shipyard:complete ]]; then
134
+ remove_issue_label "$issue" ready-for-human
135
+ fi
136
+ else
132
137
  if [[ "$branch" == "shipyard/issue-$issue" ]]; then
133
138
  echo "Could not mark ticket #$issue complete; retry issue completion when GitHub is available" >&2
134
139
  exit 75
@@ -170,6 +175,6 @@ if [[ "$status_synced" == true ]]; then
170
175
  fi
171
176
  done
172
177
  else
173
- echo "Warning: status labels need reconciliation; retaining ticket activation" >&2
178
+ echo "Warning: status labels need reconciliation; add shipyard to the ticket to retry" >&2
174
179
  fi
175
180
  printf '%s\n' "$url"
@@ -27,24 +27,27 @@ const roleModels = {
27
27
  routine: readRoleModel("routine"),
28
28
  strong: readRoleModel("strong"),
29
29
  };
30
- const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
31
- type CodexReasoningEffort = shipyard.CodexReasoningEffort;
32
- const readCodexReasoningEffort = (
30
+ const REASONING_EFFORTS = shipyard.REASONING_EFFORTS;
31
+ type ReasoningEffort = shipyard.ReasoningEffort;
32
+ const readRoleReasoningEffort = (
33
33
  role: ModelRole,
34
- ): CodexReasoningEffort | undefined => {
35
- if (!CODEX_PROVIDER) return undefined;
36
- const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
37
- const effort = process.env[envName]?.trim();
34
+ ): ReasoningEffort | undefined => {
35
+ const envName = `SHIPYARD_${role.toUpperCase()}_REASONING_EFFORT`;
36
+ const legacyName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
37
+ const sharedEffort = process.env[envName]?.trim();
38
+ const effort =
39
+ sharedEffort ||
40
+ (CODEX_PROVIDER ? process.env[legacyName]?.trim() : undefined);
38
41
  if (!effort) return undefined;
39
- if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
42
+ if (!(REASONING_EFFORTS as readonly string[]).includes(effort))
40
43
  throw new Error(
41
- `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
44
+ `${sharedEffort ? envName : legacyName} must be one of ${REASONING_EFFORTS.join(", ")}; received "${effort}"`,
42
45
  );
43
- return effort as CodexReasoningEffort;
46
+ return effort as ReasoningEffort;
44
47
  };
45
48
  const roleEfforts = {
46
- routine: readCodexReasoningEffort("routine"),
47
- strong: readCodexReasoningEffort("strong"),
49
+ routine: readRoleReasoningEffort("routine"),
50
+ strong: readRoleReasoningEffort("strong"),
48
51
  };
49
52
  const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
50
53
  if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
@@ -112,12 +115,14 @@ type Ticket = {
112
115
  body: string;
113
116
  state: string;
114
117
  blockedBy: Array<{ id: string; title: string; state: string }>;
118
+ triageReady: boolean;
115
119
  };
116
120
  type Scope = {
117
121
  id: string;
118
122
  title: string;
119
123
  branch: string;
120
124
  kind: "standalone" | "spec";
125
+ triageReady?: boolean;
121
126
  body?: string;
122
127
  tickets?: Ticket[];
123
128
  completedTicketIds?: string[];
@@ -144,7 +149,7 @@ const approved = (
144
149
  throw new Error(
145
150
  `${stage} has unresolved review findings: ${result.stdout.trim().slice(-1200)}`,
146
151
  );
147
- return complete(result, stage);
152
+ return `Review: APPROVED\n${complete(result, stage)}`;
148
153
  };
149
154
  class TicketFailures extends Error {
150
155
  constructor(readonly failures: Array<{ id: string; reason: string }>) {
@@ -176,15 +181,17 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
176
181
  baseBranch: scope.branch,
177
182
  sandbox: sandboxProvider,
178
183
  hooks,
184
+ copyToWorktree: [".shipyard/setup.sh"],
179
185
  });
180
186
  try {
181
- await sandbox.run({
182
- name: `triage #${ticket.id}`,
183
- maxIterations: 1,
184
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
185
- promptFile: "./.shipyard/triage-prompt.md",
186
- promptArgs: { TASK_ID: ticket.id },
187
- });
187
+ if (!ticket.triageReady)
188
+ await sandbox.run({
189
+ name: `triage #${ticket.id}`,
190
+ maxIterations: 1,
191
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
192
+ promptFile: "./.shipyard/triage-prompt.md",
193
+ promptArgs: { TASK_ID: ticket.id, BASE_BRANCH: targetBranch },
194
+ });
188
195
  verifyTriage(ticket.id);
189
196
  const implementation = await sandbox.run({
190
197
  name: "implementer",
@@ -235,6 +242,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
235
242
  const plan = await shipyard.run({
236
243
  hooks,
237
244
  sandbox: sandboxProvider,
245
+ copyToWorktree: [".shipyard/setup.sh"],
238
246
  name: "planner",
239
247
  branchStrategy: { type: "branch", branch: plannerBranch },
240
248
  maxIterations: 1,
@@ -280,6 +288,8 @@ for (let iteration = 0; iteration < 10; iteration++) {
280
288
  repository,
281
289
  "--add-label",
282
290
  "shipyard:pending",
291
+ "--remove-label",
292
+ "shipyard",
283
293
  ]);
284
294
  if (
285
295
  scope.branch !==
@@ -294,6 +304,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
294
304
  branch: scope.branch,
295
305
  sandbox: sandboxProvider,
296
306
  hooks,
307
+ copyToWorktree: [".shipyard/setup.sh"],
297
308
  });
298
309
  await closeClean(seed);
299
310
  const tickets = scope.tickets ?? [];
@@ -328,6 +339,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
328
339
  branch: scope.branch,
329
340
  sandbox: sandboxProvider,
330
341
  hooks,
342
+ copyToWorktree: [".shipyard/setup.sh"],
331
343
  });
332
344
  try {
333
345
  for (const [index, outcome] of settled.entries()) {
@@ -453,17 +465,19 @@ for (let iteration = 0; iteration < 10; iteration++) {
453
465
  branch: scope.branch,
454
466
  sandbox: sandboxProvider,
455
467
  hooks,
468
+ copyToWorktree: [".shipyard/setup.sh"],
456
469
  });
457
470
  let handoffEvidence: string;
458
471
  try {
459
472
  if (scope.kind === "standalone") {
460
- await integration.run({
461
- name: `triage #${id}`,
462
- maxIterations: 1,
463
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
464
- promptFile: "./.shipyard/triage-prompt.md",
465
- promptArgs: { TASK_ID: id },
466
- });
473
+ if (!scope.triageReady)
474
+ await integration.run({
475
+ name: `triage #${id}`,
476
+ maxIterations: 1,
477
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
478
+ promptFile: "./.shipyard/triage-prompt.md",
479
+ promptArgs: { TASK_ID: id, BASE_BRANCH: targetBranch },
480
+ });
467
481
  verifyTriage(id);
468
482
  const standalone = await integration.run({
469
483
  name: "implementer",
@@ -528,6 +542,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
528
542
  const publication = await shipyard.createSandbox({
529
543
  branch: scope.branch,
530
544
  sandbox: sandboxProvider,
545
+ copyToWorktree: [".shipyard/handoff.sh"],
531
546
  });
532
547
  try {
533
548
  const scopeIds = [
@@ -124,6 +124,17 @@ const dependencies = (id) => {
124
124
  };
125
125
  const planningSpec = (body) =>
126
126
  /work item type:\*?\*?\s*planning spec/i.test(body ?? "");
127
+ const triageStates = [
128
+ "needs-triage",
129
+ "needs-info",
130
+ "ready-for-agent",
131
+ "ready-for-human",
132
+ "wontfix",
133
+ ];
134
+ const triageReady = (labels) =>
135
+ triageStates
136
+ .filter((state) => labels.some((label) => label.name === state))
137
+ .join() === "ready-for-agent";
127
138
  const textParent = (body, issueId) => {
128
139
  const match = /^##?\s*Parent\b[^\n]*\n/im.exec(body ?? "");
129
140
  if (!match) return undefined;
@@ -476,9 +487,10 @@ for (const candidate of activated) {
476
487
  ),
477
488
  );
478
489
  selectedTicketIds = new Set(selectableTickets.map((ticket) => ticket.id));
479
- const tickets = selectableTickets.map(({ labels: _labels, ...ticket }) => ({
490
+ const tickets = selectableTickets.map(({ labels, ...ticket }) => ({
480
491
  ...ticket,
481
492
  blockedBy: dependencies(ticket.id),
493
+ triageReady: triageReady(labels),
482
494
  }));
483
495
  const completedTicketIds = linkedTickets
484
496
  .filter((ticket) =>
@@ -756,7 +768,13 @@ for (const candidate of activated) {
756
768
  completedTicketIds,
757
769
  outstandingTicketIds,
758
770
  }
759
- : { id, title: candidate.title, branch, kind: "standalone" },
771
+ : {
772
+ id,
773
+ title: candidate.title,
774
+ branch,
775
+ kind: "standalone",
776
+ triageReady: triageReady(candidate.labels ?? []),
777
+ },
760
778
  );
761
779
  } catch (error) {
762
780
  if (!(error instanceof SelectionError)) throw error;
@@ -18,7 +18,14 @@ if [[ -f package.json ]]; then
18
18
  else manager=npm; fi
19
19
  fi
20
20
  case "$manager" in
21
- npm) npm install ;;
21
+ npm)
22
+ if [[ -f package-lock.json ]]; then
23
+ if ! npm ci; then
24
+ echo "npm ci failed; installing from package.json without changing package-lock.json" >&2
25
+ npm install --no-package-lock
26
+ fi
27
+ else npm install --no-package-lock
28
+ fi ;;
22
29
  pnpm) corepack pnpm install ;;
23
30
  yarn) corepack yarn install ;;
24
31
  bun)
@@ -1,5 +1,7 @@
1
1
  Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
- Follow `/triage` and the repository's `docs/agents/triage.md` label mapping. Read the full issue and comments, verify its claim against the codebase, then apply the appropriate category and state label and post the brief or triage notes required by the skill. The maintainer has authorized Shipyard to apply an evidence-based recommendation during this unattended intake. If a decision requires maintainer judgment, use `needs-triage` and record the question. Do not implement the issue or start a PR in this phase.
3
+ Follow `/triage` and the repository's triage label mapping. Read the full issue and comments. Verify the reported behavior against `origin/{{BASE_BRANCH}}`, the integration baseline. The checked-out task branch may contain unfinished changes from an earlier attempt. Do not close the issue as already complete based on the task branch or comments about that candidate.
4
4
 
5
- Report the applied state and evidence. Shipyard will independently re-read GitHub labels and implement only an open issue carrying both `shipyard` and the sole triage state `ready-for-agent`.
5
+ Apply the appropriate category and state label and post the brief or triage notes required by the skill. The maintainer has authorized Shipyard to apply an evidence-based recommendation during this unattended intake. Derive acceptance criteria from the stated outcome. For example, equal spacing between named header items is a testable visual goal without a pixel target. Choose a representative supported viewport if none is specified. An inaccessible attachment does not require `needs-info` when the text establishes the current and desired behavior; record the verification limit in the brief. Do not treat equal CSS gap values alone as proof that the rendered gaps are equal. Apply `needs-info` and ask one concrete question only when missing details change the intended result or prevent meaningful verification. Use `needs-triage` only when the available facts leave a product or policy decision for the maintainer, not for routine layout or implementation choices. Preserve the `shipyard` or `shipyard:pending` label. Do not implement the issue or start a PR in this phase.
6
+
7
+ Report the applied state and evidence. Shipyard will independently re-read GitHub labels and implement only an open issue carrying `shipyard` or `shipyard:pending` and the sole triage state `ready-for-agent`.
@@ -11,9 +11,18 @@ gh issue view "$issue" --repo "$repo" --json state,labels |
11
11
  const issue = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
12
12
  const labels = new Set(issue.labels.map((label) => label.name));
13
13
  const states = ["needs-triage", "needs-info", "ready-for-agent", "ready-for-human", "wontfix"];
14
- if (issue.state !== "OPEN" || !labels.has("shipyard") ||
15
- states.filter((state) => labels.has(state)).join() !== "ready-for-agent") {
16
- console.error("Issue cannot be implemented: requires an open issue with shipyard and only ready-for-agent as its triage state");
14
+ if (issue.state !== "OPEN" ||
15
+ !(labels.has("shipyard") || labels.has("shipyard:pending"))) {
16
+ console.error("Issue cannot be implemented: requires an open issue with shipyard or shipyard:pending");
17
+ process.exit(1);
18
+ }
19
+ const triageStates = states.filter((state) => labels.has(state));
20
+ if (triageStates.join() === "needs-info") {
21
+ console.error("Issue needs information from the reporter before implementation");
22
+ process.exit(1);
23
+ }
24
+ if (triageStates.join() !== "ready-for-agent") {
25
+ console.error(`Issue cannot be implemented: triage state is ${triageStates.join() || "missing"}; requires only ready-for-agent`);
17
26
  process.exit(1);
18
27
  }
19
28
  '
@@ -108,6 +108,10 @@ Work was not handed off. To retry, resolve the problem, remove shipyard:blocked,
108
108
  fi
109
109
 
110
110
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
111
+ if ! remove_issue_status "$root" ready-for-human; then
112
+ echo "Warning: could not clear spec triage status" >&2
113
+ status_synced=false
114
+ fi
111
115
  if ! remove_issue_status "$root" shipyard:outstanding-tasks; then
112
116
  echo "Warning: could not clear spec status" >&2
113
117
  status_synced=false
@@ -122,9 +126,11 @@ else
122
126
  fi
123
127
  if [[ "$branch" == "shipyard/spec-$root" ]]; then
124
128
  for affected_id in "${affected_ids[@]:1}"; do
129
+ remove_issue_status "$affected_id" ready-for-human || status=75
125
130
  remove_issue_status "$affected_id" shipyard:pending || status=75
126
131
  done
127
132
  else
133
+ remove_issue_status "$root" ready-for-human || status=75
128
134
  remove_issue_status "$root" shipyard:pending || status=75
129
135
  fi
130
136