@yagni-app/code 0.1.0 → 0.2.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 (155) hide show
  1. package/README.md +65 -11
  2. package/dist/claudeCompat.d.ts +109 -0
  3. package/dist/claudeCompat.js +260 -0
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +79 -20
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/crashReport.d.ts +135 -0
  11. package/dist/crashReport.js +291 -0
  12. package/dist/credentials.js +2 -1
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +27 -0
  16. package/dist/doctor.js +81 -3
  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 +194 -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 +87 -0
  25. package/dist/extension/boostCommand.d.ts +144 -0
  26. package/dist/extension/boostCommand.js +263 -0
  27. package/dist/extension/branding.d.ts +79 -0
  28. package/dist/extension/branding.js +156 -0
  29. package/dist/extension/chipEditor.d.ts +117 -0
  30. package/dist/extension/chipEditor.js +373 -0
  31. package/dist/extension/claudeRules.d.ts +54 -0
  32. package/dist/extension/claudeRules.js +180 -0
  33. package/dist/extension/config.d.ts +173 -0
  34. package/dist/extension/config.js +194 -0
  35. package/dist/extension/costHud.d.ts +186 -0
  36. package/dist/extension/costHud.js +293 -0
  37. package/dist/extension/crashReport.d.ts +89 -0
  38. package/dist/extension/crashReport.js +241 -0
  39. package/dist/extension/decisionCapture.d.ts +52 -0
  40. package/dist/extension/decisionCapture.js +66 -0
  41. package/dist/extension/decisions.d.ts +83 -0
  42. package/dist/extension/decisions.js +200 -0
  43. package/dist/extension/diagnostics.d.ts +41 -0
  44. package/dist/extension/diagnostics.js +110 -0
  45. package/dist/extension/index.d.ts +155 -0
  46. package/dist/extension/index.js +597 -0
  47. package/dist/extension/initDone.d.ts +28 -0
  48. package/dist/extension/initDone.js +66 -0
  49. package/dist/extension/initPass.d.ts +170 -0
  50. package/dist/extension/initPass.js +394 -0
  51. package/dist/extension/mcpTools.d.ts +57 -0
  52. package/dist/extension/mcpTools.js +132 -0
  53. package/dist/extension/nextWorkTool.d.ts +51 -0
  54. package/dist/extension/nextWorkTool.js +80 -0
  55. package/dist/extension/permission.d.ts +91 -0
  56. package/dist/extension/permission.js +236 -0
  57. package/dist/extension/pipeline/activity.d.ts +37 -0
  58. package/dist/extension/pipeline/activity.js +151 -0
  59. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  60. package/dist/extension/pipeline/activityFeed.js +175 -0
  61. package/dist/extension/pipeline/budget.d.ts +48 -0
  62. package/dist/extension/pipeline/budget.js +68 -0
  63. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  64. package/dist/extension/pipeline/checkpoint.js +176 -0
  65. package/dist/extension/pipeline/eval.d.ts +205 -0
  66. package/dist/extension/pipeline/eval.js +226 -0
  67. package/dist/extension/pipeline/events.d.ts +56 -0
  68. package/dist/extension/pipeline/events.js +147 -0
  69. package/dist/extension/pipeline/findings.d.ts +42 -0
  70. package/dist/extension/pipeline/findings.js +144 -0
  71. package/dist/extension/pipeline/finish.d.ts +128 -0
  72. package/dist/extension/pipeline/finish.js +307 -0
  73. package/dist/extension/pipeline/goCommand.d.ts +146 -0
  74. package/dist/extension/pipeline/goCommand.js +1085 -0
  75. package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
  76. package/dist/extension/pipeline/goCompareCommand.js +203 -0
  77. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  78. package/dist/extension/pipeline/goFlags.js +46 -0
  79. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  80. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  81. package/dist/extension/pipeline/invocation.d.ts +45 -0
  82. package/dist/extension/pipeline/invocation.js +64 -0
  83. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  84. package/dist/extension/pipeline/orchestrator.js +645 -0
  85. package/dist/extension/pipeline/personas.d.ts +44 -0
  86. package/dist/extension/pipeline/personas.js +248 -0
  87. package/dist/extension/pipeline/resilience.d.ts +85 -0
  88. package/dist/extension/pipeline/resilience.js +166 -0
  89. package/dist/extension/pipeline/resume.d.ts +18 -0
  90. package/dist/extension/pipeline/resume.js +106 -0
  91. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  92. package/dist/extension/pipeline/runCostTable.js +165 -0
  93. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  94. package/dist/extension/pipeline/runRegistry.js +202 -0
  95. package/dist/extension/pipeline/runSession.d.ts +152 -0
  96. package/dist/extension/pipeline/runSession.js +167 -0
  97. package/dist/extension/pipeline/runState.d.ts +177 -0
  98. package/dist/extension/pipeline/runState.js +275 -0
  99. package/dist/extension/pipeline/runner.d.ts +98 -0
  100. package/dist/extension/pipeline/runner.js +310 -0
  101. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  102. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  103. package/dist/extension/pipeline/stages.d.ts +53 -0
  104. package/dist/extension/pipeline/stages.js +115 -0
  105. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  106. package/dist/extension/pipeline/ticketResolution.js +75 -0
  107. package/dist/extension/pipeline/types.d.ts +439 -0
  108. package/dist/extension/pipeline/types.js +48 -0
  109. package/dist/extension/pipeline/verify.d.ts +277 -0
  110. package/dist/extension/pipeline/verify.js +763 -0
  111. package/dist/extension/pipeline/workspace.d.ts +32 -0
  112. package/dist/extension/pipeline/workspace.js +73 -0
  113. package/dist/extension/pipeline/worktree.d.ts +107 -0
  114. package/dist/extension/pipeline/worktree.js +202 -0
  115. package/dist/extension/provider.d.ts +17 -0
  116. package/dist/extension/provider.js +33 -0
  117. package/dist/extension/recall.d.ts +93 -0
  118. package/dist/extension/recall.js +190 -0
  119. package/dist/extension/recordContextTool.d.ts +38 -0
  120. package/dist/extension/recordContextTool.js +85 -0
  121. package/dist/extension/recordDecisionTool.d.ts +52 -0
  122. package/dist/extension/recordDecisionTool.js +102 -0
  123. package/dist/extension/repoDocs.d.ts +81 -0
  124. package/dist/extension/repoDocs.js +260 -0
  125. package/dist/extension/rerouteNotice.d.ts +34 -0
  126. package/dist/extension/rerouteNotice.js +79 -0
  127. package/dist/extension/resilientFetch.d.ts +60 -0
  128. package/dist/extension/resilientFetch.js +133 -0
  129. package/dist/extension/reviewTool.d.ts +34 -0
  130. package/dist/extension/reviewTool.js +81 -0
  131. package/dist/extension/sessionRuns.d.ts +45 -0
  132. package/dist/extension/sessionRuns.js +77 -0
  133. package/dist/extension/spool.d.ts +92 -0
  134. package/dist/extension/spool.js +266 -0
  135. package/dist/extension/stateHome.d.ts +2 -0
  136. package/dist/extension/stateHome.js +6 -0
  137. package/dist/extension/subagents.d.ts +145 -0
  138. package/dist/extension/subagents.js +326 -0
  139. package/dist/extension/surface.d.ts +10 -0
  140. package/dist/extension/surface.js +12 -0
  141. package/dist/extension/todos.d.ts +110 -0
  142. package/dist/extension/todos.js +217 -0
  143. package/dist/extension/tokenProvider.d.ts +93 -0
  144. package/dist/extension/tokenProvider.js +234 -0
  145. package/dist/launch.d.ts +25 -3
  146. package/dist/launch.js +27 -9
  147. package/dist/login.d.ts +7 -0
  148. package/dist/login.js +3 -1
  149. package/dist/paths.d.ts +13 -4
  150. package/dist/paths.js +17 -5
  151. package/dist/profiles.d.ts +1 -1
  152. package/dist/profiles.js +5 -2
  153. package/dist/upgrade.d.ts +97 -0
  154. package/dist/upgrade.js +284 -0
  155. package/package.json +11 -15
