@yagni-app/code 0.3.1 → 0.3.3

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 (65) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/crashReport.d.ts +12 -0
  3. package/dist/crashReport.js +28 -1
  4. package/dist/extension/crashReport.d.ts +18 -0
  5. package/dist/extension/crashReport.js +35 -2
  6. package/dist/extension/footer.d.ts +1 -1
  7. package/dist/extension/hooks.d.ts +111 -0
  8. package/dist/extension/hooks.js +666 -0
  9. package/dist/extension/index.d.ts +13 -6
  10. package/dist/extension/index.js +57 -7
  11. package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
  12. package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
  13. package/dist/extension/permission/dbReadPolicy.js +227 -0
  14. package/dist/extension/{execPolicy.js → permission/execPolicy.js} +99 -8
  15. package/dist/extension/{permission.d.ts → permission/gate.d.ts} +10 -3
  16. package/dist/extension/{permission.js → permission/gate.js} +156 -9
  17. package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
  18. package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
  19. package/dist/extension/permission/index.d.ts +14 -0
  20. package/dist/extension/permission/index.js +14 -0
  21. package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
  22. package/dist/extension/permission/packageManagerPolicy.js +170 -0
  23. package/dist/extension/pipeline/activityFeed.js +19 -5
  24. package/dist/extension/pipeline/checker.d.ts +99 -0
  25. package/dist/extension/pipeline/checker.js +238 -0
  26. package/dist/extension/pipeline/fanout.d.ts +116 -0
  27. package/dist/extension/pipeline/fanout.js +248 -0
  28. package/dist/extension/pipeline/fanoutBeats.d.ts +31 -0
  29. package/dist/extension/pipeline/fanoutBeats.js +86 -0
  30. package/dist/extension/pipeline/goCommand.d.ts +14 -0
  31. package/dist/extension/pipeline/goCommand.js +38 -1
  32. package/dist/extension/pipeline/headlessGo.d.ts +163 -0
  33. package/dist/extension/pipeline/headlessGo.js +333 -0
  34. package/dist/extension/pipeline/invocation.d.ts +31 -3
  35. package/dist/extension/pipeline/invocation.js +37 -3
  36. package/dist/extension/pipeline/mission.d.ts +55 -0
  37. package/dist/extension/pipeline/mission.js +70 -0
  38. package/dist/extension/pipeline/orchestrator.d.ts +48 -3
  39. package/dist/extension/pipeline/orchestrator.js +450 -9
  40. package/dist/extension/pipeline/personas.d.ts +16 -1
  41. package/dist/extension/pipeline/personas.js +118 -7
  42. package/dist/extension/pipeline/runSession.d.ts +45 -1
  43. package/dist/extension/pipeline/runState.d.ts +57 -12
  44. package/dist/extension/pipeline/runState.js +60 -18
  45. package/dist/extension/pipeline/runner.js +10 -1
  46. package/dist/extension/pipeline/stages.d.ts +84 -7
  47. package/dist/extension/pipeline/stages.js +166 -0
  48. package/dist/extension/pipeline/tierCap.d.ts +32 -0
  49. package/dist/extension/pipeline/tierCap.js +57 -0
  50. package/dist/extension/pipeline/types.d.ts +130 -1
  51. package/dist/extension/pipeline/types.js +17 -0
  52. package/dist/extension/pipeline/verify.d.ts +86 -3
  53. package/dist/extension/pipeline/verify.js +175 -6
  54. package/dist/extension/subagents.js +13 -0
  55. package/dist/extension/turnLog.d.ts +38 -0
  56. package/dist/extension/turnLog.js +93 -0
  57. package/dist/goHeadless.d.ts +75 -0
  58. package/dist/goHeadless.js +132 -0
  59. package/dist/paths.d.ts +9 -0
  60. package/dist/paths.js +12 -0
  61. package/dist/promptEnrichment.d.ts +1 -1
  62. package/dist/promptEnrichment.js +1 -1
  63. package/package.json +2 -2
  64. /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
  65. /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
@@ -9,9 +9,11 @@
9
9
  * the fix stage gets the real errors.
10
10
  *
11
11
  * `detectVerifyCommands`, `detectVerifyTestCommands`, `parseVerifyFailures`,
