@yagni-app/code 0.1.0 → 0.2.0
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.
- package/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +42 -0
- package/dist/extension/rerouteNotice.js +67 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- 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
|