@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.1

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 (147) hide show
  1. package/LICENSE.md +55 -0
  2. package/README.md +166 -0
  3. package/dist/branding.d.ts +25 -0
  4. package/dist/branding.js +27 -0
  5. package/dist/claudeCompat.d.ts +78 -0
  6. package/dist/claudeCompat.js +198 -0
  7. package/dist/cli.d.ts +55 -0
  8. package/dist/cli.js +326 -0
  9. package/dist/config.d.ts +19 -0
  10. package/dist/config.js +28 -0
  11. package/dist/credentials.d.ts +41 -0
  12. package/dist/credentials.js +75 -0
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +94 -0
  16. package/dist/doctor.js +343 -0
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +188 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +74 -0
  25. package/dist/extension/branding.d.ts +48 -0
  26. package/dist/extension/branding.js +119 -0
  27. package/dist/extension/chipEditor.d.ts +117 -0
  28. package/dist/extension/chipEditor.js +369 -0
  29. package/dist/extension/config.d.ts +112 -0
  30. package/dist/extension/config.js +108 -0
  31. package/dist/extension/costHud.d.ts +73 -0
  32. package/dist/extension/costHud.js +123 -0
  33. package/dist/extension/decisionCapture.d.ts +52 -0
  34. package/dist/extension/decisionCapture.js +66 -0
  35. package/dist/extension/decisions.d.ts +83 -0
  36. package/dist/extension/decisions.js +200 -0
  37. package/dist/extension/diagnostics.d.ts +41 -0
  38. package/dist/extension/diagnostics.js +110 -0
  39. package/dist/extension/index.d.ts +116 -0
  40. package/dist/extension/index.js +388 -0
  41. package/dist/extension/initDone.d.ts +28 -0
  42. package/dist/extension/initDone.js +66 -0
  43. package/dist/extension/initPass.d.ts +152 -0
  44. package/dist/extension/initPass.js +394 -0
  45. package/dist/extension/nextWorkTool.d.ts +51 -0
  46. package/dist/extension/nextWorkTool.js +80 -0
  47. package/dist/extension/permission.d.ts +91 -0
  48. package/dist/extension/permission.js +236 -0
  49. package/dist/extension/pipeline/activity.d.ts +37 -0
  50. package/dist/extension/pipeline/activity.js +151 -0
  51. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  52. package/dist/extension/pipeline/activityFeed.js +175 -0
  53. package/dist/extension/pipeline/budget.d.ts +48 -0
  54. package/dist/extension/pipeline/budget.js +68 -0
  55. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  56. package/dist/extension/pipeline/checkpoint.js +176 -0
  57. package/dist/extension/pipeline/eval.d.ts +168 -0
  58. package/dist/extension/pipeline/eval.js +182 -0
  59. package/dist/extension/pipeline/events.d.ts +56 -0
  60. package/dist/extension/pipeline/events.js +147 -0
  61. package/dist/extension/pipeline/findings.d.ts +42 -0
  62. package/dist/extension/pipeline/findings.js +144 -0
  63. package/dist/extension/pipeline/finish.d.ts +128 -0
  64. package/dist/extension/pipeline/finish.js +307 -0
  65. package/dist/extension/pipeline/goCommand.d.ts +128 -0
  66. package/dist/extension/pipeline/goCommand.js +972 -0
  67. package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
  68. package/dist/extension/pipeline/goCompareCommand.js +184 -0
  69. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  70. package/dist/extension/pipeline/goFlags.js +46 -0
  71. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  72. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  73. package/dist/extension/pipeline/invocation.d.ts +45 -0
  74. package/dist/extension/pipeline/invocation.js +64 -0
  75. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  76. package/dist/extension/pipeline/orchestrator.js +636 -0
  77. package/dist/extension/pipeline/personas.d.ts +44 -0
  78. package/dist/extension/pipeline/personas.js +248 -0
  79. package/dist/extension/pipeline/resilience.d.ts +85 -0
  80. package/dist/extension/pipeline/resilience.js +166 -0
  81. package/dist/extension/pipeline/resume.d.ts +18 -0
  82. package/dist/extension/pipeline/resume.js +106 -0
  83. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  84. package/dist/extension/pipeline/runRegistry.js +202 -0
  85. package/dist/extension/pipeline/runSession.d.ts +152 -0
  86. package/dist/extension/pipeline/runSession.js +167 -0
  87. package/dist/extension/pipeline/runState.d.ts +158 -0
  88. package/dist/extension/pipeline/runState.js +264 -0
  89. package/dist/extension/pipeline/runner.d.ts +79 -0
  90. package/dist/extension/pipeline/runner.js +298 -0
  91. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  92. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  93. package/dist/extension/pipeline/stages.d.ts +51 -0
  94. package/dist/extension/pipeline/stages.js +113 -0
  95. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  96. package/dist/extension/pipeline/ticketResolution.js +75 -0
  97. package/dist/extension/pipeline/types.d.ts +436 -0
  98. package/dist/extension/pipeline/types.js +48 -0
  99. package/dist/extension/pipeline/verify.d.ts +277 -0
  100. package/dist/extension/pipeline/verify.js +758 -0
  101. package/dist/extension/pipeline/workspace.d.ts +32 -0
  102. package/dist/extension/pipeline/workspace.js +73 -0
  103. package/dist/extension/pipeline/worktree.d.ts +107 -0
  104. package/dist/extension/pipeline/worktree.js +200 -0
  105. package/dist/extension/provider.d.ts +11 -0
  106. package/dist/extension/provider.js +26 -0
  107. package/dist/extension/recall.d.ts +93 -0
  108. package/dist/extension/recall.js +187 -0
  109. package/dist/extension/recordContextTool.d.ts +38 -0
  110. package/dist/extension/recordContextTool.js +85 -0
  111. package/dist/extension/recordDecisionTool.d.ts +52 -0
  112. package/dist/extension/recordDecisionTool.js +102 -0
  113. package/dist/extension/repoDocs.d.ts +81 -0
  114. package/dist/extension/repoDocs.js +260 -0
  115. package/dist/extension/resilientFetch.d.ts +60 -0
  116. package/dist/extension/resilientFetch.js +133 -0
  117. package/dist/extension/reviewTool.d.ts +34 -0
  118. package/dist/extension/reviewTool.js +81 -0
  119. package/dist/extension/spool.d.ts +92 -0
  120. package/dist/extension/spool.js +266 -0
  121. package/dist/extension/stateHome.d.ts +2 -0
  122. package/dist/extension/stateHome.js +6 -0
  123. package/dist/extension/subagents.d.ts +135 -0
  124. package/dist/extension/subagents.js +281 -0
  125. package/dist/extension/surface.d.ts +10 -0
  126. package/dist/extension/surface.js +12 -0
  127. package/dist/extension/todos.d.ts +110 -0
  128. package/dist/extension/todos.js +217 -0
  129. package/dist/extension/tokenProvider.d.ts +93 -0
  130. package/dist/extension/tokenProvider.js +234 -0
  131. package/dist/launch.d.ts +76 -0
  132. package/dist/launch.js +111 -0
  133. package/dist/login.d.ts +45 -0
  134. package/dist/login.js +142 -0
  135. package/dist/logout.d.ts +14 -0
  136. package/dist/logout.js +34 -0
  137. package/dist/paths.d.ts +31 -0
  138. package/dist/paths.js +87 -0
  139. package/dist/piPackage.d.ts +33 -0
  140. package/dist/piPackage.js +71 -0
  141. package/dist/profiles.d.ts +80 -0
  142. package/dist/profiles.js +222 -0
  143. package/dist/refresh.d.ts +70 -0
  144. package/dist/refresh.js +117 -0
  145. package/dist/upgrade.d.ts +97 -0
  146. package/dist/upgrade.js +284 -0
  147. package/package.json +39 -3
