@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1002.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,106 @@
1
+ /**
2
+ * PURE resume-decision core — the analogue of `findings.ts#shouldStop`.
3
+ *
4
+ * Given the append-only checkpoint journal for a run key and a LIVE workspace
5
+ * snapshot, decide whether /go can safely resume the bounded review→fix loop,
6
+ * must warn about a partially-built / diverged tree, or should just run fresh.
7
+ *
8
+ * Safety boundary (the whole point): we only auto-resume the EXPENSIVE half (the
9
+ * review→fix loop) and only when the build half fully completed AND the live git
10
+ * tree still matches the boundary we checkpointed. A crash mid-build, or a tree
11
+ * that drifted since the checkpoint, would risk double-applying edits if we
12
+ * replayed — so those resolve to `warn_partial` (warn + run fresh), never a
13
+ * silent replay. Anything unknowable fails OPEN to `fresh` (mirrors
14
+ * `workspaceChanged`). The fold never throws.
15
+ */
16
+ import { EMPTY_RUN_USAGE } from "./budget.js";
17
+ /** Two snapshots match only when BOTH are present, tracked, and identical (HEAD + status). */
18
+ function snapshotsMatch(a, b) {
19
+ return Boolean(a) && Boolean(b) && a.tracked && b.tracked && a.head === b.head && a.status === b.status;
20
+ }
21
+ /** The last record of a given kind, or undefined. */
22
+ function lastOfKind(records, kind) {
23
+ for (let i = records.length - 1; i >= 0; i -= 1) {
24
+ if (records[i]?.kind === kind)
25
+ return records[i];
26
+ }
27
+ return undefined;
28
+ }
29
+ export function planResume(records, liveSnapshot) {
30
+ try {
31
+ if (!Array.isArray(records) || records.length === 0)
32
+ return { mode: "fresh" };
33
+ // A terminal run_finish means the run completed (even `round_cap`) — a
34
+ // completed run is NOT an interrupted one, so it is never resumable. A
35
+ // finish_done is equally terminal: the FINISH stage only runs after a clean
36
+ // stop, so a crash in the narrow window before run_finish lands must not
37
+ // read as "resumable" over a tree the FINISH commit has since moved.
38
+ if (records.some((r) => r?.kind === "run_finish" || r?.kind === "finish_done"))
39
+ return { mode: "fresh" };
40
+ const runStart = records.find((r) => r?.kind === "run_start");
41
+ if (!runStart)
42
+ return { mode: "fresh" }; // no anchor → nothing trustworthy to resume
43
+ const ticket = runStart.ticket;
44
+ const buildDone = lastOfKind(records, "build_done");
45
+ if (!buildDone) {
46
+ // Interrupted during the build half: the tree may hold partial edits, so
47
+ // replaying review against it would be wrong. Warn; the caller runs fresh.
48
+ return { mode: "warn_partial", reason: "build_interrupted", ticket };
49
+ }
50
+ const roundDones = records.filter((r) => r?.kind === "round_done");
51
+ const latest = roundDones.length > 0 ? maxByRound(roundDones) : buildDone;
52
+ // Unknowable tree (not a git work tree) → fail open to fresh rather than
53
+ // claim a resume we cannot verify.
54
+ if (!liveSnapshot?.tracked)
55
+ return { mode: "fresh" };
56
+ // The live tree must still be exactly where we checkpointed it; otherwise an
57
+ // orphaned child or a manual edit drifted it and replay is unsafe.
58
+ if (!snapshotsMatch(latest.snapshot, liveSnapshot)) {
59
+ return { mode: "warn_partial", reason: "tree_diverged", ticket };
60
+ }
61
+ if (roundDones.length === 0) {
62
+ // Build done, no round completed → resume at round 1 against implement output.
63
+ return {
64
+ mode: "resume_loop",
65
+ round: 1,
66
+ reviewInput: buildDone.reviewInput ?? "",
67
+ priorRounds: [],
68
+ priorUsage: buildDone.usage ?? EMPTY_RUN_USAGE,
69
+ ...handle(runStart),
70
+ ticket,
71
+ };
72
+ }
73
+ const maxRound = Math.max(...roundDones.map((r) => r.round ?? 0));
74
+ const lastRound = roundDones.find((r) => (r.round ?? 0) === maxRound) ?? buildDone;
75
+ const priorRounds = roundDones
76
+ .slice()
77
+ .sort((a, b) => (a.round ?? 0) - (b.round ?? 0))
78
+ .map((r) => ({ round: r.round ?? 0, lensResults: [], findings: r.findings ?? [] }));
79
+ return {
80
+ mode: "resume_loop",
81
+ round: maxRound + 1,
82
+ reviewInput: lastRound.reviewInput ?? "",
83
+ priorRounds,
84
+ priorUsage: lastRound.usage ?? buildDone.usage ?? EMPTY_RUN_USAGE,
85
+ ...handle(runStart),
86
+ ticket,
87
+ };
88
+ }
89
+ catch {
90
+ return { mode: "fresh" };
91
+ }
92
+ }
93
+ /** The record carrying the highest `round`. */
94
+ function maxByRound(records) {
95
+ return records.reduce((a, b) => ((b.round ?? 0) >= (a.round ?? 0) ? b : a));
96
+ }
97
+ /** Pull the optional backend handle off the run_start record (omit undefined keys). */
98
+ function handle(runStart) {
99
+ const out = {};
100
+ if (runStart.runId)
101
+ out.runId = runStart.runId;
102
+ if (runStart.workItemId)
103
+ out.workItemId = runStart.workItemId;
104
+ return out;
105
+ }
106
+ //# sourceMappingURL=resume.js.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The /go run registry (spec §3b — detached-by-default).
3
+ *
4
+ * Detached runs outlive the prompt that started them, so /go needs one place
5
+ * that answers "what is running / finished / interrupted?": a module-scoped
6
+ * IN-MEMORY registry (the live truth for THIS process: in-flight guard,
7
+ * MAX_CONCURRENT_RUNS, the completion promise) mirrored to an append-only
8
+ * NDJSON file at `~/.yagni-code/registry.ndjson` (dir 0700, file 0600 —
9
+ * mirrors checkpoint.ts) so a LATER session can list finished runs, spot an
10
+ * interrupted one, and resume into its worktree.
11
+ *
12
+ * The mirror is append-only and torn-line-tolerant (same crash model as
13
+ * checkpoint.ts): every state change appends the FULL row again and readers
14
+ * fold last-write-wins by runId, so a torn final write costs at most the last
15
+ * update, never the file. All writes are fail-soft — registry bookkeeping must
16
+ * never break /go.
17
+ *
18
+ * Liveness is decided by the PURE {@link classifyRunLiveness}: a non-terminal
19
+ * row is `in_flight` when this process holds it, otherwise the run's checkpoint
20
+ * journal decides — recent activity (< 30 min) means it is probably running in
21
+ * another process (`active_elsewhere`), stale means `interrupted` (a resume
22
+ * candidate).
23
+ */
24
+ import type { CheckpointRecord, StopReason } from "./types.js";
25
+ /** Bound on simultaneously in-flight /go runs in one process (spec §3b). */
26
+ export declare const MAX_CONCURRENT_RUNS = 3;
27
+ /**
28
+ * A non-terminal row whose journal has been quiet this long is treated as
29
+ * INTERRUPTED (its process died) rather than still running elsewhere. Sits
30
+ * comfortably past the 5-min run heartbeat and the server reaper's 30-min
31
+ * staleness window so the two views agree.
32
+ */
33
+ export declare const JOURNAL_STALE_MS: number;
34
+ /**
35
+ * One registry row (the spec's frozen shape). `status` is `running` until the
36
+ * run reaches a terminal outcome, then the honest stop reason; `cleaned` marks
37
+ * a terminal row whose worktree /go-clean has removed. `runId` is the LOCAL
38
+ * run identity (minted before the backend run row exists — the worktree dir and
39
+ * UI key derive from its first 8 chars), not the backend runs-row id.
40
+ */
41
+ export interface RunRegistryRow {
42
+ runId: string;
43
+ ticket: string;
44
+ sessionKey: string;
45
+ repo?: string;
46
+ /** Absent for an in-place (--here) run. */
47
+ worktreePath?: string;
48
+ branch?: string;
49
+ status: RunRegistryStatus;
50
+ startedAt: number;
51
+ commitSha?: string;
52
+ prUrl?: string;
53
+ }
54
+ export type RunRegistryStatus = "running" | "cleaned" | StopReason;
55
+ /** Everything except `running` is terminal (the run is not in flight anywhere). */
56
+ export declare function isTerminalStatus(status: RunRegistryStatus): boolean;
57
+ /** How a registry row relates to live execution right now. */
58
+ export type RunLiveness = "in_flight" | "active_elsewhere" | "interrupted" | "terminal";
59
+ /**
60
+ * PURE liveness verdict for one row. `inFlight` is this process's in-memory
61
+ * truth; `lastJournalTs` is the newest checkpoint-journal timestamp for the
62
+ * row's sessionKey (fall back to `startedAt` when the run never journaled).
63
+ */
64
+ export declare function classifyRunLiveness(row: RunRegistryRow, opts: {
65
+ inFlight: boolean;
66
+ lastJournalTs?: number;
67
+ now: number;
68
+ }): RunLiveness;
69
+ /** The newest checkpoint timestamp in a run's journal (0 when empty). */
70
+ export declare function lastJournalTs(records: CheckpointRecord[]): number;
71
+ /**
72
+ * Fold raw NDJSON mirror lines into rows, last-write-wins by runId, preserving
73
+ * first-seen order. Pure + torn-line-tolerant: unparseable or shape-less lines
74
+ * are dropped (the classic crash artifact), never fatal.
75
+ */
76
+ export declare function foldRegistryLines(lines: string[]): RunRegistryRow[];
77
+ export declare function _setRegistryHomeForTest(dir: string | null): void;
78
+ /** The append-only NDJSON mirror file. */
79
+ export declare function registryFile(): string;
80
+ /** Where detached run worktrees live (never inside the repo — spec §6). */
81
+ export declare function worktreesDir(): string;
82
+ /** Test seam: clear the in-memory half (simulates a process restart; the mirror survives). */
83
+ export declare function _resetRunRegistryForTest(): void;
84
+ /** Mark a run in-flight in this process and mirror the row. Never throws. */
85
+ export declare function beginRun(row: RunRegistryRow): void;
86
+ /** Attach the detached completion promise to an in-flight run (fail-soft no-op when absent). */
87
+ export declare function trackRunPromise(runId: string, promise: Promise<void>): void;
88
+ /**
89
+ * Settle an in-flight run: merge the terminal patch, drop it from the in-memory
90
+ * active set, and mirror the final row. Safe to call for an unknown runId: the
91
+ * patch lands against the last known mirror row when one exists, or against the
92
+ * patch itself when the caller supplied a full row (the /go-clean path, whose
93
+ * rows may come through an injected registry read).
94
+ */
95
+ export declare function settleRun(runId: string, patch: Partial<RunRegistryRow> & {
96
+ status: RunRegistryStatus;
97
+ }): void;
98
+ /** How many runs this process currently has in flight. */
99
+ export declare function activeRunCount(): number;
100
+ /** The in-flight row for a ticket in THIS process, if any (the same-ticket guard). */
101
+ export declare function findActiveRunByTicket(ticket: string): RunRegistryRow | undefined;
102
+ /** Is this runId in flight in THIS process? */
103
+ export declare function isRunInFlight(runId: string): boolean;
104
+ /** Test seam: await every in-flight run's completion promise. */
105
+ export declare function _awaitActiveRunsForTest(): Promise<void>;
106
+ /**
107
+ * The merged registry view: the NDJSON mirror folded last-write-wins, with this
108
+ * process's in-memory rows overlaid (they are the fresher truth when a mirror
109
+ * write was lost). Never throws; an unreadable mirror yields the in-memory rows.
110
+ */
111
+ export declare function loadRegistryRows(): RunRegistryRow[];
112
+ //# sourceMappingURL=runRegistry.d.ts.map
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The /go run registry (spec §3b — detached-by-default).
3
+ *
4
+ * Detached runs outlive the prompt that started them, so /go needs one place
5
+ * that answers "what is running / finished / interrupted?": a module-scoped
6
+ * IN-MEMORY registry (the live truth for THIS process: in-flight guard,
7
+ * MAX_CONCURRENT_RUNS, the completion promise) mirrored to an append-only
8
+ * NDJSON file at `~/.yagni-code/registry.ndjson` (dir 0700, file 0600 —
9
+ * mirrors checkpoint.ts) so a LATER session can list finished runs, spot an
10
+ * interrupted one, and resume into its worktree.
11
+ *
12
+ * The mirror is append-only and torn-line-tolerant (same crash model as
13
+ * checkpoint.ts): every state change appends the FULL row again and readers
14
+ * fold last-write-wins by runId, so a torn final write costs at most the last
15
+ * update, never the file. All writes are fail-soft — registry bookkeeping must
16
+ * never break /go.
17
+ *
18
+ * Liveness is decided by the PURE {@link classifyRunLiveness}: a non-terminal
19
+ * row is `in_flight` when this process holds it, otherwise the run's checkpoint
20
+ * journal decides — recent activity (< 30 min) means it is probably running in
21
+ * another process (`active_elsewhere`), stale means `interrupted` (a resume
22
+ * candidate).
23
+ */
24
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { codeStateHome } from "../stateHome.js";
27
+ /** Bound on simultaneously in-flight /go runs in one process (spec §3b). */
28
+ export const MAX_CONCURRENT_RUNS = 3;
29
+ /**
30
+ * A non-terminal row whose journal has been quiet this long is treated as
31
+ * INTERRUPTED (its process died) rather than still running elsewhere. Sits
32
+ * comfortably past the 5-min run heartbeat and the server reaper's 30-min
33
+ * staleness window so the two views agree.
34
+ */
35
+ export const JOURNAL_STALE_MS = 30 * 60_000;
36
+ /** Everything except `running` is terminal (the run is not in flight anywhere). */
37
+ export function isTerminalStatus(status) {
38
+ return status !== "running";
39
+ }
40
+ /**
41
+ * PURE liveness verdict for one row. `inFlight` is this process's in-memory
42
+ * truth; `lastJournalTs` is the newest checkpoint-journal timestamp for the
43
+ * row's sessionKey (fall back to `startedAt` when the run never journaled).
44
+ */
45
+ export function classifyRunLiveness(row, opts) {
46
+ if (isTerminalStatus(row.status))
47
+ return "terminal";
48
+ if (opts.inFlight)
49
+ return "in_flight";
50
+ const last = opts.lastJournalTs ?? row.startedAt;
51
+ return opts.now - last > JOURNAL_STALE_MS ? "interrupted" : "active_elsewhere";
52
+ }
53
+ /** The newest checkpoint timestamp in a run's journal (0 when empty). */
54
+ export function lastJournalTs(records) {
55
+ let max = 0;
56
+ for (const r of records) {
57
+ if (typeof r?.ts === "number" && r.ts > max)
58
+ max = r.ts;
59
+ }
60
+ return max;
61
+ }
62
+ /**
63
+ * Fold raw NDJSON mirror lines into rows, last-write-wins by runId, preserving
64
+ * first-seen order. Pure + torn-line-tolerant: unparseable or shape-less lines
65
+ * are dropped (the classic crash artifact), never fatal.
66
+ */
67
+ export function foldRegistryLines(lines) {
68
+ const byId = new Map();
69
+ for (const line of lines) {
70
+ if (!line.trim())
71
+ continue;
72
+ try {
73
+ const row = JSON.parse(line);
74
+ if (!row || typeof row.runId !== "string" || typeof row.ticket !== "string")
75
+ continue;
76
+ if (typeof row.sessionKey !== "string" || typeof row.status !== "string")
77
+ continue;
78
+ byId.set(row.runId, row);
79
+ }
80
+ catch {
81
+ /* torn/garbage line — drop it, keep the rest */
82
+ }
83
+ }
84
+ return [...byId.values()];
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // State-dir seams (mirrors checkpoint.ts `_setCheckpointHomeForTest`).
88
+ // ---------------------------------------------------------------------------
89
+ let homeOverride = null;
90
+ export function _setRegistryHomeForTest(dir) {
91
+ homeOverride = dir;
92
+ }
93
+ function registryHome() {
94
+ return codeStateHome(homeOverride);
95
+ }
96
+ /** The append-only NDJSON mirror file. */
97
+ export function registryFile() {
98
+ return join(registryHome(), "registry.ndjson");
99
+ }
100
+ /** Where detached run worktrees live (never inside the repo — spec §6). */
101
+ export function worktreesDir() {
102
+ return join(registryHome(), "worktrees");
103
+ }
104
+ const active = new Map();
105
+ /** Test seam: clear the in-memory half (simulates a process restart; the mirror survives). */
106
+ export function _resetRunRegistryForTest() {
107
+ active.clear();
108
+ }
109
+ /** Fail-soft append of one full row to the mirror (self-heals a torn previous write). */
110
+ function appendRow(row) {
111
+ try {
112
+ mkdirSync(registryHome(), { recursive: true, mode: 0o700 });
113
+ const file = registryFile();
114
+ let prefix = "";
115
+ try {
116
+ const existing = readFileSync(file);
117
+ if (existing.length > 0 && existing[existing.length - 1] !== 0x0a)
118
+ prefix = "\n";
119
+ }
120
+ catch {
121
+ /* no file yet — nothing to heal */
122
+ }
123
+ appendFileSync(file, `${prefix}${JSON.stringify(row)}\n`, { mode: 0o600 });
124
+ }
125
+ catch {
126
+ /* fail-soft: registry bookkeeping must never break /go */
127
+ }
128
+ }
129
+ /** Mark a run in-flight in this process and mirror the row. Never throws. */
130
+ export function beginRun(row) {
131
+ active.set(row.runId, { row });
132
+ appendRow(row);
133
+ }
134
+ /** Attach the detached completion promise to an in-flight run (fail-soft no-op when absent). */
135
+ export function trackRunPromise(runId, promise) {
136
+ const entry = active.get(runId);
137
+ if (entry)
138
+ entry.promise = promise;
139
+ }
140
+ /**
141
+ * Settle an in-flight run: merge the terminal patch, drop it from the in-memory
142
+ * active set, and mirror the final row. Safe to call for an unknown runId: the
143
+ * patch lands against the last known mirror row when one exists, or against the
144
+ * patch itself when the caller supplied a full row (the /go-clean path, whose
145
+ * rows may come through an injected registry read).
146
+ */
147
+ export function settleRun(runId, patch) {
148
+ const entry = active.get(runId);
149
+ const base = entry?.row ??
150
+ loadRegistryRows().find((r) => r.runId === runId) ??
151
+ (isFullRow(patch) ? patch : undefined);
152
+ active.delete(runId);
153
+ if (!base)
154
+ return;
155
+ appendRow({ ...base, ...patch, runId: base.runId ?? runId });
156
+ }
157
+ /** Does a settle patch carry the whole row shape (usable as its own base)? */
158
+ function isFullRow(patch) {
159
+ return (typeof patch.runId === "string" &&
160
+ typeof patch.ticket === "string" &&
161
+ typeof patch.sessionKey === "string" &&
162
+ typeof patch.startedAt === "number");
163
+ }
164
+ /** How many runs this process currently has in flight. */
165
+ export function activeRunCount() {
166
+ return active.size;
167
+ }
168
+ /** The in-flight row for a ticket in THIS process, if any (the same-ticket guard). */
169
+ export function findActiveRunByTicket(ticket) {
170
+ for (const entry of active.values()) {
171
+ if (entry.row.ticket === ticket)
172
+ return entry.row;
173
+ }
174
+ return undefined;
175
+ }
176
+ /** Is this runId in flight in THIS process? */
177
+ export function isRunInFlight(runId) {
178
+ return active.has(runId);
179
+ }
180
+ /** Test seam: await every in-flight run's completion promise. */
181
+ export async function _awaitActiveRunsForTest() {
182
+ await Promise.all([...active.values()].map((e) => e.promise ?? Promise.resolve()));
183
+ }
184
+ /**
185
+ * The merged registry view: the NDJSON mirror folded last-write-wins, with this
186
+ * process's in-memory rows overlaid (they are the fresher truth when a mirror
187
+ * write was lost). Never throws; an unreadable mirror yields the in-memory rows.
188
+ */
189
+ export function loadRegistryRows() {
190
+ let rows = [];
191
+ try {
192
+ rows = foldRegistryLines(readFileSync(registryFile(), "utf8").split("\n"));
193
+ }
194
+ catch {
195
+ rows = [];
196
+ }
197
+ const byId = new Map(rows.map((r) => [r.runId, r]));
198
+ for (const entry of active.values())
199
+ byId.set(entry.row.runId, entry.row);
200
+ return [...byId.values()];
201
+ }
202
+ //# sourceMappingURL=runRegistry.js.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The `/go` run-session adapter.
3
+ *
4
+ * Wraps the device-token `/api/yagni-code/runs*` endpoints so a YAGNI Code
5
+ * pipeline run becomes a tracked `runs` row visible on the Work page: open the
6
+ * run (`start`), stream each pipeline stage (`stage`), bank an in-loop
7
+ * correction (`correction`), and close it with an honest outcome (`finish`).
8
+ *
9
+ * CRITICAL invariant: recording must NEVER break the pipeline. Every call is
10
+ * fail-soft — it never throws. `start` resolves to a typed outcome that
11
+ * distinguishes a tracked run from an untracked one (flag off, unauthorized, or
12
+ * the backend unreachable/erroring) so the caller can surface a notice instead
13
+ * of failing silently; the post-start verbs (`stage`/`correction`/`finish`) stay
14
+ * silent no-ops. `/go` runs to completion regardless of the recording outcome.
15
+ *
16
+ * Mirrors the fetch-seam shape of {@link makeRecordEngineeringContextTool}
17
+ * (`baseUrl` + injectable `getToken` + `fetchImpl`) so it is unit-testable
18
+ * without a network and reuses the same auth convention.
19
+ */
20
+ /** Options for {@link makeRunSession}. */
21
+ export interface RunSessionOptions {
22
+ baseUrl: string;
23
+ getToken: () => string | undefined;
24
+ fetchImpl?: typeof fetch;
25
+ /**
26
+ * Backoff sleep + jitter seams, threaded into {@link resilientFetch} so the
27
+ * retry path is deterministic and instant under test. Default to the real
28
+ * timer / Math.random in production.
29
+ */
30
+ sleep?: (ms: number) => Promise<void>;
31
+ random?: () => number;
32
+ /**
33
+ * Opt-in debug sink for diagnosing a recording failure (the fail-soft path
34
+ * otherwise collapses 402/5xx/2xx-bad-body/unreachable into one silent
35
+ * "error"). Only ever receives a token-free line: the path, the numeric HTTP
36
+ * status, a capped slice of the SERVER response body, or a network error
37
+ * message. NEVER the request headers (the bearer token) or the request body.
38
+ * Injected for deterministic tests; defaults to an env-gated stderr writer.
39
+ */
40
+ debug?: (line: string) => void;
41
+ /**
42
+ * Environment seam for the debug gate, matching `config.ts`/`verify.ts`. The
43
+ * debug sink only fires when `YAGNI_CODE_DEBUG` is set here (default
44
+ * `process.env`), so existing callers that inject neither `debug` nor `env`
45
+ * stay silent unless the operator opts in.
46
+ */
47
+ env?: NodeJS.ProcessEnv;
48
+ }
49
+ /** The handle returned by a successful `start` (or supplied via `bind`). */
50
+ export interface RunSessionHandle {
51
+ runId: string;
52
+ workItemId: string;
53
+ }
54
+ /**
55
+ * The result of a successful `start`: the handle plus the server's governance
56
+ * verdict (`gated` = the work item needs approval in the app before it shows as
57
+ * executing, so /go should warn rather than silently proceed).
58
+ */
59
+ export interface RunStartResult extends RunSessionHandle {
60
+ gated: boolean;
61
+ gatedReason?: string;
62
+ }
63
+ /**
64
+ * Why a run could not be recorded, derived from the endpoint's HTTP status so
65
+ * the caller can show an actionable notice:
66
+ * - `disabled` — 404: the `yagni_code.work_tracking` flag is off for this
67
+ * workspace (the write endpoints 404 to hide themselves).
68
+ * - `unauthorized` — 401/403: the device token is missing/expired or lacks the
69
+ * `yagni_code:write` scope (re-run `yagni login`).
70
+ * - `error` — anything else: an over-budget 402, a 5xx, a 2xx with an
71
+ * unrecognized body, or an unreachable backend / thrown
72
+ * network error.
73
+ */
74
+ export type RunTrackingFailure = "disabled" | "unauthorized" | "error";
75
+ /**
76
+ * The outcome of opening a run. `tracked: true` carries the handle + gate
77
+ * verdict; `tracked: false` carries the reason so `/go` can warn that this run
78
+ * won't appear on the Work page. `start` never returns null and never throws.
79
+ */
80
+ export type RunStartOutcome = ({
81
+ tracked: true;
82
+ } & RunStartResult) | {
83
+ tracked: false;
84
+ reason: RunTrackingFailure;
85
+ };
86
+ /** Arguments to open a run. */
87
+ export interface StartArgs {
88
+ arg: string;
89
+ repo?: string;
90
+ branch?: string;
91
+ }
92
+ /** A pipeline stage boundary event. */
93
+ export interface StageArgs {
94
+ stage: string;
95
+ phase: "start" | "finish";
96
+ round?: number;
97
+ findings?: number;
98
+ }
99
+ /** An in-loop correction captured during the run. */
100
+ export interface CorrectionArgs {
101
+ stage?: string;
102
+ question: string;
103
+ decision: string;
104
+ rationale?: string;
105
+ }
106
+ /**
107
+ * The /go PLAN, recorded onto the work item (a `plan_recorded` activity beat) so
108
+ * it is clear what the run set out to do. `body` is the plan markdown; the server
109
+ * scrubs secrets + caps it before persisting.
110
+ */
111
+ export interface RecordPlanArgs {
112
+ stage: string;
113
+ body: string;
114
+ }
115
+ /** How a run ended; maps to the run-status enum server-side. */
116
+ export interface FinishArgs {
117
+ outcome: "completed" | "stopped" | "failed";
118
+ stopReason?: string;
119
+ prUrl?: string;
120
+ prNumber?: number;
121
+ branch?: string;
122
+ commitSha?: string;
123
+ rounds?: number;
124
+ /**
125
+ * Short "what was done" recap; the server records it as a `run_outcome`
126
+ * activity (the end-of-run beat on the Work-page timeline). Optional + additive:
127
+ * `finish` already spreads the whole args object, so it flows through untouched.
128
+ */
129
+ outcomeSummary?: string;
130
+ }
131
+ /** The run-session object returned by {@link makeRunSession}. */
132
+ export interface RunSession {
133
+ /** Adopt an externally-known handle (used by tests + cloud callers). */
134
+ bind(h: RunSessionHandle): void;
135
+ /** Open the run; resolves to a tracked outcome (handle + gate verdict) or an untracked reason. Never throws. */
136
+ start(b: StartArgs): Promise<RunStartOutcome>;
137
+ /** Stream a stage boundary; no-op until a handle exists. */
138
+ stage(b: StageArgs): Promise<void>;
139
+ /** Bank an in-loop correction; no-op until a handle exists. */
140
+ correction(b: CorrectionArgs): Promise<void>;
141
+ /** Record the /go plan onto the work item; no-op until a handle exists. */
142
+ recordPlan(b: RecordPlanArgs): Promise<void>;
143
+ /**
144
+ * Bump the run's server-side last-activity heartbeat so a long-but-alive run is
145
+ * not reaped as stale. No-op until a handle exists; never throws.
146
+ */
147
+ heartbeat(): Promise<void>;
148
+ /** Close the run with an outcome; no-op until a handle exists. */
149
+ finish(b: FinishArgs): Promise<void>;
150
+ }
151
+ export declare function makeRunSession(opts: RunSessionOptions): RunSession;
152
+ //# sourceMappingURL=runSession.d.ts.map