@snappedly-tools/shipyard 0.5.0 → 0.7.0-staging.36081473933

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 (89) hide show
  1. package/README.md +64 -117
  2. package/dist/MountConfig-BHnKnA4h.d.ts +145 -0
  3. package/dist/{chunk-WAXEMZUV.js → chunk-44I2BL6E.js} +225 -33
  4. package/dist/chunk-44I2BL6E.js.map +1 -0
  5. package/dist/{chunk-56TDSWFU.js → chunk-JI3HDDMS.js} +3 -3
  6. package/dist/{chunk-56TDSWFU.js.map → chunk-JI3HDDMS.js.map} +1 -1
  7. package/dist/{chunk-FDYOTN55.js → chunk-L6PX5QTU.js} +253 -878
  8. package/dist/chunk-L6PX5QTU.js.map +1 -0
  9. package/dist/index.d.ts +117 -97
  10. package/dist/index.js +349 -353
  11. package/dist/index.js.map +1 -1
  12. package/dist/main.js +285 -153
  13. package/dist/main.js.map +1 -1
  14. package/dist/sandboxes/docker.d.ts +1 -3
  15. package/dist/sandboxes/docker.js +2 -4
  16. package/dist/templates/parallel-planner/block-scope.sh +160 -0
  17. package/dist/templates/parallel-planner/conflict-prompt.md +22 -0
  18. package/dist/templates/parallel-planner/handoff.sh +175 -0
  19. package/dist/templates/parallel-planner/implement-prompt.md +10 -56
  20. package/dist/templates/parallel-planner/main.mts +493 -189
  21. package/dist/templates/parallel-planner/merge-prompt.md +9 -21
  22. package/dist/templates/parallel-planner/plan-prompt.md +5 -31
  23. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  24. package/dist/templates/parallel-planner/select-issues.mjs +788 -0
  25. package/dist/templates/parallel-planner/setup.sh +51 -0
  26. package/dist/templates/parallel-planner/spec-wave-prompt.md +16 -0
  27. package/dist/templates/parallel-planner/template.json +1 -1
  28. package/dist/templates/parallel-planner/triage-prompt.md +5 -0
  29. package/dist/templates/parallel-planner/verify-triage.sh +19 -0
  30. package/dist/templates/parallel-planner-with-review/block-scope.sh +160 -0
  31. package/dist/templates/parallel-planner-with-review/conflict-prompt.md +22 -0
  32. package/dist/templates/parallel-planner-with-review/handoff.sh +175 -0
  33. package/dist/templates/parallel-planner-with-review/implement-prompt.md +10 -56
  34. package/dist/templates/parallel-planner-with-review/main.mts +533 -207
  35. package/dist/templates/parallel-planner-with-review/merge-prompt.md +9 -21
  36. package/dist/templates/parallel-planner-with-review/plan-prompt.md +5 -31
  37. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  38. package/dist/templates/parallel-planner-with-review/review-prompt.md +11 -49
  39. package/dist/templates/parallel-planner-with-review/select-issues.mjs +788 -0
  40. package/dist/templates/parallel-planner-with-review/setup.sh +51 -0
  41. package/dist/templates/parallel-planner-with-review/spec-wave-prompt.md +16 -0
  42. package/dist/templates/parallel-planner-with-review/template.json +1 -1
  43. package/dist/templates/parallel-planner-with-review/triage-prompt.md +5 -0
  44. package/dist/templates/parallel-planner-with-review/verify-triage.sh +19 -0
  45. package/dist/templates/sequential-reviewer/block-scope.sh +160 -0
  46. package/dist/templates/sequential-reviewer/handoff.sh +175 -0
  47. package/dist/templates/sequential-reviewer/implement-prompt.md +12 -46
  48. package/dist/templates/sequential-reviewer/main.mts +248 -103
  49. package/dist/templates/sequential-reviewer/review-prompt.md +11 -49
  50. package/dist/templates/sequential-reviewer/select-issues.mjs +788 -0
  51. package/dist/templates/sequential-reviewer/setup.sh +51 -0
  52. package/dist/templates/sequential-reviewer/template.json +1 -1
  53. package/dist/templates/sequential-reviewer/triage-prompt.md +5 -0
  54. package/dist/templates/sequential-reviewer/verify-triage.sh +19 -0
  55. package/dist/templates/shared/block-scope.sh +160 -0
  56. package/dist/templates/shared/handoff.sh +175 -0
  57. package/dist/templates/shared/select-issues.mjs +788 -0
  58. package/dist/templates/shared/setup.sh +51 -0
  59. package/dist/templates/shared/triage-prompt.md +5 -0
  60. package/dist/templates/shared/verify-triage.sh +19 -0
  61. package/dist/templates/simple-loop/block-scope.sh +160 -0
  62. package/dist/templates/simple-loop/handoff.sh +175 -0
  63. package/dist/templates/simple-loop/main.mts +239 -45
  64. package/dist/templates/simple-loop/prompt.md +12 -46
  65. package/dist/templates/simple-loop/select-issues.mjs +788 -0
  66. package/dist/templates/simple-loop/setup.sh +51 -0
  67. package/dist/templates/simple-loop/template.json +1 -1
  68. package/dist/templates/simple-loop/triage-prompt.md +5 -0
  69. package/dist/templates/simple-loop/verify-triage.sh +19 -0
  70. package/package.json +1 -17
  71. package/dist/MountConfig-bZoCs4Dd.d.ts +0 -26
  72. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  73. package/dist/chunk-ACD46ZM4.js +0 -136
  74. package/dist/chunk-ACD46ZM4.js.map +0 -1
  75. package/dist/chunk-FDYOTN55.js.map +0 -1
  76. package/dist/chunk-KMGNFXKN.js +0 -38
  77. package/dist/chunk-KMGNFXKN.js.map +0 -1
  78. package/dist/chunk-SOJTAJTF.js +0 -78
  79. package/dist/chunk-SOJTAJTF.js.map +0 -1
  80. package/dist/chunk-WAXEMZUV.js.map +0 -1
  81. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  82. package/dist/sandboxes/no-sandbox.js +0 -4
  83. package/dist/sandboxes/no-sandbox.js.map +0 -1
  84. package/dist/sandboxes/vercel.d.ts +0 -104
  85. package/dist/sandboxes/vercel.js +0 -166
  86. package/dist/sandboxes/vercel.js.map +0 -1
  87. package/dist/templates/blank/main.mts +0 -13
  88. package/dist/templates/blank/prompt.md +0 -12
  89. package/dist/templates/blank/template.json +0 -4
