@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,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers `/go-compare <ticket>` - the M6 grounded-vs-blind eval (report-only).
|
|
3
|
+
*
|
|
4
|
+
* It runs the same ticket through the pipeline TWICE in isolated git worktrees
|
|
5
|
+
* (so the user's tree is never touched): once grounded (the real /go) and once
|
|
6
|
+
* blind (grounding tools stripped + grounding-free personas), scores each change
|
|
7
|
+
* for business fit against this company's recorded judgment via the grounded
|
|
8
|
+
* review_business_match endpoint, and prints a comparative report.
|
|
9
|
+
*
|
|
10
|
+
* The report is the deliverable; nothing here is wired to model selection or
|
|
11
|
+
* routing. The live double-run is dogfood (it spawns real children + models), so
|
|
12
|
+
* the handler takes an injectable `compare` and the default lane/scorer seams are
|
|
13
|
+
* fail-soft. The eval engine + report formatter (eval.ts) carry the unit tests.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { type ComparisonReport, type Lane } from "./eval.js";
|
|
17
|
+
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
18
|
+
/** Which two-lane comparison a /go-compare invocation runs. */
|
|
19
|
+
export type CompareMode = "grounding" | "judgment";
|
|
20
|
+
export interface RegisterGoCompareDeps {
|
|
21
|
+
/** The whole eval runner, injectable so the handler is unit-tested without spawning. */
|
|
22
|
+
compare?: (ticket: string, ctx: ExtensionCommandContext, mode: CompareMode) => Promise<ComparisonReport>;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
getToken?: () => string | undefined;
|
|
25
|
+
runPipeline?: typeof defaultRunPipeline;
|
|
26
|
+
git?: GitCommand;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse the leading `--judgment` flag. PURE.
|
|
30
|
+
*
|
|
31
|
+
* Default stays `grounding` so the existing command is byte-identical for
|
|
32
|
+
* callers who pass only a ticket.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseCompareArgs(args: string): {
|
|
35
|
+
mode: CompareMode;
|
|
36
|
+
ticket: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The per-lane stage list.
|
|
40
|
+
*
|
|
41
|
+
* Grounding mode strips the grounding tools from the blind lane. Judgment mode
|
|
42
|
+
* runs BOTH lanes fully grounded and varies only the plan/review tier, so the
|
|
43
|
+
* delta isolates the judgment upgrade rather than confounding it with grounding.
|
|
44
|
+
*/
|
|
45
|
+
export declare function stagesForLane(lane: Lane): import("./types.js").PipelineStage[];
|
|
46
|
+
export type GitCommand = (args: string[], cwd: string | undefined, signal?: AbortSignal) => Promise<string>;
|
|
47
|
+
export declare function registerGoCompareCommand(pi: ExtensionAPI, deps?: RegisterGoCompareDeps): void;
|
|
48
|
+
//# sourceMappingURL=goCompareCommand.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers `/go-compare <ticket>` - the M6 grounded-vs-blind eval (report-only).
|
|
3
|
+
*
|
|
4
|
+
* It runs the same ticket through the pipeline TWICE in isolated git worktrees
|
|
5
|
+
* (so the user's tree is never touched): once grounded (the real /go) and once
|
|
6
|
+
* blind (grounding tools stripped + grounding-free personas), scores each change
|
|
7
|
+
* for business fit against this company's recorded judgment via the grounded
|
|
8
|
+
* review_business_match endpoint, and prints a comparative report.
|
|
9
|
+
*
|
|
10
|
+
* The report is the deliverable; nothing here is wired to model selection or
|
|
11
|
+
* routing. The live double-run is dogfood (it spawns real children + models), so
|
|
12
|
+
* the handler takes an injectable `compare` and the default lane/scorer seams are
|
|
13
|
+
* fail-soft. The eval engine + report formatter (eval.ts) carry the unit tests.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { getToken as defaultGetToken, resolveBaseUrl } from "../config.js";
|
|
20
|
+
import { blindStages, makeGroundedVsBlindEval, makeJudgmentTierEval, reportOnlyStages, withJudgmentTier, } from "./eval.js";
|
|
21
|
+
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
22
|
+
/**
|
|
23
|
+
* Parse the leading `--judgment` flag. PURE.
|
|
24
|
+
*
|
|
25
|
+
* Default stays `grounding` so the existing command is byte-identical for
|
|
26
|
+
* callers who pass only a ticket.
|
|
27
|
+
*/
|
|
28
|
+
export function parseCompareArgs(args) {
|
|
29
|
+
let rest = args.trim();
|
|
30
|
+
let mode = "grounding";
|
|
31
|
+
// Accept the flag anywhere leading, so `/go-compare --judgment YAG-1` and
|
|
32
|
+
// `/go-compare YAG-1 --judgment` both work.
|
|
33
|
+
const flag = /(^|\s)--judgment(\s|$)/;
|
|
34
|
+
if (flag.test(rest)) {
|
|
35
|
+
mode = "judgment";
|
|
36
|
+
rest = rest.replace(flag, " ").trim();
|
|
37
|
+
}
|
|
38
|
+
return { mode, ticket: rest };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The per-lane stage list.
|
|
42
|
+
*
|
|
43
|
+
* Grounding mode strips the grounding tools from the blind lane. Judgment mode
|
|
44
|
+
* runs BOTH lanes fully grounded and varies only the plan/review tier, so the
|
|
45
|
+
* delta isolates the judgment upgrade rather than confounding it with grounding.
|
|
46
|
+
*/
|
|
47
|
+
export function stagesForLane(lane) {
|
|
48
|
+
switch (lane) {
|
|
49
|
+
case "blind":
|
|
50
|
+
return blindStages(reportOnlyStages());
|
|
51
|
+
case "peak_judgment":
|
|
52
|
+
return withJudgmentTier(reportOnlyStages(), "peak");
|
|
53
|
+
case "advanced_judgment":
|
|
54
|
+
return withJudgmentTier(reportOnlyStages(), "advanced");
|
|
55
|
+
default:
|
|
56
|
+
return reportOnlyStages();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Run a required git command in `cwd`, resolving to trimmed stdout or rejecting with stderr. */
|
|
60
|
+
function defaultGit(args, cwd, signal) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
execFile("git", args, { cwd, signal, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
const detail = stderr.toString().trim() || err.message;
|
|
65
|
+
reject(new Error(`git ${args.join(" ")} failed: ${detail}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve(stdout.toString().trim());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
let worktreeCounter = 0;
|
|
73
|
+
/**
|
|
74
|
+
* The default eval: each lane runs in a throwaway detached worktree off HEAD, so
|
|
75
|
+
* neither pass can clobber the user's working tree or each other. The lane's diff
|
|
76
|
+
* is captured before teardown and scored by the GROUNDED review_business_match
|
|
77
|
+
* (both lanes judged by the same recorded judgment). Every step is fail-soft.
|
|
78
|
+
*/
|
|
79
|
+
function defaultCompare(deps) {
|
|
80
|
+
const runPipeline = deps.runPipeline ?? defaultRunPipeline;
|
|
81
|
+
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
82
|
+
const getToken = deps.getToken ?? defaultGetToken;
|
|
83
|
+
const git = deps.git ?? defaultGit;
|
|
84
|
+
return (ticket, ctx, mode) => {
|
|
85
|
+
const diffs = {};
|
|
86
|
+
const evalEnv = { ...process.env, YAGNI_CODE_EVAL_MODE: "1" };
|
|
87
|
+
const runLane = async (lane) => {
|
|
88
|
+
const root = await git(["rev-parse", "--show-toplevel"], ctx.cwd, ctx.signal);
|
|
89
|
+
const base = mkdtempSync(join(tmpdir(), `yagni-eval-${lane}-${process.pid}-${worktreeCounter++}-`));
|
|
90
|
+
const tree = join(base, "wt");
|
|
91
|
+
// Detached worktree off the current HEAD; isolated from the live tree.
|
|
92
|
+
await git(["worktree", "add", "--detach", tree, "HEAD"], root, ctx.signal);
|
|
93
|
+
try {
|
|
94
|
+
const result = await runPipeline(ticket, {
|
|
95
|
+
cwd: tree,
|
|
96
|
+
signal: ctx.signal,
|
|
97
|
+
// Only the blind lane runs ungrounded. Both judgment lanes stay
|
|
98
|
+
// grounded so the delta isolates the tier, not the grounding.
|
|
99
|
+
grounded: lane !== "blind",
|
|
100
|
+
stages: stagesForLane(lane),
|
|
101
|
+
childEnv: evalEnv,
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
diffs[lane] = await git(["diff", "HEAD"], tree, ctx.signal);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
diffs[lane] = "";
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await git(["worktree", "remove", "--force", tree], root, ctx.signal);
|
|
113
|
+
try {
|
|
114
|
+
rmSync(base, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* fail-soft */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const scoreFit = async (lane) => {
|
|
122
|
+
const diff = (diffs[lane] ?? "").trim();
|
|
123
|
+
if (!diff)
|
|
124
|
+
return { review: "", citations: 0, conflicts: 0, note: "no diff produced by this lane" };
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`${baseUrl}/api/yagni-code/review`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
signal: ctx.signal,
|
|
129
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${getToken() ?? ""}` },
|
|
130
|
+
body: JSON.stringify({ diff, ticket, summary: `${mode} eval (${lane} lane)` }),
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return { review: "", citations: 0, conflicts: 0, note: `review HTTP ${res.status}` };
|
|
134
|
+
const data = (await res.json());
|
|
135
|
+
const review = typeof data.review === "string" ? data.review : "";
|
|
136
|
+
const citations = Array.isArray(data.citations) ? data.citations.length : 0;
|
|
137
|
+
// Coarse conflict signal from the prose verdict (the text is the real signal).
|
|
138
|
+
const conflicts = (review.match(/\b(conflict|contradict|mismatch|violat)/gi) ?? []).length;
|
|
139
|
+
return { review, citations, conflicts };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { review: "", citations: 0, conflicts: 0, note: "review endpoint unreachable" };
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const evalRunner = mode === "judgment"
|
|
146
|
+
? makeJudgmentTierEval({ runLane, scoreFit })
|
|
147
|
+
: makeGroundedVsBlindEval({ runLane, scoreFit });
|
|
148
|
+
return evalRunner(ticket);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function registerGoCompareCommand(pi, deps = {}) {
|
|
152
|
+
const compare = deps.compare ?? defaultCompare(deps);
|
|
153
|
+
pi.registerCommand("go-compare", {
|
|
154
|
+
description: "Eval only: run a ticket through two lane configurations in isolated worktrees and report the business-fit delta. Default compares grounded vs grounding-suppressed; --judgment compares peak vs advanced on plan+review. Never affects routing.",
|
|
155
|
+
handler: async (args, ctx) => {
|
|
156
|
+
const notify = (message, type) => {
|
|
157
|
+
if (ctx.hasUI)
|
|
158
|
+
ctx.ui.notify(message, type);
|
|
159
|
+
};
|
|
160
|
+
const { mode, ticket } = parseCompareArgs(args);
|
|
161
|
+
if (!ticket) {
|
|
162
|
+
notify("Usage: /go-compare [--judgment] <ticket>", "warning");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!ctx.isIdle()) {
|
|
166
|
+
notify("YAGNI Code is busy; wait for the current turn to finish before running /go-compare.", "warning");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
notify(mode === "judgment"
|
|
170
|
+
? "Running the peak vs advanced judgment eval (two isolated passes). This takes a while."
|
|
171
|
+
: "Running the grounded vs blind eval (two isolated passes). This takes a while.", "info");
|
|
172
|
+
try {
|
|
173
|
+
const out = await compare(ticket, ctx, mode);
|
|
174
|
+
await pi.sendUserMessage(out.report);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
notify(`/go-compare failed: ${message}`, "error");
|
|
179
|
+
await pi.sendUserMessage(`/go-compare failed: ${message}`);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=goCompareCommand.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE /go flag parsing (spec §3a — the prerequisite for detached worktree runs).
|
|
3
|
+
*
|
|
4
|
+
* There was no flag parser before this module (`/go` treated the whole remainder
|
|
5
|
+
* as the ticket). `parseGoArgs` strips the three known flags — `--here` (legacy
|
|
6
|
+
* in-place run), `--fg` (blocking foreground run), `--pr` (opt-in push + PR at
|
|
7
|
+
* FINISH) — and collects any OTHER `--flag`-shaped token as unknown so the
|
|
8
|
+
* handler can refuse with a usage warning instead of silently folding a typo'd
|
|
9
|
+
* flag into the ticket text. Everything downstream (ticket resolution,
|
|
10
|
+
* sessionKey, registry, run-row arg, ActivityFeed header, handoff copy) receives
|
|
11
|
+
* the FLAG-FREE ticket.
|
|
12
|
+
*
|
|
13
|
+
* A free-text ticket that legitimately mentions a `--something` token is the
|
|
14
|
+
* accepted trade-off: flags are only recognized in the `--word` shape, and the
|
|
15
|
+
* usage warning names the offending token so the user can rephrase.
|
|
16
|
+
*/
|
|
17
|
+
/** The three /go run-mode flags. All default false (worktree, detached, no PR). */
|
|
18
|
+
export interface GoFlags {
|
|
19
|
+
/** Run in place in the current tree (legacy behavior) instead of a run worktree. */
|
|
20
|
+
here: boolean;
|
|
21
|
+
/** Block the prompt until the run finishes (legacy behavior) instead of detaching. */
|
|
22
|
+
fg: boolean;
|
|
23
|
+
/** Opt-in: push the run branch + open a PR at the FINISH stage. */
|
|
24
|
+
pr: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The parse outcome: the flag-free ticket, the flags, and any unknown --tokens. */
|
|
27
|
+
export interface ParsedGoArgs {
|
|
28
|
+
ticket: string;
|
|
29
|
+
flags: GoFlags;
|
|
30
|
+
unknownFlags: string[];
|
|
31
|
+
}
|
|
32
|
+
/** One-line usage copy shared by the empty-ticket and unknown-flag warnings. */
|
|
33
|
+
export declare const GO_USAGE = "Usage: /go <ticket> [--here] [--fg] [--pr]";
|
|
34
|
+
/**
|
|
35
|
+
* Parse the raw /go remainder into the flag-free ticket + flags. Duplicate flags
|
|
36
|
+
* are idempotent; token order is irrelevant; non-flag tokens are rejoined with
|
|
37
|
+
* single spaces (the ticket is free text, not a byte-exact echo).
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseGoArgs(raw: string): ParsedGoArgs;
|
|
40
|
+
//# sourceMappingURL=goFlags.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE /go flag parsing (spec §3a — the prerequisite for detached worktree runs).
|
|
3
|
+
*
|
|
4
|
+
* There was no flag parser before this module (`/go` treated the whole remainder
|
|
5
|
+
* as the ticket). `parseGoArgs` strips the three known flags — `--here` (legacy
|
|
6
|
+
* in-place run), `--fg` (blocking foreground run), `--pr` (opt-in push + PR at
|
|
7
|
+
* FINISH) — and collects any OTHER `--flag`-shaped token as unknown so the
|
|
8
|
+
* handler can refuse with a usage warning instead of silently folding a typo'd
|
|
9
|
+
* flag into the ticket text. Everything downstream (ticket resolution,
|
|
10
|
+
* sessionKey, registry, run-row arg, ActivityFeed header, handoff copy) receives
|
|
11
|
+
* the FLAG-FREE ticket.
|
|
12
|
+
*
|
|
13
|
+
* A free-text ticket that legitimately mentions a `--something` token is the
|
|
14
|
+
* accepted trade-off: flags are only recognized in the `--word` shape, and the
|
|
15
|
+
* usage warning names the offending token so the user can rephrase.
|
|
16
|
+
*/
|
|
17
|
+
/** One-line usage copy shared by the empty-ticket and unknown-flag warnings. */
|
|
18
|
+
export const GO_USAGE = "Usage: /go <ticket> [--here] [--fg] [--pr]";
|
|
19
|
+
/** A token is flag-shaped when it looks like `--word` (letters/digits/dashes). */
|
|
20
|
+
const FLAG_SHAPE = /^--[A-Za-z][\w-]*$/;
|
|
21
|
+
/**
|
|
22
|
+
* Parse the raw /go remainder into the flag-free ticket + flags. Duplicate flags
|
|
23
|
+
* are idempotent; token order is irrelevant; non-flag tokens are rejoined with
|
|
24
|
+
* single spaces (the ticket is free text, not a byte-exact echo).
|
|
25
|
+
*/
|
|
26
|
+
export function parseGoArgs(raw) {
|
|
27
|
+
const flags = { here: false, fg: false, pr: false };
|
|
28
|
+
const unknownFlags = [];
|
|
29
|
+
const ticketTokens = [];
|
|
30
|
+
for (const token of raw.trim().split(/\s+/)) {
|
|
31
|
+
if (!token)
|
|
32
|
+
continue;
|
|
33
|
+
if (token === "--here")
|
|
34
|
+
flags.here = true;
|
|
35
|
+
else if (token === "--fg")
|
|
36
|
+
flags.fg = true;
|
|
37
|
+
else if (token === "--pr")
|
|
38
|
+
flags.pr = true;
|
|
39
|
+
else if (FLAG_SHAPE.test(token))
|
|
40
|
+
unknownFlags.push(token);
|
|
41
|
+
else
|
|
42
|
+
ticketTokens.push(token);
|
|
43
|
+
}
|
|
44
|
+
return { ticket: ticketTokens.join(" "), flags, unknownFlags };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=goFlags.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/go-status` and `/go-clean` — the pull-path companions to detached /go runs
|
|
3
|
+
* (spec §3b). Registered from the pipeline's registration path (goCommand.ts
|
|
4
|
+
* calls {@link registerGoStatusCommands} with the same pi object; src/index.ts
|
|
5
|
+
* is never edited for these).
|
|
6
|
+
*
|
|
7
|
+
* - `/go-status` lists every registry row with an honest liveness verdict:
|
|
8
|
+
* active runs show their stage/round straight from the checkpoint journal,
|
|
9
|
+
* finished runs show outcome + branch + commit/PR, interrupted runs say
|
|
10
|
+
* "resumable, re-run /go <ticket>". It is the always-available fallback when
|
|
11
|
+
* a detached completion message could not be delivered.
|
|
12
|
+
*
|
|
13
|
+
* - `/go-clean` removes ONLY worktrees that are verifiably CLEAN (everything
|
|
14
|
+
* committed — the WIP-commit on non-clean terminal outcomes exists exactly
|
|
15
|
+
* so this is normally true). A dirty or unverifiable worktree is SKIPPED
|
|
16
|
+
* with an honest note + path, never force-removed: /go-clean must be safe to
|
|
17
|
+
* run reflexively.
|
|
18
|
+
*
|
|
19
|
+
* The line formatting is pure ({@link describeRunRow}, {@link describeJournalStage});
|
|
20
|
+
* the impure seams (registry, journal, git snapshot, worktree removal, fs
|
|
21
|
+
* exists) are injectable for tests.
|
|
22
|
+
*/
|
|
23
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { type RunLiveness, type RunRegistryRow } from "./runRegistry.js";
|
|
25
|
+
import type { CheckpointRecord, WorkspaceSnapshot } from "./types.js";
|
|
26
|
+
export interface RegisterGoStatusDeps {
|
|
27
|
+
/** Injectable registry read (default: the module-scoped registry + mirror). */
|
|
28
|
+
loadRows?: () => RunRegistryRow[];
|
|
29
|
+
/** Injectable per-run journal read (default: the file checkpoint store). */
|
|
30
|
+
loadJournal?: (sessionKey: string) => CheckpointRecord[];
|
|
31
|
+
/** Injectable in-process liveness (default: the module-scoped registry). */
|
|
32
|
+
isInFlight?: (runId: string) => boolean;
|
|
33
|
+
/** Injectable git snapshot for the clean check (default: real git). */
|
|
34
|
+
snapshot?: (cwd: string, signal?: AbortSignal) => Promise<WorkspaceSnapshot>;
|
|
35
|
+
/** Injectable worktree removal (default: `git worktree remove`). */
|
|
36
|
+
removeWorktree?: (worktreePath: string, repoCwd: string, signal?: AbortSignal) => Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the current tree's repo identity (same value stored on a run row's
|
|
39
|
+
* `repo`), so /go-clean only touches THIS repo's worktrees — `git worktree
|
|
40
|
+
* remove` is per-repo and rejects a foreign path. Injected by goCommand with
|
|
41
|
+
* its own git resolver; the default returns undefined (no filter, so an
|
|
42
|
+
* un-wired caller keeps the prior global behavior).
|
|
43
|
+
*/
|
|
44
|
+
resolveRepo?: (cwd: string, signal?: AbortSignal) => Promise<string | undefined>;
|
|
45
|
+
exists?: (path: string) => boolean;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* PURE: where an in-flight/interrupted run got to, read off its checkpoint
|
|
50
|
+
* journal. The journal is append-only, so the latest applicable boundary wins.
|
|
51
|
+
*/
|
|
52
|
+
export declare function describeJournalStage(records: CheckpointRecord[]): string;
|
|
53
|
+
/**
|
|
54
|
+
* PURE: one status line per registry row. No em-dashes (house copy rule);
|
|
55
|
+
* pieces that do not exist for a row are simply omitted.
|
|
56
|
+
*/
|
|
57
|
+
export declare function describeRunRow(row: RunRegistryRow, liveness: RunLiveness, journal: CheckpointRecord[], now: number): string;
|
|
58
|
+
export declare function registerGoStatusCommands(pi: ExtensionAPI, deps?: RegisterGoStatusDeps): void;
|
|
59
|
+
//# sourceMappingURL=goStatusCommands.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/go-status` and `/go-clean` — the pull-path companions to detached /go runs
|
|
3
|
+
* (spec §3b). Registered from the pipeline's registration path (goCommand.ts
|
|
4
|
+
* calls {@link registerGoStatusCommands} with the same pi object; src/index.ts
|
|
5
|
+
* is never edited for these).
|
|
6
|
+
*
|
|
7
|
+
* - `/go-status` lists every registry row with an honest liveness verdict:
|
|
8
|
+
* active runs show their stage/round straight from the checkpoint journal,
|
|
9
|
+
* finished runs show outcome + branch + commit/PR, interrupted runs say
|
|
10
|
+
* "resumable, re-run /go <ticket>". It is the always-available fallback when
|
|
11
|
+
* a detached completion message could not be delivered.
|
|
12
|
+
*
|
|
13
|
+
* - `/go-clean` removes ONLY worktrees that are verifiably CLEAN (everything
|
|
14
|
+
* committed — the WIP-commit on non-clean terminal outcomes exists exactly
|
|
15
|
+
* so this is normally true). A dirty or unverifiable worktree is SKIPPED
|
|
16
|
+
* with an honest note + path, never force-removed: /go-clean must be safe to
|
|
17
|
+
* run reflexively.
|
|
18
|
+
*
|
|
19
|
+
* The line formatting is pure ({@link describeRunRow}, {@link describeJournalStage});
|
|
20
|
+
* the impure seams (registry, journal, git snapshot, worktree removal, fs
|
|
21
|
+
* exists) are injectable for tests.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { makeFileCheckpointStore } from "./checkpoint.js";
|
|
25
|
+
import { classifyRunLiveness, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, settleRun, } from "./runRegistry.js";
|
|
26
|
+
import { removeWorktree as defaultRemoveWorktree } from "./worktree.js";
|
|
27
|
+
import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
|
|
28
|
+
/**
|
|
29
|
+
* PURE: where an in-flight/interrupted run got to, read off its checkpoint
|
|
30
|
+
* journal. The journal is append-only, so the latest applicable boundary wins.
|
|
31
|
+
*/
|
|
32
|
+
export function describeJournalStage(records) {
|
|
33
|
+
let maxRound = 0;
|
|
34
|
+
let sawBuildDone = false;
|
|
35
|
+
let sawAnything = false;
|
|
36
|
+
for (const r of records) {
|
|
37
|
+
if (!r)
|
|
38
|
+
continue;
|
|
39
|
+
sawAnything = true;
|
|
40
|
+
if (r.kind === "build_done")
|
|
41
|
+
sawBuildDone = true;
|
|
42
|
+
if (r.kind === "round_done" && typeof r.round === "number" && r.round > maxRound)
|
|
43
|
+
maxRound = r.round;
|
|
44
|
+
}
|
|
45
|
+
if (maxRound > 0)
|
|
46
|
+
return `review round ${maxRound + 1}`;
|
|
47
|
+
if (sawBuildDone)
|
|
48
|
+
return "review round 1";
|
|
49
|
+
if (sawAnything)
|
|
50
|
+
return "building (map/plan/implement)";
|
|
51
|
+
return "starting";
|
|
52
|
+
}
|
|
53
|
+
/** Compact "Xm ago" for status lines. */
|
|
54
|
+
function agoMinutes(ts, now) {
|
|
55
|
+
return `${Math.max(0, Math.round((now - ts) / 60_000))}m ago`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* PURE: one status line per registry row. No em-dashes (house copy rule);
|
|
59
|
+
* pieces that do not exist for a row are simply omitted.
|
|
60
|
+
*/
|
|
61
|
+
export function describeRunRow(row, liveness, journal, now) {
|
|
62
|
+
const id = `[${row.runId.slice(0, 8)}] ${row.ticket}`;
|
|
63
|
+
if (liveness === "in_flight") {
|
|
64
|
+
return `${id}: running, ${describeJournalStage(journal)} (started ${agoMinutes(row.startedAt, now)})`;
|
|
65
|
+
}
|
|
66
|
+
if (liveness === "active_elsewhere") {
|
|
67
|
+
const seen = lastJournalTs(journal) || row.startedAt;
|
|
68
|
+
return `${id}: running in another session? ${describeJournalStage(journal)} (journal active ${agoMinutes(seen, now)})`;
|
|
69
|
+
}
|
|
70
|
+
if (liveness === "interrupted") {
|
|
71
|
+
return `${id}: interrupted at ${describeJournalStage(journal)}; resumable, re-run /go ${row.ticket}`;
|
|
72
|
+
}
|
|
73
|
+
const parts = [`${id}: ${row.status === "cleaned" ? "cleaned (worktree removed)" : row.status}`];
|
|
74
|
+
if (row.branch)
|
|
75
|
+
parts.push(`branch ${row.branch}`);
|
|
76
|
+
if (row.commitSha)
|
|
77
|
+
parts.push(`commit ${row.commitSha.slice(0, 7)}`);
|
|
78
|
+
if (row.prUrl)
|
|
79
|
+
parts.push(`PR ${row.prUrl}`);
|
|
80
|
+
if (row.worktreePath && row.status !== "cleaned")
|
|
81
|
+
parts.push(`worktree ${row.worktreePath}`);
|
|
82
|
+
return parts.join(" | ");
|
|
83
|
+
}
|
|
84
|
+
export function registerGoStatusCommands(pi, deps = {}) {
|
|
85
|
+
const loadRows = deps.loadRows ?? loadRegistryRows;
|
|
86
|
+
const loadJournal = deps.loadJournal ?? ((sessionKey) => makeFileCheckpointStore(sessionKey).load());
|
|
87
|
+
const inFlight = deps.isInFlight ?? isRunInFlight;
|
|
88
|
+
const snapshot = deps.snapshot ?? defaultSnapshotWorkspace;
|
|
89
|
+
const remove = deps.removeWorktree ?? defaultRemoveWorktree;
|
|
90
|
+
const resolveRepo = deps.resolveRepo ?? (async () => undefined);
|
|
91
|
+
const exists = deps.exists ?? existsSync;
|
|
92
|
+
const now = deps.now ?? Date.now;
|
|
93
|
+
pi.registerCommand("go-status", {
|
|
94
|
+
description: "List /go runs: active (stage/round), finished (outcome + branch/commit/PR), and interrupted (resumable).",
|
|
95
|
+
handler: async (_args, ctx) => {
|
|
96
|
+
const rows = loadRows()
|
|
97
|
+
.slice()
|
|
98
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
99
|
+
if (rows.length === 0) {
|
|
100
|
+
if (ctx.hasUI)
|
|
101
|
+
ctx.ui.notify("No /go runs recorded yet.", "info");
|
|
102
|
+
else
|
|
103
|
+
await pi.sendUserMessage("No /go runs recorded yet.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const lines = rows.map((row) => {
|
|
107
|
+
const journal = safeJournal(loadJournal, row.sessionKey);
|
|
108
|
+
const liveness = classifyRunLiveness(row, {
|
|
109
|
+
inFlight: inFlight(row.runId),
|
|
110
|
+
lastJournalTs: lastJournalTs(journal) || undefined,
|
|
111
|
+
now: now(),
|
|
112
|
+
});
|
|
113
|
+
return describeRunRow(row, liveness, journal, now());
|
|
114
|
+
});
|
|
115
|
+
await pi.sendUserMessage(`/go runs (newest first):\n${lines.join("\n")}`);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
pi.registerCommand("go-clean", {
|
|
119
|
+
description: "Remove finished /go run worktrees that are fully committed; dirty worktrees are skipped with a note.",
|
|
120
|
+
handler: async (_args, ctx) => {
|
|
121
|
+
// `git worktree remove` is per-repo, so only THIS repo's worktrees are
|
|
122
|
+
// removable from here; scoping to the current repo keeps /go-clean from
|
|
123
|
+
// failing on (and noisily reporting) every OTHER repo's run worktrees.
|
|
124
|
+
let baseRepo;
|
|
125
|
+
try {
|
|
126
|
+
baseRepo = await resolveRepo(ctx.cwd, ctx.signal);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
baseRepo = undefined; // can't resolve ours → don't filter (safe fallback)
|
|
130
|
+
}
|
|
131
|
+
const candidates = loadRows().filter((row) => isTerminalStatus(row.status) &&
|
|
132
|
+
row.status !== "cleaned" &&
|
|
133
|
+
!!row.worktreePath &&
|
|
134
|
+
!inFlight(row.runId) &&
|
|
135
|
+
// Keep rows with no recorded repo (legacy), or when we couldn't resolve
|
|
136
|
+
// ours, so nothing becomes permanently un-cleanable; otherwise match.
|
|
137
|
+
(!baseRepo || !row.repo || row.repo === baseRepo));
|
|
138
|
+
if (candidates.length === 0) {
|
|
139
|
+
if (ctx.hasUI)
|
|
140
|
+
ctx.ui.notify("No finished /go worktrees to clean.", "info");
|
|
141
|
+
else
|
|
142
|
+
await pi.sendUserMessage("No finished /go worktrees to clean.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let removed = 0;
|
|
146
|
+
const notes = [];
|
|
147
|
+
for (const row of candidates) {
|
|
148
|
+
const path = row.worktreePath;
|
|
149
|
+
const label = `[${row.runId.slice(0, 8)}] ${row.ticket}`;
|
|
150
|
+
if (!exists(path)) {
|
|
151
|
+
// Already gone (a manual rm): record it so it stops showing up.
|
|
152
|
+
settleRun(row.runId, { ...row, status: "cleaned" });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const snap = await snapshot(path, ctx.signal);
|
|
157
|
+
if (!snap.tracked) {
|
|
158
|
+
notes.push(`${label}: skipped, could not verify the worktree is clean (not a readable git tree): ${path}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (snap.status.trim().length > 0) {
|
|
162
|
+
notes.push(`${label}: skipped, worktree has uncommitted changes (left in place): ${path}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
await remove(path, ctx.cwd, ctx.signal);
|
|
166
|
+
settleRun(row.runId, { ...row, status: "cleaned" });
|
|
167
|
+
removed += 1;
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
+
notes.push(`${label}: skipped, removal failed (${msg}): ${path}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const summary = [`/go-clean: removed ${removed} clean worktree${removed === 1 ? "" : "s"}.`, ...notes].join("\n");
|
|
175
|
+
await pi.sendUserMessage(summary);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Journal read that never throws (an unreadable journal reads as empty). */
|
|
180
|
+
function safeJournal(loadJournal, sessionKey) {
|
|
181
|
+
try {
|
|
182
|
+
return loadJournal(sessionKey) ?? [];
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=goStatusCommands.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE child-invocation builder — the direct analogue of yagni-code-cli's
|
|
3
|
+
* `buildLaunch` (no spawn, no fs).
|
|
4
|
+
*
|
|
5
|
+
* `buildStageInvocation` produces the per-stage pi PASSTHROUGH argv exactly as
|
|
6
|
+
* the subagent example assembles it (runSingleAgent L294-330):
|
|
7
|
+
* --mode json -p --no-session --model <tier> --tools <csv>
|
|
8
|
+
* --append-system-prompt <file> 'Task: <rendered>'
|
|
9
|
+
*
|
|
10
|
+
* `groundedChildArgv` prepends the grounding pi-args the yagni-code launcher
|
|
11
|
+
* would otherwise add (`-e <self> --provider yagni`) because v1 spawns pi
|
|
12
|
+
* directly, NOT the yagni-code bin, to avoid a yagni-code-cli ↔ pi-extension-yagni
|
|
13
|
+
* circular dependency (token / base-url / agent-dir are inherited from the
|
|
14
|
+
* parent's env, not re-built here).
|
|
15
|
+
*
|
|
16
|
+
* `renderTemplate` does the {ticket}/{previous} substitution, mirroring the
|
|
17
|
+
* chain's `step.task.replace(/{previous}/g, …)`.
|
|
18
|
+
*/
|
|
19
|
+
import type { PipelineStage, ReviewLens } from "./types.js";
|
|
20
|
+
/** Fill {ticket}/{previous}; an absent var renders as empty string. */
|
|
21
|
+
export declare function renderTemplate(tmpl: string, vars: {
|
|
22
|
+
ticket?: string;
|
|
23
|
+
previous?: string;
|
|
24
|
+
}): string;
|
|
25
|
+
/**
|
|
26
|
+
* Build the pi passthrough argv for one stage (from `--mode` onward). The
|
|
27
|
+
* positional rendered prompt is always the last arg, prefixed `Task: `.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildStageInvocation(stage: PipelineStage, ctx: {
|
|
30
|
+
ticket: string;
|
|
31
|
+
previous?: string;
|
|
32
|
+
promptFilePath: string;
|
|
33
|
+
lens?: ReviewLens;
|
|
34
|
+
}): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Prepend the grounding args (the pi cli script, our extension, and the default
|
|
37
|
+
* `yagni` provider) before a stage passthrough. Mirrors buildLaunch's
|
|
38
|
+
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
39
|
+
* left untouched.
|
|
40
|
+
*/
|
|
41
|
+
export declare function groundedChildArgv(stageArgv: string[], opts: {
|
|
42
|
+
piCli: string;
|
|
43
|
+
extensionPath: string;
|
|
44
|
+
}): string[];
|
|
45
|
+
//# sourceMappingURL=invocation.d.ts.map
|