@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
@@ -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"
@@ -22,24 +22,27 @@ const roleModels = {
22
22
  routine: readRoleModel("routine"),
23
23
  strong: readRoleModel("strong"),
24
24
  };
25
- const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
26
- type CodexReasoningEffort = shipyard.CodexReasoningEffort;
27
- const readCodexReasoningEffort = (
25
+ const REASONING_EFFORTS = shipyard.REASONING_EFFORTS;
26
+ type ReasoningEffort = shipyard.ReasoningEffort;
27
+ const readRoleReasoningEffort = (
28
28
  role: ModelRole,
29
- ): CodexReasoningEffort | undefined => {
30
- if (!CODEX_PROVIDER) return undefined;
31
- const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
- const effort = process.env[envName]?.trim();
29
+ ): ReasoningEffort | undefined => {
30
+ const envName = `SHIPYARD_${role.toUpperCase()}_REASONING_EFFORT`;
31
+ const legacyName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
+ const sharedEffort = process.env[envName]?.trim();
33
+ const effort =
34
+ sharedEffort ||
35
+ (CODEX_PROVIDER ? process.env[legacyName]?.trim() : undefined);
33
36
  if (!effort) return undefined;
34
- if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
37
+ if (!(REASONING_EFFORTS as readonly string[]).includes(effort))
35
38
  throw new Error(
36
- `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
39
+ `${sharedEffort ? envName : legacyName} must be one of ${REASONING_EFFORTS.join(", ")}; received "${effort}"`,
37
40
  );
38
- return effort as CodexReasoningEffort;
41
+ return effort as ReasoningEffort;
39
42
  };
40
43
  const roleEfforts = {
41
- routine: readCodexReasoningEffort("routine"),
42
- strong: readCodexReasoningEffort("strong"),
44
+ routine: readRoleReasoningEffort("routine"),
45
+ strong: readRoleReasoningEffort("strong"),
43
46
  };
44
47
  const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
45
48
  if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
@@ -136,6 +139,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
136
139
  title: string;
137
140
  branch: string;
138
141
  kind: "standalone" | "spec";
142
+ triageReady?: boolean;
139
143
  body?: string;
140
144
  tickets?: Array<{
141
145
  id: string;
@@ -143,6 +147,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
143
147
  body: string;
144
148
  state: string;
145
149
  blockedBy: Array<{ id: string; title: string; state: string }>;
150
+ triageReady: boolean;
146
151
  }>;
147
152
  completedTicketIds?: string[];
148
153
  outstandingTicketIds?: string[];
@@ -176,24 +181,33 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
176
181
  repository,
177
182
  "--add-label",
178
183
  "shipyard:pending",
184
+ "--remove-label",
185
+ "shipyard",
179
186
  ]);
180
187
  const sandbox = await shipyard.createSandbox({
181
188
  branch: issue.branch,
182
189
  sandbox: sandboxProvider,
183
190
  hooks,
191
+ copyToWorktree: [".shipyard/setup.sh"],
184
192
  });
185
193
  let evidence: string;
186
194
  try {
187
195
  for (const ticketId of issue.kind === "spec"
188
196
  ? (issue.tickets ?? []).map((ticket) => ticket.id)
189
197
  : [issue.id]) {
190
- await sandbox.run({
191
- name: `triage #${ticketId}`,
192
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
193
- maxIterations: 1,
194
- promptFile: "./.shipyard/triage-prompt.md",
195
- promptArgs: { TASK_ID: ticketId },
196
- });
198
+ const ready =
199
+ issue.kind === "spec"
200
+ ? issue.tickets?.find((ticket) => ticket.id === ticketId)
201
+ ?.triageReady
202
+ : issue.triageReady;
203
+ if (!ready)
204
+ await sandbox.run({
205
+ name: `triage #${ticketId}`,
206
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
207
+ maxIterations: 1,
208
+ promptFile: "./.shipyard/triage-prompt.md",
209
+ promptArgs: { TASK_ID: ticketId, BASE_BRANCH: targetBranch },
210
+ });
197
211
  verifyTriage(ticketId);
198
212
  }
199
213
  const implement = await sandbox.run({
@@ -237,7 +251,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
237
251
  `Issue #${issue.id} has unresolved review findings: ${review.stdout.trim().slice(-1200)}`,
238
252
  );
239
253
  }
240
- evidence = `${implementationEvidence}\n\n${reviewEvidence}`;
254
+ evidence = `${implementationEvidence}\n\nReview: APPROVED\n${reviewEvidence}`;
241
255
  } finally {
242
256
  await closeClean(sandbox);
243
257
  }
@@ -245,6 +259,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
245
259
  const publication = await shipyard.createSandbox({
246
260
  branch: issue.branch,
247
261
  sandbox: sandboxProvider,
262
+ copyToWorktree: [".shipyard/handoff.sh"],
248
263
  });