@@ -1,51 +1,245 @@
1
- import { CODEX_MODELS, run, codex } from "@snappedly-tools/shipyard";
1
+ // Simple loop: select one activated issue scope per iteration, implement it,
2
+ // then hand its verified commit to a human through a pull request.
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import * as shipyard from "@snappedly-tools/shipyard";
2
6
  import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
3
7
 
4
- // Simple loop: an agent that picks open issues one by one and closes them.
5
- // Generated entrypoint: .shipyard/main.mts
6
- // Run this with: npx shipyard run
7
- // Or add to package.json scripts: "shipyard": "shipyard run"
8
-
9
- await run({
10
- // A name for this run, shown as a prefix in log output.
11
- name: "worker",
12
-
13
- // Sandbox provider — runs the agent inside an isolated container.
14
- sandbox: docker(),
15
-
16
- // The agent provider. The routine configured Codex model runs at max
17
- // reasoning by default. Switch to CODEX_MODELS.strong for more demanding
18
- // planning or review work.
19
- agent: codex(CODEX_MODELS.routine),
20
-
21
- // Path to the prompt file. Shell expressions inside are evaluated inside the
22
- // sandbox at the start of each iteration, so the agent always sees fresh data.
23
- promptFile: "./.shipyard/prompt.md",
8
+ if (process.loadEnvFile && existsSync(".shipyard/.env"))
9
+ process.loadEnvFile(".shipyard/.env");
10
+ type ModelRole = "routine" | "strong";
11
+ const CODEX_PROVIDER = true;
12
+ const agentFactory = shipyard.codex;
13
+ type AgentModel = Parameters<typeof agentFactory>[0];
14
+ const readRoleModel = (role: ModelRole): string | undefined => {
15
+ const envName = `SHIPYARD_${role.toUpperCase()}_MODEL`;
16
+ const model = process.env[envName];
17
+ if (model !== undefined && model.trim().length === 0)
18
+ throw new Error(`${envName} must not be empty`);
19
+ return model;
20
+ };
21
+ const roleModels = {
22
+ routine: readRoleModel("routine"),
23
+ strong: readRoleModel("strong"),
24
+ };
25
+ const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
26
+ type CodexReasoningEffort = shipyard.CodexReasoningEffort;
27
+ const readCodexReasoningEffort = (
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();
33
+ if (!effort) return undefined;
34
+ if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
35
+ throw new Error(
36
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
37
+ );
38
+ return effort as CodexReasoningEffort;
39
+ };
40
+ const roleEfforts = {
41
+ routine: readCodexReasoningEffort("routine"),
42
+ strong: readCodexReasoningEffort("strong"),
43
+ };
44
+ const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
45
+ if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
46
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`;
47
+ const model = process.env[envName]?.trim();
48
+ return model ? { ...defaultModel, model } : defaultModel;
49
+ };
50
+ const roleAgent = (role: ModelRole, defaultModel: AgentModel) => {
51
+ const model = roleModels[role] ?? readCodexRoleModel(role, defaultModel);
52
+ const effort = roleEfforts[role];
53
+ if (typeof model !== "string")
54
+ return effort === undefined
55
+ ? agentFactory(model)
56
+ : agentFactory(model, { effort });
57
+ return agentFactory(model, { effort: effort ?? null });
58
+ };
59
+ const targetBranch = execFileSync("git", ["branch", "--show-current"], {
60
+ encoding: "utf8",
61
+ }).trim();
62
+ const repository = execFileSync(
63
+ "gh",
64
+ ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
65
+ { encoding: "utf8" },
66
+ ).trim();
67
+ if (
68
+ !/^[A-Za-z0-9._/-]+$/.test(targetBranch) ||
69
+ !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)
70
+ ) {
71
+ throw new Error("Invalid target branch or GitHub repository");
72
+ }
73
+ process.env.GH_REPO = repository;
74
+ const hooks = {
75
+ sandbox: {
76
+ onSandboxReady: [
77
+ { command: "timeout 300 bash .shipyard/setup.sh", timeoutMs: 300_000 },
78
+ ],
79
+ },
80
+ };
81
+ const closeClean = async (sandbox: {
82
+ close: () => Promise<{ preservedWorktreePath?: string }>;
83
+ }) => {
84
+ const { preservedWorktreePath } = await sandbox.close();
85
+ if (preservedWorktreePath)
86
+ throw new Error(`Sandbox has uncommitted work at ${preservedWorktreePath}`);
87
+ };
88
+ const verifyTriage = (ticketId: string) => {
89
+ try {
90
+ execFileSync("bash", [".shipyard/verify-triage.sh", ticketId, repository], {
91
+ encoding: "utf8",
92
+ });
93
+ } catch (error) {
94
+ const detail = (error as { stderr?: string | Buffer }).stderr
95
+ ?.toString()
96
+ .trim();
97
+ throw new Error(detail || `Could not verify triage for #${ticketId}`);
98
+ }
99
+ };
24
100
 
