@yagni-app/code-staging 0.0.0 → 0.1.0-staging.997.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,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE reducers over the `--mode json` NDJSON event stream.
|
|
3
|
+
*
|
|
4
|
+
* The runner stays thin: it only line-buffers + `JSON.parse`s each line into a
|
|
5
|
+
* `JsonEvent`. These functions turn that parsed array into the final assistant
|
|
6
|
+
* text and aggregated usage, copying the subagent example's
|
|
7
|
+
* `getFinalOutput` / usage-accumulation logic (index.ts L170-180, L351-369).
|
|
8
|
+
*
|
|
9
|
+
* Note the corrected event taxonomy: the real tool-result event is
|
|
10
|
+
* `tool_execution_end`; the example's `tool_result_end` branch is dead/never
|
|
11
|
+
* emitted, so `collectToolResults` reads the former and ignores the latter.
|
|
12
|
+
*/
|
|
13
|
+
import type { JsonEvent, StageUsage } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* The final assistant text handed forward as the next stage's `{previous}`.
|
|
16
|
+
* Biased toward the model's GENUINE turn-end: prefer the last assistant message
|
|
17
|
+
* that stopped naturally (`stopReason === "stop"`) and carries text, so a
|
|
18
|
+
* trailing aborted / errored / text-less turn does not become the handoff. Falls
|
|
19
|
+
* back to the last assistant message with any text part when no clean "stop" turn
|
|
20
|
+
* exists, and returns '' when there is no assistant text at all. Pure, defensive,
|
|
21
|
+
* never throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractFinalOutput(events: JsonEvent[]): string;
|
|
24
|
+
/** Sum token/cost usage across assistant turns and count the turns. */
|
|
25
|
+
export declare function aggregateUsage(events: JsonEvent[]): StageUsage;
|
|
26
|
+
/** The last assistant stop reason / error message seen on the stream. */
|
|
27
|
+
export declare function extractStopReason(events: JsonEvent[]): {
|
|
28
|
+
stopReason?: string;
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Collect structured tool results from `tool_execution_end` events. The legacy
|
|
33
|
+
* `tool_result_end` shape is intentionally ignored so a stray dead event can't
|
|
34
|
+
* corrupt counts. (v1 findings come from the final assistant TEXT, not here —
|
|
35
|
+
* this is exposed for any future stage that needs tool-level results.)
|
|
36
|
+
*/
|
|
37
|
+
export declare function collectToolResults(events: JsonEvent[]): Array<{
|
|
38
|
+
toolName: string;
|
|
39
|
+
isError: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
export interface EventAccumulator {
|
|
42
|
+
usage: StageUsage;
|
|
43
|
+
stopReason?: string;
|
|
44
|
+
errorMessage?: string;
|
|
45
|
+
toolCalls: number;
|
|
46
|
+
toolErrors: number;
|
|
47
|
+
/** Last assistant text on a genuine "stop" turn, and on any turn — extractFinalOutput's two tiers. */
|
|
48
|
+
lastStopText?: string;
|
|
49
|
+
lastAnyText?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function newEventAccumulator(): EventAccumulator;
|
|
52
|
+
/** Fold one streamed event into `acc`. Retains nothing per-event. */
|
|
53
|
+
export declare function foldEvent(acc: EventAccumulator, ev: JsonEvent): void;
|
|
54
|
+
/** Final assistant text, biased to the last genuine "stop" turn — mirrors extractFinalOutput. */
|
|
55
|
+
export declare function finalOutputFrom(acc: EventAccumulator): string;
|
|
56
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE reducers over the `--mode json` NDJSON event stream.
|
|
3
|
+
*
|
|
4
|
+
* The runner stays thin: it only line-buffers + `JSON.parse`s each line into a
|
|
5
|
+
* `JsonEvent`. These functions turn that parsed array into the final assistant
|
|
6
|
+
* text and aggregated usage, copying the subagent example's
|
|
7
|
+
* `getFinalOutput` / usage-accumulation logic (index.ts L170-180, L351-369).
|
|
8
|
+
*
|
|
9
|
+
* Note the corrected event taxonomy: the real tool-result event is
|
|
10
|
+
* `tool_execution_end`; the example's `tool_result_end` branch is dead/never
|
|
11
|
+
* emitted, so `collectToolResults` reads the former and ignores the latter.
|
|
12
|
+
*/
|
|
13
|
+
/** True for an assistant `message_end` event carrying a message. */
|
|
14
|
+
function isAssistantMessage(ev) {
|
|
15
|
+
return ev.type === "message_end" && ev.message?.role === "assistant";
|
|
16
|
+
}
|
|
17
|
+
/** The last text part of the last assistant message matching `pred`, or undefined. */
|
|
18
|
+
function lastAssistantText(events, pred) {
|
|
19
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
20
|
+
const ev = events[i];
|
|
21
|
+
if (!isAssistantMessage(ev) || !pred(ev))
|
|
22
|
+
continue;
|
|
23
|
+
const parts = ev.message?.content ?? [];
|
|
24
|
+
for (let j = parts.length - 1; j >= 0; j--) {
|
|
25
|
+
const part = parts[j];
|
|
26
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
27
|
+
return part.text;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The final assistant text handed forward as the next stage's `{previous}`.
|
|
34
|
+
* Biased toward the model's GENUINE turn-end: prefer the last assistant message
|
|
35
|
+
* that stopped naturally (`stopReason === "stop"`) and carries text, so a
|
|
36
|
+
* trailing aborted / errored / text-less turn does not become the handoff. Falls
|
|
37
|
+
* back to the last assistant message with any text part when no clean "stop" turn
|
|
38
|
+
* exists, and returns '' when there is no assistant text at all. Pure, defensive,
|
|
39
|
+
* never throws.
|
|
40
|
+
*/
|
|
41
|
+
export function extractFinalOutput(events) {
|
|
42
|
+
// First choice: the last real turn-end ("stop") that actually produced text.
|
|
43
|
+
const stopText = lastAssistantText(events, (ev) => ev.message?.stopReason === "stop");
|
|
44
|
+
if (stopText !== undefined)
|
|
45
|
+
return stopText;
|
|
46
|
+
// Fallback (stop-reason-blind): the last assistant message with any text part.
|
|
47
|
+
return lastAssistantText(events, () => true) ?? "";
|
|
48
|
+
}
|
|
49
|
+
/** Sum token/cost usage across assistant turns and count the turns. */
|
|
50
|
+
export function aggregateUsage(events) {
|
|
51
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
52
|
+
for (const ev of events) {
|
|
53
|
+
if (!isAssistantMessage(ev))
|
|
54
|
+
continue;
|
|
55
|
+
usage.turns += 1;
|
|
56
|
+
const u = ev.message?.usage;
|
|
57
|
+
if (!u)
|
|
58
|
+
continue;
|
|
59
|
+
usage.input += u.input ?? 0;
|
|
60
|
+
usage.output += u.output ?? 0;
|
|
61
|
+
usage.cacheRead += u.cacheRead ?? 0;
|
|
62
|
+
usage.cacheWrite += u.cacheWrite ?? 0;
|
|
63
|
+
usage.cost += u.cost?.total ?? 0;
|
|
64
|
+
}
|
|
65
|
+
return usage;
|
|
66
|
+
}
|
|
67
|
+
/** The last assistant stop reason / error message seen on the stream. */
|
|
68
|
+
export function extractStopReason(events) {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const ev of events) {
|
|
71
|
+
if (!isAssistantMessage(ev))
|
|
72
|
+
continue;
|
|
73
|
+
if (ev.message?.stopReason)
|
|
74
|
+
out.stopReason = ev.message.stopReason;
|
|
75
|
+
if (ev.message?.errorMessage)
|
|
76
|
+
out.errorMessage = ev.message.errorMessage;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Collect structured tool results from `tool_execution_end` events. The legacy
|
|
82
|
+
* `tool_result_end` shape is intentionally ignored so a stray dead event can't
|
|
83
|
+
* corrupt counts. (v1 findings come from the final assistant TEXT, not here —
|
|
84
|
+
* this is exposed for any future stage that needs tool-level results.)
|
|
85
|
+
*/
|
|
86
|
+
export function collectToolResults(events) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const ev of events) {
|
|
89
|
+
if (ev.type !== "tool_execution_end")
|
|
90
|
+
continue;
|
|
91
|
+
out.push({ toolName: ev.toolName ?? "", isError: ev.isError ?? false });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
export function newEventAccumulator() {
|
|
96
|
+
return {
|
|
97
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
98
|
+
toolCalls: 0,
|
|
99
|
+
toolErrors: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The last text part of a single assistant message, or undefined. */
|
|
103
|
+
function messageText(ev) {
|
|
104
|
+
const parts = ev.message?.content ?? [];
|
|
105
|
+
for (let j = parts.length - 1; j >= 0; j--) {
|
|
106
|
+
const part = parts[j];
|
|
107
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
108
|
+
return part.text;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Fold one streamed event into `acc`. Retains nothing per-event. */
|
|
113
|
+
export function foldEvent(acc, ev) {
|
|
114
|
+
if (ev.type === "tool_execution_end") {
|
|
115
|
+
acc.toolCalls += 1;
|
|
116
|
+
if (ev.isError)
|
|
117
|
+
acc.toolErrors += 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!isAssistantMessage(ev))
|
|
121
|
+
return;
|
|
122
|
+
acc.usage.turns += 1;
|
|
123
|
+
const u = ev.message?.usage;
|
|
124
|
+
if (u) {
|
|
125
|
+
acc.usage.input += u.input ?? 0;
|
|
126
|
+
acc.usage.output += u.output ?? 0;
|
|
127
|
+
acc.usage.cacheRead += u.cacheRead ?? 0;
|
|
128
|
+
acc.usage.cacheWrite += u.cacheWrite ?? 0;
|
|
129
|
+
acc.usage.cost += u.cost?.total ?? 0;
|
|
130
|
+
}
|
|
131
|
+
// last-wins, matching extractStopReason
|
|
132
|
+
if (ev.message?.stopReason)
|
|
133
|
+
acc.stopReason = ev.message.stopReason;
|
|
134
|
+
if (ev.message?.errorMessage)
|
|
135
|
+
acc.errorMessage = ev.message.errorMessage;
|
|
136
|
+
const text = messageText(ev);
|
|
137
|
+
if (text !== undefined) {
|
|
138
|
+
acc.lastAnyText = text;
|
|
139
|
+
if (ev.message?.stopReason === "stop")
|
|
140
|
+
acc.lastStopText = text;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Final assistant text, biased to the last genuine "stop" turn — mirrors extractFinalOutput. */
|
|
144
|
+
export function finalOutputFrom(acc) {
|
|
145
|
+
return acc.lastStopText ?? acc.lastAnyText ?? "";
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE findings parser + the review→fix loop reducers — the analogue of the
|
|
3
|
+
* backend's `extractCitations`.
|
|
4
|
+
*
|
|
5
|
+
* `parseFindings` tolerantly reads the reviewer's machine-readable trailing
|
|
6
|
+
* section (a fenced ```findings block of `SEVERITY | file:line | message`
|
|
7
|
+
* lines, OR a JSON array), ignoring surrounding prose. Unparseable/empty input
|
|
8
|
+
* yields [] — honest: treated as "no blockers" and the caller logs a
|
|
9
|
+
* parse-warning rather than fabricating findings or throwing.
|
|
10
|
+
*
|
|
11
|
+
* `hasBlockingFindings` + `shouldStop` are the trivially-unit-testable stop rule
|
|
12
|
+
* (spec §7.3): only Critical + High block; stop when there are no blockers OR
|
|
13
|
+
* after MAX_REVIEW_ROUNDS rounds, always with an explicit reason.
|
|
14
|
+
*/
|
|
15
|
+
import { type Finding, type ReviewLens } from "./types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Extract the inner text of a fenced ```findings block, if present. Exported
|
|
18
|
+
* (spec §3e) so the orchestrator can distinguish "explicit empty findings
|
|
19
|
+
* fence" (a real clean review) from "no fence at all" (a likely format miss
|
|
20
|
+
* worth one re-ask) — `parseFindings` returning [] cannot tell them apart.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractFindingsBlock(raw: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Parse a reviewer's raw output into structured findings. Tries the JSON-array
|
|
25
|
+
* form first, then the fenced ```findings block, then a tolerant scan of every
|
|
26
|
+
* line. Never throws; never fabricates a finding from prose.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseFindings(raw: string, lens?: ReviewLens): Finding[];
|
|
29
|
+
/** Concatenate per-lens findings, preserving order and each lens tag. */
|
|
30
|
+
export declare function unionFindings(perLens: Finding[][]): Finding[];
|
|
31
|
+
/** True iff at least one finding is Critical or High (the only blockers). */
|
|
32
|
+
export declare function hasBlockingFindings(findings: Finding[]): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The bounded review→fix stop rule (spec §7.3): stop 'clean' when nothing
|
|
35
|
+
* blocks, else stop 'round_cap' once the round hits MAX_REVIEW_ROUNDS, else
|
|
36
|
+
* keep going. Always returns an explicit reason (no silent truncation).
|
|
37
|
+
*/
|
|
38
|
+
export declare function shouldStop(findings: Finding[], round: number): {
|
|
39
|
+
stop: boolean;
|
|
40
|
+
reason: "clean" | "round_cap" | null;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=findings.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE findings parser + the review→fix loop reducers — the analogue of the
|
|
3
|
+
* backend's `extractCitations`.
|
|
4
|
+
*
|
|
5
|
+
* `parseFindings` tolerantly reads the reviewer's machine-readable trailing
|
|
6
|
+
* section (a fenced ```findings block of `SEVERITY | file:line | message`
|
|
7
|
+
* lines, OR a JSON array), ignoring surrounding prose. Unparseable/empty input
|
|
8
|
+
* yields [] — honest: treated as "no blockers" and the caller logs a
|
|
9
|
+
* parse-warning rather than fabricating findings or throwing.
|
|
10
|
+
*
|
|
11
|
+
* `hasBlockingFindings` + `shouldStop` are the trivially-unit-testable stop rule
|
|
12
|
+
* (spec §7.3): only Critical + High block; stop when there are no blockers OR
|
|
13
|
+
* after MAX_REVIEW_ROUNDS rounds, always with an explicit reason.
|
|
14
|
+
*/
|
|
15
|
+
import { MAX_REVIEW_ROUNDS } from "./types.js";
|
|
16
|
+
const SEVERITIES = ["critical", "high", "medium", "low"];
|
|
17
|
+
const DEFAULT_LENS = "correctness";
|
|
18
|
+
function isSeverity(value) {
|
|
19
|
+
return SEVERITIES.includes(value);
|
|
20
|
+
}
|
|
21
|
+
/** Parse a `file:line` location into its parts (line optional). */
|
|
22
|
+
function parseLocation(loc) {
|
|
23
|
+
const trimmed = loc.trim();
|
|
24
|
+
if (!trimmed)
|
|
25
|
+
return {};
|
|
26
|
+
const m = trimmed.match(/^(.*):(\d+)$/);
|
|
27
|
+
if (m && m[1])
|
|
28
|
+
return { file: m[1], line: Number(m[2]) };
|
|
29
|
+
return { file: trimmed };
|
|
30
|
+
}
|
|
31
|
+
/** Parse one `SEVERITY | file:line | message` line, or null if it is not one. */
|
|
32
|
+
function parsePipeLine(line, lens) {
|
|
33
|
+
const parts = line.split("|").map((p) => p.trim());
|
|
34
|
+
if (parts.length < 2)
|
|
35
|
+
return null;
|
|
36
|
+
const severity = parts[0].toLowerCase();
|
|
37
|
+
if (!isSeverity(severity))
|
|
38
|
+
return null;
|
|
39
|
+
let finding;
|
|
40
|
+
if (parts.length >= 3) {
|
|
41
|
+
const { file, line: lineNo } = parseLocation(parts[1]);
|
|
42
|
+
const message = parts.slice(2).join(" | ").trim();
|
|
43
|
+
finding = { severity, lens, message };
|
|
44
|
+
if (file)
|
|
45
|
+
finding.file = file;
|
|
46
|
+
if (lineNo !== undefined)
|
|
47
|
+
finding.line = lineNo;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
finding = { severity, lens, message: parts[1] };
|
|
51
|
+
}
|
|
52
|
+
return finding;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract the inner text of a fenced ```findings block, if present. Exported
|
|
56
|
+
* (spec §3e) so the orchestrator can distinguish "explicit empty findings
|
|
57
|
+
* fence" (a real clean review) from "no fence at all" (a likely format miss
|
|
58
|
+
* worth one re-ask) — `parseFindings` returning [] cannot tell them apart.
|
|
59
|
+
*/
|
|
60
|
+
export function extractFindingsBlock(raw) {
|
|
61
|
+
const m = raw.match(/```findings[^\n]*\n([\s\S]*?)```/i);
|
|
62
|
+
return m ? m[1] : null;
|
|
63
|
+
}
|
|
64
|
+
/** Best-effort parse of a JSON array of findings (fenced ```json or bare). */
|
|
65
|
+
function parseJsonFindings(raw, lens) {
|
|
66
|
+
const fenced = raw.match(/```json[^\n]*\n([\s\S]*?)```/i);
|
|
67
|
+
const candidate = fenced ? fenced[1].trim() : raw.trim();
|
|
68
|
+
if (!candidate.startsWith("["))
|
|
69
|
+
return [];
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(candidate);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(parsed))
|
|
78
|
+
return [];
|
|
79
|
+
const findings = [];
|
|
80
|
+
for (const item of parsed) {
|
|
81
|
+
if (!item || typeof item !== "object")
|
|
82
|
+
continue;
|
|
83
|
+
const obj = item;
|
|
84
|
+
const severity = typeof obj.severity === "string" ? obj.severity.toLowerCase() : "";
|
|
85
|
+
if (!isSeverity(severity))
|
|
86
|
+
continue;
|
|
87
|
+
const finding = {
|
|
88
|
+
severity,
|
|
89
|
+
lens: typeof obj.lens === "string" && isLens(obj.lens) ? obj.lens : lens,
|
|
90
|
+
message: typeof obj.message === "string" ? obj.message : "",
|
|
91
|
+
};
|
|
92
|
+
if (typeof obj.file === "string")
|
|
93
|
+
finding.file = obj.file;
|
|
94
|
+
if (typeof obj.line === "number")
|
|
95
|
+
finding.line = obj.line;
|
|
96
|
+
findings.push(finding);
|
|
97
|
+
}
|
|
98
|
+
return findings;
|
|
99
|
+
}
|
|
100
|
+
function isLens(value) {
|
|
101
|
+
return value === "correctness" || value === "business_fit" || value === "does_it_hold";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a reviewer's raw output into structured findings. Tries the JSON-array
|
|
105
|
+
* form first, then the fenced ```findings block, then a tolerant scan of every
|
|
106
|
+
* line. Never throws; never fabricates a finding from prose.
|
|
107
|
+
*/
|
|
108
|
+
export function parseFindings(raw, lens = DEFAULT_LENS) {
|
|
109
|
+
if (!raw || !raw.trim())
|
|
110
|
+
return [];
|
|
111
|
+
const json = parseJsonFindings(raw, lens);
|
|
112
|
+
if (json.length > 0)
|
|
113
|
+
return json;
|
|
114
|
+
const block = extractFindingsBlock(raw);
|
|
115
|
+
const source = block ?? raw;
|
|
116
|
+
const findings = [];
|
|
117
|
+
for (const line of source.split("\n")) {
|
|
118
|
+
const parsed = parsePipeLine(line, lens);
|
|
119
|
+
if (parsed)
|
|
120
|
+
findings.push(parsed);
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
/** Concatenate per-lens findings, preserving order and each lens tag. */
|
|
125
|
+
export function unionFindings(perLens) {
|
|
126
|
+
return perLens.flat();
|
|
127
|
+
}
|
|
128
|
+
/** True iff at least one finding is Critical or High (the only blockers). */
|
|
129
|
+
export function hasBlockingFindings(findings) {
|
|
130
|
+
return findings.some((f) => f.severity === "critical" || f.severity === "high");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The bounded review→fix stop rule (spec §7.3): stop 'clean' when nothing
|
|
134
|
+
* blocks, else stop 'round_cap' once the round hits MAX_REVIEW_ROUNDS, else
|
|
135
|
+
* keep going. Always returns an explicit reason (no silent truncation).
|
|
136
|
+
*/
|
|
137
|
+
export function shouldStop(findings, round) {
|
|
138
|
+
if (!hasBlockingFindings(findings))
|
|
139
|
+
return { stop: true, reason: "clean" };
|
|
140
|
+
if (round >= MAX_REVIEW_ROUNDS)
|
|
141
|
+
return { stop: true, reason: "round_cap" };
|
|
142
|
+
return { stop: false, reason: null };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=findings.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FINISH stage (spec §3c) — the deterministic commit / push / PR step that
|
|
3
|
+
* runs ONLY when the review loop stops `clean`.
|
|
4
|
+
*
|
|
5
|
+
* What it does:
|
|
6
|
+
* - Commit the run's work: in worktree mode `git add -A` + commit on the run
|
|
7
|
+
* branch (the tree started clean, so everything is the run's own). In --here
|
|
8
|
+
* mode it commits ONLY when the pre-run baseline was verifiably clean;
|
|
9
|
+
* otherwise it skips with an honest note (never mixing user dirt into an
|
|
10
|
+
* agent commit).
|
|
11
|
+
* - Commit message: `<type>(<scope>): <title>` + a 2-3 sentence plan summary +
|
|
12
|
+
* the exact provenance trailer (YAGNI-Run / YAGNI-Ticket / YAGNI-Verify /
|
|
13
|
+
* YAGNI-Decisions / YAGNI-Rounds / Co-Authored-By: YAGNI Code).
|
|
14
|
+
* - Opt-in `--pr`: `git push -u origin <branch>` + `gh pr create` with the
|
|
15
|
+
* USER'S own gh auth. Any push/PR failure is a fail-soft note ("committed
|
|
16
|
+
* locally; …"); the run stays successful.
|
|
17
|
+
*
|
|
18
|
+
* IRON INVARIANT (§0.1): a FINISH failure NEVER flips a clean run to failed and
|
|
19
|
+
* NEVER fakes a commit — {@link runFinish} never throws, `committed` is only
|
|
20
|
+
* true when `git rev-parse HEAD` confirmed the new commit, and every skip or
|
|
21
|
+
* failure lands as an honest note the /go handoff surfaces verbatim.
|
|
22
|
+
*
|
|
23
|
+
* All message/trailer/scope derivation is PURE (unit-tested directly); the one
|
|
24
|
+
* impure seam is the injectable exec. Unlike verify's scrubbed env, the exec
|
|
25
|
+
* here inherits the user's FULL environment on purpose: `git push` needs the
|
|
26
|
+
* ssh-agent and `gh` needs the user's own auth — these are OUR commands, not
|
|
27
|
+
* repo-defined ones, so invariant §0.9 (repo commands never see YAGNI_* vars)
|
|
28
|
+
* does not apply.
|
|
29
|
+
*/
|
|
30
|
+
import type { WorktreeExec } from "./worktree.js";
|
|
31
|
+
import type { Finding } from "./types.js";
|
|
32
|
+
/** How the pre-run tree looked for a --here run (worktree mode ignores it). */
|
|
33
|
+
export type HereBaseline = "clean" | "dirty" | "unknown";
|
|
34
|
+
/** The trailer inputs, all plumbed from the pipeline result + run identity. */
|
|
35
|
+
export interface TrailerInputs {
|
|
36
|
+
runId: string;
|
|
37
|
+
/** The flag-free ticket identifier or free-text arg. */
|
|
38
|
+
ticket: string;
|
|
39
|
+
/** `passed (<cmds>)` or `skipped (<reason>)` — see {@link verifyTrailerValue}. */
|
|
40
|
+
verify: string;
|
|
41
|
+
decisionsRecorded: number;
|
|
42
|
+
rounds: number;
|
|
43
|
+
}
|
|
44
|
+
export interface RunFinishOpts extends TrailerInputs {
|
|
45
|
+
/** The run tree (the worktree path, or ctx.cwd for --here). */
|
|
46
|
+
cwd: string;
|
|
47
|
+
worktreeMode: boolean;
|
|
48
|
+
/** --here commit gate: only a verifiably-clean pre-run baseline may commit. */
|
|
49
|
+
hereBaseline: HereBaseline;
|
|
50
|
+
/** The run branch (worktree mode) or the user's current branch (--here). */
|
|
51
|
+
branch?: string;
|
|
52
|
+
/** The resolved ticket brief (verbatim title + body), when /go could fetch it. */
|
|
53
|
+
ticketBrief?: string;
|
|
54
|
+
/** The plan stage's finalOutput (absent on a resume, where the build was skipped). */
|
|
55
|
+
planText?: string;
|
|
56
|
+
/** --pr: push the branch + open a PR (fail-soft). */
|
|
57
|
+
openPr: boolean;
|
|
58
|
+
/** Unresolved medium/low findings, quoted into the PR body. */
|
|
59
|
+
remainingFindings?: Finding[];
|
|
60
|
+
/**
|
|
61
|
+
* Paths already dirty at pipeline start (post-bootstrap), so NOT the run's own
|
|
62
|
+
* work — e.g. a lockfile a fresh-worktree `install` rewrote. FINISH stages only
|
|
63
|
+
* `changed − baselinePaths` so bootstrap churn (or any pre-existing dirt) never
|
|
64
|
+
* lands in the run's provenance commit. Absent ⇒ the whole diff is the run's.
|
|
65
|
+
*/
|
|
66
|
+
baselinePaths?: string[];
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
exec?: WorktreeExec;
|
|
69
|
+
}
|
|
70
|
+
/** What FINISH actually did. `committed` true only when the commit really landed. */
|
|
71
|
+
export interface FinishResult {
|
|
72
|
+
committed: boolean;
|
|
73
|
+
commitSha?: string;
|
|
74
|
+
branch?: string;
|
|
75
|
+
prUrl?: string;
|
|
76
|
+
prNumber?: number;
|
|
77
|
+
/** Honest skip/failure notes, surfaced verbatim in the /go handoff. */
|
|
78
|
+
notes: string[];
|
|
79
|
+
}
|
|
80
|
+
/** `fix` when the ticket brief (or, blind, the raw arg) says bug; else `feat` (spec §3c). */
|
|
81
|
+
export declare function commitType(ticket: string, ticketBrief?: string): "fix" | "feat";
|
|
82
|
+
/**
|
|
83
|
+
* The commit subject's title half: the brief's first non-empty line (its
|
|
84
|
+
* verbatim ticket title) with a leading `<ticket>:` echo stripped (the trailer
|
|
85
|
+
* already carries the identifier), falling back to the raw arg. Capped.
|
|
86
|
+
*/
|
|
87
|
+
export declare function commitTitle(ticket: string, ticketBrief?: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* A conventional-commit scope from the run's changed paths: the package name
|
|
90
|
+
* when every changed path agrees on one (`packages/<name>/…`, `apps/…`,
|
|
91
|
+
* `libs/…`), else the shared top-level dir. Ambiguous or root-level changes
|
|
92
|
+
* yield no scope (the subject is `<type>: <title>`), never a guess.
|
|
93
|
+
*/
|
|
94
|
+
export declare function deriveCommitScope(paths: string[]): string | undefined;
|
|
95
|
+
/** `<type>(<scope>): <title>`, dropping the parens when no scope was derivable. */
|
|
96
|
+
export declare function commitSubject(type: "fix" | "feat", scope: string | undefined, title: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* A 2-3 sentence summary of the plan stage's output (markdown flattened, first
|
|
99
|
+
* three sentences, capped). No LLM call — this is a deterministic excerpt. A
|
|
100
|
+
* missing plan (a resumed run skipped the build half) degrades honestly.
|
|
101
|
+
*/
|
|
102
|
+
export declare function summarizePlan(planText: string | undefined, ticket: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* The trailer's YAGNI-Verify value, derived honestly from the pipeline result:
|
|
105
|
+
* `passed (<cmds>)` when the final round's verify gate ran (verify-FAILED can't
|
|
106
|
+
* reach `clean` by construction), else `skipped (<reason>)`.
|
|
107
|
+
*/
|
|
108
|
+
export declare function verifyTrailerValue(result: {
|
|
109
|
+
verifyCommand?: string;
|
|
110
|
+
verifyNote?: string;
|
|
111
|
+
}): string;
|
|
112
|
+
/** The exact provenance trailer block (spec §3c). */
|
|
113
|
+
export declare function provenanceTrailer(t: TrailerInputs): string;
|
|
114
|
+
/** Subject + blank line + plan summary + blank line + trailer. */
|
|
115
|
+
export declare function buildCommitMessage(subject: string, summary: string, trailer: string): string;
|
|
116
|
+
/** The PR body: plan summary, any unresolved medium/low findings, the trailer. */
|
|
117
|
+
export declare function buildPrBody(summary: string, trailer: string, remaining?: Finding[]): string;
|
|
118
|
+
/** Pull the PR URL (+ number) out of `gh pr create` output, if present. */
|
|
119
|
+
export declare function parsePrUrl(output: string): {
|
|
120
|
+
prUrl?: string;
|
|
121
|
+
prNumber?: number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Run the FINISH stage. NEVER throws; every failure path returns an honest
|
|
125
|
+
* {@link FinishResult} with notes. See the module docblock for the contract.
|
|
126
|
+
*/
|
|
127
|
+
export declare function runFinish(opts: RunFinishOpts): Promise<FinishResult>;
|
|
128
|
+
//# sourceMappingURL=finish.d.ts.map
|