@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
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { runDoctor } from "./doctor.js";
|
|
|
28
28
|
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
29
29
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
30
30
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
31
|
+
import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
|
|
31
32
|
import { PAD_X } from "./padding.js";
|
|
32
33
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
33
34
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
@@ -262,7 +263,12 @@ async function runDefault(passthroughArgs) {
|
|
|
262
263
|
stdio: "inherit",
|
|
263
264
|
env,
|
|
264
265
|
});
|
|
265
|
-
|
|
266
|
+
// Forward termination signals to pi instead of dying around it (see
|
|
267
|
+
// signalForward.ts for the policy: first signal graceful, second tree-kill).
|
|
268
|
+
installSignalForwarding(child);
|
|
269
|
+
// 128+n for a signal death (bash parity), so a cancelled/killed run never
|
|
270
|
+
// reads as success to scripts or CI.
|
|
271
|
+
child.on("exit", (code, signal) => resolve(exitCodeFor(code, signal)));
|
|
266
272
|
child.on("error", (err) => {
|
|
267
273
|
process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
|
|
268
274
|
resolve(1);
|
|
@@ -30,6 +30,14 @@ export declare const YAGNI_IDENTITY: string;
|
|
|
30
30
|
* exist.
|
|
31
31
|
*/
|
|
32
32
|
export declare const DRIVER_DELEGATION_PARAGRAPH: string;
|
|
33
|
+
/**
|
|
34
|
+
* The ultra-mode delegation directive (/ultra): the user has opted into
|
|
35
|
+
* aggressive multi-agent orchestration, so the driver is told to structure
|
|
36
|
+
* meaningful work as a diamond — split, fan out workers, fan out refuting
|
|
37
|
+
* checkers, synthesize — instead of delegating only when convenient.
|
|
38
|
+
* DRIVER-ONLY for the same reason as {@link DRIVER_DELEGATION_PARAGRAPH}.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ULTRA_DELEGATION_PARAGRAPH: string;
|
|
33
41
|
/**
|
|
34
42
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
35
43
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -37,7 +45,9 @@ export declare const DRIVER_DELEGATION_PARAGRAPH: string;
|
|
|
37
45
|
* effective `x-yagni-caller` attribution (config.ts's `isDriverCaller`) — this
|
|
38
46
|
* module stays a pure string, with no env dependency of its own.
|
|
39
47
|
*/
|
|
40
|
-
export declare const YAGNI_IDENTITY_DRIVER = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation: fan codebase mapping, wide searches, and mechanical multi-file work out to subagents (they run on cheaper tiers). Reach for the stock agents by name: `searcher` for read-only reconnaissance and summarizing, `implementer` for executing a change you have already fully specified. Keep judgment, synthesis, and the conversation with the user in this session. Do not spawn a subagent for work you can finish in a couple of tool calls.";
|
|
48
|
+
export declare const YAGNI_IDENTITY_DRIVER = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation: fan codebase mapping, wide searches, and mechanical multi-file work out to subagents (they run on cheaper tiers). Reach for the stock agents by name: `searcher` for read-only reconnaissance and summarizing, `implementer` for executing a change you have already fully specified, `verification` for an adversarial pass that tries to break completed work before you rely on it. Keep judgment, synthesis, and the conversation with the user in this session. Do not spawn a subagent for work you can finish in a couple of tool calls.";
|
|
49
|
+
/** The driver identity while /ultra is on: base identity + the diamond directive. */
|
|
50
|
+
export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond.";
|
|
41
51
|
export declare const PI_IDENTITY_RE: RegExp;
|
|
42
52
|
/**
|
|
43
53
|
* Env switch that bypasses the system-prompt rewrite entirely, so pi's
|
|
@@ -42,10 +42,29 @@ export const YAGNI_IDENTITY = "You are YAGNI Code, an autonomous terminal coding
|
|
|
42
42
|
export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wide searches, and mechanical multi-file " +
|
|
43
43
|
"work out to subagents (they run on cheaper tiers). Reach for the stock " +
|
|
44
44
|
"agents by name: `searcher` for read-only reconnaissance and summarizing, " +
|
|
45
|
-
"`implementer` for executing a change you have already fully specified
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"`implementer` for executing a change you have already fully specified, " +
|
|
46
|
+
"`verification` for an adversarial pass that tries to break completed work " +
|
|
47
|
+
"before you rely on it. Keep judgment, synthesis, and the conversation " +
|
|
48
|
+
"with the user in this session. Do not spawn a subagent for work you can " +
|
|
49
|
+
"finish in a couple of tool calls.";
|
|
50
|
+
/**
|
|
51
|
+
* The ultra-mode delegation directive (/ultra): the user has opted into
|
|
52
|
+
* aggressive multi-agent orchestration, so the driver is told to structure
|
|
53
|
+
* meaningful work as a diamond — split, fan out workers, fan out refuting
|
|
54
|
+
* checkers, synthesize — instead of delegating only when convenient.
|
|
55
|
+
* DRIVER-ONLY for the same reason as {@link DRIVER_DELEGATION_PARAGRAPH}.
|
|
56
|
+
*/
|
|
57
|
+
export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has switched this session to ultra mode " +
|
|
58
|
+
"— aggressive multi-agent orchestration. Structure any meaningful task as " +
|
|
59
|
+
"a diamond: SPLIT the job into independent pieces; FAN OUT parallel " +
|
|
60
|
+
"subagents on cheaper tiers (`searcher` to scout, `implementer` or " +
|
|
61
|
+
"`general` to execute); CHECK by fanning out `verification` subagents told " +
|
|
62
|
+
"to refute the work, each through a different lens (correctness, edge cases, " +
|
|
63
|
+
"fit with this codebase); then SYNTHESIZE the results yourself. Treat " +
|
|
64
|
+
"agreement between checkers — not a single pass — as confirmation, and " +
|
|
65
|
+
"surface what they could not verify. Delegate by default and reserve this " +
|
|
66
|
+
"session for splitting, judging, and synthesis; only trivial work you can " +
|
|
67
|
+
"finish in a couple of tool calls skips the diamond.";
|
|
49
68
|
/**
|
|
50
69
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
|
51
70
|
* YAGNI_IDENTITY} plus {@link DRIVER_DELEGATION_PARAGRAPH}. The caller (index.ts)
|
|
@@ -54,6 +73,14 @@ export const DRIVER_DELEGATION_PARAGRAPH = "Delegation: fan codebase mapping, wi
|
|
|
54
73
|
* module stays a pure string, with no env dependency of its own.
|
|
55
74
|
*/
|
|
56
75
|
export const YAGNI_IDENTITY_DRIVER = `${YAGNI_IDENTITY}\n\n${DRIVER_DELEGATION_PARAGRAPH}`;
|
|
76
|
+
/** The driver identity while /ultra is on: base identity + the diamond directive. */
|
|
77
|
+
export const YAGNI_IDENTITY_ULTRA = `${YAGNI_IDENTITY}\n\n${ULTRA_DELEGATION_PARAGRAPH}`;
|
|
78
|
+
/**
|
|
79
|
+
* Every identity variant this module can install, longest-composite-first, so
|
|
80
|
+
* the re-brand swap below never leaves a shorter variant's orphaned delegation
|
|
81
|
+
* paragraph behind (driver/ultra both start with the base identity).
|
|
82
|
+
*/
|
|
83
|
+
const KNOWN_IDENTITIES = [YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY];
|
|
57
84
|
// pi 0.84.1's exact identity sentence (dist/core/system-prompt.js). Exported as
|
|
58
85
|
// the identity anchor the CLI's pi-contract tripwire test reads back from pi's
|
|
59
86
|
// built system prompt, so a pi bump that reworded the opener (silently defeating
|
|
@@ -112,15 +139,26 @@ export function brandSystemPrompt(original, opts = {}) {
|
|
|
112
139
|
let s = original;
|
|
113
140
|
// 1. Drop pi's self-referential documentation block.
|
|
114
141
|
s = s.replace(PI_DOCS_BLOCK_RE, "");
|
|
115
|
-
// 2. Own the identity.
|
|
142
|
+
// 2. Own the identity. A previously-branded prompt may open with a DIFFERENT
|
|
143
|
+
// variant than the one now requested (/ultra toggles the driver between the
|
|
144
|
+
// delegation and diamond paragraphs mid-session) — swap it in place rather
|
|
145
|
+
// than prepending a second identity block.
|
|
116
146
|
if (PI_IDENTITY_RE.test(s)) {
|
|
117
147
|
s = s.replace(PI_IDENTITY_RE, identity);
|
|
118
148
|
}
|
|
119
149
|
else if (PI_IDENTITY_LOOSE_RE.test(s)) {
|
|
120
150
|
s = s.replace(PI_IDENTITY_LOOSE_RE, identity);
|
|
121
151
|
}
|
|
122
|
-
else
|
|
123
|
-
|
|
152
|
+
else {
|
|
153
|
+
const trimmed = s.trimStart();
|
|
154
|
+
const lead = s.slice(0, s.length - trimmed.length);
|
|
155
|
+
const existing = KNOWN_IDENTITIES.find((k) => trimmed.startsWith(k));
|
|
156
|
+
if (existing && existing !== identity) {
|
|
157
|
+
s = `${lead}${identity}${trimmed.slice(existing.length)}`;
|
|
158
|
+
}
|
|
159
|
+
else if (!existing && !trimmed.startsWith(identity)) {
|
|
160
|
+
s = `${identity}\n\n${s}`;
|
|
161
|
+
}
|
|
124
162
|
}
|
|
125
163
|
// 3. Safety net for residual brand tokens (never inside user content).
|
|
126
164
|
s = scrubOutsideProjectContext(s);
|
|
@@ -84,6 +84,15 @@ export declare class ChipEditor extends CustomEditor {
|
|
|
84
84
|
private stashed;
|
|
85
85
|
private chipCounter;
|
|
86
86
|
private readImage;
|
|
87
|
+
/**
|
|
88
|
+
* Fired when Ctrl+C lands on an EMPTY prompt. pi's own binding is
|
|
89
|
+
* clear-editor (Esc is the interrupt key), but with nothing typed that is a
|
|
90
|
+
* no-op, and every peer harness (Claude Code, Codex, Gemini, Aider) treats a
|
|
91
|
+
* single Ctrl+C as "stop what's happening". The hook lets the extension wire
|
|
92
|
+
* exactly that; the key still falls through to pi afterwards, so clear and
|
|
93
|
+
* the 500ms double-press exit window are untouched.
|
|
94
|
+
*/
|
|
95
|
+
onEmptyCtrlC?: () => void;
|
|
87
96
|
constructor(tui: TUI, theme: EditorTheme, keybindings: ConstructorParameters<typeof CustomEditor>[2], readImage?: ClipboardImageReader, options?: EditorOptions);
|
|
88
97
|
/**
|
|
89
98
|
* Paste-image must fire on Cmd+V too (Claude Code parity on macOS). Claude
|
|
@@ -29,6 +29,7 @@ import { tmpdir } from "node:os";
|
|
|
29
29
|
import { join, basename, isAbsolute } from "node:path";
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
31
|
import { logImagePaste } from "./diagnostics.js";
|
|
32
|
+
import { cancelActiveRuns } from "./pipeline/runRegistry.js";
|
|
32
33
|
/** Matches the `[Image #N]` chip token in the editor text. */
|
|
33
34
|
const CHIP_RE = /\[Image #(\d+)\]/g;
|
|
34
35
|
/** Inverse-video wrap so the chip token reads as a chip, not plain text. */
|
|
@@ -285,6 +286,15 @@ export class ChipEditor extends CustomEditor {
|
|
|
285
286
|
stashed = [];
|
|
286
287
|
chipCounter = 0;
|
|
287
288
|
readImage;
|
|
289
|
+
/**
|
|
290
|
+
* Fired when Ctrl+C lands on an EMPTY prompt. pi's own binding is
|
|
291
|
+
* clear-editor (Esc is the interrupt key), but with nothing typed that is a
|
|
292
|
+
* no-op, and every peer harness (Claude Code, Codex, Gemini, Aider) treats a
|
|
293
|
+
* single Ctrl+C as "stop what's happening". The hook lets the extension wire
|
|
294
|
+
* exactly that; the key still falls through to pi afterwards, so clear and
|
|
295
|
+
* the 500ms double-press exit window are untouched.
|
|
296
|
+
*/
|
|
297
|
+
onEmptyCtrlC;
|
|
288
298
|
constructor(tui, theme, keybindings, readImage = defaultClipboardImageReader, options) {
|
|
289
299
|
super(tui, theme, keybindings, options);
|
|
290
300
|
this.readImage = readImage;
|
|
@@ -333,6 +343,14 @@ export class ChipEditor extends CustomEditor {
|
|
|
333
343
|
this.onPasteImage?.();
|
|
334
344
|
return;
|
|
335
345
|
}
|
|
346
|
+
// Ctrl+C on an empty prompt: fire the stop hook, then STILL hand the key to
|
|
347
|
+
// pi so its clear + double-press-exit tracking behave exactly as before.
|
|
348
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
349
|
+
if (this.getText().length === 0)
|
|
350
|
+
this.onEmptyCtrlC?.();
|
|
351
|
+
super.handleInput(data);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
336
354
|
super.handleInput(data);
|
|
337
355
|
}
|
|
338
356
|
async pasteChip() {
|
|
@@ -428,6 +446,20 @@ export function registerChipEditor(pi) {
|
|
|
428
446
|
return;
|
|
429
447
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
430
448
|
const editor = new ChipEditor(tui, theme, keybindings);
|
|
449
|
+
// Single Ctrl+C on an empty prompt = "stop what's happening" (peer-harness
|
|
450
|
+
// parity; pi's clear binding is a no-op with nothing typed): abort a
|
|
451
|
+
// streaming turn and cancel every in-flight /go run. pi still sees the
|
|
452
|
+
// key afterwards, so Ctrl+C twice within 500ms exits as always.
|
|
453
|
+
editor.onEmptyCtrlC = () => {
|
|
454
|
+
if (!ctx.isIdle())
|
|
455
|
+
ctx.abort();
|
|
456
|
+
const cancelled = cancelActiveRuns();
|
|
457
|
+
if (cancelled.length > 0) {
|
|
458
|
+
ctx.ui.notify(`Stopping ${cancelled.length} /go run${cancelled.length === 1 ? "" : "s"}: ${cancelled
|
|
459
|
+
.map((r) => `[${r.runId.slice(0, 8)}] ${r.ticket}`)
|
|
460
|
+
.join(", ")}. Work so far lands as WIP commits (see /go-status).`, "warning");
|
|
461
|
+
}
|
|
462
|
+
};
|
|
431
463
|
liveChipEditors.add(new WeakRef(editor));
|
|
432
464
|
return editor;
|
|
433
465
|
});
|
|
@@ -122,7 +122,9 @@ export type { RecordDecisionParams } from "./recordDecisionTool.js";
|
|
|
122
122
|
export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
123
123
|
export type { RunInitPassDeps, InitPassOutcome, RunTeamSetupDeps, TeamSetupOutcome, RepoIntake, RepoIntakeFs, EngineeringDraft, DraftTeam, } from "./initPass.js";
|
|
124
124
|
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
125
|
-
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
|
|
125
|
+
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
|
|
126
|
+
export { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
127
|
+
export type { UltraHolder } from "./ultra.js";
|
|
126
128
|
export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
|
|
127
129
|
export type { CatalogResult, FetchCatalogOptions, ModelEntry } from "./config.js";
|
|
128
130
|
export { buildYagniProvider } from "./provider.js";
|
|
@@ -139,7 +141,7 @@ export type { VerifyCommand, VerifyOutcome } from "./pipeline/verify.js";
|
|
|
139
141
|
export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
|
|
140
142
|
export type { ComparisonReport, LaneFit, LaneOutcome } from "./pipeline/eval.js";
|
|
141
143
|
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
142
|
-
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
144
|
+
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
143
145
|
export type { SubagentDef, SubagentSource } from "./subagents.js";
|
|
144
146
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
145
147
|
export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { registerCmuxBridge } from "./cmux/index.js";
|
|
|
14
14
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
15
15
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
16
16
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
17
|
-
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER } from "./branding.js";
|
|
17
|
+
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA } from "./branding.js";
|
|
18
18
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
19
19
|
import { registerCostCommand } from "./costHud.js";
|
|
20
20
|
import { isDebug } from "./diagnostics.js";
|
|
@@ -29,6 +29,7 @@ import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
|
29
29
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
30
30
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
|
|
31
31
|
import { registerSubagents } from "./subagents.js";
|
|
32
|
+
import { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
32
33
|
import { registerTodos } from "./todos.js";
|
|
33
34
|
import { registerDecisionCommands } from "./decisions.js";
|
|
34
35
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
@@ -185,7 +186,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
185
186
|
// The general subagent tool: delegate self-contained tasks (optionally in
|
|
186
187
|
// parallel) to fresh-context agents defined in .claude/agents / .pi/agents,
|
|
187
188
|
// riding the /go pipeline's child runner. /agents lists what's available.
|
|
188
|
-
|
|
189
|
+
// Ultra mode (/ultra) is an explicit orchestration dial: it swaps the
|
|
190
|
+
// driver's delegation identity for the diamond directive (see the
|
|
191
|
+
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
192
|
+
const ultraHolder = createUltraHolder();
|
|
193
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get() });
|
|
194
|
+
registerUltraCommand(pi, ultraHolder);
|
|
189
195
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
190
196
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
191
197
|
// 5s, not the 10s other boot/grounding fetches use, because the user is
|
|
@@ -517,13 +523,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
517
523
|
// assembled prompt passes through byte-exact (no rewrite, no brief
|
|
518
524
|
// injection, no rules section, no delegation identity).
|
|
519
525
|
const noBranding = brandingDisabled(env);
|
|
520
|
-
|
|
526
|
+
// Resolved per turn (not once at activation): /ultra can flip the driver
|
|
527
|
+
// between the delegation and diamond identities mid-session.
|
|
528
|
+
const isDriver = isDriverCaller(env);
|
|
521
529
|
pi.on("before_agent_start", (event) => noBranding
|
|
522
530
|
? undefined
|
|
523
531
|
: {
|
|
524
532
|
systemPrompt: brandSystemPrompt(event.systemPrompt, {
|
|
525
533
|
contextBrief,
|
|
526
|
-
identity
|
|
534
|
+
identity: isDriver
|
|
535
|
+
? ultraHolder.get()
|
|
536
|
+
? YAGNI_IDENTITY_ULTRA
|
|
537
|
+
: YAGNI_IDENTITY_DRIVER
|
|
538
|
+
: undefined,
|
|
527
539
|
rulesSection,
|
|
528
540
|
}),
|
|
529
541
|
});
|
|
@@ -781,7 +793,9 @@ export { recordDecision } from "./recordDecisionTool.js";
|
|
|
781
793
|
export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
|
|
782
794
|
// Onramp Door B (F2a): the one-time init-pass idempotency marker.
|
|
783
795
|
export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
|
|
784
|
-
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, BRAND_NAME } from "./branding.js";
|
|
796
|
+
export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
|
|
797
|
+
// Ultra mode (/ultra): the aggressive fan-out/verify/synthesize dial.
|
|
798
|
+
export { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
785
799
|
export { attributionHeaders, isDriverCaller, fetchCatalog, getToken, getWorkspaceId, resolveBaseUrl, sanitizeCallerSegment, } from "./config.js";
|
|
786
800
|
export { buildYagniProvider } from "./provider.js";
|
|
787
801
|
export { registerGoCommand } from "./pipeline/goCommand.js";
|
|
@@ -796,7 +810,7 @@ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures
|
|
|
796
810
|
export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
|
|
797
811
|
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
798
812
|
// The general subagent tool: Claude Code-format agent discovery + fan-out.
|
|
799
|
-
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
813
|
+
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
800
814
|
// The session todo checklist: todo_write tool, widget renderer, /todos.
|
|
801
815
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, todoSummary, TODO_TOOL_NAME, MAX_TODOS, } from "./todos.js";
|
|
802
816
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live child-process registry + exit sweep (the "no orphaned workers" invariant).
|
|
3
|
+
*
|
|
4
|
+
* /go stage children and verify children are spawned with raw `node:child_process`
|
|
5
|
+
* and piped stdio, so nothing in pi tracks them: pi's own shutdown paths
|
|
6
|
+
* (`/exit`, double Ctrl+C, SIGTERM/SIGHUP) call `process.exit(0)` and would
|
|
7
|
+
* orphan them into the background, still editing the repo and burning tokens
|
|
8
|
+
* until their wall-clock timeout fires.
|
|
9
|
+
*
|
|
10
|
+
* Every long-lived child is registered here instead. A single `process.on("exit")`
|
|
11
|
+
* hook (installed lazily on first track) kills whatever is still alive —
|
|
12
|
+
* `"exit"` handlers must be synchronous, and the whole sweep (including the
|
|
13
|
+
* `ps`-based descendant walk) is, so this holds on every exit path short of the
|
|
14
|
+
* pi process itself being SIGKILLed.
|
|
15
|
+
*
|
|
16
|
+
* Kills are TREE kills: a verify child like `pnpm test` fans out its own
|
|
17
|
+
* grandchildren, and killing only the direct pid would orphan those instead.
|
|
18
|
+
*/
|
|
19
|
+
import { type ChildProcess } from "node:child_process";
|
|
20
|
+
/**
|
|
21
|
+
* PURE: given `ps -A -o pid=,ppid=` output lines, collect every descendant of
|
|
22
|
+
* the given roots (children, grandchildren, ...), breadth-first.
|
|
23
|
+
*/
|
|
24
|
+
export declare function descendantsOf(roots: number[], psLines: string[]): number[];
|
|
25
|
+
/**
|
|
26
|
+
* Synchronously SIGKILL a process AND its descendants. POSIX walks a `ps`
|
|
27
|
+
* snapshot (the tree-kill pattern); Windows has real tree kill via
|
|
28
|
+
* `taskkill /T /F`. Throw-proof and synchronous, so it is exit-hook safe.
|
|
29
|
+
* Fail-soft: with no readable snapshot the direct pid is still killed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function killTreeSync(pid: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Register a spawned child so the exit sweep covers it. Deregisters itself on
|
|
34
|
+
* the child's real `close`/`error`, so the set only ever holds live processes.
|
|
35
|
+
*/
|
|
36
|
+
export declare function trackChild(proc: ChildProcess): void;
|
|
37
|
+
/** Test seam: how many children are currently registered. */
|
|
38
|
+
export declare function _liveChildCountForTest(): number;
|
|
39
|
+
/** Test seam: run the sweep as the exit hook would. */
|
|
40
|
+
export declare function _sweepForTest(): void;
|
|
41
|
+
//# sourceMappingURL=childRegistry.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live child-process registry + exit sweep (the "no orphaned workers" invariant).
|
|
3
|
+
*
|
|
4
|
+
* /go stage children and verify children are spawned with raw `node:child_process`
|
|
5
|
+
* and piped stdio, so nothing in pi tracks them: pi's own shutdown paths
|
|
6
|
+
* (`/exit`, double Ctrl+C, SIGTERM/SIGHUP) call `process.exit(0)` and would
|
|
7
|
+
* orphan them into the background, still editing the repo and burning tokens
|
|
8
|
+
* until their wall-clock timeout fires.
|
|
9
|
+
*
|
|
10
|
+
* Every long-lived child is registered here instead. A single `process.on("exit")`
|
|
11
|
+
* hook (installed lazily on first track) kills whatever is still alive —
|
|
12
|
+
* `"exit"` handlers must be synchronous, and the whole sweep (including the
|
|
13
|
+
* `ps`-based descendant walk) is, so this holds on every exit path short of the
|
|
14
|
+
* pi process itself being SIGKILLed.
|
|
15
|
+
*
|
|
16
|
+
* Kills are TREE kills: a verify child like `pnpm test` fans out its own
|
|
17
|
+
* grandchildren, and killing only the direct pid would orphan those instead.
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
const live = new Set();
|
|
21
|
+
let sweepInstalled = false;
|
|
22
|
+
/**
|
|
23
|
+
* PURE: given `ps -A -o pid=,ppid=` output lines, collect every descendant of
|
|
24
|
+
* the given roots (children, grandchildren, ...), breadth-first.
|
|
25
|
+
*/
|
|
26
|
+
export function descendantsOf(roots, psLines) {
|
|
27
|
+
const childrenByParent = new Map();
|
|
28
|
+
for (const line of psLines) {
|
|
29
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
30
|
+
if (!m)
|
|
31
|
+
continue;
|
|
32
|
+
const pid = Number(m[1]);
|
|
33
|
+
const ppid = Number(m[2]);
|
|
34
|
+
const list = childrenByParent.get(ppid) ?? [];
|
|
35
|
+
list.push(pid);
|
|
36
|
+
childrenByParent.set(ppid, list);
|
|
37
|
+
}
|
|
38
|
+
const found = [];
|
|
39
|
+
const queue = [...roots];
|
|
40
|
+
while (queue.length > 0) {
|
|
41
|
+
const next = queue.shift();
|
|
42
|
+
for (const child of childrenByParent.get(next) ?? []) {
|
|
43
|
+
found.push(child);
|
|
44
|
+
queue.push(child);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Synchronously SIGKILL a process AND its descendants. POSIX walks a `ps`
|
|
51
|
+
* snapshot (the tree-kill pattern); Windows has real tree kill via
|
|
52
|
+
* `taskkill /T /F`. Throw-proof and synchronous, so it is exit-hook safe.
|
|
53
|
+
* Fail-soft: with no readable snapshot the direct pid is still killed.
|
|
54
|
+
*/
|
|
55
|
+
export function killTreeSync(pid) {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
try {
|
|
58
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* best effort */
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let descendants = [];
|
|
66
|
+
try {
|
|
67
|
+
const ps = spawnSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" });
|
|
68
|
+
if (ps.status === 0 && typeof ps.stdout === "string") {
|
|
69
|
+
descendants = descendantsOf([pid], ps.stdout.split("\n"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* no snapshot: fall through to the direct kill */
|
|
74
|
+
}
|
|
75
|
+
// Root first so it cannot spawn replacements for the descendants we listed.
|
|
76
|
+
for (const target of [pid, ...descendants]) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(target, "SIGKILL");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* already dead */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Kill every still-registered child's tree. Synchronous and throw-proof (exit-hook safe). */
|
|
86
|
+
function sweep() {
|
|
87
|
+
for (const proc of live) {
|
|
88
|
+
// pid guard: a child whose spawn FAILED has no pid; kill() would resolve
|
|
89
|
+
// that to pid 0 (the whole process group) and take the parent down too.
|
|
90
|
+
if (!proc.pid)
|
|
91
|
+
continue;
|
|
92
|
+
killTreeSync(proc.pid);
|
|
93
|
+
}
|
|
94
|
+
live.clear();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register a spawned child so the exit sweep covers it. Deregisters itself on
|
|
98
|
+
* the child's real `close`/`error`, so the set only ever holds live processes.
|
|
99
|
+
*/
|
|
100
|
+
export function trackChild(proc) {
|
|
101
|
+
if (!sweepInstalled) {
|
|
102
|
+
sweepInstalled = true;
|
|
103
|
+
process.on("exit", sweep);
|
|
104
|
+
}
|
|
105
|
+
live.add(proc);
|
|
106
|
+
const drop = () => live.delete(proc);
|
|
107
|
+
proc.once("close", drop);
|
|
108
|
+
proc.once("error", drop);
|
|
109
|
+
}
|
|
110
|
+
/** Test seam: how many children are currently registered. */
|
|
111
|
+
export function _liveChildCountForTest() {
|
|
112
|
+
return live.size;
|
|
113
|
+
}
|
|
114
|
+
/** Test seam: run the sweep as the exit hook would. */
|
|
115
|
+
export function _sweepForTest() {
|
|
116
|
+
sweep();
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=childRegistry.js.map
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* does not apply.
|
|
29
29
|
*/
|
|
30
30
|
import { execFile } from "node:child_process";
|
|
31
|
+
import { trackChild } from "./childRegistry.js";
|
|
31
32
|
import { scrubSecrets } from "./scrubSecrets.js";
|
|
32
33
|
import { parseChangedPaths } from "./verify.js";
|
|
33
34
|
/** Cap on the commit subject's title half (keeps `git log --oneline` readable). */
|
|
@@ -42,7 +43,7 @@ const NOTE_TAIL_MAX = 200;
|
|
|
42
43
|
* a spawn-level error (ENOENT / abort), which callers catch into a note.
|
|
43
44
|
*/
|
|
44
45
|
const defaultExec = (argv, cwd, signal) => new Promise((resolve, reject) => {
|
|
45
|
-
execFile(argv[0], argv.slice(1), { cwd, signal, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
46
|
+
const child = execFile(argv[0], argv.slice(1), { cwd, signal, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
46
47
|
const output = `${stdout ?? ""}${stderr ?? ""}`;
|
|
47
48
|
if (err) {
|
|
48
49
|
const code = err.code;
|
|
@@ -52,6 +53,9 @@ const defaultExec = (argv, cwd, signal) => new Promise((resolve, reject) => {
|
|
|
52
53
|
}
|
|
53
54
|
resolve({ code: 0, output });
|
|
54
55
|
});
|
|
56
|
+
// A push or PR create can run for a while; register it with the exit sweep
|
|
57
|
+
// so quitting pi mid-FINISH never leaves it running in the background.
|
|
58
|
+
trackChild(child);
|
|
55
59
|
});
|
|
56
60
|
// ---------------------------------------------------------------------------
|
|
57
61
|
// Pure message derivation
|
|
@@ -88,7 +88,7 @@ export interface RegisterGoDeps {
|
|
|
88
88
|
* without a network; a non-identifier arg short-circuits to null (raw arg passes
|
|
89
89
|
* through). Returns null on any miss / transport error (the honest blind fallback).
|
|
90
90
|
*/
|
|
91
|
-
resolveTicketBrief?: (rawArg: string) => Promise<string | null>;
|
|
91
|
+
resolveTicketBrief?: (rawArg: string, signal?: AbortSignal) => Promise<string | null>;
|
|
92
92
|
/** Injectable worktree creation (default: real `git worktree add -b … HEAD`). */
|
|
93
93
|
createWorktree?: typeof defaultCreateRunWorktree;
|
|
94
94
|
/** Injectable worktree bootstrap (default: best-effort `<pm> install --prefer-offline`). */
|
|
@@ -75,8 +75,9 @@ import { runFinish as defaultRunFinish, verifyTrailerValue, } from "./finish.js"
|
|
|
75
75
|
import { GO_USAGE, parseGoArgs } from "./goFlags.js";
|
|
76
76
|
import { registerGoStatusCommands } from "./goStatusCommands.js";
|
|
77
77
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
78
|
+
import { composeAbortSignal } from "./resilience.js";
|
|
78
79
|
import { planResume } from "./resume.js";
|
|
79
|
-
import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, MAX_CONCURRENT_RUNS, settleRun, trackRunPromise, worktreesDir, } from "./runRegistry.js";
|
|
80
|
+
import { activeRunCount, beginRun, classifyRunLiveness, findActiveRunByTicket, isRunInFlight, isTerminalStatus, lastJournalTs, loadRegistryRows, MAX_CONCURRENT_RUNS, settleRun, trackRunAbort, trackRunPromise, worktreesDir, } from "./runRegistry.js";
|
|
80
81
|
import { makeRunSession as defaultMakeRunSession } from "./runSession.js";
|
|
81
82
|
import { recordSessionRun } from "../sessionRuns.js";
|
|
82
83
|
import { resolveTicketBrief as defaultResolveTicketBrief } from "./ticketResolution.js";
|
|
@@ -426,7 +427,11 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
426
427
|
// back to the local client estimate).
|
|
427
428
|
const fetchRunSpend = deps.fetchRunSpend;
|
|
428
429
|
const resolveTicketBrief = deps.resolveTicketBrief ??
|
|
429
|
-
((rawArg) => defaultResolveTicketBrief({
|
|
430
|
+
((rawArg, signal) => defaultResolveTicketBrief({
|
|
431
|
+
baseUrl: deps.baseUrl ?? resolveBaseUrl(),
|
|
432
|
+
getToken: deps.getToken ?? defaultGetToken,
|
|
433
|
+
...(signal ? { signal } : {}),
|
|
434
|
+
}, rawArg));
|
|
430
435
|
const reportCrash = deps.reportCrash ??
|
|
431
436
|
makeCrashReporter({
|
|
432
437
|
baseUrl: deps.baseUrl ?? resolveBaseUrl(),
|
|
@@ -830,6 +835,13 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
830
835
|
return undefined;
|
|
831
836
|
}
|
|
832
837
|
};
|
|
838
|
+
// The run's OWN abort controller. `ctx.signal` is undefined here (/go only
|
|
839
|
+
// starts while the agent is idle, and pi's signal is the active turn's), so
|
|
840
|
+
// without this the pipeline runs unstoppable until its wall-clock timeout.
|
|
841
|
+
// /stop fires it via the registry (trackRunAbort below); composed with
|
|
842
|
+
// ctx.signal for the resumed-command case where one does exist.
|
|
843
|
+
const runAbort = new AbortController();
|
|
844
|
+
const runSignal = composeAbortSignal(ctx.signal, runAbort.signal);
|
|
833
845
|
const runToCompletion = async () => {
|
|
834
846
|
try {
|
|
835
847
|
// Steady animation ticker: keeps the spinner + elapsed clock live between
|
|
@@ -843,7 +855,9 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
843
855
|
// the agents plan on the actual ticket, not the bare "YAG-234". Fail-soft:
|
|
844
856
|
// null keeps the raw arg (the honest blind fallback). The raw `ticket` stays
|
|
845
857
|
// the session key, the run row's arg, and the handoff/re-run copy.
|
|
846
|
-
|
|
858
|
+
// Signalled so /stop lands immediately instead of waiting out the
|
|
859
|
+
// resolver's retry/timeout budget (up to ~90s of network patience).
|
|
860
|
+
const ticketBrief = await resolveTicketBrief(ticket, runSignal);
|
|
847
861
|
// The plan stage's output, captured off the onStage boundary for the
|
|
848
862
|
// FINISH commit message's 2-3 sentence summary (absent on a resume).
|
|
849
863
|
let planText;
|
|
@@ -861,9 +875,9 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
861
875
|
catch {
|
|
862
876
|
/* fail-soft: leave baseline empty */
|
|
863
877
|
}
|
|
864
|
-
|
|
878
|
+
let result = await runPipeline(ticket, {
|
|
865
879
|
cwd: runCwd,
|
|
866
|
-
signal:
|
|
880
|
+
signal: runSignal,
|
|
867
881
|
...(ticketBrief ? { ticketBrief } : {}),
|
|
868
882
|
onProgress: (p) => {
|
|
869
883
|
feed?.applyProgress(p);
|
|
@@ -909,6 +923,12 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
909
923
|
checkpointMeta,
|
|
910
924
|
...(resumeFrom ? { resumeFrom } : {}),
|
|
911
925
|
});
|
|
926
|
+
// /stop can land in the race between the pipeline's last abort check
|
|
927
|
+
// and its clean return: honor it BEFORE FINISH, so a stopped run never
|
|
928
|
+
// starts committing and the promised WIP-preserve path runs instead.
|
|
929
|
+
if (result.stopReason === "clean" && runSignal.aborted) {
|
|
930
|
+
result = { ...result, stopReason: "aborted" };
|
|
931
|
+
}
|
|
912
932
|
// FINISH stage (spec §3c): ONLY on a clean stop. Commit the run's work
|
|
913
933
|
// (worktree always; --here only over a clean pre-run baseline) with the
|
|
914
934
|
// provenance trailer, and push + PR when --pr asked for it. Iron
|
|
@@ -939,7 +959,7 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
939
959
|
openPr: parsed.flags.pr,
|
|
940
960
|
remainingFindings: result.findings.filter((f) => f.severity === "medium" || f.severity === "low"),
|
|
941
961
|
...(finishBaselinePaths.length > 0 ? { baselinePaths: finishBaselinePaths } : {}),
|
|
942
|
-
|
|
962
|
+
signal: runSignal,
|
|
943
963
|
});
|
|
944
964
|
}
|
|
945
965
|
catch (err) {
|
|
@@ -972,6 +992,14 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
972
992
|
}
|
|
973
993
|
}
|
|
974
994
|
clearUI();
|
|
995
|
+
// /stop DURING FINISH: the abort degrades FINISH mid-way (its execs
|
|
996
|
+
// share runSignal). If no commit landed, the aborted status + WIP
|
|
997
|
+
// preservation the user was promised win over "clean"; a commit that
|
|
998
|
+
// DID land means the work is durable and the clean report stays honest.
|
|
999
|
+
if (result.stopReason === "clean" && runSignal.aborted && !finish?.commitSha) {
|
|
1000
|
+
result = { ...result, stopReason: "aborted" };
|
|
1001
|
+
finish = undefined;
|
|
1002
|
+
}
|
|
975
1003
|
// Fold what FINISH landed onto a NEW result (never mutate the pipeline's
|
|
976
1004
|
// return). `finish` is present ONLY when a commit really happened (§0.1).
|
|
977
1005
|
const finishBranch = finish?.branch ?? repoCtx.branch;
|
|
@@ -1049,6 +1077,7 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
1049
1077
|
}
|
|
1050
1078
|
};
|
|
1051
1079
|
beginRun(registryRow);
|
|
1080
|
+
trackRunAbort(runId, runAbort);
|
|
1052
1081
|
if (parsed.flags.fg) {
|
|
1053
1082
|
// --fg: the legacy blocking behavior — the prompt returns when the run does.
|
|
1054
1083
|
await runToCompletion();
|
|
@@ -44,7 +44,17 @@ export interface RegisterGoStatusDeps {
|
|
|
44
44
|
resolveRepo?: (cwd: string, signal?: AbortSignal) => Promise<string | undefined>;
|
|
45
45
|
exists?: (path: string) => boolean;
|
|
46
46
|
now?: () => number;
|
|
47
|
+
/** Injectable cancel seams for /stop (default: the module-scoped registry). */
|
|
48
|
+
activeRows?: () => RunRegistryRow[];
|
|
49
|
+
cancelOne?: (runId: string) => RunRegistryRow | undefined;
|
|
50
|
+
cancelAll?: () => RunRegistryRow[];
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* PURE: resolve a /stop argument against the in-flight rows. Matches the ticket
|
|
54
|
+
* (case-insensitive exact) first, then a runId prefix of at least 4 chars (the
|
|
55
|
+
* short id /go-status prints is 8).
|
|
56
|
+
*/
|
|
57
|
+
export declare function matchStopTarget(rows: RunRegistryRow[], arg: string): RunRegistryRow | undefined;
|
|
48
58
|
/**
|
|
49
59
|
* PURE: where an in-flight/interrupted run got to, read off its checkpoint
|
|
50
60
|
* journal. The journal is append-only, so the latest applicable boundary wins.
|