@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.
- package/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- 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
|