25
- // Maximum number of iterations (agent invocations) to run in a session.
26
- // Each iteration works on a single issue. Increase this to process more issues
27
- // per run, or set it to 1 for a single-shot mode.
28
- maxIterations: 3,
101
+ const blockScope = (
102
+ scope: { id: string; branch: string; tickets?: Array<{ id: string }> },
103
+ error: unknown,
104
+ ) => {
105
+ const reason = (error instanceof Error ? error.message : String(error)).slice(
106
+ 0,
107
+ 3000,
108
+ );
109
+ execFileSync(
110
+ "bash",
111
+ [
112
+ ".shipyard/block-scope.sh",
113
+ scope.id,
114
+ scope.id,
115
+ repository,
116
+ [scope.id, ...(scope.tickets ?? []).map((ticket) => ticket.id)].join(","),
117
+ scope.branch,
118
+ ],
119
+ { input: reason, encoding: "utf8" },
120
+ );
121
+ console.error(`Shipyard blocked issue #${scope.id}: ${reason}`);
122
+ };
29
123
 
30
- // Branch strategy — merge-to-head creates a temporary branch for the agent
31
- // to work on, then merges the result back to HEAD when the run completes.
32
- // This is required when using copyToWorktree, since head mode bind-mounts
33
- // the host directory directly (no worktree to copy into).
34
- branchStrategy: { type: "merge-to-head" },
124
+ for (let iteration = 0; iteration < 3; iteration++) {
125
+ const issues = JSON.parse(
126
+ execFileSync("node", [".shipyard/select-issues.mjs"], { encoding: "utf8" }),
127
+ ) as Array<{
128
+ id: string;
129
+ title: string;
130
+ branch: string;
131
+ kind: "standalone" | "spec";
132
+ body?: string;
133
+ tickets?: Array<{
134
+ id: string;
135
+ title: string;
136
+ body: string;
137
+ state: string;
138
+ blockedBy: Array<{ id: string; title: string; state: string }>;
139
+ }>;
140
+ completedTicketIds?: string[];
141
+ outstandingTicketIds?: string[];
142
+ }>;
143
+ const issue = issues[0];
144
+ if (!issue) break;
35
145
 
36
- // Copy node_modules from the host into the worktree before the sandbox
37
- // starts. This avoids a full npm install from scratch on every iteration.
38
- // The onSandboxReady hook still runs npm install as a safety net to handle
39
- // platform-specific binaries and any packages added since the last copy.
40
- copyToWorktree: ["node_modules"],
146
+ let handedOff = false;
147
+ let publicationUncertain = false;
148
+ try {
149
+ execFileSync("gh", [
150
+ "label",
151
+ "create",
152
+ "shipyard:pending",
153
+ "--repo",
154
+ repository,
155
+ "--color",
156
+ "1D76DB",
157
+ "--description",
158
+ "Shipyard is working on this ticket",
159
+ "--force",
160
+ ]);
161
+ for (const ticketId of issue.kind === "spec"
162
+ ? (issue.tickets ?? []).map((ticket) => ticket.id)
163
+ : [issue.id])
164
+ execFileSync("gh", [
165
+ "issue",
166
+ "edit",
167
+ ticketId,
168
+ "--repo",
169
+ repository,
170
+ "--add-label",
171
+ "shipyard:pending",
172
+ ]);
173
+ const sandbox = await shipyard.createSandbox({
174
+ branch: issue.branch,
175
+ sandbox: docker(),
176
+ hooks,
177
+ });
178
+ let evidence: string;
179
+ try {
180
+ for (const ticketId of issue.kind === "spec"
181
+ ? (issue.tickets ?? []).map((ticket) => ticket.id)
182
+ : [issue.id]) {
183
+ await sandbox.run({
184
+ name: `triage #${ticketId}`,
185
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
186
+ maxIterations: 1,
187
+ promptFile: "./.shipyard/triage-prompt.md",
188
+ promptArgs: { TASK_ID: ticketId },
189
+ });
190
+ verifyTriage(ticketId);
191
+ }
192
+ const result = await sandbox.run({
193
+ name: "implementer",
194
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
195
+ maxIterations: 1,
196
+ promptFile: "./.shipyard/prompt.md",
197
+ promptArgs: {
198
+ TASK_ID: issue.id,
199
+ ISSUE_TITLE: issue.title,
200
+ BRANCH: issue.branch,
201
+ SCOPE: JSON.stringify(issue),
202
+ SKILL: issue.kind === "spec" ? "/implement-spec" : "/implement",
203
+ },
204
+ });
205
+ const packet = [
206
+ ...result.stdout.matchAll(/<handoff>([\s\S]*?)<\/handoff>/g),
207
+ ]
208
+ .at(-1)?.[1]
209
+ ?.trim();
210
+ if (!result.completionSignal || !packet)
211
+ throw new Error(
212
+ `Issue #${issue.id} has no verified completion evidence: ${result.stdout.trim().slice(-1200)}`,
213
+ );
214
+ evidence = packet;
215
+ } finally {
216
+ await closeClean(sandbox);
217
+ }
41
218
 
42
- // Lifecycle hooks — commands grouped by where they run (host or sandbox).
43
- hooks: {
44
- sandbox: {
45
- // onSandboxReady runs once after the sandbox is initialised and the repo is
46
- // synced in, before the agent starts. Use it to install dependencies or run
47
- // any other setup steps your project needs.
48
- onSandboxReady: [{ command: "npm install" }],
49
- },
50
- },
51
- });
219
+ // Sync-out rewrites sandbox commits on the host. Publish from the synced
220
+ // branch so a later invocation can fast-forward the same PR.
221
+ const publication = await shipyard.createSandbox({
222
+ branch: issue.branch,
223
+ sandbox: docker(),
224
+ });
225
+ try {
226
+ publicationUncertain = true;
227
+ const handoff = await publication.exec(
228
+ `bash .shipyard/handoff.sh ${issue.id} ${issue.branch} ${targetBranch} ${repository} ${[issue.id, ...(issue.tickets ?? []).map((ticket) => ticket.id)].join(",")} ${issue.outstandingTicketIds?.join(",") || "-"} ${issue.completedTicketIds?.join(",") || "-"}`,
229
+ { stdin: evidence },
230
+ );
231
+ publicationUncertain = handoff.exitCode === 75;
232
+ if (handoff.exitCode !== 0)
233
+ throw new Error(
234
+ `PR handoff for #${issue.id} failed: ${handoff.stderr || handoff.stdout}`,
235
+ );
236
+ console.log(handoff.stdout.trim());
237
+ handedOff = true;
238
+ } finally {
239
+ await publication.close();
240
+ }
241
+ } catch (error) {
242
+ if (handedOff || publicationUncertain) throw error;
243
+ blockScope(issue, error);
244
+ }
245
+ }
@@ -1,53 +1,19 @@
1
- # Context
1
+ # Assigned issue scope
2
2
 