@@ -0,0 +1,307 @@
1
+ /**
2
+ * The FINISH stage (spec §3c) — the deterministic commit / push / PR step that
3
+ * runs ONLY when the review loop stops `clean`.
4
+ *
5
+ * What it does:
6
+ * - Commit the run's work: in worktree mode `git add -A` + commit on the run
7
+ * branch (the tree started clean, so everything is the run's own). In --here
8
+ * mode it commits ONLY when the pre-run baseline was verifiably clean;
9
+ * otherwise it skips with an honest note (never mixing user dirt into an
10
+ * agent commit).
11
+ * - Commit message: `<type>(<scope>): <title>` + a 2-3 sentence plan summary +
12
+ * the exact provenance trailer (YAGNI-Run / YAGNI-Ticket / YAGNI-Verify /
13
+ * YAGNI-Decisions / YAGNI-Rounds / Co-Authored-By: YAGNI Code).
14
+ * - Opt-in `--pr`: `git push -u origin <branch>` + `gh pr create` with the
15
+ * USER'S own gh auth. Any push/PR failure is a fail-soft note ("committed
16
+ * locally; …"); the run stays successful.
17
+ *
18
+ * IRON INVARIANT (§0.1): a FINISH failure NEVER flips a clean run to failed and
19
+ * NEVER fakes a commit — {@link runFinish} never throws, `committed` is only
20
+ * true when `git rev-parse HEAD` confirmed the new commit, and every skip or
21
+ * failure lands as an honest note the /go handoff surfaces verbatim.
22
+ *
23
+ * All message/trailer/scope derivation is PURE (unit-tested directly); the one
24
+ * impure seam is the injectable exec. Unlike verify's scrubbed env, the exec
25
+ * here inherits the user's FULL environment on purpose: `git push` needs the
26
+ * ssh-agent and `gh` needs the user's own auth — these are OUR commands, not
27
+ * repo-defined ones, so invariant §0.9 (repo commands never see YAGNI_* vars)
28
+ * does not apply.
29
+ */
30
+ import { execFile } from "node:child_process";
31
+ import { scrubSecrets } from "./scrubSecrets.js";
32
+ import { parseChangedPaths } from "./verify.js";
33
+ /** Cap on the commit subject's title half (keeps `git log --oneline` readable). */
34
+ const TITLE_MAX = 72;
35
+ /** Cap on the plan-summary body paragraph. */
36
+ const SUMMARY_MAX = 400;
37
+ /** Cap on a command-output tail quoted into an honest note. */
38
+ const NOTE_TAIL_MAX = 200;
39
+ /**
40
+ * Full-env exec (deliberately NOT {@link buildVerifyEnv}-scrubbed — see the
41
+ * module docblock): resolves `{code, output}` for a real exit, rejects only on
42
+ * a spawn-level error (ENOENT / abort), which callers catch into a note.
43
+ */
44
+ const defaultExec = (argv, cwd, signal) => new Promise((resolve, reject) => {
45
+ execFile(argv[0], argv.slice(1), { cwd, signal, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
46
+ const output = `${stdout ?? ""}${stderr ?? ""}`;
47
+ if (err) {
48
+ const code = err.code;
49
+ if (typeof code === "number")
50
+ return resolve({ code, output });
51
+ return reject(err);
52
+ }
53
+ resolve({ code: 0, output });
54
+ });
55
+ });
56
+ // ---------------------------------------------------------------------------
57
+ // Pure message derivation
58
+ // ---------------------------------------------------------------------------
59
+ /** `fix` when the ticket brief (or, blind, the raw arg) says bug; else `feat` (spec §3c). */
60
+ export function commitType(ticket, ticketBrief) {
61
+ return /\bbug/i.test(ticketBrief ?? ticket) ? "fix" : "feat";
62
+ }
63
+ /**
64
+ * The commit subject's title half: the brief's first non-empty line (its
65
+ * verbatim ticket title) with a leading `<ticket>:` echo stripped (the trailer
66
+ * already carries the identifier), falling back to the raw arg. Capped.
67
+ */
68
+ export function commitTitle(ticket, ticketBrief) {
69
+ let title = ticketBrief?.split("\n").find((l) => l.trim().length > 0)?.trim() ?? ticket;
70
+ const echo = `${ticket.toLowerCase()}:`;
71
+ if (title.toLowerCase().startsWith(echo))
72
+ title = title.slice(echo.length).trim() || ticket;
73
+ return title.length > TITLE_MAX ? title.slice(0, TITLE_MAX).trimEnd() : title;
74
+ }
75
+ /**
76
+ * A conventional-commit scope from the run's changed paths: the package name
77
+ * when every changed path agrees on one (`packages/<name>/…`, `apps/…`,
78
+ * `libs/…`), else the shared top-level dir. Ambiguous or root-level changes
79
+ * yield no scope (the subject is `<type>: <title>`), never a guess.
80
+ */
81
+ export function deriveCommitScope(paths) {
82
+ const candidates = new Set();
83
+ for (const p of paths) {
84
+ const segs = p.split("/").filter(Boolean);
85
+ if (segs.length < 2)
86
+ return undefined; // a root-level file has no scope
87
+ if (segs.length >= 3 && ["packages", "apps", "libs"].includes(segs[0]))
88
+ candidates.add(segs[1]);
89
+ else
90
+ candidates.add(segs[0]);
91
+ }
92
+ if (candidates.size !== 1)
93
+ return undefined;
94
+ return [...candidates][0];
95
+ }
96
+ /** `<type>(<scope>): <title>`, dropping the parens when no scope was derivable. */
97
+ export function commitSubject(type, scope, title) {
98
+ return scope ? `${type}(${scope}): ${title}` : `${type}: ${title}`;
99
+ }
100
+ /**
101
+ * A 2-3 sentence summary of the plan stage's output (markdown flattened, first
102
+ * three sentences, capped). No LLM call — this is a deterministic excerpt. A
103
+ * missing plan (a resumed run skipped the build half) degrades honestly.
104
+ */
105
+ export function summarizePlan(planText, ticket) {
106
+ const flat = (planText ?? "")
107
+ .replace(/^#+\s*/gm, "")
108
+ .replace(/^\s*(?:[-*+]|\d+[.)])\s+/gm, "")
109
+ .replace(/`{1,3}/g, "")
110
+ .replace(/\s+/g, " ")
111
+ .trim();
112
+ if (!flat)
113
+ return `YAGNI Code /go run for ${ticket} (no plan text was available for this pass).`;
114
+ const sentences = flat.match(/[^.!?]+[.!?]+(?:\s+|$)/g);
115
+ let summary = sentences ? sentences.slice(0, 3).join("").trim() : flat;
116
+ if (summary.length > SUMMARY_MAX)
117
+ summary = `${summary.slice(0, SUMMARY_MAX).trimEnd()}…`;
118
+ return summary;
119
+ }
120
+ /**
121
+ * The trailer's YAGNI-Verify value, derived honestly from the pipeline result:
122
+ * `passed (<cmds>)` when the final round's verify gate ran (verify-FAILED can't
123
+ * reach `clean` by construction), else `skipped (<reason>)`.
124
+ */
125
+ export function verifyTrailerValue(result) {
126
+ if (result.verifyCommand)
127
+ return `passed (${oneLine(result.verifyCommand)})`;
128
+ return `skipped (${oneLine(result.verifyNote ?? "verify did not run")})`;
129
+ }
130
+ /** Collapse a value onto one line so it can never break the trailer block. */
131
+ function oneLine(s) {
132
+ return s.replace(/\s*\n\s*/g, " ").trim();
133
+ }
134
+ /** The exact provenance trailer block (spec §3c). */
135
+ export function provenanceTrailer(t) {
136
+ return [
137
+ `YAGNI-Run: ${oneLine(t.runId)}`,
138
+ `YAGNI-Ticket: ${oneLine(t.ticket)}`,
139
+ `YAGNI-Verify: ${t.verify}`,
140
+ `YAGNI-Decisions: ${t.decisionsRecorded}`,
141
+ `YAGNI-Rounds: ${t.rounds}`,
142
+ "Co-Authored-By: YAGNI Code <code@yagni.app>",
143
+ ].join("\n");
144
+ }
145
+ /** Subject + blank line + plan summary + blank line + trailer. */
146
+ export function buildCommitMessage(subject, summary, trailer) {
147
+ return `${subject}\n\n${summary}\n\n${trailer}`;
148
+ }
149
+ /** The PR body: plan summary, any unresolved medium/low findings, the trailer. */
150
+ export function buildPrBody(summary, trailer, remaining = []) {
151
+ const parts = [summary];
152
+ if (remaining.length > 0) {
153
+ const lines = remaining.map((f) => {
154
+ const loc = f.file ? `${f.file}${f.line != null ? `:${f.line}` : ""}` : "(no location)";
155
+ return `- ${f.severity} | ${loc} | ${f.message}`;
156
+ });
157
+ parts.push(`Unresolved non-blocking findings:\n${lines.join("\n")}`);
158
+ }
159
+ parts.push(trailer);
160
+ return parts.join("\n\n");
161
+ }
162
+ /** Pull the PR URL (+ number) out of `gh pr create` output, if present. */
163
+ export function parsePrUrl(output) {
164
+ const m = output.match(/https:\/\/\S+\/pull\/(\d+)/);
165
+ if (!m)
166
+ return {};
167
+ const prNumber = Number(m[1]);
168
+ return { prUrl: m[0], ...(Number.isFinite(prNumber) ? { prNumber } : {}) };
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // The impure runner
172
+ // ---------------------------------------------------------------------------
173
+ /**
174
+ * The last few output lines, flattened, for an honest note. Scrubbed FIRST
175
+ * (invariant §0.9): unlike verify's env-scrubbed commands, git push / gh run
176
+ * with the user's full environment, so their output can echo a credential
177
+ * embedded in a remote URL or a token in a gh error — and every note here is
178
+ * delivered as the durable /go handoff, landing in permanent scrollback.
179
+ */
180
+ function tail(output) {
181
+ const t = scrubSecrets(output).trim().split("\n").slice(-3).join(" ").slice(0, NOTE_TAIL_MAX).trim();
182
+ return t;
183
+ }
184
+ /**
185
+ * Run the FINISH stage. NEVER throws; every failure path returns an honest
186
+ * {@link FinishResult} with notes. See the module docblock for the contract.
187
+ */
188
+ export async function runFinish(opts) {
189
+ const exec = opts.exec ?? defaultExec;
190
+ const notes = [];
191
+ const skipped = (note) => ({
192
+ committed: false,
193
+ ...(opts.branch ? { branch: opts.branch } : {}),
194
+ notes: [...notes, note],
195
+ });
196
+ // A guarded exec: a spawn-level rejection (ENOENT / abort) becomes a non-zero
197
+ // ExecResult carrying the message, so every caller below handles one shape.
198
+ const run = async (argv) => {
199
+ try {
200
+ return await exec(argv, opts.cwd, opts.signal);
201
+ }
202
+ catch (err) {
203
+ return { code: -1, output: err instanceof Error ? err.message : String(err) };
204
+ }
205
+ };
206
+ try {
207
+ // --here commit gate: only a verifiably-clean pre-run baseline may commit
208
+ // (spec §3c — never mix user dirt into an agent commit). Worktree trees
209
+ // started clean, so everything in them is the run's own.
210
+ if (!opts.worktreeMode && opts.hereBaseline !== "clean") {
211
+ return skipped(opts.hereBaseline === "dirty"
212
+ ? "FINISH skipped the commit: the tree already had uncommitted changes before the run, and an agent commit must never mix in your work. Review the diff and commit it yourself."
213
+ : "FINISH skipped the commit: could not verify the pre-run tree was clean, so nothing was committed. Review the diff and commit it yourself.");
214
+ }
215
+ const status = await run(["git", "status", "--porcelain"]);
216
+ if (status.code !== 0) {
217
+ return skipped(`FINISH could not read the git status (${tail(status.output) || "git failed"}); nothing was committed.`);
218
+ }
219
+ if (!status.output.trim()) {
220
+ return skipped("FINISH found nothing to commit (the tree is already clean).");
221
+ }
222
+ // The run's OWN paths = everything changed now, minus what was already dirty
223
+ // at pipeline start (bootstrap lockfile churn, or a pre-existing edit). We
224
+ // commit only these, so the provenance commit never folds in non-run work.
225
+ const changed = parseChangedPaths(status.output);
226
+ const baseline = new Set(opts.baselinePaths ?? []);
227
+ const runOwned = baseline.size === 0 ? changed : changed.filter((p) => !baseline.has(p));
228
+ if (runOwned.length === 0) {
229
+ return skipped("FINISH found nothing of the run's own to commit (the only changes were already present before the run).");
230
+ }
231
+ // Message derivation off the run's real diff.
232
+ const subject = commitSubject(commitType(opts.ticket, opts.ticketBrief), deriveCommitScope(runOwned), commitTitle(opts.ticket, opts.ticketBrief));
233
+ const summary = summarizePlan(opts.planText, opts.ticket);
234
+ const trailer = provenanceTrailer(opts);
235
+ const message = buildCommitMessage(subject, summary, trailer);
236
+ // No baseline dirt ⇒ everything is the run's own: `git add -A` (preserves
237
+ // rename detection). With a baseline, stage only the run-owned paths so
238
+ // bootstrap/pre-existing dirt never lands (`--` guards flag-like paths).
239
+ const add = baseline.size === 0
240
+ ? await run(["git", "add", "-A"])
241
+ : await run(["git", "add", "--", ...runOwned]);
242
+ if (add.code !== 0) {
243
+ return skipped(`FINISH could not stage the changes (git add failed: ${tail(add.output)}); nothing was committed.`);
244
+ }
245
+ let commit = await run(["git", "commit", "-m", message]);
246
+ if (commit.code !== 0) {
247
+ // Most likely a missing git identity in a fresh environment (mirrors
248
+ // commitWip): retry once with an explicit YAGNI Code identity.
249
+ commit = await run([
250
+ "git",
251
+ "-c",
252
+ "user.name=YAGNI Code",
253
+ "-c",
254
+ "user.email=code@yagni.app",
255
+ "commit",
256
+ "-m",
257
+ message,
258
+ ]);
259
+ }
260
+ if (commit.code !== 0) {
261
+ return skipped(`FINISH could not commit (${tail(commit.output)}); the changes are left uncommitted in ${opts.cwd}.`);
262
+ }
263
+ const rev = await run(["git", "rev-parse", "HEAD"]);
264
+ if (rev.code !== 0 || !rev.output.trim()) {
265
+ // The commit reported success but we cannot confirm the sha: report the
266
+ // commit honestly without a sha rather than inventing one.
267
+ notes.push("FINISH committed, but could not read the commit sha (git rev-parse failed).");
268
+ }
269
+ const commitSha = rev.code === 0 ? rev.output.trim().split("\n")[0] : undefined;
270
+ const result = {
271
+ committed: true,
272
+ ...(commitSha ? { commitSha } : {}),
273
+ ...(opts.branch ? { branch: opts.branch } : {}),
274
+ notes,
275
+ };
276
+ if (!opts.openPr)
277
+ return result;
278
+ // Opt-in --pr half: push + gh, both fail-soft (the run stays successful).
279
+ if (!opts.branch) {
280
+ notes.push("Committed locally; push/PR skipped: the run branch is unknown.");
281
+ return result;
282
+ }
283
+ const push = await run(["git", "push", "-u", "origin", opts.branch]);
284
+ if (push.code !== 0) {
285
+ notes.push(`Committed locally; push failed: ${tail(push.output) || "git push failed"}.`);
286
+ return result;
287
+ }
288
+ const pr = await run(["gh", "pr", "create", "--title", subject, "--body", buildPrBody(summary, trailer, opts.remainingFindings)]);
289
+ if (pr.code !== 0) {
290
+ notes.push(`Committed and pushed ${opts.branch}; PR creation failed: ${tail(pr.output) || "gh failed"}.`);
291
+ return result;
292
+ }
293
+ const { prUrl, prNumber } = parsePrUrl(pr.output);
294
+ if (!prUrl) {
295
+ notes.push("Committed and pushed; gh pr create succeeded but no PR URL could be read from its output.");
296
+ return result;
297
+ }
298
+ return { ...result, prUrl, ...(prNumber != null ? { prNumber } : {}) };
299
+ }
300
+ catch (err) {
301
+ // Belt and braces: nothing above should throw, but the invariant (§0.1)
302
+ // outranks tidiness — a FINISH failure must never fail a clean run.
303
+ const msg = scrubSecrets(err instanceof Error ? err.message : String(err));
304
+ return { committed: false, ...(opts.branch ? { branch: opts.branch } : {}), notes: [...notes, `FINISH failed: ${msg}. The run itself is still clean; the changes are left in ${opts.cwd}.`] };
305
+ }
306
+ }
307
+ //# sourceMappingURL=finish.js.map
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Registers the `/go <ticket>` entry point (spec decision §5 — NOT `/run` or
3
+ * `/work`, which collide with the Run and Work concepts), plus its companions
4
+ * `/go-status` and `/go-clean` (registered from this same pipeline registration
5
+ * path — src/index.ts only ever calls registerGoCommand).
6
+ *
7
+ * A `registerCommand` (not a static chain prompt) is used because the review→fix
8
+ * LOOP needs a runtime condition a markdown chain cannot express. The handler
9
+ * parses the remainder through the flag parser (`--here` in-place, `--fg`
10
+ * blocking, `--pr` push + PR at FINISH; unknown flags are refused with
11
+ * a usage warning), guards on `ctx.isIdle()` plus the run registry (same-ticket
12
+ * in-flight refusal, MAX_CONCURRENT_RUNS), and runs the pipeline. Everything
13
+ * downstream — ticket resolution, sessionKey, registry, run-row arg, the
14
+ * ActivityFeed header, and the handoff copy — receives the FLAG-FREE ticket.
15
+ *
16
+ * FINISH STAGE (spec §3c): when the review loop stops `clean`, the handler runs
17
+ * the deterministic FINISH stage (finish.ts): commit the run's work (worktree
18
+ * mode always; --here only when the pre-run baseline was verifiably clean) with
19
+ * a conventional subject + plan summary + the provenance trailer, and — with
20
+ * `--pr` — push the run branch and open a PR under the user's own gh auth. A
21
+ * FINISH failure NEVER flips a clean run to failed and never fakes a commit
22
+ * (§0.1): every skip/failure is an honest note on the handoff. The commit/PR
23
+ * land on `PipelineResult.finish`, `session.finish`, the registry row, a
24
+ * `finish_done` checkpoint record, and a `finish` row in the activity feed.
25
+ *
26
+ * WORKTREE BY DEFAULT (spec §3b): the run gets its own worktree + branch FIRST
27
+ * (`git worktree add -b yagni/<slug>-<shortId> ~/.yagni-code/worktrees/<shortId>
28
+ * HEAD`), and repoCtx, sessionKey, the checkpoint journal, and every resume
29
+ * snapshot are resolved against the WORKTREE path — fresh runs and
30
+ * registry-driven resumes agree on the same tree, so the same run always maps to
31
+ * the same sha1(sessionKey) journal file. A best-effort bootstrap installs
32
+ * dependencies (fail-open with an honest note; verify's infra-failure
33
+ * classification covers the rest). `--here` opts back into the legacy in-place
34
+ * behavior: dirty-tree guard + no-op guard unchanged, sessionKey from ctx.cwd,
35
+ * and never a WIP commit. If worktree creation itself fails, default /go stops
36
+ * before mutating the user's tree; `--here` is the explicit in-place escape
37
+ * hatch.
38
+ *
39
+ * DETACHED BY DEFAULT: /go returns the prompt as soon as the run session opens;
40
+ * the pipeline promise is tracked in the module-scoped run registry (mirrored to
41
+ * ~/.yagni-code/registry.ndjson) and completion delivers the existing handoff
42
+ * via `pi.sendUserMessage(handoff, { deliverAs: "followUp" })` (try/catch with a
43
+ * notify fallback — pi requires deliverAs when the agent is streaming). `--fg`
44
+ * restores blocking. On a NON-clean terminal outcome in worktree mode the run's
45
+ * work is preserved as a clearly-marked `wip(yagni): <ticket> - <stopReason>`
46
+ * commit on the run branch, so nothing is lost and /go-clean can remove the tree.
47
+ *
48
+ * When the session has a UI, it also paints a live activity feed in an
49
+ * `aboveEditor` widget (spec §1) under a PER-RUN key (`yagni-go:<runShortId>`) so
50
+ * two concurrent runs never flicker-fight one widget and one run's clearUI only
51
+ * removes its own panel. Every UI call is guarded behind `ctx.hasUI` and
52
+ * optional-chained so headless runs degrade gracefully. `runPipeline` is
53
+ * injected so the handler is unit-testable without spawning.
54
+ *
55
+ * Run tracking: the handler opens a tracked run session (a `runs` row visible on
56
+ * the Work page) around the pipeline — `start` before, `stage` for each pipeline
57
+ * boundary, and `finish` with an honest outcome afterwards. Every session call
58
+ * is fail-soft, so recording can never break `/go`.
59
+ */
60
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
61
+ import { runFinish as defaultRunFinish } from "./finish.js";
62
+ import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
63
+ import { makeRunSession as defaultMakeRunSession } from "./runSession.js";
64
+ import { bootstrapWorktree as defaultBootstrapWorktree, createRunWorktree as defaultCreateRunWorktree } from "./worktree.js";
65
+ import type { CheckpointRecord, CheckpointStore, WorkspaceSnapshot } from "./types.js";
66
+ /** Best-effort git context for the current worktree (all fields optional). */
67
+ export interface RepoContext {
68
+ repo?: string;
69
+ branch?: string;
70
+ commitSha?: string;
71
+ }
72
+ export interface RegisterGoDeps {
73
+ runPipeline?: typeof defaultRunPipeline;
74
+ /** Injectable so the handler can be unit-tested without a network. */
75
+ makeRunSession?: typeof defaultMakeRunSession;
76
+ /** Injectable so the handler can be unit-tested without spawning git. */
77
+ resolveRepoContext?: (cwd: string | undefined, signal?: AbortSignal) => Promise<RepoContext>;
78
+ /** Injectable durable journal (default = local file + pi-journal combined). */
79
+ makeCheckpointStore?: (sessionKey: string, pi: ExtensionAPI, ctx: ExtensionCommandContext) => CheckpointStore;
80
+ /** Injectable live workspace snapshot used by the resume decision (default = git). */
81
+ resolveSnapshot?: (cwd: string, signal?: AbortSignal) => Promise<WorkspaceSnapshot>;
82
+ /**
83
+ * Resolve the ticket reference to its REAL brief (verbatim title + body) before
84
+ * planning. Default = a fail-soft POST to YAGNI's `/ask` ticketRef federation
85
+ * (live, workspace-scoped connector read). Injected so the handler is unit-testable
86
+ * without a network; a non-identifier arg short-circuits to null (raw arg passes
87
+ * through). Returns null on any miss / transport error (the honest blind fallback).
88
+ */
89
+ resolveTicketBrief?: (rawArg: string) => Promise<string | null>;
90
+ /** Injectable worktree creation (default: real `git worktree add -b … HEAD`). */
91
+ createWorktree?: typeof defaultCreateRunWorktree;
92
+ /** Injectable worktree bootstrap (default: best-effort `<pm> install --prefer-offline`). */
93
+ bootstrap?: typeof defaultBootstrapWorktree;
94
+ /** Injectable WIP-commit seam (default: real `git add -A` + commit in the worktree). */
95
+ commitWip?: (worktreePath: string, message: string) => Promise<string | undefined>;
96
+ /** Injectable FINISH stage (default: real git commit + optional push/PR). Never throws. */
97
+ runFinish?: typeof defaultRunFinish;
98
+ /** Injectable journal read for cross-process liveness (default: the file checkpoint store). */
99
+ loadJournal?: (sessionKey: string) => CheckpointRecord[];
100
+ /** Injectable fs existence check (worktree adoption on resume). */
101
+ exists?: (path: string) => boolean;
102
+ /** Clock seam for registry rows + staleness. */
103
+ now?: () => number;
104
+ baseUrl?: string;
105
+ getToken?: () => string | undefined;
106
+ }
107
+ /**
108
+ * Per-run UI key prefix: the widget/status keys are `yagni-go:<runShortId>` so
109
+ * two concurrent runs each own their panel and clearUI removes only its own.
110
+ */
111
+ export declare const STATUS_KEY_PREFIX = "yagni-go:";
112
+ /**
113
+ * Per-run key for the desktop's structured state record. Distinct from
114
+ * STATUS_KEY_PREFIX so the app routes by key rather than sniffing content.
115
+ */
116
+ export declare const STATE_KEY_PREFIX = "yagni-go:state:";
117
+ /**
118
+ * Steady animation cadence. The pipeline only emits events at stage/tool
119
+ * boundaries, so between them nothing would repaint and the spinner (and elapsed
120
+ * clock) freeze. A steady interval re-paints on its own, and the spinner frame is
121
+ * derived from wall-clock time so it advances smoothly no matter what triggered
122
+ * the paint. `setInterval` (not a self-rescheduling `setTimeout`) keeps the
123
+ * existing setTimeout-only mock-timer tests green: a real, immediately-cleared
124
+ * interval never fires inside those synchronous runs.
125
+ */
126
+ export declare const ANIMATION_INTERVAL_MS = 120;
127
+ export declare function registerGoCommand(pi: ExtensionAPI, deps?: RegisterGoDeps): void;
128
+ //# sourceMappingURL=goCommand.d.ts.map