249
264
  try {
250
265
  publicationUncertain = true;
@@ -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"
@@ -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"
@@ -22,24 +22,27 @@ const roleModels = {
22
22
  routine: readRoleModel("routine"),
23
23
  strong: readRoleModel("strong"),
24
24
  };
25
- const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
26
- type CodexReasoningEffort = shipyard.CodexReasoningEffort;
27
- const readCodexReasoningEffort = (
25
+ const REASONING_EFFORTS = shipyard.REASONING_EFFORTS;
26
+ type ReasoningEffort = shipyard.ReasoningEffort;
27
+ const readRoleReasoningEffort = (
28
28
  role: ModelRole,
29
- ): CodexReasoningEffort | undefined => {
30
- if (!CODEX_PROVIDER) return undefined;
31
- const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
- const effort = process.env[envName]?.trim();
29
+ ): ReasoningEffort | undefined => {
30
+ const envName = `SHIPYARD_${role.toUpperCase()}_REASONING_EFFORT`;
31
+ const legacyName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
+ const sharedEffort = process.env[envName]?.trim();
33
+ const effort =
34
+ sharedEffort ||
35
+ (CODEX_PROVIDER ? process.env[legacyName]?.trim() : undefined);
33
36
  if (!effort) return undefined;
34
- if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
37
+ if (!(REASONING_EFFORTS as readonly string[]).includes(effort))
35
38
  throw new Error(
36
- `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
39
+ `${sharedEffort ? envName : legacyName} must be one of ${REASONING_EFFORTS.join(", ")}; received "${effort}"`,
37
40
  );
38
- return effort as CodexReasoningEffort;
41
+ return effort as ReasoningEffort;
39
42
  };
40
43
  const roleEfforts = {
41
- routine: readCodexReasoningEffort("routine"),
42
- strong: readCodexReasoningEffort("strong"),
44
+ routine: readRoleReasoningEffort("routine"),
45
+ strong: readRoleReasoningEffort("strong"),
43
46
  };
44
47
  const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
45
48
  if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
@@ -133,6 +136,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
133
136
  title: string;
134
137
  branch: string;
135
138
  kind: "standalone" | "spec";
139
+ triageReady?: boolean;
136
140
  body?: string;
137
141
  tickets?: Array<{
138
142
  id: string;
@@ -140,6 +144,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
140
144
  body: string;
141
145
  state: string;
142
146
  blockedBy: Array<{ id: string; title: string; state: string }>;
147
+ triageReady: boolean;
143
148
  }>;
144
149
  completedTicketIds?: string[];
145
150
  outstandingTicketIds?: string[];
@@ -173,24 +178,33 @@ for (let iteration = 0; iteration < 3; iteration++) {
173
178
  repository,
174
179
  "--add-label",
175
180
  "shipyard:pending",
181
+ "--remove-label",
182
+ "shipyard",
176
183
  ]);
177
184
  const sandbox = await shipyard.createSandbox({
178
185
  branch: issue.branch,
179
186
  sandbox: sandboxProvider,
180
187
  hooks,
188
+ copyToWorktree: [".shipyard/setup.sh"],
181
189
  });
182
190
  let evidence: string;
183
191
  try {
184
192
  for (const ticketId of issue.kind === "spec"
185
193
  ? (issue.tickets ?? []).map((ticket) => ticket.id)
186
194
  : [issue.id]) {
187
- await sandbox.run({
188
- name: `triage #${ticketId}`,
189
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
190
- maxIterations: 1,
191
- promptFile: "./.shipyard/triage-prompt.md",
192
- promptArgs: { TASK_ID: ticketId },
193
- });
195
+ const ready =
196
+ issue.kind === "spec"
197
+ ? issue.tickets?.find((ticket) => ticket.id === ticketId)
198
+ ?.triageReady
199
+ : issue.triageReady;
200
+ if (!ready)
201
+ await sandbox.run({
202
+ name: `triage #${ticketId}`,
203
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
204
+ maxIterations: 1,
205
+ promptFile: "./.shipyard/triage-prompt.md",
206
+ promptArgs: { TASK_ID: ticketId, BASE_BRANCH: targetBranch },
207
+ });
194
208
  verifyTriage(ticketId);
195
209
  }
196
210
  const result = await sandbox.run({
@@ -225,6 +239,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
225
239
  const publication = await shipyard.createSandbox({
226
240
  branch: issue.branch,
227
241
  sandbox: sandboxProvider,
242
+ copyToWorktree: [".shipyard/handoff.sh"],
228
243
  });
229
244
  try {
230
245
  publicationUncertain = true;
@@ -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`.