@snappedly-tools/shipyard 0.10.0 → 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.
package/README.md CHANGED
@@ -86,10 +86,11 @@ npx shipyard uninstall
86
86
 
87
87
  Then:
88
88
 
89
- 1. Write an issue with a clear goal and add the `shipyard` and
90
- `ready-for-agent` labels.
91
- 2. Run Shipyard to check for the issue, implement it in Docker,
92
- review the work, and open a pull request.
89
+ 1. Write an issue with a clear goal. If it is already triaged and approved for
90
+ agent implementation, add `ready-for-agent`. Add `shipyard` last to activate
91
+ the issue.
92
+ 2. Run Shipyard. It triages issues without `ready-for-agent`, verifies ready
93
+ issues, implements them in Docker, reviews the work, and opens a pull request.
93
94
  3. Inspect the pull request and merge it when you are happy with the result.
94
95
 
95
96
  ## Choose a workflow
package/dist/main.js CHANGED
@@ -2400,7 +2400,7 @@ var runnerCommand = Command.make(
2400
2400
  purgeRunnerCommand
2401
2401
  ])
2402
2402
  );
2403
- var VERSION = "0.10.0" ;
2403
+ var VERSION = "0.11.0" ;
2404
2404
  var SHIPYARD_PACKAGE_NAME = "@snappedly-tools/shipyard";
2405
2405
  var isMissingPath = (error) => error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
2406
2406
  var lstatIfPresent = async (path) => {
@@ -115,12 +115,14 @@ type Ticket = {
115
115
  body: string;
116
116
  state: string;
117
117
  blockedBy: Array<{ id: string; title: string; state: string }>;
118
+ triageReady: boolean;
118
119
  };
119
120
  type Scope = {
120
121
  id: string;
121
122
  title: string;
122
123
  branch: string;
123
124
  kind: "standalone" | "spec";
125
+ triageReady?: boolean;
124
126
  body?: string;
125
127
  tickets?: Ticket[];
126
128
  completedTicketIds?: string[];
@@ -172,13 +174,14 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
172
174
  copyToWorktree: [".shipyard/setup.sh"],
173
175
  });
174
176
  try {
175
- await sandbox.run({
176
- name: `triage #${ticket.id}`,
177
- maxIterations: 1,
178
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
179
- promptFile: "./.shipyard/triage-prompt.md",
180
- promptArgs: { TASK_ID: ticket.id, BASE_BRANCH: targetBranch },
181
- });
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
+ });
182
185
  verifyTriage(ticket.id);