12
- * `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths` and
13
- * `resolvePackageDirs` are PURE (unit-tested directly). `makeRunVerify` is the
14
- * thin impure runner; it shells out (pi's ExtensionContext exposes no exec,
12
+ * `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths`,
13
+ * `resolvePackageDirs` and `resolveClaimDirs` are PURE (unit-tested directly).
14
+ * `makeRunVerify` (the whole gate) and `makeWorkstreamCheck` (the implement
15
+ * diamond's per-workstream scoped typecheck, spec decision 6) are the thin impure
16
+ * runners; they shell out (pi's ExtensionContext exposes no exec,
15
17
  * mirroring workspace.ts) and is wrapped with R1's timeout composition
16
18
  * (`composeAbortSignal`) so a slow or flaky suite is bounded.
17
19
  *
@@ -215,6 +217,22 @@ export declare function classifyTestFailure(output: string, exitCode: number): T
215
217
  * throws; always emits at least one finding on a non-zero exit.
216
218
  */
217
219
  export declare function parseVerifyFailures(output: string, exitCode: number, label: string): Finding[];
220
+ /**
221
+ * One finding's file as the REPO ROOT sees it.
222
+ *
223
+ * The gate is scoped by design: a monorepo package's `typecheck` / `test` runs
224
+ * IN that package's dir, so its diagnostics come back package-relative
225
+ * (`src/routes/x.ts`). Everything downstream speaks repo-root-relative paths —
226
+ * the partition's file claims, the handoff, the fix loop's attribution
227
+ * (`checker.ts`'s `ownerOf`) — so an un-lifted path can never match the
228
+ * workstream that owns it and the implicated builder is never re-engaged.
229
+ *
230
+ * Lifts a package-relative path onto the command's dir, makes an absolute path
231
+ * under the repo root relative to it, and leaves anything else (a path outside
232
+ * the tree, a path already carrying the package prefix) untouched. Idempotent,
233
+ * so a finding that passes through twice is not double-prefixed.
234
+ */
235
+ export declare function toRepoRelativePath(file: string, repoRoot: string, commandCwd: string): string;
218
236
  /** Result of actually executing the resolved command. */
219
237
  export interface VerifyExecResult {
220
238
  code: number;
@@ -268,6 +286,71 @@ export interface MakeRunVerifyOpts {
268
286
  /** Test-half env override for determinism in tests (default: buildTestEnv() per run). */
269
287
  testEnv?: NodeJS.ProcessEnv;
270
288
  }
289
+ /**
290
+ * One workstream as the scoped checker sees it: the name its findings are
291
+ * attributed to, and the prefix claims the partition gave it.
292
+ */
293
+ export interface WorkstreamCheckTarget {
294
+ name: string;
295
+ files: string[];
296
+ }
297
+ /**
298
+ * One workstream's scoped typecheck verdict (spec decision 6, first half).
299
+ * `ran: false` is the honest fail-open: the claims resolved to no package with a
300
+ * usable build check, so the workstream is UNCHECKED rather than falsely clean.
301
+ */
302
+ export interface WorkstreamCheckResult {
303
+ name: string;
304
+ ran: boolean;
305
+ ok: boolean;
306
+ command?: string;
307
+ reason?: string;
308
+ findings: Finding[];
309
+ }
310
+ /**
311
+ * A workstream's claims resolved to package dirs. Claims are PREFIXES and a claim
312
+ * is very often a directory (`packages/backend/src/routes`), while
313
+ * {@link resolvePackageDirs} takes the FILE paths git porcelain emits and walks up
314
+ * from each one's dirname. Appending a probe segment makes a directory claim walk
315
+ * up from the directory ITSELF, so a claim that IS a package dir resolves to that
316
+ * package instead of its parent; a claim that is a real file still resolves exactly
317
+ * as it does for the full gate. Pure (the `readFile` seam keeps it so).
318
+ */
319
+ export declare function resolveClaimDirs(repoRoot: string, claims: string[], readFile: (path: string) => string): string[];
320
+ export interface MakeWorkstreamCheckOpts {
321
+ /** Claims → package dirs. Default: {@link resolveClaimDirs} over the real fs. */
322
+ claimDirs?: (cwd: string, claims: string[]) => string[];
323
+ /** The build check(s) for those dirs. Default: the same detection the full gate uses. */
324
+ detect?: (cwd: string, claimDirs: string[]) => VerifyCommand[];
325
+ exec?: VerifyExec;
326
+ timeoutMs?: number;
327
+ }
328
+ /**
329
+ * The per-workstream scoped check the implement diamond runs right after the fan
330
+ * (spec decision 6): the TYPECHECK half only, run in the package dirs a
331
+ * workstream's own claims resolve to, so a broken workstream is caught WITH
332
+ * attribution instead of surfacing later as an anonymous failure of the merged
333
+ * tree. The test half is deliberately not run here — the repo's tests run ONCE, on
334
+ * the merged candidate, through {@link makeRunVerify}.
335
+ *
336
+ * Three rules keep the attribution honest:
337
+ * - a command is kept only when it runs IN one of the workstream's own claim dirs.
338
+ * {@link detectVerifyCommands} falls back to the repo-root script when nothing
339
+ * scoped qualifies, and a repo-wide typecheck blamed on one workstream is
340
+ * exactly the false attribution this check exists to avoid.
341
+ * - identical commands are executed ONCE per pass and their result shared, so two
342
+ * workstreams inside the same package do not typecheck it twice.
343
+ * - a workstream is tagged only with the failures its OWN claims cover. A shared
344
+ * package's check can fail on its sibling's files (or on files neither claims):
345
+ * tagging every target with the whole output would report a healthy workstream
346
+ * broken and defeat the checker's file+line+message dedupe against the full
347
+ * verify. Failures outside the target's claims surface ONCE, untagged, so
348
+ * {@link attributeFindings} routes them by file or hands them to the synthesizer.
349
+ *
350
+ * Fail-open everywhere, like the full gate: an unrunnable check records a reason
351
+ * and produces no findings.
352
+ */
353
+ export declare function makeWorkstreamCheck(opts?: MakeWorkstreamCheckOpts): (cwd: string, targets: WorkstreamCheckTarget[], signal?: AbortSignal) => Promise<WorkstreamCheckResult[]>;
271
354
  /**
272
355
  * Build the runVerify function injected into the orchestrator. It discovers the
273
356
  * changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
@@ -9,9 +9,11 @@
9
9
  * the fix stage gets the real errors.
10
10
  *
11
11
  * `detectVerifyCommands`, `detectVerifyTestCommands`, `parseVerifyFailures`,
12
- * `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths` and
13
- * `resolvePackageDirs` are PURE (unit-tested directly). `makeRunVerify` is the
14
- * thin impure runner; it shells out (pi's ExtensionContext exposes no exec,
12
+ * `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths`,
13
+ * `resolvePackageDirs` and `resolveClaimDirs` are PURE (unit-tested directly).
14
+ * `makeRunVerify` (the whole gate) and `makeWorkstreamCheck` (the implement
15
+ * diamond's per-workstream scoped typecheck, spec decision 6) are the thin impure
16
+ * runners; they shell out (pi's ExtensionContext exposes no exec,
15
17
  * mirroring workspace.ts) and is wrapped with R1's timeout composition
16
18
  * (`composeAbortSignal`) so a slow or flaky suite is bounded.
17
19
  *
@@ -53,6 +55,7 @@ import { readFileSync } from "node:fs";
53
55
  // and would break the walk-up termination check.
54
56
  import { basename, dirname, join } from "node:path/posix";
55
57
  import { killTreeSync, trackChild } from "./childRegistry.js";
58
+ import { claimCovers } from "./fanout.js";
56
59
  import { composeAbortSignal } from "./resilience.js";
57
60
  import { scrubSecrets } from "./scrubSecrets.js";
58
61
  import { snapshotWorkspace } from "./workspace.js";
@@ -544,6 +547,49 @@ export function parseVerifyFailures(output, exitCode, label) {
544
547
  },
545
548
  ];
546
549
  }
550
+ /** Trim a trailing slash and normalise separators for the path math below. */
551
+ const normDir = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
552
+ /** Does this look like an absolute path on either platform? */
553
+ const isAbsolutePath = (p) => p.startsWith("/") || /^[A-Za-z]:\//.test(p);
554
+ /**
555
+ * One finding's file as the REPO ROOT sees it.
556
+ *
557
+ * The gate is scoped by design: a monorepo package's `typecheck` / `test` runs
558
+ * IN that package's dir, so its diagnostics come back package-relative
559
+ * (`src/routes/x.ts`). Everything downstream speaks repo-root-relative paths —
560
+ * the partition's file claims, the handoff, the fix loop's attribution
561
+ * (`checker.ts`'s `ownerOf`) — so an un-lifted path can never match the
562
+ * workstream that owns it and the implicated builder is never re-engaged.
563
+ *
564
+ * Lifts a package-relative path onto the command's dir, makes an absolute path
565
+ * under the repo root relative to it, and leaves anything else (a path outside
566
+ * the tree, a path already carrying the package prefix) untouched. Idempotent,
567
+ * so a finding that passes through twice is not double-prefixed.
568
+ */
569
+ export function toRepoRelativePath(file, repoRoot, commandCwd) {
570
+ const raw = file.replace(/\\/g, "/").trim();
571
+ if (!raw)
572
+ return file;
573
+ const root = normDir(repoRoot);
574
+ if (isAbsolutePath(raw)) {
575
+ return raw.startsWith(`${root}/`) ? raw.slice(root.length + 1) : raw;
576
+ }
577
+ const cleaned = raw.replace(/^\.\/+/, "");
578
+ const cwd = normDir(commandCwd);
579
+ if (cwd === root || !cwd.startsWith(`${root}/`))
580
+ return cleaned;
581
+ const prefix = cwd.slice(root.length + 1);
582
+ if (cleaned === prefix || cleaned.startsWith(`${prefix}/`))
583
+ return cleaned;
584
+ return `${prefix}/${cleaned}`;
585
+ }
586
+ /** {@link toRepoRelativePath} applied to a finding's location, if it has one. */
587
+ function liftFinding(finding, repoRoot, commandCwd) {
588
+ if (!finding.file)
589
+ return finding;
590
+ const file = toRepoRelativePath(finding.file, repoRoot, commandCwd);
591
+ return file === finding.file ? finding : { ...finding, file };
592
+ }
547
593
  const SAFE_ENV_KEYS = [
548
594
  "PATH",
549
595
  "HOME",
@@ -644,6 +690,128 @@ export function makeDefaultExec(opts = {}) {
644
690
  });
645
691
  }
646
692
  const defaultExec = makeDefaultExec();
693
+ /**
694
+ * A workstream's claims resolved to package dirs. Claims are PREFIXES and a claim
695
+ * is very often a directory (`packages/backend/src/routes`), while
696
+ * {@link resolvePackageDirs} takes the FILE paths git porcelain emits and walks up
697
+ * from each one's dirname. Appending a probe segment makes a directory claim walk
698
+ * up from the directory ITSELF, so a claim that IS a package dir resolves to that
699
+ * package instead of its parent; a claim that is a real file still resolves exactly
700
+ * as it does for the full gate. Pure (the `readFile` seam keeps it so).
701
+ */
702
+ export function resolveClaimDirs(repoRoot, claims, readFile) {
703
+ const probes = claims
704
+ .map((c) => c.trim().replace(/^\.\/+/, "").replace(/\/+$/, ""))
705
+ .filter((c) => c.length > 0 && c !== ".")
706
+ .map((c) => `${c}/.claim`);
707
+ return resolvePackageDirs(repoRoot, probes, readFile);
708
+ }
709
+ /** Attribute a scoped finding to the workstream whose claims produced it. */
710
+ function tagFinding(finding, name) {
711
+ return { ...finding, message: `[${name}] ${finding.message}` };
712
+ }
713
+ /**
714
+ * The per-workstream scoped check the implement diamond runs right after the fan
715
+ * (spec decision 6): the TYPECHECK half only, run in the package dirs a
716
+ * workstream's own claims resolve to, so a broken workstream is caught WITH
717
+ * attribution instead of surfacing later as an anonymous failure of the merged
718
+ * tree. The test half is deliberately not run here — the repo's tests run ONCE, on
719
+ * the merged candidate, through {@link makeRunVerify}.
720
+ *
721
+ * Three rules keep the attribution honest:
722
+ * - a command is kept only when it runs IN one of the workstream's own claim dirs.
723
+ * {@link detectVerifyCommands} falls back to the repo-root script when nothing
724
+ * scoped qualifies, and a repo-wide typecheck blamed on one workstream is
725
+ * exactly the false attribution this check exists to avoid.
726
+ * - identical commands are executed ONCE per pass and their result shared, so two
727
+ * workstreams inside the same package do not typecheck it twice.
728
+ * - a workstream is tagged only with the failures its OWN claims cover. A shared
729
+ * package's check can fail on its sibling's files (or on files neither claims):
730
+ * tagging every target with the whole output would report a healthy workstream
731
+ * broken and defeat the checker's file+line+message dedupe against the full
732
+ * verify. Failures outside the target's claims surface ONCE, untagged, so
733
+ * {@link attributeFindings} routes them by file or hands them to the synthesizer.
734
+ *
735
+ * Fail-open everywhere, like the full gate: an unrunnable check records a reason
736
+ * and produces no findings.
737
+ */
738
+ export function makeWorkstreamCheck(opts = {}) {
739
+ const claimDirs = opts.claimDirs ?? ((cwd, claims) => resolveClaimDirs(cwd, claims, (p) => readFileSync(p, "utf8")));
740
+ const detect = opts.detect ?? ((cwd, dirs) => detectVerifyCommands(cwd, { changedDirs: dirs }));
741
+ const exec = opts.exec ?? defaultExec;
742
+ const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
743
+ return async function runWorkstreamCheck(cwd, targets, signal) {
744
+ const runs = new Map();
745
+ // Failures a target's own claims do not cover are emitted exactly once per
746
+ // command, untagged, by whichever target reaches the command first.
747
+ const uncoveredEmitted = new Set();
748
+ const results = [];
749
+ for (const target of targets) {
750
+ const dirs = claimDirs(cwd, target.files);
751
+ const commands = dirs.length > 0 ? detect(cwd, dirs).filter((c) => dirs.includes(c.cwd)) : [];
752
+ if (commands.length === 0) {
753
+ results.push({
754
+ name: target.name,
755
+ ran: false,
756
+ ok: true,
757
+ reason: "no scoped build check for its claimed packages",
758
+ findings: [],
759
+ });
760
+ continue;
761
+ }
762
+ const findings = [];
763
+ const reasons = [];
764
+ const labels = [];
765
+ let anyRan = false;
766
+ for (const command of commands) {
767
+ const memoKey = `${command.cwd}\u0000${command.argv.join(" ")}`;
768
+ let run = runs.get(memoKey);
769
+ if (!run) {
770
+ const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
771
+ try {
772
+ const { code, output } = await exec(command.argv, command.cwd, composed);
773
+ run = { code, output: scrubSecrets(output) };
774
+ }
775
+ catch (err) {
776
+ run = { error: scrubSecrets(err instanceof Error ? err.message : String(err)) };
777
+ }
778
+ runs.set(memoKey, run);
779
+ }
780
+ if ("error" in run) {
781
+ reasons.push(`check (${command.label}) could not run: ${run.error}`);
782
+ continue;
783
+ }
784
+ if (run.code !== 0) {
785
+ const infra = classifyVerifyInfraFailure(run.output, run.code);
786
+ if (infra) {
787
+ reasons.push(`check (${command.label}) could not run: ${infra}`);
788
+ continue;
789
+ }
790
+ }
791
+ anyRan = true;
792
+ labels.push(command.label);
793
+ if (run.code !== 0) {
794
+ const parsed = parseVerifyFailures(run.output, run.code, command.label).map((f) => liftFinding(f, cwd, command.cwd));
795
+ const covered = parsed.filter((f) => f.file != null && target.files.some((claim) => claimCovers(claim, f.file)));
796
+ findings.push(...covered.map((f) => tagFinding(f, target.name)));
797
+ if (!uncoveredEmitted.has(memoKey)) {
798
+ uncoveredEmitted.add(memoKey);
799
+ findings.push(...parsed.filter((f) => !covered.includes(f)));
800
+ }
801
+ }
802
+ }
803
+ results.push({
804
+ name: target.name,
805
+ ran: anyRan,
806
+ ok: findings.length === 0,
807
+ ...(labels.length > 0 ? { command: [...new Set(labels)].join(", ") } : {}),
808
+ ...(reasons.length > 0 ? { reason: reasons.join("; ") } : {}),
809
+ findings,
810
+ });
811
+ }
812
+ return results;
813
+ };
814
+ }
647
815
  /**
648
816
  * Build the runVerify function injected into the orchestrator. It discovers the
649
817
  * changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
@@ -710,8 +878,9 @@ export function makeRunVerify(opts = {}) {
710
878
  }
711
879
  anyRan = true;
712
880
  typecheckLabels.push(command.label);
713
- if (code !== 0)
714
- findings.push(...parseVerifyFailures(output, code, command.label));
881
+ if (code !== 0) {
882
+ findings.push(...parseVerifyFailures(output, code, command.label).map((f) => liftFinding(f, cwd, command.cwd)));
883
+ }
715
884
  }
716
885
  catch (err) {
717
886
  const msg = err instanceof Error ? err.message : String(err);
@@ -754,7 +923,7 @@ export function makeRunVerify(opts = {}) {
754
923
  testLabels.push(command.label);
755
924
  if (cls.kind === "failures") {
756
925
  const room = Math.max(0, MAX_LOCATED_FINDINGS - testFindingCount);
757
- const take = cls.findings.slice(0, room);
926
+ const take = cls.findings.slice(0, room).map((f) => liftFinding(f, cwd, command.cwd));
758
927
  testFindingCount += take.length;
759
928
  findings.push(...take);
760
929
  }
@@ -72,6 +72,8 @@ const GENERAL_BODY = `You are a capable software-engineering subagent with a fre
72
72
 
73
73
  You are grounded in how THIS company works: call ask_yagni before inferring a convention, an ownership rule, or anything organization-specific.
74
74
 
75
+ Never fabricate file paths, contents, or findings. If you cannot find something, say so.
76
+
75
77
  Your final message is your report back to the driving agent, which has NOT seen what you read or did. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
76
78
  const GENERAL_AGENT = {
77
79
  name: GENERAL_AGENT_NAME,
@@ -87,6 +89,11 @@ not write code and you do not run commands; you read and report.
87
89
  You are grounded in how THIS company works: call ask_yagni before inferring a
88
90
  convention, an ownership rule, or anything organization-specific.
89
91
 
92
+ Never fabricate file paths, contents, or code. Every path you cite must be
93
+ one you actually read with a tool. If you cannot find something, say "not
94
+ found" — a plausible-sounding invention is worse than no answer because the
95
+ driving agent trusts your report.
96
+
90
97
  Your final message is your report back to the driving agent, which has NOT
91
98
  seen what you read. Make it compressed and complete: exact file paths, the
92
99
  key excerpts, and a one-paragraph map of how the pieces relate. Say what you
@@ -112,6 +119,9 @@ your final message instead of guessing.
112
119
  You are grounded in how THIS company works: call ask_yagni before inferring a
113
120
  convention, an ownership rule, or anything organization-specific.
114
121
 
122
+ Never fabricate file paths or results. Report what you actually did and what
123
+ you actually found.
124
+
115
125
  Your final message is your report back to the driving agent, which has NOT
116
126
  seen what you did. List every file you touched, what changed in each, the
117
127
  commands you ran with their outcomes, and anything you deliberately left
@@ -141,6 +151,9 @@ verifiers.
141
151
  You are grounded in how THIS company works: call ask_yagni before inferring
142
152
  a convention, an ownership rule, or anything organization-specific.
143
153
 
154
+ Never fabricate file paths or findings. If you could not verify something,
155
+ say exactly what you tried and why you could not.
156
+
144
157
  Your final message is your verdict back to the driving agent, which has NOT
145
158
  seen what you read. Format:
146
159
  ## Verdict
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Client-side turn-lifecycle log — makes a "stuck on Working…" turn legible
3
+ * without spelunking the raw session `.jsonl`.
4
+ *
5
+ * A session write-only WAL, not a buffered in-memory structure: the whole
6
+ * point is that a turn which STARTS but never ENDS (the silent-freeze failure
7
+ * this log exists to catch) still leaves a `turn_start` record with no
8
+ * matching `turn_end`, even if the process is later killed. Each record is a
9
+ * single sanitized JSON line (no message content, tokens, or tool payloads —
10
+ * only a timestamp, the session id, a stable event name, and the turn's final
11
+ * stop reason / error class when it ends). Follows `diagnostics.ts` exactly:
12
+ * rotating file under `~/.yagni-code/logs/`, fail-soft, hermetic under
13
+ * `node --test`.
14
+ */
15
+ export declare function _setTurnLogHomeForTest(dir: string | null): void;
16
+ export declare function turnLogPath(): string;
17
+ export type TurnLifecycleEvent = {
18
+ kind: "turn_start";
19
+ sessionId?: string;
20
+ turnIndex?: number;
21
+ } | {
22
+ kind: "turn_end";
23
+ sessionId?: string;
24
+ turnIndex?: number;
25
+ /** The fixed stopReason enum from the final assistant message — never a raw error message. */
26
+ stopReason?: string;
27
+ /** Time since the matching turn_start, ms. */
28
+ elapsedMs?: number;
29
+ };
30
+ /**
31
+ * Append one sanitized lifecycle record. Fail-soft — a logging failure must
32
+ * never break the turn. `errorClass` is a caller-mapped category, never a
33
+ * raw error message.
34
+ */
35
+ export declare function logTurnLifecycle(ev: TurnLifecycleEvent): void;
36
+ /** List existing lifecycle log files (active + rotations), for a report. */
37
+ export declare function listTurnLogFiles(): string[];
38
+ //# sourceMappingURL=turnLog.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Client-side turn-lifecycle log — makes a "stuck on Working…" turn legible
3
+ * without spelunking the raw session `.jsonl`.
4
+ *
5
+ * A session write-only WAL, not a buffered in-memory structure: the whole
6
+ * point is that a turn which STARTS but never ENDS (the silent-freeze failure
7
+ * this log exists to catch) still leaves a `turn_start` record with no
8
+ * matching `turn_end`, even if the process is later killed. Each record is a
9
+ * single sanitized JSON line (no message content, tokens, or tool payloads —
10
+ * only a timestamp, the session id, a stable event name, and the turn's final
11
+ * stop reason / error class when it ends). Follows `diagnostics.ts` exactly:
12
+ * rotating file under `~/.yagni-code/logs/`, fail-soft, hermetic under
13
+ * `node --test`.
14
+ */
15
+ import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { codeStateHome } from "./stateHome.js";
18
+ /** Test seam: point the log at a tmpdir (mirrors _setDiagnosticsHomeForTest). */
19
+ let homeOverride = null;
20
+ export function _setTurnLogHomeForTest(dir) {
21
+ homeOverride = dir;
22
+ }
23
+ function turnLogDir() {
24
+ return join(codeStateHome(homeOverride), "logs");
25
+ }
26
+ export function turnLogPath() {
27
+ return join(turnLogDir(), "turn-lifecycle.log");
28
+ }
29
+ const MAX_LOG_BYTES = 256 * 1024;
30
+ const KEEP_ROTATIONS = 2;
31
+ /** Shift turn-lifecycle.log -> .1 -> .2 once the active file passes the cap. */
32
+ function rotateIfNeeded(path) {
33
+ try {
34
+ if (!statSync(path).isFile() || statSync(path).size < MAX_LOG_BYTES)
35
+ return;
36
+ for (let i = KEEP_ROTATIONS; i >= 1; i--) {
37
+ const from = i === 1 ? path : `${path}.${i - 1}`;
38
+ const to = `${path}.${i}`;
39
+ try {
40
+ renameSync(from, to);
41
+ }
42
+ catch {
43
+ /* absent source — fine */
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ /* rotation is best-effort */
49
+ }
50
+ }
51
+ /**
52
+ * Append one sanitized lifecycle record. Fail-soft — a logging failure must
53
+ * never break the turn. `errorClass` is a caller-mapped category, never a
54
+ * raw error message.
55
+ */
56
+ export function logTurnLifecycle(ev) {
57
+ try {
58
+ // Hermetic under `node --test`: never touch the real home dir unless a test
59
+ // explicitly overrode it (same rule as diagnostics.ts, which prevents the
60
+ // suite's exit from depending on the CI filesystem).
61
+ if (process.env.NODE_TEST_CONTEXT && homeOverride === null)
62
+ return;
63
+ const line = {
64
+ ts: new Date().toISOString(),
65
+ event: ev.kind,
66
+ ...(ev.sessionId ? { sessionId: ev.sessionId } : {}),
67
+ ...(ev.turnIndex !== undefined ? { turnIndex: ev.turnIndex } : {}),
68
+ ...(ev.kind === "turn_end" && ev.stopReason !== undefined ? { stopReason: ev.stopReason } : {}),
69
+ ...(ev.kind === "turn_end" && ev.elapsedMs !== undefined ? { elapsedMs: ev.elapsedMs } : {}),
70
+ };
71
+ const path = turnLogPath();
72
+ mkdirSync(dirname(path), { recursive: true });
73
+ rotateIfNeeded(path);
74
+ appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
75
+ }
76
+ catch {
77
+ /* logging must never throw into the editor */
78
+ }
79
+ }
80
+ /** List existing lifecycle log files (active + rotations), for a report. */
81
+ export function listTurnLogFiles() {
82
+ try {
83
+ const dir = turnLogDir();
84
+ return readdirSync(dir)
85
+ .filter((f) => f.startsWith("turn-lifecycle.log"))
86
+ .sort()
87
+ .map((f) => join(dir, f));
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ }
93
+ //# sourceMappingURL=turnLog.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `yagni go --headless …` — the scriptable front door to the /go pipeline.
3
+ *
4
+ * The pipeline itself lives in the bundled extension (pi-extension-yagni's
5
+ * `pipeline/headlessGo.ts`). The launcher keeps its usual job: resolve the
6
+ * active environment's credentials, build the hermetic child environment, point
7
+ * the pipeline's child resolver at the pinned pi binary, and hand over. As
8
+ * everywhere else in this package, the extension is reached by FILE PATH and
9
+ * called through a locally-declared structural type — the two packages stay
10
+ * independent, and the CLI never takes a build-time dependency on a private
11
+ * workspace package.
12
+ *
13
+ * The mission sandbox invokes exactly this command (`--ticket-file`,
14
+ * `--plan-file`, optional `--memo-file`, `--run-id`, `--json`), so what Jack
15
+ * runs locally and what the fleet executes are the same binary and the same
16
+ * pipeline.
17
+ */
18
+ /** Mirrors HEADLESS_GO_EXIT in the extension; duplicated to keep the packages independent. */
19
+ export declare const GO_EXIT: {
20
+ readonly verified: 0;
21
+ readonly unverified: 1;
22
+ readonly usage: 2;
23
+ };
24
+ /** The structural slice of the extension's headless entry that we call. */
25
+ interface HeadlessGoModule {
26
+ runHeadlessGo: (argv: string[], deps: {
27
+ cwd?: string;
28
+ env?: NodeJS.ProcessEnv;
29
+ childEnv?: NodeJS.ProcessEnv;
30
+ }) => Promise<{
31
+ exitCode: number;
32
+ }>;
33
+ }
34
+ export interface GoCommandDeps {
35
+ /** Import seam for the bundled headless entry (tests inject a fake). */
36
+ loadHeadless?: () => Promise<HeadlessGoModule>;
37
+ /** Credential seam; defaults to the active profile on disk. */
38
+ loadCredentials?: () => Promise<{
39
+ token?: string;
40
+ baseUrl: string;
41
+ name: string;
42
+ expiresAt?: string;
43
+ workspaceId?: string;
44
+ }>;
45
+ /** Resolve pi's CLI entry (the binary every stage child is spawned with). */
46
+ piCliPath?: () => string;
47
+ writeErr?: (line: string) => void;
48
+ env?: NodeJS.ProcessEnv;
49
+ cwd?: string;
50
+ }
51
+ /**
52
+ * Build the environment every pipeline stage child inherits: the proxy
53
+ * credentials (so the `yagni` provider and the grounding tools reach the
54
+ * backend), the hermetic pi agent dir, and pi's quiet flags. `YAGNI_CALLER` /
55
+ * `YAGNI_RUN_ID` are NOT set here — the pipeline stamps a per-stage caller onto
56
+ * every child, and the run id rides the `--run-id` flag.
57
+ */
58
+ export declare function buildHeadlessChildEnv(opts: {
59
+ baseEnv: NodeJS.ProcessEnv;
60
+ token: string;
61
+ baseUrl: string;
62
+ profileName: string;
63
+ expiresAt?: string;
64
+ workspaceId?: string;
65
+ cliVersion?: string;
66
+ sessionId?: string;
67
+ }): NodeJS.ProcessEnv;
68
+ /**
69
+ * Run `yagni go`. Returns the process exit code: 0 only on a verified
70
+ * candidate, 1 on any other pipeline outcome, 2 on a usage or credential
71
+ * problem. Never throws.
72
+ */
73
+ export declare function goCommand(args: string[], deps?: GoCommandDeps, cliVersion?: string): Promise<number>;
74
+ export {};
75
+ //# sourceMappingURL=goHeadless.d.ts.map