@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,37 @@
1
+ /**
2
+ * PURE curation layer for the live `/go` activity feed (spec §3).
3
+ *
4
+ * Mirrors `events.ts`: deterministic functions over canned `JsonEvent`s, with no
5
+ * clock, no I/O, and no model call. This is where "summary, not every event" is
6
+ * enforced — we surface assistant narration and concrete tool actions and drop
7
+ * everything else. Every function is defensive on missing / oddly-shaped data:
8
+ * it never throws, only ever reads basenames (no absolute paths leak into the
9
+ * feed), and falls back to the bare tool name when args are unusable.
10
+ */
11
+ import { type ActivityLine, type JsonEvent, type StageTag } from "./types.js";
12
+ /**
13
+ * Deterministic, defensive label for a tool action. Reads only the fields the
14
+ * spec table names, only ever surfaces a basename, and falls back to the bare
15
+ * tool name whenever the expected arg is missing or not a usable string.
16
+ */
17
+ export declare function toolLabel(toolName: string, args?: Record<string, unknown>): string;
18
+ /**
19
+ * The first non-empty line of an assistant text part, collapsed and clipped to
20
+ * `NARRATION_MAX`. Returns null when there is no usable headline.
21
+ */
22
+ export declare function narrationHeadline(text: string): string | null;
23
+ /**
24
+ * Curate one raw NDJSON event into an `ActivityLine`, or null to drop it.
25
+ *
26
+ * - assistant `message_end` with a text part -> narration (drops pure tool-call
27
+ * turns and empty headlines)
28
+ * - `tool_execution_start` -> running action carrying its `toolCallId` + label
29
+ * - `tool_execution_end` -> resolving action (done / error) with no text; the
30
+ * feed model resolves it against the running line by `toolCallId`
31
+ * - everything else -> null
32
+ *
33
+ * `tag` travels alongside the line (the feed attributes it to a stage / lens);
34
+ * the curated line content itself is stage-agnostic, so `tag` is not read here.
35
+ */
36
+ export declare function eventToLine(ev: JsonEvent, _tag: StageTag): ActivityLine | null;
37
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * PURE curation layer for the live `/go` activity feed (spec §3).
3
+ *
4
+ * Mirrors `events.ts`: deterministic functions over canned `JsonEvent`s, with no
5
+ * clock, no I/O, and no model call. This is where "summary, not every event" is
6
+ * enforced — we surface assistant narration and concrete tool actions and drop
7
+ * everything else. Every function is defensive on missing / oddly-shaped data:
8
+ * it never throws, only ever reads basenames (no absolute paths leak into the
9
+ * feed), and falls back to the bare tool name when args are unusable.
10
+ */
11
+ import { NARRATION_MAX } from "./types.js";
12
+ /** ~40 chars of a bash command is enough to recognize it without flooding the row. */
13
+ const BASH_LABEL_MAX = 40;
14
+ /** First usable non-empty string among the candidates, else undefined. */
15
+ function firstString(...vals) {
16
+ for (const v of vals) {
17
+ if (typeof v === "string" && v.trim())
18
+ return v;
19
+ }
20
+ return undefined;
21
+ }
22
+ /** Last path segment only — never an absolute path. Empty string if unusable. */
23
+ function basename(value) {
24
+ if (typeof value !== "string")
25
+ return "";
26
+ const parts = value.split(/[/\\]/).filter(Boolean);
27
+ return parts.length > 0 ? parts[parts.length - 1] : "";
28
+ }
29
+ /** Collapse whitespace and clip to `max`, appending an ellipsis when cut. */
30
+ function clip(text, max) {
31
+ const collapsed = text.replace(/\s+/g, " ").trim();
32
+ if (collapsed.length <= max)
33
+ return collapsed;
34
+ return `${collapsed.slice(0, max - 1)}…`;
35
+ }
36
+ /** Turn `record_decision` into a readable `record decision`. */
37
+ function humanize(toolName) {
38
+ return toolName.replace(/_/g, " ");
39
+ }
40
+ /**
41
+ * Deterministic, defensive label for a tool action. Reads only the fields the
42
+ * spec table names, only ever surfaces a basename, and falls back to the bare
43
+ * tool name whenever the expected arg is missing or not a usable string.
44
+ */
45
+ export function toolLabel(toolName, args) {
46
+ const a = args ?? {};
47
+ switch (toolName) {
48
+ case "read": {
49
+ const f = firstString(a.path, a.file_path);
50
+ return f ? `read ${basename(f)}` : toolName;
51
+ }
52
+ case "edit":
53
+ case "write": {
54
+ const f = firstString(a.path, a.file_path);
55
+ return f ? `edit ${basename(f)}` : toolName;
56
+ }
57
+ case "bash": {
58
+ const cmd = firstString(a.command);
59
+ return cmd ? `bash ${clip(cmd, BASH_LABEL_MAX)}` : toolName;
60
+ }
61
+ case "grep": {
62
+ const pattern = firstString(a.pattern);
63
+ return pattern ? `grep "${clip(pattern, BASH_LABEL_MAX)}"` : toolName;
64
+ }
65
+ case "find":
66
+ case "ls": {
67
+ const target = firstString(a.path, a.pattern);
68
+ return target ? `find ${basename(target) || target}` : toolName;
69
+ }
70
+ case "ask_yagni":
71
+ case "review_business_match":
72
+ case "record_decision":
73
+ return humanize(toolName);
74
+ default:
75
+ return toolName;
76
+ }
77
+ }
78
+ /**
79
+ * The first non-empty line of an assistant text part, collapsed and clipped to
80
+ * `NARRATION_MAX`. Returns null when there is no usable headline.
81
+ */
82
+ export function narrationHeadline(text) {
83
+ if (typeof text !== "string")
84
+ return null;
85
+ const firstLine = text.split("\n").find((l) => l.trim().length > 0);
86
+ if (!firstLine)
87
+ return null;
88
+ const headline = clip(firstLine, NARRATION_MAX);
89
+ return headline.length > 0 ? headline : null;
90
+ }
91
+ /** The last text part of an assistant `message_end`, if any. */
92
+ function lastAssistantText(ev) {
93
+ if (ev.message?.role !== "assistant")
94
+ return undefined;
95
+ const parts = ev.message.content ?? [];
96
+ for (let i = parts.length - 1; i >= 0; i--) {
97
+ const part = parts[i];
98
+ if (part.type === "text" && typeof part.text === "string")
99
+ return part.text;
100
+ }
101
+ return undefined;
102
+ }
103
+ /**
104
+ * Curate one raw NDJSON event into an `ActivityLine`, or null to drop it.
105
+ *
106
+ * - assistant `message_end` with a text part -> narration (drops pure tool-call
107
+ * turns and empty headlines)
108
+ * - `tool_execution_start` -> running action carrying its `toolCallId` + label
109
+ * - `tool_execution_end` -> resolving action (done / error) with no text; the
110
+ * feed model resolves it against the running line by `toolCallId`
111
+ * - everything else -> null
112
+ *
113
+ * `tag` travels alongside the line (the feed attributes it to a stage / lens);
114
+ * the curated line content itself is stage-agnostic, so `tag` is not read here.
115
+ */
116
+ export function eventToLine(ev, _tag) {
117
+ switch (ev.type) {
118
+ case "message_end": {
119
+ const text = lastAssistantText(ev);
120
+ if (text === undefined)
121
+ return null;
122
+ const headline = narrationHeadline(text);
123
+ if (!headline)
124
+ return null;
125
+ return { kind: "narration", text: headline, state: "done" };
126
+ }
127
+ case "tool_execution_start": {
128
+ const line = {
129
+ kind: "action",
130
+ text: toolLabel(ev.toolName ?? "", ev.args),
131
+ state: "running",
132
+ };
133
+ if (ev.toolCallId !== undefined)
134
+ line.toolCallId = ev.toolCallId;
135
+ return line;
136
+ }
137
+ case "tool_execution_end": {
138
+ const line = {
139
+ kind: "action",
140
+ text: "",
141
+ state: ev.isError ? "error" : "done",
142
+ };
143
+ if (ev.toolCallId !== undefined)
144
+ line.toolCallId = ev.toolCallId;
145
+ return line;
146
+ }
147
+ default:
148
+ return null;
149
+ }
150
+ }
151
+ //# sourceMappingURL=activity.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The TERMINAL renderer for the live `/go` feed (spec §4).
3
+ *
4
+ * State lives in {@link RunState}; this file is presentation only. It renders
5
+ * the boxed panel painted above the editor, owning no I/O and no clock: the
6
+ * caller (the `/go` handler) injects the elapsed string and the spinner frame so
7
+ * `render` stays a pure function of current state and is fully deterministic in
8
+ * tests.
9
+ *
10
+ * Every constraint here is a TUI constraint and stays here rather than moving
11
+ * into the model: `ROW_MAX` (terminal width), `MAX_VISIBLE_ACTIONS` (pi's
12
+ * 10-line widget cap), and the flat ring buffer's `FEED_WINDOW` bound. The
13
+ * desktop renders the same state into a scrolling pane where none of them apply.
14
+ */
15
+ import { type ActivityLine, type PipelineProgress, type PipelineResult, type StageTag } from "./types.js";
16
+ /** The minimal slice of pi's `Theme` the feed styles with (a subset, like the masthead). */
17
+ export interface FeedTheme {
18
+ bold(s: string): string;
19
+ fg(color: string, s: string): string;
20
+ }
21
+ /** Spinner frames advanced by the caller; `render` is handed one frame, never a timer. */
22
+ export declare const SPINNER_FRAMES: string[];
23
+ /**
24
+ * Cap on the action rows actually rendered for the buffer stage. The ring buffer
25
+ * still holds up to FEED_WINDOW entries (state), but pi's aboveEditor widget keeps
26
+ * only the first MAX_WIDGET_LINES (10) lines and appends a "(widget truncated)"
27
+ * marker above that, dropping the NEWEST rows. We paint at most this many of the
28
+ * most-recent rows so the panel is header(1) + 5 stage rows + 4 actions = exactly
29
+ * 10 lines: at pi's ceiling (it truncates only ABOVE 10), and showing the newest
30
+ * actions rather than pi's first-10 (which would keep the oldest and add the marker).
31
+ */
32
+ export declare const MAX_VISIBLE_ACTIONS = 4;
33
+ export declare class ActivityFeed {
34
+ private readonly run;
35
+ /**
36
+ * The stage whose actions the ring buffer currently holds, and the buffer
37
+ * itself. Terminal-only: this flat, arrival-ordered ring is what lets the panel
38
+ * paint interleaved rows from the parallel review lenses under one `review`
39
+ * row, and it is bounded by FEED_WINDOW — the "can't get noisy" guarantee.
40
+ */
41
+ private bufferStageId;
42
+ private actions;
43
+ constructor(ticket: string);
44
+ /** Fold a structured progress signal into stage transitions + the header tally. */
45
+ applyProgress(p: PipelineProgress): void;
46
+ /** Fold one curated line: narration updates a stage summary; actions ride the ring buffer. */
47
+ applyEvent(line: ActivityLine, tag: StageTag): void;
48
+ /** The boxed panel lines (spec §4). Pure: spinner frame + elapsed are injected. */
49
+ render(theme: FeedTheme, elapsed?: string, spinnerFrame?: string): string[];
50
+ /**
51
+ * One compact line summarizing the finished run (spec §4). This is the feed's
52
+ * own terse outcome glyph-line and is intentionally DISTINCT from goCommand's
53
+ * `formatSummary`: that one is the durable `sendUserMessage` record and must
54
+ * also work on headless (no-feed) runs, so the two carry deliberately different
55
+ * phrasings rather than sharing one outcome-copy source.
56
+ */
57
+ finalLine(result: PipelineResult): string;
58
+ }
59
+ //# sourceMappingURL=activityFeed.d.ts.map
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The TERMINAL renderer for the live `/go` feed (spec §4).
3
+ *
4
+ * State lives in {@link RunState}; this file is presentation only. It renders
5
+ * the boxed panel painted above the editor, owning no I/O and no clock: the
6
+ * caller (the `/go` handler) injects the elapsed string and the spinner frame so
7
+ * `render` stays a pure function of current state and is fully deterministic in
8
+ * tests.
9
+ *
10
+ * Every constraint here is a TUI constraint and stays here rather than moving
11
+ * into the model: `ROW_MAX` (terminal width), `MAX_VISIBLE_ACTIONS` (pi's
12
+ * 10-line widget cap), and the flat ring buffer's `FEED_WINDOW` bound. The
13
+ * desktop renders the same state into a scrolling pane where none of them apply.
14
+ */
15
+ import { BRAND_NAME } from "../branding.js";
16
+ import { RunState } from "./runState.js";
17
+ import { FEED_WINDOW, } from "./types.js";
18
+ /** Spinner frames advanced by the caller; `render` is handed one frame, never a timer. */
19
+ export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
20
+ /** Defensive cap so a row never blows the terminal width even with a long summary. */
21
+ const ROW_MAX = 96;
22
+ /**
23
+ * Cap on the action rows actually rendered for the buffer stage. The ring buffer
24
+ * still holds up to FEED_WINDOW entries (state), but pi's aboveEditor widget keeps
25
+ * only the first MAX_WIDGET_LINES (10) lines and appends a "(widget truncated)"
26
+ * marker above that, dropping the NEWEST rows. We paint at most this many of the
27
+ * most-recent rows so the panel is header(1) + 5 stage rows + 4 actions = exactly
28
+ * 10 lines: at pi's ceiling (it truncates only ABOVE 10), and showing the newest
29
+ * actions rather than pi's first-10 (which would keep the oldest and add the marker).
30
+ */
31
+ export const MAX_VISIBLE_ACTIONS = 4;
32
+ /** Compact outcome phrasing per stop reason (no em-dashes). */
33
+ const FINAL_REASON = {
34
+ clean: "clean, no blockers remain",
35
+ round_cap: "round cap reached, blockers remain",
36
+ aborted: "aborted before review completed",
37
+ failed: "a stage failed before completing",
38
+ no_changes: "no changes produced, the implement stage wrote nothing. Re-run /go",
39
+ budget_exceeded: "budget ceiling reached, stopped before finishing",
40
+ partial: "a stage stopped early, partial changes left in your working tree",
41
+ };
42
+ function glyphForStatus(status, spinnerFrame) {
43
+ switch (status) {
44
+ case "done":
45
+ return "✔";
46
+ case "active":
47
+ return spinnerFrame;
48
+ case "failed":
49
+ return "✗";
50
+ default:
51
+ return "○";
52
+ }
53
+ }
54
+ function glyphForAction(state, spinnerFrame) {
55
+ if (state === "done")
56
+ return "✔";
57
+ if (state === "error")
58
+ return "✗";
59
+ return spinnerFrame;
60
+ }
61
+ const themeColorForStatus = {
62
+ done: "success",
63
+ active: "accent",
64
+ failed: "error",
65
+ pending: "dim",
66
+ };
67
+ function plural(n, word) {
68
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
69
+ }
70
+ function clipRow(text, max) {
71
+ if (text.length <= max)
72
+ return text;
73
+ return `${text.slice(0, max - 1)}…`;
74
+ }
75
+ function countBlocking(result) {
76
+ return result.findings.filter((f) => f.severity === "critical" || f.severity === "high").length;
77
+ }
78
+ export class ActivityFeed {
79
+ run;
80
+ /**
81
+ * The stage whose actions the ring buffer currently holds, and the buffer
82
+ * itself. Terminal-only: this flat, arrival-ordered ring is what lets the panel
83
+ * paint interleaved rows from the parallel review lenses under one `review`
84
+ * row, and it is bounded by FEED_WINDOW — the "can't get noisy" guarantee.
85
+ */
86
+ bufferStageId;
87
+ actions = [];
88
+ constructor(ticket) {
89
+ // The terminal surface has no run id and no clock: `render` is handed the
90
+ // elapsed string and spinner frame, so timestamps are never read here.
91
+ this.run = new RunState({ runId: "", ticket, startedAt: 0 });
92
+ }
93
+ /** Fold a structured progress signal into stage transitions + the header tally. */
94
+ applyProgress(p) {
95
+ // A per-lens signal is a desktop concern: it must not steal the buffer from
96
+ // the review stage that owns it, nor reset it mid fan-out.
97
+ if (p.kind === "stage_start" && !p.lens && p.stageId !== this.bufferStageId) {
98
+ this.bufferStageId = p.stageId;
99
+ this.actions = [];
100
+ }
101
+ this.run.applyProgress(p);
102
+ }
103
+ /** Fold one curated line: narration updates a stage summary; actions ride the ring buffer. */
104
+ applyEvent(line, tag) {
105
+ this.run.applyLine(line, tag);
106
+ if (line.kind !== "action")
107
+ return;
108
+ if (line.state === "running") {
109
+ if (this.bufferStageId === undefined)
110
+ this.bufferStageId = tag.stageId;
111
+ this.actions.push({ ...line });
112
+ if (this.actions.length > FEED_WINDOW)
113
+ this.actions.shift();
114
+ return;
115
+ }
116
+ if (line.toolCallId === undefined)
117
+ return;
118
+ const match = this.actions.find((a) => a.toolCallId === line.toolCallId);
119
+ if (match)
120
+ match.state = line.state;
121
+ }
122
+ /** The boxed panel lines (spec §4). Pure: spinner frame + elapsed are injected. */
123
+ render(theme, elapsed, spinnerFrame = SPINNER_FRAMES[0]) {
124
+ const { ticket, round, findingsTotal, findingsBlocking } = this.run.header();
125
+ const headerParts = [`${BRAND_NAME} · /go ${ticket}`, `round ${round}`, plural(findingsTotal, "finding")];
126
+ if (findingsBlocking > 0)
127
+ headerParts.push(`${findingsBlocking} blocking`);
128
+ if (elapsed)
129
+ headerParts.push(elapsed);
130
+ const header = theme.bold(clipRow(headerParts.join(" · "), ROW_MAX));
131
+ const lines = [header];
132
+ // Only the lens-less stage rows: the review fan-out's per-lens children are
133
+ // the desktop's business, and surfacing them here would break the 10-line cap.
134
+ for (const s of this.run.stageAgents()) {
135
+ // The FINISH row only exists once the stage actually starts (a clean stop):
136
+ // hiding it while pending keeps every non-clean run's panel byte-identical
137
+ // to the pre-FINISH layout and inside pi's 10-line widget cap.
138
+ if (s.id === "finish" && s.status === "pending")
139
+ continue;
140
+ const glyph = glyphForStatus(s.status, spinnerFrame);
141
+ const label = s.stageId.padEnd(9);
142
+ const row = clipRow(`${glyph} ${label}${s.summary}`.trimEnd(), ROW_MAX);
143
+ lines.push(theme.fg(themeColorForStatus[s.status], row));
144
+ // The ring buffer renders under its OWNING stage (see RunState), so a
145
+ // just-finished stage's resolved actions stay briefly visible beneath its ✔
146
+ // row. This keeps the immediate stage_done paint useful rather than blanking
147
+ // the actions the instant the stage flips to "done".
148
+ if (s.stageId === this.bufferStageId) {
149
+ // Only the most-recent rows render. A taller panel trips pi's
150
+ // MAX_WIDGET_LINES=10 truncation, which keeps the FIRST 10 lines and
151
+ // drops the newest actions plus a "(widget truncated)" marker; capping
152
+ // here keeps the newest actions visible and avoids the marker.
153
+ for (const a of this.actions.slice(-MAX_VISIBLE_ACTIONS)) {
154
+ const aGlyph = glyphForAction(a.state, spinnerFrame);
155
+ lines.push(theme.fg("dim", clipRow(` ↳ ${aGlyph} ${a.text}`, ROW_MAX)));
156
+ }
157
+ }
158
+ }
159
+ return lines;
160
+ }
161
+ /**
162
+ * One compact line summarizing the finished run (spec §4). This is the feed's
163
+ * own terse outcome glyph-line and is intentionally DISTINCT from goCommand's
164
+ * `formatSummary`: that one is the durable `sendUserMessage` record and must
165
+ * also work on headless (no-feed) runs, so the two carry deliberately different
166
+ * phrasings rather than sharing one outcome-copy source.
167
+ */
168
+ finalLine(result) {
169
+ const glyph = result.stopReason === "clean" ? "✔" : result.stopReason === "round_cap" ? "⚠" : "✗";
170
+ const blocking = countBlocking(result);
171
+ return (`${glyph} ${BRAND_NAME} /go ${this.run.header().ticket}: ${FINAL_REASON[result.stopReason]} · ` +
172
+ `${plural(result.rounds.length, "round")} · ${plural(result.findings.length, "finding")} (${blocking} blocking)`);
173
+ }
174
+ }
175
+ //# sourceMappingURL=activityFeed.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * R3-b - run-level budget / turn ceiling, enforced orchestrator-side.
3
+ *
4
+ * The frontier harnesses bound a run with a turn/budget ceiling; /go had none, so
5
+ * a pathological loop could spend without limit. These PURE helpers aggregate the
6
+ * per-stage usage the runner already records (StageResult.usage) across the whole
7
+ * run (build stages + every review lens + each fix) and report when an aggregate
8
+ * crosses a ceiling. The orchestrator stops honestly with `budget_exceeded` rather
9
+ * than running on.
10
+ *
11
+ * The turn cap is enforced here via aggregated `usage.turns` (not a pi --max-turns
12
+ * flag, which is unverified in the pi flag set; that is on the upstream-pi
13
+ * wishlist). Ceilings are generous safety bounds, not tight budgets, so a normal
14
+ * run never trips them.
15
+ */
16
+ import type { ReviewRound, StageResult } from "./types.js";
17
+ /** Aggregated usage across a whole run. */
18
+ export interface RunUsage {
19
+ turns: number;
20
+ cost: number;
21
+ input: number;
22
+ output: number;
23
+ }
24
+ /** The run-level ceilings. */
25
+ export interface RunBudget {
26
+ /** Max aggregate assistant turns across all stages. */
27
+ maxTurns: number;
28
+ /** Max aggregate cost (same unit as StageUsage.cost). */
29
+ maxCost: number;
30
+ /** Max aggregate input+output tokens. */
31
+ maxTokens: number;
32
+ }
33
+ /**
34
+ * Generous default ceilings: a 3-round /go fans out review across 3 lenses on top
35
+ * of the build half, so dozens of turns and millions of tokens are normal; these
36
+ * only catch a runaway. Tunable per lane.
37
+ */
38
+ export declare const DEFAULT_RUN_BUDGET: RunBudget;
39
+ export declare const EMPTY_RUN_USAGE: RunUsage;
40
+ export declare function addRunUsage(a: RunUsage, b: RunUsage): RunUsage;
41
+ /** Sum usage across the build/fix stages AND every review lens result. */
42
+ export declare function aggregateRunUsage(stages: StageResult[], rounds: ReviewRound[]): RunUsage;
43
+ /**
44
+ * Return an honest reason string when an aggregate usage crosses any ceiling, else
45
+ * null. Checked at each stage boundary so the run stops at the first crossing.
46
+ */
47
+ export declare function exceedsBudget(usage: RunUsage, budget: RunBudget): string | null;
48
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * R3-b - run-level budget / turn ceiling, enforced orchestrator-side.
3
+ *
4
+ * The frontier harnesses bound a run with a turn/budget ceiling; /go had none, so
5
+ * a pathological loop could spend without limit. These PURE helpers aggregate the
6
+ * per-stage usage the runner already records (StageResult.usage) across the whole
7
+ * run (build stages + every review lens + each fix) and report when an aggregate
8
+ * crosses a ceiling. The orchestrator stops honestly with `budget_exceeded` rather
9
+ * than running on.
10
+ *
11
+ * The turn cap is enforced here via aggregated `usage.turns` (not a pi --max-turns
12
+ * flag, which is unverified in the pi flag set; that is on the upstream-pi
13
+ * wishlist). Ceilings are generous safety bounds, not tight budgets, so a normal
14
+ * run never trips them.
15
+ */
16
+ /**
17
+ * Generous default ceilings: a 3-round /go fans out review across 3 lenses on top
18
+ * of the build half, so dozens of turns and millions of tokens are normal; these
19
+ * only catch a runaway. Tunable per lane.
20
+ */
21
+ export const DEFAULT_RUN_BUDGET = {
22
+ maxTurns: 400,
23
+ maxCost: 50,
24
+ maxTokens: 8_000_000,
25
+ };
26
+ export const EMPTY_RUN_USAGE = { turns: 0, cost: 0, input: 0, output: 0 };
27
+ export function addRunUsage(a, b) {
28
+ return {
29
+ turns: a.turns + b.turns,
30
+ cost: a.cost + b.cost,
31
+ input: a.input + b.input,
32
+ output: a.output + b.output,
33
+ };
34
+ }
35
+ /** Sum usage across the build/fix stages AND every review lens result. */
36
+ export function aggregateRunUsage(stages, rounds) {
37
+ const acc = { ...EMPTY_RUN_USAGE };
38
+ const add = (u) => {
39
+ acc.turns += u.turns;
40
+ acc.cost += u.cost;
41
+ acc.input += u.input;
42
+ acc.output += u.output;
43
+ };
44
+ for (const s of stages)
45
+ add(s.usage);
46
+ for (const r of rounds)
47
+ for (const l of r.lensResults)
48
+ add(l.usage);
49
+ return acc;
50
+ }
51
+ /**
52
+ * Return an honest reason string when an aggregate usage crosses any ceiling, else
53
+ * null. Checked at each stage boundary so the run stops at the first crossing.
54
+ */
55
+ export function exceedsBudget(usage, budget) {
56
+ if (usage.turns > budget.maxTurns) {
57
+ return `turn ceiling reached (${usage.turns} of ${budget.maxTurns} model turns)`;
58
+ }
59
+ if (usage.cost > budget.maxCost) {
60
+ return `cost ceiling reached (${usage.cost.toFixed(2)} of ${budget.maxCost})`;
61
+ }
62
+ const tokens = usage.input + usage.output;
63
+ if (tokens > budget.maxTokens) {
64
+ return `token ceiling reached (${tokens} of ${budget.maxTokens} tokens)`;
65
+ }
66
+ return null;
67
+ }
68
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Fail-soft {@link CheckpointStore} implementations — the durable substrate
3
+ * behind /go resume.
4
+ *
5
+ * Three stores, all sharing the iron contract: NEVER throw, NEVER block /go.
6
+ * - {@link makeFileCheckpointStore} — the durable PRIMARY. An append-only NDJSON
7
+ * file at `~/.yagni-code/runs/<sha1(sessionKey)>.ndjson` (mkdir 0700, file
8
+ * 0600 — mirrors credentials.ts). Session-independent and auth-free, so a
9
+ * LATER pi session (or one started after `/new`) can still read where /go
10
+ * left off, even when the backend was unreachable. A torn final write is
11
+ * harmless: load drops any unparseable line (the classic crash artifact).
12
+ * - {@link makePiJournalCheckpointStore} — a MIRROR into pi's native append-only
13
+ * session journal via `pi.appendEntry` / `sessionManager.getEntries`, the
14
+ * pi-idiomatic persistence the user asked about. Constructed from function
15
+ * seams so it is unit-testable without a pi process.
16
+ * - {@link makeCombinedCheckpointStore} — writes to all stores; reads from the
17
+ * first non-empty (file primary).
18
+ */
19
+ import type { CheckpointStore } from "./types.js";
20
+ export declare function _setCheckpointHomeForTest(dir: string | null): void;
21
+ export declare function makeFileCheckpointStore(sessionKey: string): CheckpointStore;
22
+ /** The pi customType under which checkpoint records live in the session journal. */
23
+ export declare const PI_CHECKPOINT_CUSTOM_TYPE = "yagni-go-checkpoint";
24
+ /** Function seams onto pi's ExtensionAPI/SessionManager (so this is testable without pi). */
25
+ export interface PiJournalSeams {
26
+ appendEntry: (customType: string, data: unknown) => void;
27
+ getEntries: () => unknown[];
28
+ }
29
+ export declare function makePiJournalCheckpointStore(sessionKey: string, seams: PiJournalSeams): CheckpointStore;
30
+ export declare function makeCombinedCheckpointStore(stores: CheckpointStore[]): CheckpointStore;
31
+ //# sourceMappingURL=checkpoint.d.ts.map