183
186
  const implementation = await sandbox.run({
184
187
  name: "implementer",
@@ -444,13 +447,14 @@ for (let iteration = 0; iteration < 10; iteration++) {
444
447
  let handoffEvidence: string;
445
448
  try {
446
449
  if (scope.kind === "standalone") {
447
- await integration.run({
448
- name: `triage #${id}`,
449
- maxIterations: 1,
450
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
451
- promptFile: "./.shipyard/triage-prompt.md",
452
- promptArgs: { TASK_ID: id, BASE_BRANCH: targetBranch },
453
- });
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
+ });
454
458
  verifyTriage(id);
455
459
  const standalone = await integration.run({
456
460
  name: "implementer",
@@ -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;
@@ -2,6 +2,6 @@ Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
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
- 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.
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
6
 
7
- 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`.
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
  '
@@ -115,12 +115,14 @@ type Ticket = {
115
115
  body: string;
116
116
  state: string;
117
117
  blockedBy: Array<{ id: string; title: string; state: string }>;
118
+ triageReady: boolean;
118
119
  };
119
120
  type Scope = {
120
121
  id: string;
121
122
  title: string;
122
123
  branch: string;
123
124
  kind: "standalone" | "spec";
125
+ triageReady?: boolean;
124
126
  body?: string;
125
127
  tickets?: Ticket[];
126
128
  completedTicketIds?: string[];
@@ -182,13 +184,14 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
182
184
  copyToWorktree: [".shipyard/setup.sh"],
183
185
  });
184
186
  try {
185
- await sandbox.run({
186
- name: `triage #${ticket.id}`,
187
- maxIterations: 1,
188
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
189
- promptFile: "./.shipyard/triage-prompt.md",
190
- promptArgs: { TASK_ID: ticket.id, BASE_BRANCH: targetBranch },
191
- });
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
+ });
192
195
  verifyTriage(ticket.id);
193
196
  const implementation = await sandbox.run({
194
197
  name: "implementer",
@@ -467,13 +470,14 @@ for (let iteration = 0; iteration < 10; iteration++) {
467
470
  let handoffEvidence: string;
468
471
  try {
469
472
  if (scope.kind === "standalone") {
470
- await integration.run({
471
- name: `triage #${id}`,
472
- maxIterations: 1,
473
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
474
- promptFile: "./.shipyard/triage-prompt.md",
475
- promptArgs: { TASK_ID: id, BASE_BRANCH: targetBranch },
476
- });
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
+ });
477
481
  verifyTriage(id);
478
482
  const standalone = await integration.run({
479
483
  name: "implementer",
@@ -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;
@@ -2,6 +2,6 @@ Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
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
- 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.
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
6
 
7
- 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`.
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
  '
@@ -139,6 +139,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
139
139
  title: string;
140
140
  branch: string;
141
141
  kind: "standalone" | "spec";
142
+ triageReady?: boolean;
142
143
  body?: string;
143
144
  tickets?: Array<{
144
145
  id: string;
@@ -146,6 +147,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
146
147
  body: string;
147
148
  state: string;
148
149
  blockedBy: Array<{ id: string; title: string; state: string }>;
150
+ triageReady: boolean;
149
151
  }>;
150
152
  completedTicketIds?: string[];
151
153
  outstandingTicketIds?: string[];
@@ -193,13 +195,19 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
193
195
  for (const ticketId of issue.kind === "spec"
194
196
  ? (issue.tickets ?? []).map((ticket) => ticket.id)
195
197
  : [issue.id]) {
196
- await sandbox.run({
197
- name: `triage #${ticketId}`,
198
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
199
- maxIterations: 1,
200
- promptFile: "./.shipyard/triage-prompt.md",
201
- promptArgs: { TASK_ID: ticketId, BASE_BRANCH: targetBranch },
202
- });
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
+ });
203
211
  verifyTriage(ticketId);
204
212
  }
205
213
  const implement = await sandbox.run({
@@ -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;
@@ -2,6 +2,6 @@ Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
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
- 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.
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
6
 
7
- 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`.
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
  '
@@ -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;
@@ -2,6 +2,6 @@ Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
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
- 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.
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
6
 
7
- 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`.
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
  '
@@ -136,6 +136,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
136
136
  title: string;
137
137
  branch: string;
138
138
  kind: "standalone" | "spec";
139
+ triageReady?: boolean;
139
140
  body?: string;
140
141
  tickets?: Array<{
141
142
  id: string;
@@ -143,6 +144,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
143
144
  body: string;
144
145
  state: string;
145
146
  blockedBy: Array<{ id: string; title: string; state: string }>;
147
+ triageReady: boolean;
146
148
  }>;
147
149
  completedTicketIds?: string[];
148
150
  outstandingTicketIds?: string[];
@@ -190,13 +192,19 @@ for (let iteration = 0; iteration < 3; iteration++) {
190
192
  for (const ticketId of issue.kind === "spec"
191
193
  ? (issue.tickets ?? []).map((ticket) => ticket.id)
192
194
  : [issue.id]) {
193
- await sandbox.run({
194
- name: `triage #${ticketId}`,
195
- agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
196
- maxIterations: 1,
197
- promptFile: "./.shipyard/triage-prompt.md",
198
- promptArgs: { TASK_ID: ticketId, BASE_BRANCH: targetBranch },
199
- });
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
+ });
200
208
  verifyTriage(ticketId);
201
209
  }
202
210
  const result = await sandbox.run({
@@ -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;
@@ -2,6 +2,6 @@ Triage GitHub issue #{{TASK_ID}} before Shipyard implementation.
2
2
 
3
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
- 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.
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
6
 
7
- 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`.
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
  '
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snappedly-tools/shipyard",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Run AI coding agents in isolated sandboxes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",