@@ -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,37 @@
1
+ /**
2
+ * Task 8 (/go summary prints a per-stage, server-priced cost table): a pure
3
+ * formatter over `GET /api/yagni-code/spend?runId=`'s response (see
4
+ * costHud.ts's SpendResponse/SpendRow mirror for the shape this reads).
5
+ *
6
+ * Kept out of costHud.ts deliberately: that module's `formatServerCostLines`
7
+ * renders a SESSION's spend grouped by rate tier (for /cost); this renders ONE
8
+ * RUN's spend grouped by PIPELINE STAGE (for /go's end-of-run summary), and the
9
+ * grouping/merge rules below (fold the three review lenses into one line,
10
+ * mixed-tier tie-break) are specific enough to this call site that sharing a
11
+ * module would just make costHud.ts harder to read for its own surface.
12
+ *
13
+ * Caller labels are minted by runner.ts as `go:<stage.id>` (or
14
+ * `go:<stage.id>:<lens>` for the fanned-out review stage) — see
15
+ * `callerLabel` in runner.ts. This module's `stageKeyForCaller` is the inverse
16
+ * mapping back to a display row.
17
+ */
18
+ import type { SpendResponse } from "../costHud.js";
19
+ /**
20
+ * Render /go's end-of-run, server-priced cost table. Pure, no em-dashes,
21
+ * multi-line (joined with "\n"), leading space on the header/total lines so it
22
+ * reads as a continuation of the preceding recap sentence (matches
23
+ * runCostNote's own leading-space convention in goCommand.ts).
24
+ *
25
+ * Example:
26
+ * ```
27
+ * Run cost (server):
28
+ * map standard $0.05
29
+ * plan peak $0.31
30
+ * implement standard $0.12
31
+ * review peak $0.34
32
+ * fix standard $0.04
33
+ * Run: $0.84 billed. $2.71 at frontier list. 69% saved.
34
+ * ```
35
+ */
36
+ export declare function formatRunCostTable(spend: SpendResponse): string;
37
+ //# sourceMappingURL=runCostTable.d.ts.map
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Task 8 (/go summary prints a per-stage, server-priced cost table): a pure
3
+ * formatter over `GET /api/yagni-code/spend?runId=`'s response (see
4
+ * costHud.ts's SpendResponse/SpendRow mirror for the shape this reads).
5
+ *
6
+ * Kept out of costHud.ts deliberately: that module's `formatServerCostLines`
7
+ * renders a SESSION's spend grouped by rate tier (for /cost); this renders ONE
8
+ * RUN's spend grouped by PIPELINE STAGE (for /go's end-of-run summary), and the
9
+ * grouping/merge rules below (fold the three review lenses into one line,
10
+ * mixed-tier tie-break) are specific enough to this call site that sharing a
11
+ * module would just make costHud.ts harder to read for its own surface.
12
+ *
13
+ * Caller labels are minted by runner.ts as `go:<stage.id>` (or
14
+ * `go:<stage.id>:<lens>` for the fanned-out review stage) — see
15
+ * `callerLabel` in runner.ts. This module's `stageKeyForCaller` is the inverse
16
+ * mapping back to a display row.
17
+ */
18
+ import { usd } from "../costHud.js";
19
+ import { PIPELINE_V1 } from "./stages.js";
20
+ /**
21
+ * Pipeline stages in the order /go actually runs them, DERIVED from
22
+ * PIPELINE_V1 (stages.ts's single source of truth) rather than a hand-copied
23
+ * list — a future stage added there self-registers here in order, with
24
+ * nothing in this file to update or drift out of sync.
25
+ */
26
+ const STAGE_ORDER = PIPELINE_V1.map((s) => s.id);
27
+ /**
28
+ * Map a `GET /spend` caller label onto its display stage, or undefined for a
29
+ * caller this table does not recognize (surfaced instead as its own row,
30
+ * verbatim, after the known stages — see {@link buildRows}). `go:review` and
31
+ * every `go:review:<lens>` caller fold onto the single "review" row: the three
32
+ * adversarial lenses are one conceptual review pass, and a per-lens
33
+ * breakdown would just be noise in an end-of-run summary.
34
+ *
35
+ * Derives the mapping from STAGE_ORDER (advisor is the one non-pipeline
36
+ * exception, matched exactly) rather than hand-listing each `go:<id>`
37
+ * caller, so a stage added to PIPELINE_V1 needs no update here either: strip
38
+ * the `go:` prefix, take the first `:`-delimited segment (folds `go:review`
39
+ * and every `go:review:<lens>` onto the same "review" segment), and check it
40
+ * against the known stage ids.
41
+ */
42
+ function stageKeyForCaller(caller) {
43
+ if (caller === "advisor")
44
+ return "advisor";
45
+ if (!caller.startsWith("go:"))
46
+ return undefined;
47
+ const segment = caller.slice(3).split(":")[0];
48
+ return STAGE_ORDER.includes(segment) ? segment : undefined;
49
+ }
50
+ const sumSell = (rows) => rows.reduce((acc, r) => acc + r.sellMillicents, 0);
51
+ /**
52
+ * The tier shown for a (possibly multi-row) line: the rows' COMMON tier when
53
+ * they all billed under the same one, else the tier of whichever contributing
54
+ * row spent the most (a re-ask retry on `efficient`, or a server-side reroute,
55
+ * can leave one caller split across two tiers — the biggest-spend tier is the
56
+ * more honest single label than an arbitrary first-seen one).
57
+ */
58
+ function bucketTier(rows) {
59
+ const first = rows[0];
60
+ if (!first)
61
+ return "";
62
+ if (rows.every((r) => r.rateTier === first.rateTier))
63
+ return first.rateTier;
64
+ let biggest = first;
65
+ for (const r of rows) {
66
+ if (r.sellMillicents > biggest.sellMillicents)
67
+ biggest = r;
68
+ }
69
+ return biggest.rateTier;
70
+ }
71
+ /**
72
+ * Group spend rows into display rows: known pipeline stages (in pipeline
73
+ * order, review merged), then advisor when present, then every unrecognized
74
+ * caller (e.g. `subagent:foo`, `unattributed`, a bare `driver`) as its own
75
+ * row, sorted by spend descending (ties broken alphabetically, so output is
76
+ * deterministic). A stage with zero recorded dispatches is omitted rather
77
+ * than shown at $0.00: /go always runs map/plan/implement, but review/fix are
78
+ * absent on a run that stopped before reaching them (e.g. `no_changes`).
79
+ */
80
+ function buildRows(spendRows) {
81
+ const known = new Map();
82
+ const unknown = new Map();
83
+ for (const row of spendRows) {
84
+ const key = stageKeyForCaller(row.caller);
85
+ const bucket = key ?? row.caller;
86
+ const target = key ? known : unknown;
87
+ const list = target.get(bucket) ?? [];
88
+ list.push(row);
89
+ target.set(bucket, list);
90
+ }
91
+ const rows = [];
92
+ for (const stage of STAGE_ORDER) {
93
+ const stageRows = known.get(stage);
94
+ if (!stageRows || stageRows.length === 0)
95
+ continue;
96
+ rows.push({ label: stage, tier: bucketTier(stageRows), sellMillicents: sumSell(stageRows) });
97
+ }
98
+ const advisorRows = known.get("advisor");
99
+ if (advisorRows && advisorRows.length > 0) {
100
+ rows.push({ label: "advisor", tier: bucketTier(advisorRows), sellMillicents: sumSell(advisorRows) });
101
+ }
102
+ const unknownRows = [...unknown.entries()]
103
+ .map(([caller, r]) => ({ label: caller, tier: bucketTier(r), sellMillicents: sumSell(r) }))
104
+ .sort((a, b) => b.sellMillicents - a.sellMillicents || a.label.localeCompare(b.label));
105
+ rows.push(...unknownRows);
106
+ return rows;
107
+ }
108
+ /**
109
+ * `Run: $A billed. $B at frontier list. NN% saved.` — the "at frontier
110
+ * list" + savings clause only renders when `savingsPercent` resolved (typeof
111
+ * guard, matching formatServerCostLines's own malformed-response caution: a
112
+ * network response is untyped at runtime).
113
+ *
114
+ * `counterfactualIncomplete` and a numeric `savingsPercent` are MUTUALLY
115
+ * EXCLUSIVE by construction on the server (yagniCodeSpend.ts sets
116
+ * `savingsPercent` null whenever `counterfactualIncomplete` is true — there is
117
+ * no honest percentage to report when some billed spend fell on a tier with no
118
+ * Anthropic list-price counterfactual). So `counterfactualIncomplete` is
119
+ * checked on the non-number branch, not as a caveat appended to the number
120
+ * branch (that would be unreachable dead code, and would silently drop the
121
+ * whole savings clause on a partial counterfactual with no explanation).
122
+ */
123
+ function totalLine(spend) {
124
+ const billed = `$${usd(spend.totalSellMillicents)} billed.`;
125
+ if (typeof spend.savingsPercent !== "number") {
126
+ return spend.counterfactualIncomplete
127
+ ? ` Run: ${billed} Savings not shown: some spend billed on an unpriced tier.`
128
+ : ` Run: ${billed}`;
129
+ }
130
+ return ` Run: ${billed} $${usd(spend.totalCounterfactualMillicents)} at frontier list. ${spend.savingsPercent}% saved.`;
131
+ }
132
+ /**
133
+ * Render /go's end-of-run, server-priced cost table. Pure, no em-dashes,
134
+ * multi-line (joined with "\n"), leading space on the header/total lines so it
135
+ * reads as a continuation of the preceding recap sentence (matches
136
+ * runCostNote's own leading-space convention in goCommand.ts).
137
+ *
138
+ * Example:
139
+ * ```
140
+ * Run cost (server):
141
+ * map standard $0.05
142
+ * plan peak $0.31
143
+ * implement standard $0.12
144
+ * review peak $0.34
145
+ * fix standard $0.04
146
+ * Run: $0.84 billed. $2.71 at frontier list. 69% saved.
147
+ * ```
148
+ */
149
+ export function formatRunCostTable(spend) {
150
+ const rows = buildRows(spend.rows);
151
+ const labelWidth = rows.reduce((m, r) => Math.max(m, r.label.length), 0);
152
+ const tierWidth = rows.reduce((m, r) => Math.max(m, r.tier.length), 0);
153
+ const lines = [" Run cost (server):"];
154
+ for (const r of rows) {
155
+ lines.push(` ${r.label.padEnd(labelWidth + 2)}${r.tier.padEnd(tierWidth + 2)}$${usd(r.sellMillicents)}`);
156
+ }
157
+ lines.push(totalLine(spend));
158
+ if (spend.unbilledSellMillicents > 0) {
159
+ // Matches formatServerCostLines' + YagniCodeUsageView.tsx's wording for
160
+ // the same figure.
161
+ lines.push(` Includes $${usd(spend.unbilledSellMillicents)} not yet billed (insufficient credits or a billing error).`);
162
+ }
163
+ return lines.join("\n");
164
+ }
165
+ //# sourceMappingURL=runCostTable.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