3
- ## Open issues
3
+ Implement scope #{{TASK_ID}} ({{ISSUE_TITLE}}) on branch `{{BRANCH}}`.
4
+ Read it with `gh issue view {{TASK_ID}} --comments`, including acceptance criteria, and read `AGENTS.md`, `CONTEXT.md`, `docs/agents/workflow.md`, and relevant ADRs. The selector resolved the selected tickets and their dependencies below.
4
5
 
5
- !`{{LIST_TASKS_COMMAND}}`
6
+ Follow {{SKILL}} for this scope. For a planning spec, deliver only tickets in `SCOPE.tickets` in dependency order on this one branch. Other linked tickets are context; do not implement them. Integrate, clean up, and review the selected work before handoff. Child workers may follow `/implement` under `/implement-spec` coordination. Do not create separate child PRs. Use `/tdd` for changed logic, then `/code-cleanup` and `/code-review` as the skill and repository policy require. Run candidate dependency installation again after changing manifests: `bash .shipyard/setup.sh`. Complete the required checks, resolve review findings, and commit all task work. Do not close the issue, merge into the target branch, or publish the PR; the surrounding workflow handles handoff.
6
7
 
7
- The list above has already been filtered to issues ready for work and is the sole source of truth for what work exists. Do not run your own unfiltered query to find more issues — if the list is empty, there is nothing to do.
8
+ If requirements, checks, or review are unresolved, describe the blocker and stop without the completion marker. Only after the verified commit, finish with a concise evidence packet containing changed scope, `Checks: <commands and results>`, `Review: APPROVED`, and limitations:
8
9
 
