@yagni-app/code 0.2.1 → 0.3.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/dist/cli.d.ts +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +44 -1
- package/dist/extension/config.js +37 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -23,6 +23,7 @@ import * as fs from "node:fs";
|
|
|
23
23
|
import * as os from "node:os";
|
|
24
24
|
import * as path from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { trackChild } from "./childRegistry.js";
|
|
26
27
|
import { finalOutputFrom, foldEvent, newEventAccumulator } from "./events.js";
|
|
27
28
|
import { buildStageInvocation, groundedChildArgv } from "./invocation.js";
|
|
28
29
|
import { personaBody } from "./personas.js";
|
|
@@ -151,6 +152,9 @@ export async function runStage(stage, ctx, deps) {
|
|
|
151
152
|
shell: false,
|
|
152
153
|
stdio: ["ignore", "pipe", "pipe"],
|
|
153
154
|
});
|
|
155
|
+
// Register with the exit sweep so quitting pi (/exit, double Ctrl+C,
|
|
156
|
+
// SIGTERM) can never orphan a stage child into the background.
|
|
157
|
+
trackChild(proc);
|
|
154
158
|
let buffer = "";
|
|
155
159
|
// `overlong` = we are mid-way through a line that already blew the cap; we
|
|
156
160
|
// discard incoming bytes until its terminating newline, then drop the line.
|
|
@@ -239,6 +239,10 @@ export declare function buildVerifyEnv(source?: NodeJS.ProcessEnv): NodeJS.Proce
|
|
|
239
239
|
* once instead of parking in watch mode until the 10-min cap kills it.
|
|
240
240
|
*/
|
|
241
241
|
export declare function buildTestEnv(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
242
|
+
export declare function makeDefaultExec(opts?: {
|
|
243
|
+
graceMs?: number;
|
|
244
|
+
killTree?: (pid: number) => void;
|
|
245
|
+
}): VerifyExec;
|
|
242
246
|
export interface MakeRunVerifyOpts {
|
|
243
247
|
detect?: (cwd: string, changedDirs: string[]) => VerifyCommand[];
|
|
244
248
|
/**
|
|
@@ -52,6 +52,7 @@ import { readFileSync } from "node:fs";
|
|
|
52
52
|
// backslashed strings that no longer compare equal to the git-derived repoRoot
|
|
53
53
|
// and would break the walk-up termination check.
|
|
54
54
|
import { basename, dirname, join } from "node:path/posix";
|
|
55
|
+
import { killTreeSync, trackChild } from "./childRegistry.js";
|
|
55
56
|
import { composeAbortSignal } from "./resilience.js";
|
|
56
57
|
import { scrubSecrets } from "./scrubSecrets.js";
|
|
57
58
|
import { snapshotWorkspace } from "./workspace.js";
|
|
@@ -595,33 +596,54 @@ export function buildTestEnv(source = process.env) {
|
|
|
595
596
|
// — signalling the WHOLE process group and killing the parent. Abort is wired
|
|
596
597
|
// manually with an explicit pid guard; a killed child reaches the callback with
|
|
597
598
|
// a code-less error, which stays the same fail-open rejection as before.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
599
|
+
//
|
|
600
|
+
// Abort escalates: SIGTERM first (a test runner gets its grace to flush), then
|
|
601
|
+
// after `graceMs` a TREE SIGKILL — a stubborn runner (or a descendant it
|
|
602
|
+
// spawned that holds the stdio pipes open) would otherwise leave the promise
|
|
603
|
+
// pending forever, hanging /stop and the run with it.
|
|
604
|
+
export function makeDefaultExec(opts = {}) {
|
|
605
|
+
const graceMs = opts.graceMs ?? 5000;
|
|
606
|
+
const killTree = opts.killTree ?? killTreeSync;
|
|
607
|
+
return (argv, cwd, signal, env) => new Promise((resolve, reject) => {
|
|
608
|
+
if (signal.aborted)
|
|
609
|
+
return reject(new Error("verify aborted before start"));
|
|
610
|
+
let settled = false;
|
|
611
|
+
let killTimer;
|
|
612
|
+
const child = execFile(argv[0], argv.slice(1), { cwd, env: env ?? buildVerifyEnv(), maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
613
|
+
settled = true;
|
|
614
|
+
if (killTimer)
|
|
615
|
+
clearTimeout(killTimer);
|
|
616
|
+
signal.removeEventListener("abort", onAbort);
|
|
617
|
+
const output = `${stdout ?? ""}${stderr ?? ""}`;
|
|
618
|
+
if (err) {
|
|
619
|
+
// A non-zero EXIT is a verify failure (carries a numeric code); a spawn
|
|
620
|
+
// error (ENOENT) or an abort has no numeric code and is a fail-open.
|
|
621
|
+
const code = err.code;
|
|
622
|
+
if (typeof code === "number")
|
|
623
|
+
return resolve({ code, output });
|
|
624
|
+
return reject(err);
|
|
625
|
+
}
|
|
626
|
+
resolve({ code: 0, output });
|
|
627
|
+
});
|
|
628
|
+
trackChild(child);
|
|
629
|
+
const onAbort = () => {
|
|
630
|
+
try {
|
|
631
|
+
if (child.pid)
|
|
632
|
+
child.kill("SIGTERM");
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
/* ignore */
|
|
636
|
+
}
|
|
637
|
+
killTimer = setTimeout(() => {
|
|
638
|
+
if (!settled && child.pid)
|
|
639
|
+
killTree(child.pid);
|
|
640
|
+
}, graceMs);
|
|
641
|
+
killTimer.unref?.();
|
|
642
|
+
};
|
|
643
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
613
644
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (child.pid)
|
|
617
|
-
child.kill("SIGTERM");
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
/* ignore */
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
624
|
-
});
|
|
645
|
+
}
|
|
646
|
+
const defaultExec = makeDefaultExec();
|
|
625
647
|
/**
|
|
626
648
|
* Build the runVerify function injected into the orchestrator. It discovers the
|
|
627
649
|
* changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { type FlywheelState } from "./flywheel.js";
|
|
3
4
|
/** Options for {@link makeRecordDecisionTool}. */
|
|
4
5
|
export interface MakeRecordDecisionToolOptions {
|
|
5
6
|
baseUrl: string;
|
|
@@ -7,6 +8,13 @@ export interface MakeRecordDecisionToolOptions {
|
|
|
7
8
|
fetchImpl?: typeof fetch;
|
|
8
9
|
/** Idempotency-key source (default: crypto.randomUUID); injected in tests. */
|
|
9
10
|
makeIdempotencyKey?: () => string;
|
|
11
|
+
/**
|
|
12
|
+
* Shared flywheel session state (Run 7). A record_decision that follows a
|
|
13
|
+
* surfaced no-position suggestion sends `dedupe: true` — a mid-run agent
|
|
14
|
+
* has no human to adjudicate a near-duplicate. A human `/decide` never
|
|
15
|
+
* rides this state.
|
|
16
|
+
*/
|
|
17
|
+
flywheel?: FlywheelState;
|
|
10
18
|
}
|
|
11
19
|
/** The durable fields of a recorded product-intent decision. */
|
|
12
20
|
export interface RecordDecisionParams {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { consumeFlywheelAttribution } from "./flywheel.js";
|
|
3
4
|
import { sendOrSpool } from "./spool.js";
|
|
4
5
|
/**
|
|
5
6
|
* POST a single decision to the token-scoped grounding endpoint and return its
|
|
@@ -67,12 +68,20 @@ export function makeRecordDecisionTool(opts) {
|
|
|
67
68
|
// never bank the same decision twice. Transport failures and 5xx are
|
|
68
69
|
// spooled durably instead of lost (R4 write half).
|
|
69
70
|
const idempotencyKey = (opts.makeIdempotencyKey ?? randomUUID)();
|
|
71
|
+
// Run 7 flywheel attribution: a record answering the QUESTION a
|
|
72
|
+
// surfaced no-position suggestion asked about asks the backend to
|
|
73
|
+
// dedupe against active decisions first (decisive, not advisory — no
|
|
74
|
+
// human is present). An unrelated record never inherits the flag.
|
|
75
|
+
const flywheelAttributed = opts.flywheel
|
|
76
|
+
? consumeFlywheelAttribution(opts.flywheel, params.question)
|
|
77
|
+
: false;
|
|
70
78
|
const outcome = await sendOrSpool(opts, "record_decision", "/api/yagni-code/decisions", {
|
|
71
79
|
question: params.question,
|
|
72
80
|
decision: params.decision,
|
|
73
81
|
rationale: params.rationale,
|
|
74
82
|
repo: params.repo,
|
|
75
83
|
workItemId: params.workItemId,
|
|
84
|
+
...(flywheelAttributed ? { dedupe: true } : {}),
|
|
76
85
|
}, idempotencyKey, signal);
|
|
77
86
|
if (outcome.kind === "rejected") {
|
|
78
87
|
throw new Error(outcome.message);
|
|
@@ -92,6 +101,21 @@ export function makeRecordDecisionTool(opts) {
|
|
|
92
101
|
};
|
|
93
102
|
}
|
|
94
103
|
const data = outcome.json;
|
|
104
|
+
if (data?.deduped) {
|
|
105
|
+
// The backend matched an existing active decision and inserted
|
|
106
|
+
// nothing; surface it so the agent leans on the recorded judgment.
|
|
107
|
+
const existing = data.existing;
|
|
108
|
+
const summary = existing?.decision ? ` ${existing.decision}` : "";
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: `An equivalent decision is already recorded; nothing new was banked.${summary}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
details: { id: existing?.id ?? null, spooled: false },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
95
119
|
return {
|
|
96
120
|
content: [{ type: "text", text: "Recorded the decision in YAGNI." }],
|
|
97
121
|
details: { id: data?.id ?? null, spooled: false },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic secret redaction for Guardian storage events (YAG-510).
|
|
3
|
+
*
|
|
4
|
+
* Applied client-side, BEFORE anything leaves the machine, to both the
|
|
5
|
+
* command and the Guardian rationale (which routinely quotes the command) —
|
|
6
|
+
* and only on the raw storage tier; the base tier never transmits either.
|
|
7
|
+
*
|
|
8
|
+
* Honest scope: this catches the obvious, well-known secret shapes. A secret
|
|
9
|
+
* in a novel shape gets through — which is why raw-tier storage is an
|
|
10
|
+
* explicit-consent, per-workspace opt-in and never a default.
|
|
11
|
+
*
|
|
12
|
+
* Pure, no external deps (bundling constraint — see execPolicy.ts header).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Redact known secret shapes from a command or rationale string. Structure
|
|
16
|
+
* is preserved (only matched values become [REDACTED]) so the redacted text
|
|
17
|
+
* stays analyzable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function redactCommand(text: string): string;
|
|
20
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic secret redaction for Guardian storage events (YAG-510).
|
|
3
|
+
*
|
|
4
|
+
* Applied client-side, BEFORE anything leaves the machine, to both the
|
|
5
|
+
* command and the Guardian rationale (which routinely quotes the command) —
|
|
6
|
+
* and only on the raw storage tier; the base tier never transmits either.
|
|
7
|
+
*
|
|
8
|
+
* Honest scope: this catches the obvious, well-known secret shapes. A secret
|
|
9
|
+
* in a novel shape gets through — which is why raw-tier storage is an
|
|
10
|
+
* explicit-consent, per-workspace opt-in and never a default.
|
|
11
|
+
*
|
|
12
|
+
* Pure, no external deps (bundling constraint — see execPolicy.ts header).
|
|
13
|
+
*/
|
|
14
|
+
const REDACTED = "[REDACTED]";
|
|
15
|
+
/** Exactly 40 hex chars = a git SHA; keep those readable for analysis. */
|
|
16
|
+
function isGitSha(token) {
|
|
17
|
+
return /^[0-9a-f]{40}$/i.test(token);
|
|
18
|
+
}
|
|
19
|
+
const RULES = [
|
|
20
|
+
// Known token shapes (provider-prefixed credentials).
|
|
21
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
22
|
+
{ re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
23
|
+
{ re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g },
|
|
24
|
+
{ re: /\bsk-[A-Za-z0-9_-]{16,}\b/g },
|
|
25
|
+
{ re: /\bxox[bpsaro]-[A-Za-z0-9-]{10,}\b/g },
|
|
26
|
+
{ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{4,}(?:\.[A-Za-z0-9_-]+)?\b/g }, // JWT
|
|
27
|
+
{ re: /\bglsa_[A-Za-z0-9_]{20,}\b/g },
|
|
28
|
+
{ re: /\b(?:fly|glpat|npm)_[A-Za-z0-9_-]{16,}\b/g },
|
|
29
|
+
// Authorization headers: keep the scheme, redact the credential.
|
|
30
|
+
{ re: /\b(Authorization:\s*(?:Bearer|Basic|Token)\s+)[^\s"']+/gi, keepGroup1: true },
|
|
31
|
+
// key=value / --key value / --key=value with a secret-shaped key.
|
|
32
|
+
{
|
|
33
|
+
re: /((?:--?[A-Za-z0-9_-]*)?(?:password|passwd|secret|token|api[_-]?key|auth)[A-Za-z0-9_-]*\s*[=\s]\s*)[^\s"']+/gi,
|
|
34
|
+
keepGroup1: true,
|
|
35
|
+
},
|
|
36
|
+
// user:pass@ in URLs — redact the password only.
|
|
37
|
+
{ re: /(\/\/[^\s/:@"']+:)[^\s@"']+(?=@)/g, keepGroup1: true },
|
|
38
|
+
];
|
|
39
|
+
/** High-entropy runs: 32+ base64/hex chars — except exact git SHAs. */
|
|
40
|
+
const LONG_RUN_RE = /[A-Za-z0-9+/=_-]{32,}/g;
|
|
41
|
+
/**
|
|
42
|
+
* Redact known secret shapes from a command or rationale string. Structure
|
|
43
|
+
* is preserved (only matched values become [REDACTED]) so the redacted text
|
|
44
|
+
* stays analyzable.
|
|
45
|
+
*/
|
|
46
|
+
export function redactCommand(text) {
|
|
47
|
+
let out = text;
|
|
48
|
+
for (const rule of RULES) {
|
|
49
|
+
out = out.replace(rule.re, (match, g1) => rule.keepGroup1 && typeof g1 === "string" ? `${g1}${REDACTED}` : REDACTED);
|
|
50
|
+
}
|
|
51
|
+
out = out.replace(LONG_RUN_RE, (match) => {
|
|
52
|
+
if (isGitSha(match))
|
|
53
|
+
return match;
|
|
54
|
+
if (match === REDACTED.slice(1, -1))
|
|
55
|
+
return match;
|
|
56
|
+
// Plain long words (paths, flags already handled above) that are all
|
|
57
|
+
// lowercase letters are more likely English than entropy; keep them.
|
|
58
|
+
if (/^[a-z]+$/.test(match) || /^[A-Z]+$/.test(match))
|
|
59
|
+
return match;
|
|
60
|
+
return REDACTED;
|
|
61
|
+
});
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The backend sets `x-yagni-model-reroute: <requested>-><effective>:vision` on
|
|
5
5
|
* every response it silently upgrades for image content (yagniCodeV1.ts). pi's
|
|
6
6
|
* `after_provider_response` event is the only client-side seam that sees raw
|
|
7
|
-
* response headers (verified against pi 0.
|
|
7
|
+
* response headers (verified against pi 0.84.1's AfterProviderResponseEvent:
|
|
8
8
|
* `{ type: "after_provider_response", status: number, headers: Record<string,
|
|
9
9
|
* string> }` in dist/core/extensions/types.d.ts). Its docs (extensions.md,
|
|
10
10
|
* "after_provider_response") describe `event.headers` as "normalized response
|
|
@@ -22,9 +22,8 @@ export interface Reroute {
|
|
|
22
22
|
export type HeaderMap = Record<string, string | undefined>;
|
|
23
23
|
/**
|
|
24
24
|
* Parse the `x-yagni-model-reroute` header. Returns null when the header is
|
|
25
|
-
* absent, does not match `<from>-><to>:<reason>`, or the reason is not
|
|
26
|
-
*
|
|
27
|
-
* can honestly claim).
|
|
25
|
+
* absent, does not match `<from>-><to>:<reason>`, or the reason is not a
|
|
26
|
+
* known reason (vision or policy; see KNOWN_REASONS).
|
|
28
27
|
*/
|
|
29
28
|
export declare function parseReroute(headers: HeaderMap): Reroute | null;
|
|
30
29
|
export declare class RerouteNotifier {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The backend sets `x-yagni-model-reroute: <requested>-><effective>:vision` on
|
|
5
5
|
* every response it silently upgrades for image content (yagniCodeV1.ts). pi's
|
|
6
6
|
* `after_provider_response` event is the only client-side seam that sees raw
|
|
7
|
-
* response headers (verified against pi 0.
|
|
7
|
+
* response headers (verified against pi 0.84.1's AfterProviderResponseEvent:
|
|
8
8
|
* `{ type: "after_provider_response", status: number, headers: Record<string,
|
|
9
9
|
* string> }` in dist/core/extensions/types.d.ts). Its docs (extensions.md,
|
|
10
10
|
* "after_provider_response") describe `event.headers` as "normalized response
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
const REROUTE_HEADER = "x-yagni-model-reroute";
|
|
16
16
|
const REROUTE_PATTERN = /^(.+?)->(.+?):(\w+)$/;
|
|
17
|
+
/** Reroute reasons the client understands. `policy` (caller→tier routing,
|
|
18
|
+
* 2026-08-11 spec) is parsed but deliberately produces NO user notice: the
|
|
19
|
+
* spec keeps per-request routing quiet — /cost and the savings receipts are
|
|
20
|
+
* the user-facing surface. `vision` keeps its one-time notice ("Your image…"
|
|
21
|
+
* is copy only a vision reroute can honestly claim). */
|
|
22
|
+
const KNOWN_REASONS = new Set(["vision", "policy"]);
|
|
17
23
|
/**
|
|
18
24
|
* Parse the `x-yagni-model-reroute` header. Returns null when the header is
|
|
19
|
-
* absent, does not match `<from>-><to>:<reason>`, or the reason is not
|
|
20
|
-
*
|
|
21
|
-
* can honestly claim).
|
|
25
|
+
* absent, does not match `<from>-><to>:<reason>`, or the reason is not a
|
|
26
|
+
* known reason (vision or policy; see KNOWN_REASONS).
|
|
22
27
|
*/
|
|
23
28
|
export function parseReroute(headers) {
|
|
24
29
|
const value = headers[REROUTE_HEADER];
|
|
@@ -28,17 +33,19 @@ export function parseReroute(headers) {
|
|
|
28
33
|
if (!match)
|
|
29
34
|
return null;
|
|
30
35
|
const [, from, to, reason] = match;
|
|
31
|
-
if (reason
|
|
36
|
+
if (!KNOWN_REASONS.has(reason))
|
|
32
37
|
return null;
|
|
33
38
|
return { from, to, reason };
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
36
41
|
* Dedupe wrapper around {@link parseReroute}: `observe` returns the notice
|
|
37
|
-
* message at most once per distinct from->to
|
|
38
|
-
* instance (i.e. per session), and null otherwise (absent, malformed,
|
|
39
|
-
* already-seen
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* message at most once per distinct from->to->reason triple for the life of
|
|
43
|
+
* the instance (i.e. per session), and null otherwise (absent, malformed,
|
|
44
|
+
* already-seen, or a non-vision reason — `policy` reroutes are parsed but
|
|
45
|
+
* deliberately silent; see KNOWN_REASONS). Returning the message rather than
|
|
46
|
+
* taking a notify callback keeps the caller in charge of the ctx it has on
|
|
47
|
+
* hand at call time, instead of this class holding a stale reference across
|
|
48
|
+
* calls.
|
|
42
49
|
*/
|
|
43
50
|
/**
|
|
44
51
|
* The concrete tier ladder cheapest-first, mirroring the proxy's
|
|
@@ -53,7 +60,9 @@ export class RerouteNotifier {
|
|
|
53
60
|
const reroute = parseReroute(headers);
|
|
54
61
|
if (!reroute)
|
|
55
62
|
return null;
|
|
56
|
-
|
|
63
|
+
if (reroute.reason !== "vision")
|
|
64
|
+
return null;
|
|
65
|
+
const key = `${reroute.from}->${reroute.to}:${reroute.reason}`;
|
|
57
66
|
if (this.seen.has(key))
|
|
58
67
|
return null;
|
|
59
68
|
this.seen.add(key);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live progress model + TUI renderers for the `subagent` tool.
|
|
3
|
+
*
|
|
4
|
+
* The model half is PURE (mirrors `pipeline/activity.ts`): `applyChildEvent`
|
|
5
|
+
* folds one NDJSON event from a child into a bounded per-task progress record,
|
|
6
|
+
* and `finalizeTask` stamps the outcome from the runner's `StageResult`. The
|
|
7
|
+
* tool carries the folded records in its `details`, so every partial update the
|
|
8
|
+
* TUI sees is a complete picture of all tasks.
|
|
9
|
+
*
|
|
10
|
+
* The renderer half implements pi's per-tool rendering seam (`renderCall` /
|
|
11
|
+
* `renderResult`). Collapsed-while-running is the two-line pattern: a stable
|
|
12
|
+
* `agent — task` title over a churning `↳ current tool` line. Completion is a
|
|
13
|
+
* one-line receipt (`✓ agent · N tool uses · Xk tokens · Ys`); expanded shows
|
|
14
|
+
* the curated action log and the full report as markdown. String assembly is
|
|
15
|
+
* kept in pure helpers over a minimal {@link RenderTheme} so tests run against
|
|
16
|
+
* plain text — renderer exceptions are swallowed by pi (silently degrading to
|
|
17
|
+
* the bare title bar), so everything here must stay boringly total.
|
|
18
|
+
*/
|
|
19
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
20
|
+
import type { JsonEvent, StageResult, StageUsage } from "./pipeline/types.js";
|
|
21
|
+
/** The minimal slice of pi's `Theme` the renderers style with (same shape as FeedTheme). */
|
|
22
|
+
export interface RenderTheme {
|
|
23
|
+
bold(text: string): string;
|
|
24
|
+
fg(color: string, text: string): string;
|
|
25
|
+
}
|
|
26
|
+
/** One curated line of a child's activity (tool action or narration headline). */
|
|
27
|
+
export interface SubagentActionEntry {
|
|
28
|
+
kind: "action" | "narration";
|
|
29
|
+
text: string;
|
|
30
|
+
state: "running" | "done" | "error";
|
|
31
|
+
/** Resolves a running tool start against its end event. */
|
|
32
|
+
toolCallId?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Live/final progress of one subagent task; rides the tool's `details`. */
|
|
35
|
+
export interface SubagentTaskProgress {
|
|
36
|
+
agent: string;
|
|
37
|
+
task: string;
|
|
38
|
+
status: "running" | "done" | "error";
|
|
39
|
+
/** -1 while the child is still running (mirrors the pi subagent example). */
|
|
40
|
+
exitCode: number;
|
|
41
|
+
startedAt: number;
|
|
42
|
+
endedAt?: number;
|
|
43
|
+
toolCalls: number;
|
|
44
|
+
toolErrors: number;
|
|
45
|
+
usage: StageUsage;
|
|
46
|
+
/** Bounded curated log; oldest entries are dropped past ACTION_LOG_MAX. */
|
|
47
|
+
actions: SubagentActionEntry[];
|
|
48
|
+
droppedActions: number;
|
|
49
|
+
/** The child's final report, present once the task resolved. */
|
|
50
|
+
report?: string;
|
|
51
|
+
stopReason?: string;
|
|
52
|
+
errorMessage?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SubagentDetails {
|
|
55
|
+
tasks: SubagentTaskProgress[];
|
|
56
|
+
}
|
|
57
|
+
/** Bound on the retained action log so a chatty child cannot grow details unbounded. */
|
|
58
|
+
export declare const ACTION_LOG_MAX = 120;
|
|
59
|
+
export declare function newTaskProgress(agent: string, task: string, startedAt: number): SubagentTaskProgress;
|
|
60
|
+
/**
|
|
61
|
+
* Fold one child NDJSON event into the task's progress. Returns true when the
|
|
62
|
+
* record changed (the tool emits an update), false for events we drop.
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyChildEvent(p: SubagentTaskProgress, ev: JsonEvent): boolean;
|
|
65
|
+
/** Stamp the runner's outcome onto the progress record. */
|
|
66
|
+
export declare function finalizeTask(p: SubagentTaskProgress, result: StageResult, endedAt: number): void;
|
|
67
|
+
/** 532 → "532", 41_234 → "41.2k", 1_240_000 → "1.2M". */
|
|
68
|
+
export declare function formatTokens(n: number): string;
|
|
69
|
+
/** 42_000 → "42s", 81_000 → "1m 21s", 3_720_000 → "1h 2m". */
|
|
70
|
+
export declare function formatDuration(ms: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* The two-line live status for one running task: a stable `agent — task` title
|
|
73
|
+
* over the churning current-action line with elapsed time and live tokens.
|
|
74
|
+
*/
|
|
75
|
+
export declare function runningLines(p: SubagentTaskProgress, theme: RenderTheme, now: number, frame: string): string[];
|
|
76
|
+
/** One-line completion receipt: `✓ agent · N tool uses · Xk tokens · Ys · $c`. */
|
|
77
|
+
export declare function receiptLine(p: SubagentTaskProgress, theme: RenderTheme): string;
|
|
78
|
+
/**
|
|
79
|
+
* Plain-text (no theme) summary for the partial result's `content`, so headless
|
|
80
|
+
* consumers and pi's fallback renderer still see live progress.
|
|
81
|
+
*/
|
|
82
|
+
export declare function progressSummaryText(tasks: SubagentTaskProgress[], now: number): string;
|
|
83
|
+
/** The harness "Working…" replacement while subagents run. */
|
|
84
|
+
export declare function formatWorkingMessage(tasks: SubagentTaskProgress[], now: number): string;
|
|
85
|
+
/** The subagent tool's argument shape, partial while the model streams it. */
|
|
86
|
+
interface SubagentCallArgs {
|
|
87
|
+
task?: string;
|
|
88
|
+
agent?: string;
|
|
89
|
+
tasks?: Array<{
|
|
90
|
+
task?: string;
|
|
91
|
+
agent?: string;
|
|
92
|
+
}>;
|
|
93
|
+
}
|
|
94
|
+
/** Renderer-row state shared across renders of one tool call (context.state). */
|
|
95
|
+
interface LiveRenderState {
|
|
96
|
+
timer?: ReturnType<typeof setInterval>;
|
|
97
|
+
}
|
|
98
|
+
interface RenderContextSlice {
|
|
99
|
+
state?: LiveRenderState;
|
|
100
|
+
invalidate: () => void;
|
|
101
|
+
}
|
|
102
|
+
/** Title painted the moment the call streams in (before any execution output). */
|
|
103
|
+
export declare function renderSubagentCall(args: SubagentCallArgs | undefined, theme: RenderTheme, _context: unknown): Component;
|
|
104
|
+
/**
|
|
105
|
+
* A prose body as markdown when the TUI's markdown theme is available.
|
|
106
|
+
* `getMarkdownTheme()` hands back a lazy proxy that only throws when a style is
|
|
107
|
+
* first USED, so the fallback must wrap `render`, not construction — otherwise
|
|
108
|
+
* an uninitialized theme would blow up mid-paint and pi would silently degrade
|
|
109
|
+
* the whole row to the bare title bar.
|
|
110
|
+
*/
|
|
111
|
+
export declare function markdownOrPlain(body: string, theme: RenderTheme): Component;
|
|
112
|
+
/**
|
|
113
|
+
* Result renderer: live two-line status per task while partial; receipts plus
|
|
114
|
+
* report preview when collapsed; action log plus full markdown report when
|
|
115
|
+
* expanded. Drives its own refresh while running via an unref'd interval on
|
|
116
|
+
* `context.state` (pi has no unmount hook — the final render clears it).
|
|
117
|
+
*/
|
|
118
|
+
export declare function renderSubagentResult(result: {
|
|
119
|
+
content: Array<{
|
|
120
|
+
type: string;
|
|
121
|
+
text?: string;
|
|
122
|
+
}>;
|
|
123
|
+
details?: unknown;
|
|
124
|
+
}, options: {
|
|
125
|
+
expanded: boolean;
|
|
126
|
+
isPartial: boolean;
|
|
127
|
+
}, theme: RenderTheme, context: RenderContextSlice): Component;
|
|
128
|
+
export {};
|
|
129
|
+
//# sourceMappingURL=subagentRender.d.ts.map
|