@yagni-app/code-staging 0.3.0-staging.1081.1 → 0.3.0-staging.1085.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.js +7 -1
- package/dist/extension/branding.d.ts +11 -1
- package/dist/extension/branding.js +45 -7
- package/dist/extension/chipEditor.d.ts +9 -0
- package/dist/extension/chipEditor.js +32 -0
- package/dist/extension/index.d.ts +4 -2
- package/dist/extension/index.js +20 -6
- 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 +35 -6
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/runRegistry.d.ts +14 -0
- package/dist/extension/pipeline/runRegistry.js +35 -0
- 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/subagents.d.ts +7 -1
- package/dist/extension/subagents.js +60 -5
- package/dist/extension/ultra.d.ts +23 -0
- package/dist/extension/ultra.js +61 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/package.json +2 -2
|
@@ -22,9 +22,25 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
24
|
import { makeFileCheckpointStore } from "./checkpoint.js";
|
|
25
|
-
import { classifyRunLiveness, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, settleRun, } from "./runRegistry.js";
|
|
25
|
+
import { activeRunRows, cancelActiveRuns, cancelRun, classifyRunLiveness, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, settleRun, } from "./runRegistry.js";
|
|
26
26
|
import { removeWorktree as defaultRemoveWorktree } from "./worktree.js";
|
|
27
27
|
import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
|
|
28
|
+
/**
|
|
29
|
+
* PURE: resolve a /stop argument against the in-flight rows. Matches the ticket
|
|
30
|
+
* (case-insensitive exact) first, then a runId prefix of at least 4 chars (the
|
|
31
|
+
* short id /go-status prints is 8).
|
|
32
|
+
*/
|
|
33
|
+
export function matchStopTarget(rows, arg) {
|
|
34
|
+
const needle = arg.trim().toLowerCase();
|
|
35
|
+
if (!needle)
|
|
36
|
+
return undefined;
|
|
37
|
+
const byTicket = rows.find((r) => r.ticket.toLowerCase() === needle);
|
|
38
|
+
if (byTicket)
|
|
39
|
+
return byTicket;
|
|
40
|
+
if (needle.length < 4)
|
|
41
|
+
return undefined;
|
|
42
|
+
return rows.find((r) => r.runId.toLowerCase().startsWith(needle));
|
|
43
|
+
}
|
|
28
44
|
/**
|
|
29
45
|
* PURE: where an in-flight/interrupted run got to, read off its checkpoint
|
|
30
46
|
* journal. The journal is append-only, so the latest applicable boundary wins.
|
|
@@ -90,6 +106,50 @@ export function registerGoStatusCommands(pi, deps = {}) {
|
|
|
90
106
|
const resolveRepo = deps.resolveRepo ?? (async () => undefined);
|
|
91
107
|
const exists = deps.exists ?? existsSync;
|
|
92
108
|
const now = deps.now ?? Date.now;
|
|
109
|
+
const activeRows = deps.activeRows ?? activeRunRows;
|
|
110
|
+
const cancelOne = deps.cancelOne ?? cancelRun;
|
|
111
|
+
const cancelAll = deps.cancelAll ?? cancelActiveRuns;
|
|
112
|
+
pi.registerCommand("stop", {
|
|
113
|
+
description: "Hard-stop /go runs in this session: /stop cancels them all, /stop <ticket|run id> cancels one. Work so far is preserved as a WIP commit.",
|
|
114
|
+
handler: async (args, ctx) => {
|
|
115
|
+
const say = async (message, type = "info") => {
|
|
116
|
+
if (ctx.hasUI)
|
|
117
|
+
ctx.ui.notify(message, type);
|
|
118
|
+
else
|
|
119
|
+
await pi.sendUserMessage(message);
|
|
120
|
+
};
|
|
121
|
+
const label = (row) => `[${row.runId.slice(0, 8)}] ${row.ticket}`;
|
|
122
|
+
const rows = activeRows();
|
|
123
|
+
if (rows.length === 0) {
|
|
124
|
+
await say("No /go runs are in flight in this session. (Esc interrupts the interactive agent; /go-status lists runs from other sessions.)");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const arg = args.trim();
|
|
128
|
+
if (arg) {
|
|
129
|
+
const target = matchStopTarget(rows, arg);
|
|
130
|
+
if (!target) {
|
|
131
|
+
await say(`No in-flight run matches "${arg}". Running here: ${rows.map(label).join(", ")}.`, "warning");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!cancelOne(target.runId)) {
|
|
135
|
+
await say(`${label(target)} is still starting and cannot be signalled yet; try /stop again.`, "warning");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await say(`Stopping ${label(target)}. Its workers are being killed; any work lands as a WIP commit and the handoff will report the aborted run.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const cancelled = cancelAll();
|
|
142
|
+
if (cancelled.length === 0) {
|
|
143
|
+
// The window between beginRun and trackRunAbort is one tick wide, but be
|
|
144
|
+
// honest if we hit it rather than claiming a stop that did not fire.
|
|
145
|
+
await say("Could not signal any run yet (they are still starting); try /stop again.", "warning");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await say(`Stopping ${cancelled.length} /go run${cancelled.length === 1 ? "" : "s"}: ${cancelled
|
|
149
|
+
.map(label)
|
|
150
|
+
.join(", ")}. Workers are being killed; work so far lands as WIP commits (see /go-status).`);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
93
153
|
pi.registerCommand("go-status", {
|
|
94
154
|
description: "List /go runs: active (stage/round), finished (outcome + branch/commit/PR), and interrupted (resumable).",
|
|
95
155
|
handler: async (_args, ctx) => {
|
|
@@ -85,6 +85,20 @@ export declare function _resetRunRegistryForTest(): void;
|
|
|
85
85
|
export declare function beginRun(row: RunRegistryRow): void;
|
|
86
86
|
/** Attach the detached completion promise to an in-flight run (fail-soft no-op when absent). */
|
|
87
87
|
export declare function trackRunPromise(runId: string, promise: Promise<void>): void;
|
|
88
|
+
/** Attach the run's abort controller so /stop can cancel it (fail-soft no-op when absent). */
|
|
89
|
+
export declare function trackRunAbort(runId: string, abort: AbortController): void;
|
|
90
|
+
/**
|
|
91
|
+
* Cancel one in-flight run in THIS process: fire its abort controller and return
|
|
92
|
+
* its row. The run itself settles through the normal pipeline path (the abort
|
|
93
|
+
* propagates to every stage child as SIGTERM→SIGKILL, the orchestrator stops with
|
|
94
|
+
* an honest `aborted`, and runToCompletion preserves WIP + settles the registry).
|
|
95
|
+
* Returns undefined for an unknown/foreign runId or a run with no controller yet.
|
|
96
|
+
*/
|
|
97
|
+
export declare function cancelRun(runId: string): RunRegistryRow | undefined;
|
|
98
|
+
/** Cancel EVERY in-flight run in this process (the /stop no-arg path). Returns the rows fired. */
|
|
99
|
+
export declare function cancelActiveRuns(): RunRegistryRow[];
|
|
100
|
+
/** The in-flight rows in THIS process (newest first), for /stop's matching + messaging. */
|
|
101
|
+
export declare function activeRunRows(): RunRegistryRow[];
|
|
88
102
|
/**
|
|
89
103
|
* Settle an in-flight run: merge the terminal patch, drop it from the in-memory
|
|
90
104
|
* active set, and mirror the final row. Safe to call for an unknown runId: the
|
|
@@ -137,6 +137,41 @@ export function trackRunPromise(runId, promise) {
|
|
|
137
137
|
if (entry)
|
|
138
138
|
entry.promise = promise;
|
|
139
139
|
}
|
|
140
|
+
/** Attach the run's abort controller so /stop can cancel it (fail-soft no-op when absent). */
|
|
141
|
+
export function trackRunAbort(runId, abort) {
|
|
142
|
+
const entry = active.get(runId);
|
|
143
|
+
if (entry)
|
|
144
|
+
entry.abort = abort;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Cancel one in-flight run in THIS process: fire its abort controller and return
|
|
148
|
+
* its row. The run itself settles through the normal pipeline path (the abort
|
|
149
|
+
* propagates to every stage child as SIGTERM→SIGKILL, the orchestrator stops with
|
|
150
|
+
* an honest `aborted`, and runToCompletion preserves WIP + settles the registry).
|
|
151
|
+
* Returns undefined for an unknown/foreign runId or a run with no controller yet.
|
|
152
|
+
*/
|
|
153
|
+
export function cancelRun(runId) {
|
|
154
|
+
const entry = active.get(runId);
|
|
155
|
+
if (!entry?.abort)
|
|
156
|
+
return undefined;
|
|
157
|
+
entry.abort.abort();
|
|
158
|
+
return entry.row;
|
|
159
|
+
}
|
|
160
|
+
/** Cancel EVERY in-flight run in this process (the /stop no-arg path). Returns the rows fired. */
|
|
161
|
+
export function cancelActiveRuns() {
|
|
162
|
+
const cancelled = [];
|
|
163
|
+
for (const entry of active.values()) {
|
|
164
|
+
if (!entry.abort)
|
|
165
|
+
continue;
|
|
166
|
+
entry.abort.abort();
|
|
167
|
+
cancelled.push(entry.row);
|
|
168
|
+
}
|
|
169
|
+
return cancelled;
|
|
170
|
+
}
|
|
171
|
+
/** The in-flight rows in THIS process (newest first), for /stop's matching + messaging. */
|
|
172
|
+
export function activeRunRows() {
|
|
173
|
+
return [...active.values()].map((e) => e.row).sort((a, b) => b.startedAt - a.startedAt);
|
|
174
|
+
}
|
|
140
175
|
/**
|
|
141
176
|
* Settle an in-flight run: merge the terminal patch, drop it from the in-memory
|
|
142
177
|
* active set, and mirror the final row. Safe to call for an unknown runId: the
|
|
@@ -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
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
22
|
import { runStage } from "./pipeline/runner.js";
|
|
23
|
-
import type
|
|
23
|
+
import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
|
|
24
24
|
import { renderSubagentCall, renderSubagentResult } from "./subagentRender.js";
|
|
25
25
|
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
26
26
|
export declare const GENERAL_AGENT_NAME = "general";
|
|
27
27
|
export declare const MAX_PARALLEL_SUBAGENTS = 4;
|
|
28
|
+
/** Wider fan-out ceiling while the session is in ultra mode (/ultra). */
|
|
29
|
+
export declare const MAX_PARALLEL_SUBAGENTS_ULTRA = 8;
|
|
28
30
|
/**
|
|
29
31
|
* The default tool surface a subagent gets when its definition declares none:
|
|
30
32
|
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
@@ -92,6 +94,8 @@ export interface MakeSubagentToolDeps {
|
|
|
92
94
|
runStageImpl?: typeof runStage;
|
|
93
95
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
94
96
|
homeDir?: string;
|
|
97
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
98
|
+
isUltra?: () => boolean;
|
|
95
99
|
}
|
|
96
100
|
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
97
101
|
name: string;
|
|
@@ -135,6 +139,8 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
135
139
|
export interface RegisterSubagentsDeps {
|
|
136
140
|
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
137
141
|
homeDir?: string;
|
|
142
|
+
/** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
|
|
143
|
+
isUltra?: () => boolean;
|
|
138
144
|
}
|
|
139
145
|
/** Wire the subagent tool and the /agents listing command. */
|
|
140
146
|
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
@@ -23,7 +23,9 @@ import { delimiter, join } from "node:path";
|
|
|
23
23
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import { Type } from "typebox";
|
|
25
25
|
import { sanitizeCallerSegment } from "./config.js";
|
|
26
|
+
import { withResilience } from "./pipeline/resilience.js";
|
|
26
27
|
import { runStage } from "./pipeline/runner.js";
|
|
28
|
+
import { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
|
|
27
29
|
import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, renderSubagentCall, renderSubagentResult, } from "./subagentRender.js";
|
|
28
30
|
/**
|
|
29
31
|
* YAG-471 attribution: the `x-yagni-caller` prefix for a subagent invocation.
|
|
@@ -34,6 +36,8 @@ const SUBAGENT_CALLER_PREFIX = "subagent:";
|
|
|
34
36
|
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
35
37
|
export const GENERAL_AGENT_NAME = "general";
|
|
36
38
|
export const MAX_PARALLEL_SUBAGENTS = 4;
|
|
39
|
+
/** Wider fan-out ceiling while the session is in ultra mode (/ultra). */
|
|
40
|
+
export const MAX_PARALLEL_SUBAGENTS_ULTRA = 8;
|
|
37
41
|
/**
|
|
38
42
|
* The default tool surface a subagent gets when its definition declares none:
|
|
39
43
|
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
@@ -123,6 +127,50 @@ const IMPLEMENTER_AGENT = {
|
|
|
123
127
|
body: IMPLEMENTER_BODY,
|
|
124
128
|
source: "builtin",
|
|
125
129
|
};
|
|
130
|
+
const VERIFICATION_BODY = `You are an adversarial verifier. Another agent
|
|
131
|
+
produced work — a change, a plan, or a claim — and your job is to try to
|
|
132
|
+
BREAK it, not to summarize it. Default to skepticism: hunt for the concrete
|
|
133
|
+
failure scenario (the inputs, state, or sequence that makes it wrong). Bash
|
|
134
|
+
is read-only here (\`git diff\`, \`git log\`, \`git show\`); do NOT modify files
|
|
135
|
+
or run builds.
|
|
136
|
+
|
|
137
|
+
If your task names a lens (correctness, edge cases, codebase fit, security,
|
|
138
|
+
…), judge ONLY through that lens and leave the rest to your sibling
|
|
139
|
+
verifiers.
|
|
140
|
+
|
|
141
|
+
You are grounded in how THIS company works: call ask_yagni before inferring
|
|
142
|
+
a convention, an ownership rule, or anything organization-specific.
|
|
143
|
+
|
|
144
|
+
Your final message is your verdict back to the driving agent, which has NOT
|
|
145
|
+
seen what you read. Format:
|
|
146
|
+
## Verdict
|
|
147
|
+
BROKEN or HOLDS, with one sentence why.
|
|
148
|
+
## Findings
|
|
149
|
+
Each real problem: file:line, the concrete failure scenario, severity. No
|
|
150
|
+
style nits.
|
|
151
|
+
## Not verified
|
|
152
|
+
What you could not check, and why.
|
|
153
|
+
|
|
154
|
+
A HOLDS after real digging is valuable; a rubber stamp is not. If you found
|
|
155
|
+
nothing, say exactly what you tried to break and how.`;
|
|
156
|
+
/** The diamond's reduce layer: refute-first review of completed work on the
|
|
157
|
+
* advanced tier (judgment is the whole job, so the premium is worth paying —
|
|
158
|
+
* the server clamps subagent children at advanced anyway). It has no
|
|
159
|
+
* edit/write tools; bash is included for `git diff`-style inspection and is
|
|
160
|
+
* restricted to read-only USE by the persona, the same prompt-level stance as
|
|
161
|
+
* /go's reviewer — not a technical guarantee. */
|
|
162
|
+
const VERIFICATION_AGENT = {
|
|
163
|
+
name: "verification",
|
|
164
|
+
description: "Adversarial verification of completed work: tries to refute a change, " +
|
|
165
|
+
"plan, or claim and reports concrete failure scenarios. Does not edit " +
|
|
166
|
+
"files. Fan out 2-3 with different lenses (correctness, edge cases, " +
|
|
167
|
+
"codebase fit) for anything significant and treat agreement as " +
|
|
168
|
+
"confirmation.",
|
|
169
|
+
model: "advanced",
|
|
170
|
+
tools: ["read", "grep", "find", "ls", "bash", "ask_yagni"],
|
|
171
|
+
body: VERIFICATION_BODY,
|
|
172
|
+
source: "builtin",
|
|
173
|
+
};
|
|
126
174
|
// Concrete tiers a subagent can actually run on. `balanced` is deliberately NOT
|
|
127
175
|
// a member here even though it is a member of `ModelTier`: a subagent needs
|
|
128
176
|
// ONE model for its whole run, and balanced is a session-level routing policy,
|
|
@@ -210,7 +258,7 @@ function loadAgentsFromDir(dir, source) {
|
|
|
210
258
|
export function discoverSubagents(deps) {
|
|
211
259
|
const home = deps.homeDir ?? homedir();
|
|
212
260
|
const layers = [
|
|
213
|
-
[GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT],
|
|
261
|
+
[GENERAL_AGENT, SEARCHER_AGENT, IMPLEMENTER_AGENT, VERIFICATION_AGENT],
|
|
214
262
|
...pluginAgentDirs(deps.env ?? process.env).map((dir) => loadAgentsFromDir(dir, "plugin")),
|
|
215
263
|
loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
|
|
216
264
|
loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
|
|
@@ -261,11 +309,17 @@ const parameters = Type.Object({
|
|
|
261
309
|
task: Type.String(),
|
|
262
310
|
agent: Type.Optional(Type.String()),
|
|
263
311
|
}), {
|
|
264
|
-
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}). Use INSTEAD of task.`,
|
|
312
|
+
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}; ${MAX_PARALLEL_SUBAGENTS_ULTRA} in ultra mode). Use INSTEAD of task.`,
|
|
265
313
|
})),
|
|
266
314
|
});
|
|
267
315
|
export function makeSubagentTool(deps = {}) {
|
|
268
|
-
|
|
316
|
+
// The default runner rides the /go pipeline's resilience wrapper, so a chat
|
|
317
|
+
// subagent gets the same idle + wall-clock ceilings and transient-only retry
|
|
318
|
+
// as a /go stage child (previously a hung subagent hung the tool call until
|
|
319
|
+
// the user pressed Esc). The synthetic stage id is "implement", so the
|
|
320
|
+
// wrapper's write-gate already refuses to re-run a child that may have
|
|
321
|
+
// landed a partial edit.
|
|
322
|
+
const run = deps.runStageImpl ?? withResilience(runStage, DEFAULT_RESILIENCE_POLICY);
|
|
269
323
|
const discover = deps.discover ?? discoverSubagents;
|
|
270
324
|
return {
|
|
271
325
|
name: SUBAGENT_TOOL_NAME,
|
|
@@ -293,8 +347,9 @@ export function makeSubagentTool(deps = {}) {
|
|
|
293
347
|
if (requested.length === 0) {
|
|
294
348
|
return fail("Error: pass `task` (or a `tasks` array) describing what to do.");
|
|
295
349
|
}
|
|
296
|
-
|
|
297
|
-
|
|
350
|
+
const maxParallel = deps.isUltra?.() ? MAX_PARALLEL_SUBAGENTS_ULTRA : MAX_PARALLEL_SUBAGENTS;
|
|
351
|
+
if (requested.length > maxParallel) {
|
|
352
|
+
return fail(`Error: at most ${maxParallel} parallel tasks per call.`);
|
|
298
353
|
}
|
|
299
354
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
300
355
|
const agents = discover({ cwd, homeDir: deps.homeDir });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultra mode (/ultra) — an explicit, session-scoped dial for aggressive
|
|
3
|
+
* multi-agent orchestration ("the diamond": fan out → reduce → synthesize).
|
|
4
|
+
*
|
|
5
|
+
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
|
+
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA, read live per turn in index.ts) and widens the
|
|
8
|
+
* subagent tool's per-call fan-out ceiling (subagents.ts). Ultra is a prompt +
|
|
9
|
+
* ceiling change only: it never touches the permission mode, the model tier,
|
|
10
|
+
* or the /go pipeline.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
export interface UltraHolder {
|
|
14
|
+
get(): boolean;
|
|
15
|
+
set(on: boolean): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createUltraHolder(initial?: boolean): UltraHolder;
|
|
18
|
+
/**
|
|
19
|
+
* Wire the /ultra command onto a shared holder. No argument toggles; `on` /
|
|
20
|
+
* `off` set explicitly; `status` reports without changing anything.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerUltraCommand(pi: ExtensionAPI, holder: UltraHolder): void;
|
|
23
|
+
//# sourceMappingURL=ultra.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultra mode (/ultra) — an explicit, session-scoped dial for aggressive
|
|
3
|
+
* multi-agent orchestration ("the diamond": fan out → reduce → synthesize).
|
|
4
|
+
*
|
|
5
|
+
* Off by default so the trial-default behavior is unchanged; toggling on swaps
|
|
6
|
+
* the driver's delegation paragraph for the diamond directive (branding.ts's
|
|
7
|
+
* YAGNI_IDENTITY_ULTRA, read live per turn in index.ts) and widens the
|
|
8
|
+
* subagent tool's per-call fan-out ceiling (subagents.ts). Ultra is a prompt +
|
|
9
|
+
* ceiling change only: it never touches the permission mode, the model tier,
|
|
10
|
+
* or the /go pipeline.
|
|
11
|
+
*/
|
|
12
|
+
export function createUltraHolder(initial = false) {
|
|
13
|
+
let on = initial;
|
|
14
|
+
return {
|
|
15
|
+
get: () => on,
|
|
16
|
+
set: (v) => {
|
|
17
|
+
on = v;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Status chip shown while ultra is on (same footer surface as the /mode chip). */
|
|
22
|
+
const ULTRA_STATUS = "◆ ultra";
|
|
23
|
+
const ULTRA_ON_COPY = "Ultra mode ON: meaningful work fans out to parallel subagents, adversarial " +
|
|
24
|
+
"verification agents try to break the result, then the agent synthesizes. " +
|
|
25
|
+
"Expect more subagent spend per task.";
|
|
26
|
+
const ULTRA_OFF_COPY = "Ultra mode OFF: back to delegate-when-useful.";
|
|
27
|
+
/**
|
|
28
|
+
* Wire the /ultra command onto a shared holder. No argument toggles; `on` /
|
|
29
|
+
* `off` set explicitly; `status` reports without changing anything.
|
|
30
|
+
*/
|
|
31
|
+
export function registerUltraCommand(pi, holder) {
|
|
32
|
+
pi.registerCommand("ultra", {
|
|
33
|
+
description: "Toggle ultra mode: aggressive fan-out/verify/synthesize orchestration. /ultra on | off | status.",
|
|
34
|
+
handler: async (args, ctx) => {
|
|
35
|
+
const notify = (m, t) => {
|
|
36
|
+
if (ctx.hasUI)
|
|
37
|
+
ctx.ui.notify(m, t);
|
|
38
|
+
};
|
|
39
|
+
const arg = args.trim().toLowerCase();
|
|
40
|
+
if (arg && arg !== "on" && arg !== "off" && arg !== "status") {
|
|
41
|
+
notify(`Unknown argument "${arg}". Use /ultra, /ultra on, /ultra off, or /ultra status.`, "warning");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "status") {
|
|
45
|
+
notify(holder.get() ? ULTRA_ON_COPY : ULTRA_OFF_COPY, "info");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const next = arg === "on" ? true : arg === "off" ? false : !holder.get();
|
|
49
|
+
holder.set(next);
|
|
50
|
+
try {
|
|
51
|
+
if (ctx.hasUI)
|
|
52
|
+
ctx.ui.setStatus?.("yagni-ultra", next ? ULTRA_STATUS : undefined);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// The chip is chrome; never let it break /ultra.
|
|
56
|
+
}
|
|
57
|
+
notify(next ? ULTRA_ON_COPY : ULTRA_OFF_COPY, "info");
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=ultra.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Termination-signal forwarding: launcher → pi child.
|
|
3
|
+
*
|
|
4
|
+
* In the interactive TUI the tty is in raw mode, so Ctrl+C never raises
|
|
5
|
+
* SIGINT; this path covers `yagni -p`/piped/CI runs and an external
|
|
6
|
+
* `kill <launcher pid>`. Policy:
|
|
7
|
+
*
|
|
8
|
+
* - The FIRST signal asks pi to shut down cleanly. SIGINT is translated to
|
|
9
|
+
* SIGTERM because pi installs a graceful handler for SIGTERM (it kills its
|
|
10
|
+
* tracked children) but none for SIGINT — a raw SIGINT would drop pi on the
|
|
11
|
+
* default disposition with no cleanup.
|
|
12
|
+
* - Any SECOND signal hard-kills pi's whole process TREE. A bare SIGKILL to
|
|
13
|
+
* pi's pid would bypass its exit sweep (SIGKILL runs no handlers) and leave
|
|
14
|
+
* /go workers alive, so the launcher enumerates and kills the descendants
|
|
15
|
+
* itself: `ps` walk on POSIX, `taskkill /T /F` on Windows.
|
|
16
|
+
*/
|
|
17
|
+
/** The minimal child surface the forwarder needs (ChildProcess satisfies it). */
|
|
18
|
+
export interface KillableChild {
|
|
19
|
+
pid?: number | undefined;
|
|
20
|
+
kill(signal: NodeJS.Signals): boolean;
|
|
21
|
+
}
|
|
22
|
+
/** The launcher-terminating signals that are forwarded rather than obeyed. */
|
|
23
|
+
export declare const FORWARDED_SIGNALS: readonly NodeJS.Signals[];
|
|
24
|
+
/**
|
|
25
|
+
* PURE policy for the FIRST signal: which signal the child receives when the
|
|
26
|
+
* launcher gets `received` (SIGINT is translated, the rest pass through).
|
|
27
|
+
* Escalation is not expressed here — a second signal goes through the tree
|
|
28
|
+
* kill, not a forwarded signal.
|
|
29
|
+
*/
|
|
30
|
+
export declare function forwardedSignal(received: NodeJS.Signals): NodeJS.Signals;
|
|
31
|
+
/**
|
|
32
|
+
* PURE: the launcher's own exit code for the child's (code, signal) exit tuple.
|
|
33
|
+
* A signal-terminated child maps to the conventional 128+n (bash parity:
|
|
34
|
+
* SIGTERM→143, SIGKILL→137, SIGINT→130), so a cancelled run never reads as
|
|
35
|
+
* success to scripts or CI.
|
|
36
|
+
*/
|
|
37
|
+
export declare function exitCodeFor(code: number | null, signal: NodeJS.Signals | null): number;
|
|
38
|
+
/**
|
|
39
|
+
* PURE: given `ps -A -o pid=,ppid=` output lines, collect every descendant of
|
|
40
|
+
* the given roots (children, grandchildren, ...), breadth-first. Mirrored in
|
|
41
|
+
* pi-extension-yagni's childRegistry (the packages are intentionally
|
|
42
|
+
* independent); keep the two in sync.
|
|
43
|
+
*/
|
|
44
|
+
export declare function descendantsOf(roots: number[], psLines: string[]): number[];
|
|
45
|
+
/**
|
|
46
|
+
* Synchronously SIGKILL a process AND its descendants (POSIX `ps` walk /
|
|
47
|
+
* Windows `taskkill /T /F`). Throw-proof; fail-soft to a direct kill when no
|
|
48
|
+
* process snapshot is readable.
|
|
49
|
+
*/
|
|
50
|
+
export declare function killTreeSync(pid: number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe the forwarding policy for every signal in {@link FORWARDED_SIGNALS}.
|
|
53
|
+
* `subscribe` and `killTree` default to the real process surfaces and are
|
|
54
|
+
* injectable so tests never install real handlers, raise real signals, or kill
|
|
55
|
+
* real processes. Returns the forward function itself (also for tests).
|
|
56
|
+
* pid guard: a child whose spawn failed has no pid, and `kill()` would resolve
|
|
57
|
+
* that to the whole process group.
|
|
58
|
+
*/
|
|
59
|
+
export declare function installSignalForwarding(child: KillableChild, subscribe?: (sig: NodeJS.Signals, handler: () => void) => void, killTree?: (pid: number) => void): (sig: NodeJS.Signals) => void;
|
|
60
|
+
//# sourceMappingURL=signalForward.d.ts.map
|