9
- ## Recent RALPH commits (last 10)
10
-
11
- !`git log --oneline --grep="RALPH" -10`
12
-
13
- # Task
14
-
15
- You are RALPH — an autonomous coding agent working through issues one at a time.
16
-
17
- ## Priority order
18
-
19
- Work on issues in this order:
20
-
21
- 1. **Bug fixes** — broken behaviour affecting users
22
- 2. **Tracer bullets** — thin end-to-end slices that prove an approach works
23
- 3. **Polish** — improving existing functionality (error messages, UX, docs)
24
- 4. **Refactors** — internal cleanups with no user-visible change
25
-
26
- Pick the highest-priority open issue that is not blocked by another open issue.
27
-
28
- ## Workflow
29
-
30
- 1. **Explore** — read the issue carefully. Pull in the parent PRD if referenced. Read the relevant source files and tests before writing any code.
31
- 2. **Plan** — decide what to change and why. Keep the change as small as possible.
32
- 3. **Execute** — use RGR (Red → Green → Repeat → Refactor): write a failing test first, then write the implementation to pass it.
33
- 4. **Verify** — read the repository's configured feedback-loop contract and run every applicable check for this change. Use the configured static check and focused behavior tests when they exist; include formatting, build, or broader checks when the contract or change requires them. Fix failures before proceeding.
34
- 5. **Commit** — make a single git commit. The message MUST:
35
- - Start with `RALPH:` prefix
36
- - Include the task completed and any PRD reference
37
- - List key decisions made
38
- - List files changed
39
- - Note any blockers for the next iteration
40
- 6. **Close** — close the issue with `{{CLOSE_TASK_COMMAND}}` explaining what was done.
41
-
42
- ## Rules
43
-
44
- - Work on **one issue per iteration**. Do not attempt multiple issues in a single iteration.
45
- - Do not close an issue until you have committed the fix and verified tests pass.
46
- - Do not leave commented-out code or TODO comments in committed code.
47
- - If you are blocked (missing context, failing tests you cannot fix, external dependency), leave a comment on the issue and move on — do not close it.
10
+ <handoff>...</handoff>
11
+ <promise>COMPLETE</promise>
48
12
 
49
- # Done
13
+ ## Resolved scope
50
14
 
51
- When all actionable issues are complete (or you are blocked on all remaining ones), or the open-issues block at the top of this prompt is empty, output the completion signal:
15
+ ```json
16
+ {{SCOPE}}
17
+ ```
52
18
 
53
- <promise>COMPLETE</promise>
19
+ Read the complete parent and child issue bodies and comments with `gh issue view`, including repository guidance and dependency links.