@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,298 @@
1
+ /**
2
+ * THIN, IMPURE per-stage spawn-runner — lifted almost verbatim from the pi
3
+ * subagent example's `runSingleAgent` (index.ts L267-429).
4
+ *
5
+ * It writes the grounded persona body to a 0600 temp file, builds the grounded
6
+ * child argv, spawns ONE pi child, line-buffers the stdout NDJSON, delegates all
7
+ * parsing to `events.ts`, applies `PER_TASK_OUTPUT_CAP`, wires abort
8
+ * (SIGTERM then SIGKILL after 5s if the child has not really exited), and cleans
9
+ * up the temp file.
10
+ *
11
+ * v1 spawns `pi` directly (NOT the `yagni-code` launcher) and re-injects
12
+ * `-e <self> --provider yagni`, inheriting the parent's env (token / base URL /
13
+ * agent dir). This avoids a yagni-code-cli ↔ pi-extension-yagni circular
14
+ * dependency — see the blueprint risks.
15
+ *
16
+ * ALL impurity sits behind injectable seams (`spawn`, `writePrompt`,
17
+ * `resolveChild`) so the runner is unit-tested with a fake spawn emitting canned
18
+ * NDJSON — no real process, no model, no network. The live end-to-end run is
19
+ * Jack dogfood, not CI.
20
+ */
21
+ import { spawn as nodeSpawn } from "node:child_process";
22
+ import * as fs from "node:fs";
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { finalOutputFrom, foldEvent, newEventAccumulator } from "./events.js";
27
+ import { buildStageInvocation, groundedChildArgv } from "./invocation.js";
28
+ import { personaBody } from "./personas.js";
29
+ import { PER_TASK_OUTPUT_CAP, } from "./types.js";
30
+ /**
31
+ * Default per-line stdout ceiling (16 MiB). A legitimate NDJSON event — even one
32
+ * carrying a large tool payload — is far below this; a line beyond it is a
33
+ * runaway/garbage line, dropped rather than accumulated. Keeps the stdout reader
34
+ * O(n) and memory-bounded (a single ~GB line previously OOM-crashed `runStage`).
35
+ */
36
+ export const DEFAULT_MAX_STDOUT_LINE_BYTES = 16 * 1024 * 1024;
37
+ /** Default: write the persona body to a private (0600) temp file. */
38
+ async function defaultWritePrompt(body) {
39
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "yagni-pipeline-"));
40
+ const filePath = path.join(dir, "persona.md");
41
+ await fs.promises.writeFile(filePath, body, { encoding: "utf-8", mode: 0o600 });
42
+ const cleanup = () => {
43
+ try {
44
+ fs.unlinkSync(filePath);
45
+ }
46
+ catch {
47
+ /* ignore */
48
+ }
49
+ try {
50
+ fs.rmdirSync(dir);
51
+ }
52
+ catch {
53
+ /* ignore */
54
+ }
55
+ };
56
+ return { filePath, cleanup };
57
+ }
58
+ /**
59
+ * Default child resolution: the parent process IS pi (so `process.argv[1]` is
60
+ * pi's cli), and our compiled extension entry sits one dir up from this module.
61
+ */
62
+ function defaultResolveChild() {
63
+ const piCli = process.argv[1] ?? "pi";
64
+ const extensionPath = fileURLToPath(new URL("../index.js", import.meta.url));
65
+ return { piCli, extensionPath };
66
+ }
67
+ const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
68
+ /** Truncate to PER_TASK_OUTPUT_CAP bytes, never exceeding the cap. */
69
+ function capOutput(output) {
70
+ if (Buffer.byteLength(output, "utf8") <= PER_TASK_OUTPUT_CAP)
71
+ return output;
72
+ let truncated = output.slice(0, PER_TASK_OUTPUT_CAP);
73
+ while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) {
74
+ truncated = truncated.slice(0, -1);
75
+ }
76
+ return truncated;
77
+ }
78
+ /**
79
+ * Run one grounded child stage and reduce its NDJSON stream into a StageResult.
80
+ * `ctx.lens` (when set, for a fanned-out review) selects the lens clause in the
81
+ * persona body and is threaded through the invocation builder.
82
+ */
83
+ export async function runStage(stage, ctx, deps) {
84
+ const spawnFn = deps.spawn ?? nodeSpawn;
85
+ const writePrompt = deps.writePrompt ?? defaultWritePrompt;
86
+ const resolveChild = deps.resolveChild ?? defaultResolveChild;
87
+ const personaStage = ctx.lens ? { ...stage, lens: ctx.lens } : stage;
88
+ // grounded defaults true; only the M6 blind eval lane passes grounded: false.
89
+ const body = (deps.personaBody ?? personaBody)(personaStage, { grounded: ctx.grounded !== false });
90
+ const { filePath, cleanup } = await writePrompt(body);
91
+ const result = {
92
+ stageId: stage.id,
93
+ agent: stage.agent,
94
+ exitCode: 0,
95
+ finalOutput: "",
96
+ usage: { ...EMPTY_USAGE },
97
+ stderr: "",
98
+ toolCalls: 0,
99
+ toolErrors: 0,
100
+ };
101
+ try {
102
+ const passthrough = buildStageInvocation(stage, {
103
+ ticket: ctx.ticket,
104
+ previous: ctx.previous,
105
+ promptFilePath: filePath,
106
+ lens: ctx.lens,
107
+ });
108
+ const { piCli, extensionPath } = resolveChild();
109
+ const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
110
+ // Fold events into a fixed-size accumulator as they stream — never retain
111
+ // the full array (that OOMs on a verbose stage; YAG-317 follow-up).
112
+ const acc = newEventAccumulator();
113
+ let eventCount = 0;
114
+ let stderr = "";
115
+ let wasAborted = false;
116
+ const maxLineBytes = deps.maxLineBytes ?? DEFAULT_MAX_STDOUT_LINE_BYTES;
117
+ let overlongLinesDropped = 0;
118
+ // Env-gated diagnostic (no-op unless YAGNI_PIPELINE_DEBUG_DIR set). Captures
119
+ // per-stage stream metrics — total bytes, max single NDJSON line, event count,
120
+ // interim heap — plus the full final output. Used to characterise the plan
121
+ // `length` truncation and the implement-stage OOM. Safe to leave in.
122
+ const DEBUG_DIR = process.env.YAGNI_PIPELINE_DEBUG_DIR;
123
+ const dbgLabel = ctx.lens ? `${stage.id}-${ctx.lens}` : stage.id;
124
+ let dbgTotalBytes = 0, dbgMaxLineBytes = 0, dbgLineCount = 0, dbgLastLogBytes = 0;
125
+ const dbgLog = (note) => {
126
+ if (!DEBUG_DIR)
127
+ return;
128
+ try {
129
+ fs.mkdirSync(DEBUG_DIR, { recursive: true });
130
+ const m = process.memoryUsage();
131
+ fs.appendFileSync(path.join(DEBUG_DIR, `${dbgLabel}.stream.log`), `${new Date().toISOString()} ${note} bytes=${dbgTotalBytes} maxLineBytes=${dbgMaxLineBytes} lines=${dbgLineCount} events=${eventCount} heapUsedMB=${Math.round(m.heapUsed / 1048576)} rssMB=${Math.round(m.rss / 1048576)}\n`);
132
+ }
133
+ catch { /* ignore */ }
134
+ };
135
+ const exitCode = await new Promise((resolve) => {
136
+ const proc = spawnFn(process.execPath, argv, {
137
+ cwd: deps.cwd,
138
+ ...(deps.env ? { env: deps.env } : {}),
139
+ shell: false,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ });
142
+ let buffer = "";
143
+ // `overlong` = we are mid-way through a line that already blew the cap; we
144
+ // discard incoming bytes until its terminating newline, then drop the line.
145
+ let overlong = false;
146
+ const processLine = (line) => {
147
+ if (DEBUG_DIR) {
148
+ const b = Buffer.byteLength(line, "utf8");
149
+ if (b > dbgMaxLineBytes)
150
+ dbgMaxLineBytes = b;
151
+ dbgLineCount++;
152
+ }
153
+ if (!line.trim())
154
+ return;
155
+ let event;
156
+ try {
157
+ event = JSON.parse(line);
158
+ }
159
+ catch {
160
+ return;
161
+ }
162
+ foldEvent(acc, event);
163
+ eventCount += 1;
164
+ deps.onEvent?.(event);
165
+ };
166
+ // Track real process exit explicitly. `ChildProcess.killed` is set the
167
+ // instant a signal is *sent*, not when the process exits — so it cannot
168
+ // gate the SIGKILL escalation (it is already true after SIGTERM). This
169
+ // flag flips only on the real `close`/`error`, so a child that ignores
170
+ // SIGTERM still gets force-killed after the grace window.
171
+ const signal = deps.signal;
172
+ let closed = false;
173
+ let killTimer;
174
+ const onAbort = () => {
175
+ wasAborted = true;
176
+ // pid guard: kill() on a child whose spawn FAILED resolves to pid 0 —
177
+ // the whole process group — and would take the parent down with it.
178
+ if (proc.pid)
179
+ proc.kill("SIGTERM");
180
+ killTimer = setTimeout(() => {
181
+ if (!closed && proc.pid)
182
+ proc.kill("SIGKILL");
183
+ }, 5000);
184
+ killTimer.unref?.();
185
+ };
186
+ const finish = (code) => {
187
+ closed = true;
188
+ if (killTimer)
189
+ clearTimeout(killTimer);
190
+ // Detach the abort listener on normal completion so it does not
191
+ // accumulate on the shared, long-lived pipeline signal across stages.
192
+ if (signal)
193
+ signal.removeEventListener("abort", onAbort);
194
+ resolve(code);
195
+ };
196
+ proc.stdout?.on("data", (data) => {
197
+ const s = data.toString();
198
+ if (DEBUG_DIR) {
199
+ dbgTotalBytes += Buffer.byteLength(s, "utf8");
200
+ if (dbgTotalBytes - dbgLastLogBytes > 20_000_000) {
201
+ dbgLastLogBytes = dbgTotalBytes;
202
+ dbgLog("stream");
203
+ }
204
+ }
205
+ // Incremental newline scan — never re-split a growing buffer (that was
206
+ // O(n²) and OOM-crashed on a runaway single line). `buffer` only ever
207
+ // holds the current *partial* line, and is capped at maxLineBytes: an
208
+ // over-cap line is dropped, not accumulated.
209
+ let start = 0;
210
+ for (;;) {
211
+ const nl = s.indexOf("\n", start);
212
+ if (nl === -1) {
213
+ if (!overlong) {
214
+ buffer += s.slice(start);
215
+ if (buffer.length > maxLineBytes) {
216
+ overlong = true;
217
+ buffer = "";
218
+ }
219
+ }
220
+ break;
221
+ }
222
+ const seg = s.slice(start, nl);
223
+ start = nl + 1;
224
+ if (overlong) {
225
+ overlong = false;
226
+ overlongLinesDropped += 1;
227
+ continue;
228
+ }
229
+ const line = buffer + seg;
230
+ buffer = "";
231
+ if (line.length > maxLineBytes) {
232
+ overlongLinesDropped += 1;
233
+ continue;
234
+ }
235
+ processLine(line);
236
+ }
237
+ });
238
+ proc.stderr?.on("data", (data) => {
239
+ stderr += data.toString();
240
+ });
241
+ proc.on("close", (code) => {
242
+ if (overlong) {
243
+ overlongLinesDropped += 1;
244
+ overlong = false;
245
+ buffer = "";
246
+ }
247
+ else if (buffer.trim())
248
+ processLine(buffer);
249
+ finish(code ?? 0);
250
+ });
251
+ proc.on("error", () => finish(1));
252
+ if (signal) {
253
+ if (signal.aborted)
254
+ onAbort();
255
+ else
256
+ signal.addEventListener("abort", onAbort);
257
+ }
258
+ });
259
+ result.exitCode = exitCode;
260
+ result.stderr = stderr;
261
+ result.finalOutput = capOutput(finalOutputFrom(acc));
262
+ result.usage = acc.usage;
263
+ // R3-a: tap tool-result counts into stage health. The orchestrator turns
264
+ // this ratio into a write-stage failure when most edits errored, so a
265
+ // clean-looking summary can't mask a silent partial implement.
266
+ result.toolCalls = acc.toolCalls;
267
+ result.toolErrors = acc.toolErrors;
268
+ if (acc.stopReason)
269
+ result.stopReason = acc.stopReason;
270
+ if (acc.errorMessage)
271
+ result.errorMessage = acc.errorMessage;
272
+ if (wasAborted)
273
+ result.stopReason = "aborted";
274
+ if (overlongLinesDropped > 0)
275
+ result.overlongLinesDropped = overlongLinesDropped;
276
+ if (DEBUG_DIR) {
277
+ try {
278
+ fs.mkdirSync(DEBUG_DIR, { recursive: true });
279
+ const finalOut = finalOutputFrom(acc);
280
+ fs.writeFileSync(path.join(DEBUG_DIR, `${dbgLabel}.summary.json`), JSON.stringify({
281
+ stageId: stage.id, lens: ctx.lens, exitCode,
282
+ stopReason: result.stopReason, usage: result.usage,
283
+ events_count: eventCount, total_stream_bytes: dbgTotalBytes,
284
+ max_line_bytes: dbgMaxLineBytes, line_count: dbgLineCount,
285
+ final_output_bytes: Buffer.byteLength(finalOut, "utf8"),
286
+ final_output: finalOut,
287
+ }, null, 2));
288
+ dbgLog("stage_end");
289
+ }
290
+ catch { /* ignore */ }
291
+ }
292
+ return result;
293
+ }
294
+ finally {
295
+ cleanup();
296
+ }
297
+ }
298
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Conservative secret redaction for captured command output before it enters
3
+ * findings, handoffs, or run records (invariant §0.9). The verify gate runs
4
+ * repo-defined typecheck/test commands whose output can echo connection
5
+ * strings, tokens, or key material (a failing DB test prints its DATABASE_URL,
6
+ * a debug line dumps process.env); that text flows into critical findings, the
7
+ * /go handoff, and the backend run record, so it is scrubbed FIRST.
8
+ *
9
+ * This mirrors the backend's `yagniCode/scrubSecrets.ts` pattern set verbatim
10
+ * (the "existing scrubSecrets path" of spec §3d) — the pipeline cannot import
11
+ * across packages, so the patterns are duplicated here; keep the two in sync.
12
+ * Over-redacts rather than under-redacts; pure; never throws.
13
+ */
14
+ export declare function scrubSecrets(text: string): string;
15
+ //# sourceMappingURL=scrubSecrets.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Conservative secret redaction for captured command output before it enters
3
+ * findings, handoffs, or run records (invariant §0.9). The verify gate runs
4
+ * repo-defined typecheck/test commands whose output can echo connection
5
+ * strings, tokens, or key material (a failing DB test prints its DATABASE_URL,
6
+ * a debug line dumps process.env); that text flows into critical findings, the
7
+ * /go handoff, and the backend run record, so it is scrubbed FIRST.
8
+ *
9
+ * This mirrors the backend's `yagniCode/scrubSecrets.ts` pattern set verbatim
10
+ * (the "existing scrubSecrets path" of spec §3d) — the pipeline cannot import
11
+ * across packages, so the patterns are duplicated here; keep the two in sync.
12
+ * Over-redacts rather than under-redacts; pure; never throws.
13
+ */
14
+ const PATTERNS = [
15
+ // Credentials embedded in connection-string URLs: scheme://user:password@host
16
+ // (the password segment has no secret-named key and is usually too short for the
17
+ // base64 rule, so it would otherwise survive — common in config/env diffs).
18
+ [/\b([a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+):[^\s:@/]+@/gi, "$1:[REDACTED]@"],
19
+ // Provider key prefixes (OpenAI, GitHub, Slack, Stripe, AWS access key id, etc.)
20
+ [/\b(sk-[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
21
+ // key = "value" / key: value for secret-named keys
22
+ [/\b([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|token|private[_-]?key|access[_-]?key)[A-Za-z0-9_]*)\b(\s*[:=]\s*)("[^"]+"|'[^']+'|`[^`]+`|[^\s"']+)/gi, "$1$2[REDACTED]"],
23
+ // Long base64-ish blobs (likely keys/JWTs)
24
+ [/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]"],
25
+ ];
26
+ export function scrubSecrets(text) {
27
+ let out = text;
28
+ for (const [re, repl] of PATTERNS)
29
+ out = out.replace(re, repl);
30
+ return out;
31
+ }
32
+ //# sourceMappingURL=scrubSecrets.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The declarative v1 stage list as DATA (spec §3 + §7.4) plus a selector.
3
+ *
4
+ * This is control-flow-as-data: map(standard) → plan(peak) →
5
+ * implement(standard) → review(peak, fanned over 3 lenses) → fix(standard).
6
+ * Routing follows the judgment, not the typing: the strongest `peak` tier on the
7
+ * steps that decide-what / catch-wrong (Plan, Review); the `standard` tier on the
8
+ * execution steps (Map, Implement, Fix). The cheaper `efficient` tier stays a
9
+ * selectable catalog tier but is intentionally not wired into a stage here.
10
+ *
11
+ * Peak on judgment is affordable precisely BECAUSE the judgment stages are short
12
+ * (YAG-380): plan plus the three review lenses is roughly 21c per round on
13
+ * advanced and 51c on peak, while the token-heavy stages that dominate a run
14
+ * stay down-tier. Quality of the output is the product, so the strongest model
15
+ * goes where being wrong is most expensive, not where the tokens are.
16
+ *
17
+ * `selectStages(lane)` returns the full list for v1 and is the seam where v2
18
+ * complexity-lanes plug in without a rewrite.
19
+ */
20
+ import type { PipelineStage, ReviewLens } from "./types.js";
21
+ /** The three adversarial review angles, fanned out per round (spec §3). */
22
+ export declare const REVIEW_LENSES: ReviewLens[];
23
+ /**
24
+ * The review lenses the round cannot proceed without (R2 partial-failure
25
+ * tolerance). When ONE of these is the lens that fails, the round fails honestly
26
+ * rather than proceeding on a partial review; a non-required lens failing only
27
+ * degrades the round (the survivors carry it, recorded as `degradedLenses`).
28
+ * Correctness is load-bearing: a change that might be wrong must never sail
29
+ * through just because the cheaper lenses passed. Tunable, widen to make another
30
+ * lens load-bearing.
31
+ */
32
+ export declare const REQUIRED_LENSES: ReviewLens[];
33
+ export declare const PIPELINE_V1: PipelineStage[];
34
+ /** The single review stage the orchestrator fans out across REVIEW_LENSES. */
35
+ export declare function reviewStage(): PipelineStage;
36
+ /**
37
+ * Select the stage list for a lane. v1 has exactly one lane ('full'); this is
38
+ * the identity selector and the extension point for v2 complexity-lanes.
39
+ */
40
+ export declare function selectStages(_lane?: "full"): PipelineStage[];
41
+ /**
42
+ * The findings-contract re-ask stage (spec §3e): when a HEALTHY lens returns
43
+ * non-empty prose with no ```findings fence, the orchestrator re-invokes it
44
+ * ONCE on the cheap `efficient` tier with this minimal prompt — a pure
45
+ * format-recovery pass, not a second review. The lens's original output rides
46
+ * in as `{previous}`. Tools are the minimal REAL list `["read"]`:
47
+ * `buildStageInvocation` joins `--tools` with a comma, so an empty array would
48
+ * emit the broken `--tools ""`.
49
+ */
50
+ export declare function reaskStage(review: PipelineStage): PipelineStage;
51
+ //# sourceMappingURL=stages.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The declarative v1 stage list as DATA (spec §3 + §7.4) plus a selector.
3
+ *
4
+ * This is control-flow-as-data: map(standard) → plan(peak) →
5
+ * implement(standard) → review(peak, fanned over 3 lenses) → fix(standard).
6
+ * Routing follows the judgment, not the typing: the strongest `peak` tier on the
7
+ * steps that decide-what / catch-wrong (Plan, Review); the `standard` tier on the
8
+ * execution steps (Map, Implement, Fix). The cheaper `efficient` tier stays a
9
+ * selectable catalog tier but is intentionally not wired into a stage here.
10
+ *
11
+ * Peak on judgment is affordable precisely BECAUSE the judgment stages are short
12
+ * (YAG-380): plan plus the three review lenses is roughly 21c per round on
13
+ * advanced and 51c on peak, while the token-heavy stages that dominate a run
14
+ * stay down-tier. Quality of the output is the product, so the strongest model
15
+ * goes where being wrong is most expensive, not where the tokens are.
16
+ *
17
+ * `selectStages(lane)` returns the full list for v1 and is the seam where v2
18
+ * complexity-lanes plug in without a rewrite.
19
+ */
20
+ /** The three adversarial review angles, fanned out per round (spec §3). */
21
+ export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
22
+ /**
23
+ * The review lenses the round cannot proceed without (R2 partial-failure
24
+ * tolerance). When ONE of these is the lens that fails, the round fails honestly
25
+ * rather than proceeding on a partial review; a non-required lens failing only
26
+ * degrades the round (the survivors carry it, recorded as `degradedLenses`).
27
+ * Correctness is load-bearing: a change that might be wrong must never sail
28
+ * through just because the cheaper lenses passed. Tunable, widen to make another
29
+ * lens load-bearing.
30
+ */
31
+ export const REQUIRED_LENSES = ["correctness"];
32
+ /** Read-only recon + grounding tools shared by scout and reviewer. */
33
+ const READ_TOOLS = ["read", "grep", "find", "ls", "bash"];
34
+ /** Full write tools for the worker stages. */
35
+ const WRITE_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls"];
36
+ export const PIPELINE_V1 = [
37
+ {
38
+ id: "map",
39
+ agent: "scout",
40
+ model: "standard",
41
+ tools: [...READ_TOOLS, "ask_yagni"],
42
+ taskTemplate: "Map the codebase for this ticket and return a compressed context brief: " +
43
+ "where the relevant code lives, the conventions in play, and anything " +
44
+ "ask_yagni surfaces about how this company wants this done.\n\nTicket: {ticket}",
45
+ },
46
+ {
47
+ id: "plan",
48
+ agent: "planner",
49
+ model: "peak",
50
+ // Read-only recon (read/grep/find/ls) so the planner can actually navigate
51
+ // instead of flailing on a single `read` and ending mid-exploration; NO bash
52
+ // (it must not build/run), and ask_yagni for grounding.
53
+ tools: ["read", "grep", "find", "ls", "ask_yagni"],
54
+ taskTemplate: "Turn the ticket and the repo map into an ordered, grounded implementation " +
55
+ "plan. Call ask_yagni before inferring a convention.\n\n" +
56
+ "Ticket: {ticket}\n\nRepo map:\n{previous}",
57
+ },
58
+ {
59
+ id: "implement",
60
+ agent: "worker",
61
+ model: "standard",
62
+ tools: [...WRITE_TOOLS, "ask_yagni", "record_decision"],
63
+ taskTemplate: "Implement the plan. Write the code against it, and call record_decision " +
64
+ "for any product-intent call you are forced to make.\n\nPlan:\n{previous}",
65
+ },
66
+ {
67
+ id: "review",
68
+ agent: "reviewer",
69
+ model: "peak",
70
+ tools: [...READ_TOOLS, "ask_yagni", "review_business_match"],
71
+ taskTemplate: "Review the implementation adversarially. Diff/summary of the change:\n{previous}",
72
+ },
73
+ {
74
+ id: "fix",
75
+ agent: "worker",
76
+ model: "standard",
77
+ tools: [...WRITE_TOOLS, "ask_yagni"],
78
+ taskTemplate: "Reconcile ONLY the blocking review findings against the plan. Leave " +
79
+ "non-blocking findings alone.\n\nBlocking findings:\n{previous}",
80
+ },
81
+ ];
82
+ /** The single review stage the orchestrator fans out across REVIEW_LENSES. */
83
+ export function reviewStage() {
84
+ const stage = PIPELINE_V1.find((s) => s.id === "review");
85
+ if (!stage)
86
+ throw new Error("PIPELINE_V1 is missing the review stage");
87
+ return stage;
88
+ }
89
+ /**
90
+ * Select the stage list for a lane. v1 has exactly one lane ('full'); this is
91
+ * the identity selector and the extension point for v2 complexity-lanes.
92
+ */
93
+ export function selectStages(_lane = "full") {
94
+ return PIPELINE_V1;
95
+ }
96
+ /**
97
+ * The findings-contract re-ask stage (spec §3e): when a HEALTHY lens returns
98
+ * non-empty prose with no ```findings fence, the orchestrator re-invokes it
99
+ * ONCE on the cheap `efficient` tier with this minimal prompt — a pure
100
+ * format-recovery pass, not a second review. The lens's original output rides
101
+ * in as `{previous}`. Tools are the minimal REAL list `["read"]`:
102
+ * `buildStageInvocation` joins `--tools` with a comma, so an empty array would
103
+ * emit the broken `--tools ""`.
104
+ */
105
+ export function reaskStage(review) {
106
+ return {
107
+ ...review,
108
+ model: "efficient",
109
+ tools: ["read"],
110
+ taskTemplate: "Re-emit ONLY the ```findings block for the review you just produced:\n{previous}",
111
+ };
112
+ }
113
+ //# sourceMappingURL=stages.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pre-resolve the /go {ticket} slot to the REAL ticket before planning.
3
+ *
4
+ * `/go YAG-234` interpolates the raw arg string into every stage's {ticket}
5
+ * prompt slot, so without this the scout/planner plan blind on the literal
6
+ * "YAG-234". This fail-soft helper asks YAGNI to resolve the reference (POST
7
+ * /api/yagni-code/ask with a structured `ticketRef`), and on a hit returns a
8
+ * formatted brief carrying the VERBATIM ticket title + description (the
9
+ * acceptance criteria live inside the body) for the {ticket} slot.
10
+ *
11
+ * It is the read-path twin of {@link makeRunSession}'s fetch seam (baseUrl +
12
+ * injectable getToken + fetchImpl, via resilientFetch). The server resolves the
13
+ * ticket with a live, workspace-scoped connector READ, never an LLM call, so
14
+ * this never bills the ask compose path and never risks a model-rewritten body.
15
+ *
16
+ * FAIL-SOFT by contract: a non-identifier arg, an unconnected ticket connector,
17
+ * a miss, or any transport error returns null so /go passes the raw arg through
18
+ * unchanged (today's honest blind) rather than planning on a wrong body.
19
+ */
20
+ export interface ResolveTicketBriefOptions {
21
+ baseUrl: string;
22
+ getToken: () => string | undefined;
23
+ fetchImpl?: typeof fetch;
24
+ signal?: AbortSignal;
25
+ /** Backoff seams threaded into resilientFetch so the retry path is instant under test. */
26
+ sleep?: (ms: number) => Promise<void>;
27
+ random?: () => number;
28
+ }
29
+ /**
30
+ * Resolve a /go arg to a verbatim ticket brief, or null to keep the raw arg.
31
+ * Never throws; a non-identifier arg short-circuits before any network call.
32
+ */
33
+ export declare function resolveTicketBrief(opts: ResolveTicketBriefOptions, rawArg: string): Promise<string | null>;
34
+ //# sourceMappingURL=ticketResolution.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Pre-resolve the /go {ticket} slot to the REAL ticket before planning.
3
+ *
4
+ * `/go YAG-234` interpolates the raw arg string into every stage's {ticket}
5
+ * prompt slot, so without this the scout/planner plan blind on the literal
6
+ * "YAG-234". This fail-soft helper asks YAGNI to resolve the reference (POST
7
+ * /api/yagni-code/ask with a structured `ticketRef`), and on a hit returns a
8
+ * formatted brief carrying the VERBATIM ticket title + description (the
9
+ * acceptance criteria live inside the body) for the {ticket} slot.
10
+ *
11
+ * It is the read-path twin of {@link makeRunSession}'s fetch seam (baseUrl +
12
+ * injectable getToken + fetchImpl, via resilientFetch). The server resolves the
13
+ * ticket with a live, workspace-scoped connector READ, never an LLM call, so
14
+ * this never bills the ask compose path and never risks a model-rewritten body.
15
+ *
16
+ * FAIL-SOFT by contract: a non-identifier arg, an unconnected ticket connector,
17
+ * a miss, or any transport error returns null so /go passes the raw arg through
18
+ * unchanged (today's honest blind) rather than planning on a wrong body.
19
+ */
20
+ import { DEFAULT_FETCH_POLICY, resilientFetch } from "../resilientFetch.js";
21
+ /** A Linear-style identifier ("YAG-234"); only these are worth a resolve attempt. */
22
+ const IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
23
+ /** Format the resolved ticket into the verbatim brief injected into {ticket}. */
24
+ function formatBrief(ticket) {
25
+ const header = `${ticket.identifier}: ${ticket.title}`;
26
+ const body = (ticket.description ?? "").trim();
27
+ return body.length > 0 ? `${header}\n\n${body}` : header;
28
+ }
29
+ /**
30
+ * Resolve a /go arg to a verbatim ticket brief, or null to keep the raw arg.
31
+ * Never throws; a non-identifier arg short-circuits before any network call.
32
+ */
33
+ export async function resolveTicketBrief(opts, rawArg) {
34
+ const ref = rawArg.trim();
35
+ // Only an identifier-shaped arg is a ticket reference; free-text task
36
+ // descriptions pass straight through so /go uses them verbatim.
37
+ if (!IDENTIFIER_RE.test(ref))
38
+ return null;
39
+ let res;
40
+ try {
41
+ res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/ask`, {
42
+ method: "POST",
43
+ headers: {
44
+ "content-type": "application/json",
45
+ authorization: `Bearer ${opts.getToken() ?? ""}`,
46
+ },
47
+ body: JSON.stringify({ ticketRef: ref }),
48
+ }, {
49
+ fetchImpl: opts.fetchImpl,
50
+ policy: DEFAULT_FETCH_POLICY,
51
+ ...(opts.signal ? { signal: opts.signal } : {}),
52
+ ...(opts.sleep ? { sleep: opts.sleep } : {}),
53
+ ...(opts.random ? { random: opts.random } : {}),
54
+ });
55
+ }
56
+ catch {
57
+ // Unreachable backend after retries: degrade to blind (raw arg).
58
+ return null;
59
+ }
60
+ if (!res.ok)
61
+ return null;
62
+ let data;
63
+ try {
64
+ data = (await res.json());
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ const ticket = data.ticket;
70
+ if (!ticket || typeof ticket.identifier !== "string" || typeof ticket.title !== "string") {
71
+ return null;
72
+ }
73
+ return formatBrief(ticket);
74
+ }
75
+ //# sourceMappingURL=ticketResolution.js.map