@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1002.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- 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 +188 -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 +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -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 +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -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 +168 -0
- package/dist/extension/pipeline/eval.js +182 -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 +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -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 +636 -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/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 +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -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 +51 -0
- package/dist/extension/pipeline/stages.js +113 -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 +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -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 +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -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/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/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 +135 -0
- package/dist/extension/subagents.js +281 -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 +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/go` run-session adapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the device-token `/api/yagni-code/runs*` endpoints so a YAGNI Code
|
|
5
|
+
* pipeline run becomes a tracked `runs` row visible on the Work page: open the
|
|
6
|
+
* run (`start`), stream each pipeline stage (`stage`), bank an in-loop
|
|
7
|
+
* correction (`correction`), and close it with an honest outcome (`finish`).
|
|
8
|
+
*
|
|
9
|
+
* CRITICAL invariant: recording must NEVER break the pipeline. Every call is
|
|
10
|
+
* fail-soft — it never throws. `start` resolves to a typed outcome that
|
|
11
|
+
* distinguishes a tracked run from an untracked one (flag off, unauthorized, or
|
|
12
|
+
* the backend unreachable/erroring) so the caller can surface a notice instead
|
|
13
|
+
* of failing silently; the post-start verbs (`stage`/`correction`/`finish`) stay
|
|
14
|
+
* silent no-ops. `/go` runs to completion regardless of the recording outcome.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the fetch-seam shape of {@link makeRecordEngineeringContextTool}
|
|
17
|
+
* (`baseUrl` + injectable `getToken` + `fetchImpl`) so it is unit-testable
|
|
18
|
+
* without a network and reuses the same auth convention.
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { DEFAULT_FETCH_POLICY, METERED_POST_FETCH_POLICY, resilientFetch, } from "../resilientFetch.js";
|
|
22
|
+
/** Map a failed POST's HTTP status onto a user-facing tracking-failure reason. */
|
|
23
|
+
function failureReason(status) {
|
|
24
|
+
if (status === 404)
|
|
25
|
+
return "disabled";
|
|
26
|
+
if (status === 401 || status === 403)
|
|
27
|
+
return "unauthorized";
|
|
28
|
+
return "error";
|
|
29
|
+
}
|
|
30
|
+
/** Cap a debug body excerpt so a giant HTML/JSON error never floods the channel. */
|
|
31
|
+
const MAX_DEBUG_BODY = 200;
|
|
32
|
+
/**
|
|
33
|
+
* A token-free, capped `: <body>` suffix for a failed-response debug line. Reads
|
|
34
|
+
* the body only on the `!res.ok` branch (where `post` never reads it otherwise,
|
|
35
|
+
* so there is no double-consume with the ok-path `res.json()`), and swallows any
|
|
36
|
+
* read error so observability never breaks the fail-soft contract.
|
|
37
|
+
*/
|
|
38
|
+
async function bodyTail(res) {
|
|
39
|
+
try {
|
|
40
|
+
const t = (await res.text()).trim();
|
|
41
|
+
if (!t)
|
|
42
|
+
return "";
|
|
43
|
+
return `: ${t.length > MAX_DEBUG_BODY ? `${t.slice(0, MAX_DEBUG_BODY)} ...` : t}`;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function makeRunSession(opts) {
|
|
50
|
+
const f = opts.fetchImpl ?? fetch;
|
|
51
|
+
let handle = null;
|
|
52
|
+
// Opt-in observability: the default sink writes to stderr, and the `YAGNI_CODE_DEBUG`
|
|
53
|
+
// gate governs an injected sink too, so callers that pass neither stay silent. The
|
|
54
|
+
// resolved sink is wrapped so it can NEVER throw out of `debug`: an injected sink
|
|
55
|
+
// that throws must not breach the fail-soft never-throw contract (in the `!res.ok`
|
|
56
|
+
// branch an unguarded throw would land in the surrounding catch and re-fire debug,
|
|
57
|
+
// escaping `post` and rejecting the recording call).
|
|
58
|
+
const sink = opts.debug ??
|
|
59
|
+
((line) => {
|
|
60
|
+
try {
|
|
61
|
+
process.stderr.write(`${line}\n`);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* never break recording */
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const debug = (line) => {
|
|
68
|
+
try {
|
|
69
|
+
sink(line);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* observability never breaks recording */
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const debugOn = !!(opts.env ?? process.env).YAGNI_CODE_DEBUG;
|
|
76
|
+
// A run-open (`start`) is the one recording call worth retrying: if it is lost,
|
|
77
|
+
// the WHOLE run goes untracked. It is made replay-safe by an idempotency key (the
|
|
78
|
+
// server dedups a re-sent create), so it uses the retrying policy. Every other
|
|
79
|
+
// verb is append-only / terminal and stays single-attempt (METERED) so a recorded
|
|
80
|
+
// judgment (a correction) is never duplicated by a client replay.
|
|
81
|
+
const post = async (path, body, postOpts = {}) => {
|
|
82
|
+
try {
|
|
83
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/${path}`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
}, {
|
|
91
|
+
fetchImpl: f,
|
|
92
|
+
policy: postOpts.retry ? DEFAULT_FETCH_POLICY : METERED_POST_FETCH_POLICY,
|
|
93
|
+
...(opts.sleep ? { sleep: opts.sleep } : {}),
|
|
94
|
+
...(opts.random ? { random: opts.random } : {}),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
if (debugOn)
|
|
98
|
+
debug(`[yagni-code] run-record ${path} -> HTTP ${res.status}${await bodyTail(res)}`);
|
|
99
|
+
return { ok: false, status: res.status };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, data: (await res.json()) };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
// fail-soft: recording must never break the pipeline (no status = unreachable
|
|
105
|
+
// after retries).
|
|
106
|
+
if (debugOn) {
|
|
107
|
+
debug(`[yagni-code] run-record ${path} unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
return { ok: false };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
bind(h) {
|
|
114
|
+
handle = h;
|
|
115
|
+
},
|
|
116
|
+
async start(b) {
|
|
117
|
+
// One idempotency key per open attempt: resilientFetch replays the SAME
|
|
118
|
+
// request body across retries, so a create whose 201 was lost to a blip is
|
|
119
|
+
// deduped by the server (startCodeRun.findRunByIdempotencyKey) instead of
|
|
120
|
+
// opening a second run + a second free-text work item.
|
|
121
|
+
const r = await post("runs", { ...b, idempotencyKey: randomUUID() }, { retry: true });
|
|
122
|
+
if (!r.ok)
|
|
123
|
+
return { tracked: false, reason: failureReason(r.status) };
|
|
124
|
+
const data = r.data;
|
|
125
|
+
if (typeof data.runId === "string" && typeof data.workItemId === "string") {
|
|
126
|
+
handle = { runId: data.runId, workItemId: data.workItemId };
|
|
127
|
+
return {
|
|
128
|
+
tracked: true,
|
|
129
|
+
runId: handle.runId,
|
|
130
|
+
workItemId: handle.workItemId,
|
|
131
|
+
gated: data.gated === true,
|
|
132
|
+
gatedReason: typeof data.gatedReason === "string" ? data.gatedReason : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// A 2xx with an unrecognized body: we can't bind a handle, so it's untracked.
|
|
136
|
+
if (debugOn)
|
|
137
|
+
debug("[yagni-code] run-record runs -> 2xx but body had no runId/workItemId");
|
|
138
|
+
return { tracked: false, reason: "error" };
|
|
139
|
+
},
|
|
140
|
+
async stage(b) {
|
|
141
|
+
if (!handle)
|
|
142
|
+
return;
|
|
143
|
+
await post(`runs/${handle.runId}/stages`, { ...b, workItemId: handle.workItemId });
|
|
144
|
+
},
|
|
145
|
+
async correction(b) {
|
|
146
|
+
if (!handle)
|
|
147
|
+
return;
|
|
148
|
+
await post(`runs/${handle.runId}/correction`, { ...b, workItemId: handle.workItemId });
|
|
149
|
+
},
|
|
150
|
+
async recordPlan(b) {
|
|
151
|
+
if (!handle)
|
|
152
|
+
return;
|
|
153
|
+
await post(`runs/${handle.runId}/plan`, { ...b, workItemId: handle.workItemId });
|
|
154
|
+
},
|
|
155
|
+
async heartbeat() {
|
|
156
|
+
if (!handle)
|
|
157
|
+
return;
|
|
158
|
+
await post(`runs/${handle.runId}/heartbeat`, { workItemId: handle.workItemId });
|
|
159
|
+
},
|
|
160
|
+
async finish(b) {
|
|
161
|
+
if (!handle)
|
|
162
|
+
return;
|
|
163
|
+
await post(`runs/${handle.runId}/finish`, { ...b, workItemId: handle.workItemId });
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=runSession.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headless model behind every `/go` progress display.
|
|
3
|
+
*
|
|
4
|
+
* `RunState` folds the structured `PipelineProgress` signals, curated
|
|
5
|
+
* `ActivityLine`s and raw assistant `JsonEvent` usage into one small model that
|
|
6
|
+
* both surfaces render from: the terminal's boxed ANSI panel (via
|
|
7
|
+
* `ActivityFeed`, which is now a pure renderer over this) and the desktop's
|
|
8
|
+
* Agents tab (via `toJSON()`, pushed as one line over `setWidget`).
|
|
9
|
+
*
|
|
10
|
+
* It owns no I/O and no clock — the caller injects `at` — so it is fully
|
|
11
|
+
* deterministic in tests, exactly like the feed it replaces.
|
|
12
|
+
*
|
|
13
|
+
* Two properties are load-bearing:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Lens events never touch their parent stage.** A `stage_start` /
|
|
16
|
+
* `stage_done` carrying a `lens` mutates only that lens's agent. Without
|
|
17
|
+
* this the per-lens signals would mark the terminal's `review` row done
|
|
18
|
+
* while two lenses were still running.
|
|
19
|
+
*
|
|
20
|
+
* 2. **Per-agent action lists only.** The terminal's flat, arrival-ordered ring
|
|
21
|
+
* buffer stays in `ActivityFeed`: it exists to fit pi's widget cap, which is
|
|
22
|
+
* a property of that renderer rather than of the run. Keeping it there means
|
|
23
|
+
* the panel's cross-lens interleaving is preserved by code that never moved.
|
|
24
|
+
*/
|
|
25
|
+
import type { ActivityLine, FeedStageId, JsonEvent, PipelineProgress, ReviewLens, StageTag, StopReason } from "./types.js";
|
|
26
|
+
/** The fixed stage order shown by both surfaces (v1 stages + the post-loop FINISH row). */
|
|
27
|
+
export declare const STAGE_ORDER: FeedStageId[];
|
|
28
|
+
/**
|
|
29
|
+
* Per-agent action cap. Generous compared to the terminal's `MAX_VISIBLE_ACTIONS`
|
|
30
|
+
* because the desktop pane scrolls: the TUI's 4-row ceiling is a widget-height
|
|
31
|
+
* constraint, not a truth about how much happened.
|
|
32
|
+
*/
|
|
33
|
+
export declare const AGENT_ACTION_WINDOW = 40;
|
|
34
|
+
export type AgentStatus = "pending" | "active" | "done" | "failed";
|
|
35
|
+
export interface AgentAction {
|
|
36
|
+
toolCallId?: string;
|
|
37
|
+
text: string;
|
|
38
|
+
state: "running" | "done" | "error";
|
|
39
|
+
}
|
|
40
|
+
export interface AgentUsage {
|
|
41
|
+
input: number;
|
|
42
|
+
output: number;
|
|
43
|
+
cacheRead: number;
|
|
44
|
+
cacheWrite: number;
|
|
45
|
+
cost: number;
|
|
46
|
+
turns: number;
|
|
47
|
+
}
|
|
48
|
+
export interface AgentNode {
|
|
49
|
+
/**
|
|
50
|
+
* Stable across paints: the stage id, `review:<lens>:r<round>` for a lens
|
|
51
|
+
* child, or `fix:r<round>` for a per-round fix execution.
|
|
52
|
+
*/
|
|
53
|
+
id: string;
|
|
54
|
+
stageId: FeedStageId;
|
|
55
|
+
lens?: ReviewLens;
|
|
56
|
+
round?: number;
|
|
57
|
+
status: AgentStatus;
|
|
58
|
+
summary: string;
|
|
59
|
+
startedAt?: number;
|
|
60
|
+
endedAt?: number;
|
|
61
|
+
actions: AgentAction[];
|
|
62
|
+
usage: AgentUsage;
|
|
63
|
+
/** Per-lens finding counts, attached when the round's parse loop resolves this lens. */
|
|
64
|
+
findings?: number;
|
|
65
|
+
blocking?: number;
|
|
66
|
+
/** Why this lens could not fully carry the round (spec §3e). */
|
|
67
|
+
degraded?: "failed" | "unparseable";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One completed round's finding tally, kept as history rather than overwritten:
|
|
71
|
+
* the trajectory (blocking 6 → 5 → 5) is what tells whether the loop converged.
|
|
72
|
+
*/
|
|
73
|
+
export interface RoundTally {
|
|
74
|
+
round: number;
|
|
75
|
+
findings: number;
|
|
76
|
+
blocking: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The wire record. Deliberately carries NO local-only fields — no worktree paths,
|
|
80
|
+
* no pids, no absolute paths — so it stays portable to a non-local executor at
|
|
81
|
+
* zero cost. The tab does not need them.
|
|
82
|
+
*/
|
|
83
|
+
export interface GoRunState {
|
|
84
|
+
v: 1;
|
|
85
|
+
runId: string;
|
|
86
|
+
ticket: string;
|
|
87
|
+
round: number;
|
|
88
|
+
findingsTotal: number;
|
|
89
|
+
findingsBlocking: number;
|
|
90
|
+
rounds: RoundTally[];
|
|
91
|
+
startedAt: number;
|
|
92
|
+
endedAt?: number;
|
|
93
|
+
stopReason?: StopReason;
|
|
94
|
+
agents: AgentNode[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stable agent id: a lens child is keyed by its lens AND round, and fix — the
|
|
98
|
+
* only lens-less stage that re-executes per round — by its round, so each
|
|
99
|
+
* round's fix keeps its own clock and usage instead of accumulating tokens on a
|
|
100
|
+
* timer that resets (the "181k tok in 59s" artifact). Review stage-level events
|
|
101
|
+
* stay on the bare `review` node: they mark the round boundary, and the lens
|
|
102
|
+
* children carry the round's real work.
|
|
103
|
+
*/
|
|
104
|
+
export declare function agentId(stageId: FeedStageId, lens?: ReviewLens, round?: number): string;
|
|
105
|
+
export interface RunStateInit {
|
|
106
|
+
runId: string;
|
|
107
|
+
ticket: string;
|
|
108
|
+
startedAt: number;
|
|
109
|
+
}
|
|
110
|
+
export declare class RunState {
|
|
111
|
+
private readonly runId;
|
|
112
|
+
private readonly ticket;
|
|
113
|
+
private readonly startedAt;
|
|
114
|
+
private endedAt;
|
|
115
|
+
private stopReason;
|
|
116
|
+
private round;
|
|
117
|
+
private findingsTotal;
|
|
118
|
+
private findingsBlocking;
|
|
119
|
+
private readonly rounds;
|
|
120
|
+
private readonly agents;
|
|
121
|
+
constructor(init: RunStateInit);
|
|
122
|
+
/** The lens-less stage agent, or undefined for an unknown stage. */
|
|
123
|
+
private stageAgent;
|
|
124
|
+
/**
|
|
125
|
+
* The agent for a tag, created on demand. Lens children are inserted directly
|
|
126
|
+
* after their parent stage and any existing siblings, so the array order still
|
|
127
|
+
* reads top-to-bottom for the desktop and the lens-less filter still yields
|
|
128
|
+
* STAGE_ORDER for the terminal.
|
|
129
|
+
*/
|
|
130
|
+
private ensureAgent;
|
|
131
|
+
/** Fold a structured progress signal. `at` is only read by the desktop surface. */
|
|
132
|
+
applyProgress(p: PipelineProgress, at?: number): void;
|
|
133
|
+
/**
|
|
134
|
+
* Fold one curated line. Narration becomes the emitting agent's summary;
|
|
135
|
+
* actions ride BOTH the agent's own list and the terminal's flat ring buffer.
|
|
136
|
+
*/
|
|
137
|
+
applyLine(line: ActivityLine, tag: StageTag, at?: number): void;
|
|
138
|
+
/** Fold an assistant turn's usage into the emitting agent. Ignores everything else. */
|
|
139
|
+
applyUsage(ev: JsonEvent, tag: StageTag, at?: number): void;
|
|
140
|
+
/** Stamp the run's end. Separate from `done` because FINISH runs after the pipeline returns. */
|
|
141
|
+
markEnded(at: number): void;
|
|
142
|
+
/**
|
|
143
|
+
* Stage rows for the panel: one row per stage, in order. Lens children are the
|
|
144
|
+
* desktop's business alone, and a looped stage that re-executes per round
|
|
145
|
+
* (fix) paints its LATEST round's node so the row shows the live execution,
|
|
146
|
+
* not a stale bare placeholder.
|
|
147
|
+
*/
|
|
148
|
+
stageAgents(): AgentNode[];
|
|
149
|
+
header(): {
|
|
150
|
+
ticket: string;
|
|
151
|
+
round: number;
|
|
152
|
+
findingsTotal: number;
|
|
153
|
+
findingsBlocking: number;
|
|
154
|
+
};
|
|
155
|
+
/** Named `toJSON` so `JSON.stringify(run)` yields the wire shape directly. */
|
|
156
|
+
toJSON(): GoRunState;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=runState.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headless model behind every `/go` progress display.
|
|
3
|
+
*
|
|
4
|
+
* `RunState` folds the structured `PipelineProgress` signals, curated
|
|
5
|
+
* `ActivityLine`s and raw assistant `JsonEvent` usage into one small model that
|
|
6
|
+
* both surfaces render from: the terminal's boxed ANSI panel (via
|
|
7
|
+
* `ActivityFeed`, which is now a pure renderer over this) and the desktop's
|
|
8
|
+
* Agents tab (via `toJSON()`, pushed as one line over `setWidget`).
|
|
9
|
+
*
|
|
10
|
+
* It owns no I/O and no clock — the caller injects `at` — so it is fully
|
|
11
|
+
* deterministic in tests, exactly like the feed it replaces.
|
|
12
|
+
*
|
|
13
|
+
* Two properties are load-bearing:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Lens events never touch their parent stage.** A `stage_start` /
|
|
16
|
+
* `stage_done` carrying a `lens` mutates only that lens's agent. Without
|
|
17
|
+
* this the per-lens signals would mark the terminal's `review` row done
|
|
18
|
+
* while two lenses were still running.
|
|
19
|
+
*
|
|
20
|
+
* 2. **Per-agent action lists only.** The terminal's flat, arrival-ordered ring
|
|
21
|
+
* buffer stays in `ActivityFeed`: it exists to fit pi's widget cap, which is
|
|
22
|
+
* a property of that renderer rather than of the run. Keeping it there means
|
|
23
|
+
* the panel's cross-lens interleaving is preserved by code that never moved.
|
|
24
|
+
*/
|
|
25
|
+
/** The fixed stage order shown by both surfaces (v1 stages + the post-loop FINISH row). */
|
|
26
|
+
export const STAGE_ORDER = ["map", "plan", "implement", "review", "fix", "finish"];
|
|
27
|
+
/**
|
|
28
|
+
* Per-agent action cap. Generous compared to the terminal's `MAX_VISIBLE_ACTIONS`
|
|
29
|
+
* because the desktop pane scrolls: the TUI's 4-row ceiling is a widget-height
|
|
30
|
+
* constraint, not a truth about how much happened.
|
|
31
|
+
*/
|
|
32
|
+
export const AGENT_ACTION_WINDOW = 40;
|
|
33
|
+
function emptyUsage() {
|
|
34
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stable agent id: a lens child is keyed by its lens AND round, and fix — the
|
|
38
|
+
* only lens-less stage that re-executes per round — by its round, so each
|
|
39
|
+
* round's fix keeps its own clock and usage instead of accumulating tokens on a
|
|
40
|
+
* timer that resets (the "181k tok in 59s" artifact). Review stage-level events
|
|
41
|
+
* stay on the bare `review` node: they mark the round boundary, and the lens
|
|
42
|
+
* children carry the round's real work.
|
|
43
|
+
*/
|
|
44
|
+
export function agentId(stageId, lens, round) {
|
|
45
|
+
if (lens)
|
|
46
|
+
return `${stageId}:${lens}:r${round ?? 1}`;
|
|
47
|
+
if (stageId === "fix" && round !== undefined)
|
|
48
|
+
return `fix:r${round}`;
|
|
49
|
+
return stageId;
|
|
50
|
+
}
|
|
51
|
+
export class RunState {
|
|
52
|
+
runId;
|
|
53
|
+
ticket;
|
|
54
|
+
startedAt;
|
|
55
|
+
endedAt;
|
|
56
|
+
stopReason;
|
|
57
|
+
round = 0;
|
|
58
|
+
findingsTotal = 0;
|
|
59
|
+
findingsBlocking = 0;
|
|
60
|
+
rounds = [];
|
|
61
|
+
agents;
|
|
62
|
+
constructor(init) {
|
|
63
|
+
this.runId = init.runId;
|
|
64
|
+
this.ticket = init.ticket;
|
|
65
|
+
this.startedAt = init.startedAt;
|
|
66
|
+
this.agents = STAGE_ORDER.map((id) => ({
|
|
67
|
+
id,
|
|
68
|
+
stageId: id,
|
|
69
|
+
status: "pending",
|
|
70
|
+
summary: "",
|
|
71
|
+
actions: [],
|
|
72
|
+
usage: emptyUsage(),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
/** The lens-less stage agent, or undefined for an unknown stage. */
|
|
76
|
+
stageAgent(stageId) {
|
|
77
|
+
return this.agents.find((a) => a.id === stageId);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The agent for a tag, created on demand. Lens children are inserted directly
|
|
81
|
+
* after their parent stage and any existing siblings, so the array order still
|
|
82
|
+
* reads top-to-bottom for the desktop and the lens-less filter still yields
|
|
83
|
+
* STAGE_ORDER for the terminal.
|
|
84
|
+
*/
|
|
85
|
+
ensureAgent(stageId, lens, round, at) {
|
|
86
|
+
const id = agentId(stageId, lens, round);
|
|
87
|
+
const found = this.agents.find((a) => a.id === id);
|
|
88
|
+
if (found)
|
|
89
|
+
return found;
|
|
90
|
+
const node = {
|
|
91
|
+
id,
|
|
92
|
+
stageId,
|
|
93
|
+
...(lens ? { lens } : {}),
|
|
94
|
+
...(round !== undefined ? { round } : {}),
|
|
95
|
+
status: "active",
|
|
96
|
+
summary: "",
|
|
97
|
+
startedAt: at,
|
|
98
|
+
actions: [],
|
|
99
|
+
usage: emptyUsage(),
|
|
100
|
+
};
|
|
101
|
+
const parentIdx = this.agents.findIndex((a) => a.id === stageId);
|
|
102
|
+
if (parentIdx < 0) {
|
|
103
|
+
this.agents.push(node);
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
let insertAt = parentIdx + 1;
|
|
107
|
+
// Skip existing children (lens or per-round nodes: any id that is not its own
|
|
108
|
+
// stage row) so siblings append in arrival order after their parent.
|
|
109
|
+
while (insertAt < this.agents.length && this.agents[insertAt]?.id !== this.agents[insertAt]?.stageId)
|
|
110
|
+
insertAt += 1;
|
|
111
|
+
this.agents.splice(insertAt, 0, node);
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
/** Fold a structured progress signal. `at` is only read by the desktop surface. */
|
|
115
|
+
applyProgress(p, at = 0) {
|
|
116
|
+
switch (p.kind) {
|
|
117
|
+
case "stage_start": {
|
|
118
|
+
if (typeof p.round === "number")
|
|
119
|
+
this.round = p.round;
|
|
120
|
+
// `ensureAgent` routes by id: a lens or per-round fix signal creates its
|
|
121
|
+
// own child; a bare stage signal (including review's round boundary,
|
|
122
|
+
// which must return to active even though a prior round left it done)
|
|
123
|
+
// lands on the pre-seeded stage node.
|
|
124
|
+
const target = this.ensureAgent(p.stageId, p.lens, p.round, at);
|
|
125
|
+
target.status = "active";
|
|
126
|
+
target.startedAt = at;
|
|
127
|
+
// `delete`, not `= undefined`: an explicitly-undefined property still
|
|
128
|
+
// shows up in Object.keys, and the wire shape is asserted by key set.
|
|
129
|
+
delete target.endedAt;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "stage_done": {
|
|
133
|
+
if (typeof p.round === "number")
|
|
134
|
+
this.round = p.round;
|
|
135
|
+
const target = this.ensureAgent(p.stageId, p.lens, p.round, at);
|
|
136
|
+
if (p.degraded === "failed") {
|
|
137
|
+
target.status = "failed";
|
|
138
|
+
}
|
|
139
|
+
else if (target.status !== "failed") {
|
|
140
|
+
target.status = "done";
|
|
141
|
+
}
|
|
142
|
+
target.endedAt = at;
|
|
143
|
+
if (typeof p.findings === "number")
|
|
144
|
+
target.findings = p.findings;
|
|
145
|
+
if (typeof p.blocking === "number")
|
|
146
|
+
target.blocking = p.blocking;
|
|
147
|
+
if (p.degraded)
|
|
148
|
+
target.degraded = p.degraded;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "findings": {
|
|
152
|
+
this.round = p.round;
|
|
153
|
+
this.findingsTotal = p.total;
|
|
154
|
+
this.findingsBlocking = p.blocking;
|
|
155
|
+
// Keep the per-round history too: the header tally is the latest round,
|
|
156
|
+
// but the trajectory across rounds is what shows the loop converging.
|
|
157
|
+
const tally = { round: p.round, findings: p.total, blocking: p.blocking };
|
|
158
|
+
const existing = this.rounds.findIndex((r) => r.round === p.round);
|
|
159
|
+
if (existing >= 0)
|
|
160
|
+
this.rounds[existing] = tally;
|
|
161
|
+
else
|
|
162
|
+
this.rounds.push(tally);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "done": {
|
|
166
|
+
this.stopReason = p.stopReason;
|
|
167
|
+
const failed = p.stopReason === "failed" || p.stopReason === "aborted";
|
|
168
|
+
for (const a of this.agents) {
|
|
169
|
+
if (a.status === "active") {
|
|
170
|
+
a.status = failed ? "failed" : "done";
|
|
171
|
+
a.endedAt = at;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Fold one curated line. Narration becomes the emitting agent's summary;
|
|
180
|
+
* actions ride BOTH the agent's own list and the terminal's flat ring buffer.
|
|
181
|
+
*/
|
|
182
|
+
applyLine(line, tag, at = 0) {
|
|
183
|
+
const target = this.ensureAgent(tag.stageId, tag.lens, tag.round, at);
|
|
184
|
+
if (line.kind === "narration") {
|
|
185
|
+
target.summary = line.text;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (line.state === "running") {
|
|
189
|
+
target.actions.push({
|
|
190
|
+
...(line.toolCallId !== undefined ? { toolCallId: line.toolCallId } : {}),
|
|
191
|
+
text: line.text,
|
|
192
|
+
state: "running",
|
|
193
|
+
});
|
|
194
|
+
if (target.actions.length > AGENT_ACTION_WINDOW)
|
|
195
|
+
target.actions.shift();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// A resolving end: update the matching running entry in place.
|
|
199
|
+
if (line.toolCallId === undefined)
|
|
200
|
+
return;
|
|
201
|
+
const mine = target.actions.find((a) => a.toolCallId === line.toolCallId);
|
|
202
|
+
if (mine)
|
|
203
|
+
mine.state = line.state;
|
|
204
|
+
}
|
|
205
|
+
/** Fold an assistant turn's usage into the emitting agent. Ignores everything else. */
|
|
206
|
+
applyUsage(ev, tag, at = 0) {
|
|
207
|
+
if (ev.type !== "message_end" || ev.message?.role !== "assistant")
|
|
208
|
+
return;
|
|
209
|
+
const target = this.ensureAgent(tag.stageId, tag.lens, tag.round, at);
|
|
210
|
+
target.usage.turns += 1;
|
|
211
|
+
const u = ev.message.usage;
|
|
212
|
+
if (!u)
|
|
213
|
+
return;
|
|
214
|
+
target.usage.input += u.input ?? 0;
|
|
215
|
+
target.usage.output += u.output ?? 0;
|
|
216
|
+
target.usage.cacheRead += u.cacheRead ?? 0;
|
|
217
|
+
target.usage.cacheWrite += u.cacheWrite ?? 0;
|
|
218
|
+
target.usage.cost += u.cost?.total ?? 0;
|
|
219
|
+
}
|
|
220
|
+
/** Stamp the run's end. Separate from `done` because FINISH runs after the pipeline returns. */
|
|
221
|
+
markEnded(at) {
|
|
222
|
+
this.endedAt = at;
|
|
223
|
+
}
|
|
224
|
+
// ---- Terminal renderer accessors (never serialized) ----------------------
|
|
225
|
+
/**
|
|
226
|
+
* Stage rows for the panel: one row per stage, in order. Lens children are the
|
|
227
|
+
* desktop's business alone, and a looped stage that re-executes per round
|
|
228
|
+
* (fix) paints its LATEST round's node so the row shows the live execution,
|
|
229
|
+
* not a stale bare placeholder.
|
|
230
|
+
*/
|
|
231
|
+
stageAgents() {
|
|
232
|
+
return STAGE_ORDER.flatMap((id) => {
|
|
233
|
+
const perRound = this.agents.filter((a) => a.stageId === id && a.lens === undefined && a.id !== id);
|
|
234
|
+
const row = perRound[perRound.length - 1] ?? this.stageAgent(id);
|
|
235
|
+
return row ? [row] : [];
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
header() {
|
|
239
|
+
return {
|
|
240
|
+
ticket: this.ticket,
|
|
241
|
+
round: this.round,
|
|
242
|
+
findingsTotal: this.findingsTotal,
|
|
243
|
+
findingsBlocking: this.findingsBlocking,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// ---- Wire record ---------------------------------------------------------
|
|
247
|
+
/** Named `toJSON` so `JSON.stringify(run)` yields the wire shape directly. */
|
|
248
|
+
toJSON() {
|
|
249
|
+
return {
|
|
250
|
+
v: 1,
|
|
251
|
+
runId: this.runId,
|
|
252
|
+
ticket: this.ticket,
|
|
253
|
+
round: this.round,
|
|
254
|
+
findingsTotal: this.findingsTotal,
|
|
255
|
+
findingsBlocking: this.findingsBlocking,
|
|
256
|
+
rounds: this.rounds,
|
|
257
|
+
startedAt: this.startedAt,
|
|
258
|
+
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
|
259
|
+
...(this.stopReason ? { stopReason: this.stopReason } : {}),
|
|
260
|
+
agents: this.agents,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=runState.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THIN, IMPURE per-stage spawn-runner — lifted almost verbatim from the pi
|
|
3
|
+
* subagent example's `runSingleAgent` (index.ts L267-429).
|
|
4
|
+
*
|
|
5
|
+
* It writes the grounded persona body to a 0600 temp file, builds the grounded
|
|
6
|
+
* child argv, spawns ONE pi child, line-buffers the stdout NDJSON, delegates all
|
|
7
|
+
* parsing to `events.ts`, applies `PER_TASK_OUTPUT_CAP`, wires abort
|
|
8
|
+
* (SIGTERM then SIGKILL after 5s if the child has not really exited), and cleans
|
|
9
|
+
* up the temp file.
|
|
10
|
+
*
|
|
11
|
+
* v1 spawns `pi` directly (NOT the `yagni-code` launcher) and re-injects
|
|
12
|
+
* `-e <self> --provider yagni`, inheriting the parent's env (token / base URL /
|
|
13
|
+
* agent dir). This avoids a yagni-code-cli ↔ pi-extension-yagni circular
|
|
14
|
+
* dependency — see the blueprint risks.
|
|
15
|
+
*
|
|
16
|
+
* ALL impurity sits behind injectable seams (`spawn`, `writePrompt`,
|
|
17
|
+
* `resolveChild`) so the runner is unit-tested with a fake spawn emitting canned
|
|
18
|
+
* NDJSON — no real process, no model, no network. The live end-to-end run is
|
|
19
|
+
* Jack dogfood, not CI.
|
|
20
|
+
*/
|
|
21
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
22
|
+
import { type JsonEvent, type PipelineStage, type ReviewLens, type StageResult } from "./types.js";
|
|
23
|
+
/** Injectable seams so the runner is unit-testable without fs / process / net. */
|
|
24
|
+
export interface RunStageDeps {
|
|
25
|
+
cwd: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
spawn?: typeof nodeSpawn;
|
|
28
|
+
writePrompt?: (body: string) => Promise<{
|
|
29
|
+
filePath: string;
|
|
30
|
+
cleanup: () => void;
|
|
31
|
+
}>;
|
|
32
|
+
resolveChild?: () => {
|
|
33
|
+
piCli: string;
|
|
34
|
+
extensionPath: string;
|
|
35
|
+
};
|
|
36
|
+
/** Optional environment for spawned child stages. Defaults to Node's inherited env. */
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
/**
|
|
39
|
+
* Additive live tap: invoked once per parsed NDJSON event, in stream order,
|
|
40
|
+
* right after it is buffered for the reducers. PURE side-channel for the
|
|
41
|
+
* activity feed — it changes nothing about buffering, capping, abort, or the
|
|
42
|
+
* StageResult reduction, and a headless run simply omits it.
|
|
43
|
+
*/
|
|
44
|
+
onEvent?: (ev: JsonEvent) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Max bytes for a single stdout (NDJSON) line before it is discarded. Guards
|
|
47
|
+
* against a pathological runaway line growing `buffer` without bound and
|
|
48
|
+
* OOM-crashing the harness. Defaults to {@link DEFAULT_MAX_STDOUT_LINE_BYTES};
|
|
49
|
+
* tests inject a small value.
|
|
50
|
+
*/
|
|
51
|
+
maxLineBytes?: number;
|
|
52
|
+
/**
|
|
53
|
+
* System-prompt body builder override. The general subagent tool injects
|
|
54
|
+
* custom agent bodies (from .claude/agents files) through this seam; /go's
|
|
55
|
+
* stages keep the default grounded personas.
|
|
56
|
+
*/
|
|
57
|
+
personaBody?: (stage: PipelineStage, opts: {
|
|
58
|
+
grounded?: boolean;
|
|
59
|
+
}) => string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Default per-line stdout ceiling (16 MiB). A legitimate NDJSON event — even one
|
|
63
|
+
* carrying a large tool payload — is far below this; a line beyond it is a
|
|
64
|
+
* runaway/garbage line, dropped rather than accumulated. Keeps the stdout reader
|
|
65
|
+
* O(n) and memory-bounded (a single ~GB line previously OOM-crashed `runStage`).
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_MAX_STDOUT_LINE_BYTES: number;
|
|
68
|
+
/**
|
|
69
|
+
* Run one grounded child stage and reduce its NDJSON stream into a StageResult.
|
|
70
|
+
* `ctx.lens` (when set, for a fanned-out review) selects the lens clause in the
|
|
71
|
+
* persona body and is threaded through the invocation builder.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runStage(stage: PipelineStage, ctx: {
|
|
74
|
+
ticket: string;
|
|
75
|
+
previous?: string;
|
|
76
|
+
lens?: ReviewLens;
|
|
77
|
+
grounded?: boolean;
|
|
78
|
+
}, deps: RunStageDeps): Promise<StageResult>;
|
|
79
|
+
//# sourceMappingURL=runner.d.ts.map
|