pi-crew 0.11.24 → 0.11.26
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/CHANGELOG.md +72 -0
- package/README.md +2 -2
- package/agents/designer.md +23 -0
- package/agents/orchestrator.md +17 -0
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +568 -559
- package/{workflows/chain.workflow.md → docs/chain-workflow.md} +8 -0
- package/docs/dynamic-workflows.md +23 -0
- package/docs/publishing.md +34 -5
- package/docs/troubleshooting.md +50 -2
- package/docs/usage.md +7 -2
- package/install.mjs +2 -1
- package/package.json +4 -2
- package/skills/read-only-explorer/SKILL.md +15 -5
- package/skills/real-test-pi-crew/SKILL.md +7 -7
- package/src/config/config-validation.ts +58 -0
- package/src/config/config.ts +90 -5
- package/src/config/env-vars.ts +14 -7
- package/src/config/role-tools.ts +7 -5
- package/src/config/types.ts +4 -1
- package/src/extension/crew-cleanup.ts +6 -0
- package/src/extension/project-init.ts +19 -1
- package/src/extension/register.ts +68 -30
- package/src/extension/registration/artifact-cleanup.ts +52 -11
- package/src/extension/registration/commands/manage.ts +9 -2
- package/src/extension/registration/hook-registration.ts +3 -0
- package/src/extension/registration/lazy-configurers.ts +2 -0
- package/src/extension/registration/lifecycle-handlers.ts +79 -18
- package/src/extension/registration/render-loop.ts +27 -2
- package/src/extension/registration/tool-loop-guard.ts +5 -3
- package/src/extension/registration/verify-gate-tool.ts +7 -3
- package/src/extension/team-tool/api/read.ts +12 -3
- package/src/extension/team-tool/cancel.ts +11 -0
- package/src/extension/team-tool/config-patch.ts +56 -11
- package/src/extension/team-tool/destructive-gate.ts +72 -12
- package/src/extension/team-tool/dispatch/manage.ts +9 -1
- package/src/extension/team-tool/handle-schedule.ts +38 -4
- package/src/extension/team-tool/handle-settings.ts +76 -6
- package/src/extension/team-tool/health-monitor.ts +14 -2
- package/src/extension/team-tool/lifecycle-actions.ts +28 -10
- package/src/extension/team-tool/respond.ts +12 -5
- package/src/extension/team-tool/run-deadline.ts +29 -2
- package/src/extension/team-tool/run.ts +11 -0
- package/src/extension/team-tool/status.ts +64 -8
- package/src/extension/team-tool/workflow-manage.ts +5 -1
- package/src/extension/team-tool.ts +3 -0
- package/src/i18n.ts +18 -14
- package/src/prompt/inbox-poll.ts +14 -2
- package/src/prompt/message-tool.ts +1 -1
- package/src/prompt/prompt-runtime.ts +25 -3
- package/src/prompt/scratchpad-lifecycle.ts +14 -13
- package/src/prompt/surface-worker.ts +35 -9
- package/src/runtime/broker/crew-broker.ts +113 -111
- package/src/runtime/broker/delegate/delegate-abort.ts +118 -0
- package/src/runtime/broker/delegate/delegate-event.ts +3 -1
- package/src/runtime/broker/delegate/delegate-finalize.ts +179 -0
- package/src/runtime/broker/protocol/wait-resolve.ts +71 -5
- package/src/runtime/budget-enforcement.ts +26 -6
- package/src/runtime/chain-parser.ts +5 -3
- package/src/runtime/chain-runner.ts +41 -22
- package/src/runtime/child-pi/mock-fixtures.ts +12 -5
- package/src/runtime/compaction/tool-output-pruner.ts +8 -3
- package/src/runtime/delegate-spawn.ts +61 -8
- package/src/runtime/dispatch-batch.ts +10 -4
- package/src/runtime/effectiveness.ts +31 -3
- package/src/runtime/goal-workflow/plan-templates.ts +2 -2
- package/src/runtime/live-session/live-agent-control.ts +89 -6
- package/src/runtime/live-session/live-agent-manager.ts +94 -10
- package/src/runtime/live-session/live-session-runtime.ts +9 -2
- package/src/runtime/model/model-fallback.ts +6 -9
- package/src/runtime/model/pi-args.ts +3 -3
- package/src/runtime/model/runtime-policy.ts +4 -2
- package/src/runtime/model/runtime-resolver.ts +6 -2
- package/src/runtime/process/orphan-reaper.ts +143 -0
- package/src/runtime/process-status.ts +38 -16
- package/src/runtime/resilient-edit.ts +170 -93
- package/src/runtime/role-permission.ts +3 -1
- package/src/runtime/run-tracker.ts +9 -1
- package/src/runtime/sanitize/worker-text.ts +98 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +82 -28
- package/src/runtime/spawn-policy.ts +190 -8
- package/src/runtime/stale-reconciler.ts +36 -1
- package/src/runtime/subagent-manager.ts +29 -0
- package/src/runtime/surface/herdr-provider.ts +31 -1
- package/src/runtime/task-output-context.ts +58 -11
- package/src/runtime/task-runner/post-execution.ts +150 -0
- package/src/runtime/task-runner/prompt-builder.ts +41 -8
- package/src/runtime/task-runner.ts +16 -3
- package/src/runtime/verification/classifier-pre-gate.ts +27 -6
- package/src/runtime/verification/verify-gate.ts +153 -30
- package/src/state/atomic-write.ts +73 -4
- package/src/state/contracts.ts +7 -0
- package/src/state/coordination/mailbox.ts +40 -11
- package/src/state/event-log/event-log-rotation.ts +28 -6
- package/src/state/event-log/poison-marker.ts +16 -1
- package/src/state/stores/artifact-store.ts +181 -6
- package/src/state/stores/manifest-io.ts +126 -8
- package/src/state/stores/state-store.ts +30 -31
- package/src/state/types.ts +13 -0
- package/src/state/usage.ts +41 -0
- package/src/ui/run-dashboard.ts +3 -3
- package/src/ui/settings-overlay.ts +19 -1
- package/src/utils/internal-error.ts +5 -1
- package/src/utils/timer-limits.ts +30 -0
- package/src/utils/token-counter.ts +47 -0
- package/src/workflows/discover-workflows.ts +9 -1
- package/src/worktree/worktree-manager.ts +128 -18
- package/workflows/default.workflow.md +1 -1
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
package/src/config/types.ts
CHANGED
|
@@ -386,7 +386,10 @@ export interface CrewReliabilityConfig {
|
|
|
386
386
|
retryPolicy?: CrewRetryPolicyConfig;
|
|
387
387
|
autoRecover?: boolean;
|
|
388
388
|
deadletterThreshold?: number;
|
|
389
|
-
/** Interval (ms) for periodic stale-run auto-repair. Default
|
|
389
|
+
/** Interval (ms) for periodic stale-run auto-repair. Default 300_000 (5min) —
|
|
390
|
+
* the canonical value lives in the `?? 300_000` fallback at
|
|
391
|
+
* `src/extension/registration/observability.ts:256` (RT-F2 raised it from 60s;
|
|
392
|
+
* R45-F2). Set to 0 to disable. */
|
|
390
393
|
autoRepairIntervalMs?: number;
|
|
391
394
|
/** Remove /tmp/pi-crew-* directories after their orphaned runs are reconciled. Default: true. */
|
|
392
395
|
cleanupOrphanedTempDirs?: boolean;
|
|
@@ -79,6 +79,12 @@ export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTermina
|
|
|
79
79
|
// suppression; on REAL shutdowns (quit/reload) the flag is unset
|
|
80
80
|
// and this cleanup still drains the registry, as does the
|
|
81
81
|
// process-exit hook (tryRegisterSessionCleanup) when pi dies.
|
|
82
|
+
// R49-F6: this gate is a SWITCH FLAG (`isSessionSwitchInFlight()`), not the
|
|
83
|
+
// reason-aware gate used by lifecycle-handlers.ts:165-166. On a `reload` shutdown
|
|
84
|
+
// the flag can still read true (it is cleared on the next session_start), so
|
|
85
|
+
// this handler may skip a cleanup the reason-gate would have run — the
|
|
86
|
+
// process-exit hook (tryRegisterSessionCleanup) is the backstop. Divergence
|
|
87
|
+
// is deliberate (never kill workers on a navigational switch) but not obvious.
|
|
82
88
|
if (isSessionSwitchInFlight()) return;
|
|
83
89
|
// Kill all child-pi processes
|
|
84
90
|
await cleanupChildProcesses();
|
|
@@ -21,6 +21,13 @@ export interface ProjectInitResult {
|
|
|
21
21
|
configScope: "global" | "project" | "none";
|
|
22
22
|
configCreated: boolean;
|
|
23
23
|
configSkipped: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* F13-A: set when an EXPLICIT `overwrite: true` replaced an existing config
|
|
26
|
+
* file. The previous content is preserved at this path — an overwrite of the
|
|
27
|
+
* global (`~/.pi/agent/pi-crew.json`) or project config is destructive and may
|
|
28
|
+
* discard user-customized settings, so it must never be silent.
|
|
29
|
+
*/
|
|
30
|
+
configBackupPath?: string;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
function ensureDir(dir: string, createdDirs: string[]): void {
|
|
@@ -112,10 +119,20 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
|
|
|
112
119
|
|
|
113
120
|
let configCreated = false;
|
|
114
121
|
let configSkipped = false;
|
|
122
|
+
let configBackupPath: string | undefined;
|
|
115
123
|
if (configPath) {
|
|
116
124
|
if (configScope === "project") ensureDir(path.dirname(configPath), createdDirs);
|
|
117
125
|
else fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
118
|
-
|
|
126
|
+
const configExists = fs.existsSync(configPath);
|
|
127
|
+
if (!configExists) {
|
|
128
|
+
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
|
|
129
|
+
configCreated = true;
|
|
130
|
+
} else if (options.overwrite === true) {
|
|
131
|
+
// F13-A: overwriting an existing config is destructive — it may hold the
|
|
132
|
+
// user's own settings (global scope writes ~/.pi/agent/pi-crew.json).
|
|
133
|
+
// Keep the previous content and report it instead of replacing silently.
|
|
134
|
+
configBackupPath = `${configPath}.bak-${Date.now()}`;
|
|
135
|
+
fs.copyFileSync(configPath, configBackupPath);
|
|
119
136
|
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
|
|
120
137
|
configCreated = true;
|
|
121
138
|
} else {
|
|
@@ -166,5 +183,6 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
|
|
|
166
183
|
configScope,
|
|
167
184
|
configCreated,
|
|
168
185
|
configSkipped,
|
|
186
|
+
...(configBackupPath ? { configBackupPath } : {}),
|
|
169
187
|
};
|
|
170
188
|
}
|
|
@@ -24,6 +24,7 @@ import { startRuntimeWarmup } from "../runtime/model/runtime-warmup.ts";
|
|
|
24
24
|
import { primePeerDep } from "../runtime/peer-dep.ts";
|
|
25
25
|
import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
|
|
26
26
|
import { installChildProcessAbortShield } from "../utils/child-process-shield.ts";
|
|
27
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
27
28
|
import { resetTimings, time } from "../utils/timings.ts";
|
|
28
29
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
29
30
|
import { registerContextStatusInjection } from "./context-status-injection.ts";
|
|
@@ -97,56 +98,93 @@ export async function registerPiTeams(pi: ExtensionAPI): Promise<void> {
|
|
|
97
98
|
const ctx = buildRegistrationContext(pi);
|
|
98
99
|
ctx.importCrashRecovery = importCrashRecovery;
|
|
99
100
|
ctx.purgeStaleActiveRunIndexSyncIfLoaded = purgeStaleActiveRunIndexSyncIfLoaded;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
|
|
102
|
+
// R49-F5 (wave-6 M4-R2) — registrar guard. The registrars below run in a
|
|
103
|
+
// flat sequence; before this guard ANY throw aborted the remaining
|
|
104
|
+
// registrars and left a PARTIAL extension with no diagnostic (Pi's loader
|
|
105
|
+
// discards the rejection). Guard each call: on throw, record the subsystem
|
|
106
|
+
// name, log it through the internal-error channel, and CONTINUE — extension
|
|
107
|
+
// load must never break the session, but a failed subsystem must be
|
|
108
|
+
// visible. ONE consolidated warning is surfaced before the skill check.
|
|
109
|
+
const registrarFailures: string[] = [];
|
|
110
|
+
const withRegistrar = <T>(name: string, fn: () => T): T | undefined => {
|
|
111
|
+
try {
|
|
112
|
+
return fn();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
115
|
+
registrarFailures.push(`${name} (${detail})`);
|
|
116
|
+
logInternalError(`register.${name}`, err, "extension registration subsystem");
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
withRegistrar("runtimeCleanup", () => installRuntimeCleanup(pi, ctx));
|
|
122
|
+
withRegistrar("foregroundRunController", () => installForegroundRunController(pi, ctx));
|
|
123
|
+
withRegistrar("subagentManager", () => installSubagentManager(pi, ctx));
|
|
124
|
+
withRegistrar("lazyConfigurers", () => installLazyConfigurers(pi, ctx));
|
|
125
|
+
withRegistrar("crossExtensionWiring", () => installCrossExtensionWiring(pi, ctx));
|
|
105
126
|
|
|
106
127
|
time("register.policy");
|
|
107
|
-
registerAutonomousPolicy(pi);
|
|
108
|
-
registerKnowledgeInjection(pi);
|
|
128
|
+
withRegistrar("autonomousPolicy", () => registerAutonomousPolicy(pi));
|
|
129
|
+
withRegistrar("knowledgeInjection", () => registerKnowledgeInjection(pi));
|
|
109
130
|
|
|
110
|
-
registerPiTools(pi, ctx);
|
|
111
|
-
registerPiCommands(pi, ctx);
|
|
112
|
-
installPiHooks(pi, ctx);
|
|
113
|
-
installSessionLifecycleHandlers(pi, ctx);
|
|
131
|
+
withRegistrar("piTools", () => registerPiTools(pi, ctx));
|
|
132
|
+
withRegistrar("piCommands", () => registerPiCommands(pi, ctx));
|
|
133
|
+
withRegistrar("piHooks", () => installPiHooks(pi, ctx));
|
|
134
|
+
withRegistrar("sessionLifecycleHandlers", () => installSessionLifecycleHandlers(pi, ctx));
|
|
114
135
|
// Phase 0 inter-pi broker: install the lifecycle controller immediately
|
|
115
136
|
// after the session handlers. The controller's gate (broker.enabled AND
|
|
116
137
|
// root-session only) decides whether anything is actually done; for
|
|
117
138
|
// subagents or when the flag is off, it returns a no-op controller.
|
|
118
|
-
ctx.brokerController = installCrewBrokerLifecycleController(pi, ctx);
|
|
139
|
+
ctx.brokerController = withRegistrar("crewBrokerLifecycle", () => installCrewBrokerLifecycleController(pi, ctx));
|
|
119
140
|
|
|
120
141
|
// M3-1 (UI-AUDIT P0-2/P1-1): wire the tab-title + Ghostty progress
|
|
121
142
|
// controller to runEventBus. Before this the controller was never
|
|
122
143
|
// constructed (dead since v0.8.3). Registers the dispose hook that the
|
|
123
144
|
// SIGTERM/SIGHUP handler calls (crew-cleanup.ts).
|
|
124
|
-
installTerminalStatus(pi, ctx);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
withRegistrar("terminalStatus", () => installTerminalStatus(pi, ctx));
|
|
146
|
+
withRegistrar("compactionGuard", () =>
|
|
147
|
+
registerCompactionGuard(pi, {
|
|
148
|
+
foregroundControllers: ctx.foregroundControllers,
|
|
149
|
+
foregroundTeamRunControllers: ctx.foregroundTeamRunControllers,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
129
152
|
|
|
130
153
|
if (process.env.CREW_RESILIENT_EDIT === "1") {
|
|
154
|
+
// R49-F5: do NOT swallow the outcome. `wrapEditWithResilientReplace`
|
|
155
|
+
// returns false when the hook was NOT registered (pi-diff present or the
|
|
156
|
+
// host lacks the tool_result seam) and the dynamic import can reject;
|
|
157
|
+
// both were previously discarded, so an operator who set
|
|
158
|
+
// CREW_RESILIENT_EDIT=1 got a silent no-op.
|
|
131
159
|
// LAZY: resilient-edit only loads when the opt-in env var is set.
|
|
132
160
|
import("../runtime/resilient-edit.ts")
|
|
133
|
-
.then(({ wrapEditWithResilientReplace }) =>
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
.then(({ wrapEditWithResilientReplace }) => {
|
|
162
|
+
if (!wrapEditWithResilientReplace(pi)) {
|
|
163
|
+
console.warn(
|
|
164
|
+
"[pi-crew] CREW_RESILIENT_EDIT=1 but the resilient-edit wrapper did NOT register " +
|
|
165
|
+
"(pi-diff detected or the host lacks the tool_result seam) — the opt-in is a no-op this session.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch((err: unknown) => {
|
|
170
|
+
logInternalError("register.resilientEdit", err, "CREW_RESILIENT_EDIT=1");
|
|
171
|
+
console.warn(`[pi-crew] resilient-edit failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
136
172
|
});
|
|
137
173
|
}
|
|
138
174
|
|
|
139
|
-
registerCrewMessageRenderers(pi);
|
|
140
|
-
registerCrewInputRouter(pi);
|
|
141
|
-
registerCrewShortcuts(pi);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
withRegistrar("crewMessageRenderers", () => registerCrewMessageRenderers(pi));
|
|
176
|
+
withRegistrar("crewInputRouter", () => registerCrewInputRouter(pi));
|
|
177
|
+
withRegistrar("crewShortcuts", () => registerCrewShortcuts(pi));
|
|
178
|
+
withRegistrar("contextStatusInjection", () =>
|
|
179
|
+
registerContextStatusInjection(pi, {
|
|
180
|
+
enabled: loadConfig(process.cwd()).config.reliability?.ambientStatusInjection !== false,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
145
183
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
console.warn(
|
|
184
|
+
withRegistrar("crewVibes", () => registerCrewVibes(pi));
|
|
185
|
+
|
|
186
|
+
if (registrarFailures.length > 0) {
|
|
187
|
+
console.warn(`[pi-crew] ${registrarFailures.length} subsystem(s) failed to register: ${registrarFailures.join(", ")}`);
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
const skillCheck = await runPostInitSkillCheck(process.cwd());
|
|
@@ -5,33 +5,69 @@ import { CLEANUP_MARKER_FILE, cleanupOldArtifacts, pruneExpiredArtifacts } from
|
|
|
5
5
|
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
6
6
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
7
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
8
|
+
import { resolveContainedRelativePath } from "../../utils/safe-paths.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* ST-10:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* ST-10 + R44-N1: artifact descriptors for ONE run, paired with the run's
|
|
12
|
+
* HOST-DERIVED artifacts root (containment anchor). The anchor is computed
|
|
13
|
+
* from the runs directory being scanned (`<crewRoot>/state/runs/<runId>` →
|
|
14
|
+
* `<crewRoot>/artifacts/<runId>` — the same convention `createRunPaths`
|
|
15
|
+
* derives), NEVER from the worker-writable manifest itself: a malicious
|
|
16
|
+
* manifest must not be able to nominate its own prune boundary.
|
|
13
17
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
export interface RunArtifactGroup {
|
|
19
|
+
runId: string;
|
|
20
|
+
/** Host-derived per-run artifacts root (prune containment anchor). */
|
|
21
|
+
artifactsRoot: string;
|
|
22
|
+
descriptors: ArtifactDescriptor[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ST-10: Load artifact descriptors from all manifests in a runs directory,
|
|
27
|
+
* grouped per run with the host-derived artifacts root for each run.
|
|
28
|
+
* Best-effort: unreadable or corrupt manifests are silently skipped. Runs
|
|
29
|
+
* whose DIRECTORY NAME cannot form a safe artifacts root (path-escape) are
|
|
30
|
+
* skipped with a warn (fail-closed — their descriptors are never pruned).
|
|
31
|
+
*/
|
|
32
|
+
export function collectArtifactDescriptors(runsDir: string): RunArtifactGroup[] {
|
|
33
|
+
const groups: RunArtifactGroup[] = [];
|
|
16
34
|
let dirs: fs.Dirent[];
|
|
17
35
|
try {
|
|
18
36
|
dirs = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
19
37
|
} catch {
|
|
20
|
-
return
|
|
38
|
+
return groups;
|
|
21
39
|
}
|
|
40
|
+
// runsDir = <crewRoot>/<state.runsSubdir> → the crew root sits two levels up
|
|
41
|
+
// (mirrors state-store.ts validateRunManifestPaths' baseRoot derivation).
|
|
42
|
+
const crewRoot = path.resolve(runsDir, "..", "..");
|
|
43
|
+
const artifactsParent = path.join(crewRoot, DEFAULT_PATHS.state.artifactsSubdir);
|
|
22
44
|
for (const dir of dirs) {
|
|
23
45
|
if (!dir.isDirectory()) continue;
|
|
46
|
+
let artifactsRoot: string;
|
|
47
|
+
try {
|
|
48
|
+
artifactsRoot = resolveContainedRelativePath(artifactsParent, dir.name, "runId");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// R44-N1 fail-closed: an unsafe run-dir name must not reach the prune
|
|
51
|
+
// path at all — its descriptors are dropped, not anchored elsewhere.
|
|
52
|
+
logInternalError(
|
|
53
|
+
"artifact-cleanup.run-anchor",
|
|
54
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
55
|
+
`runsDir=${runsDir} runId=${dir.name}`,
|
|
56
|
+
"warn",
|
|
57
|
+
);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
24
60
|
const manifestPath = path.join(runsDir, dir.name, DEFAULT_PATHS.state.manifestFile);
|
|
25
61
|
try {
|
|
26
62
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest;
|
|
27
63
|
if (Array.isArray(manifest.artifacts)) {
|
|
28
|
-
|
|
64
|
+
groups.push({ runId: dir.name, artifactsRoot, descriptors: manifest.artifacts });
|
|
29
65
|
}
|
|
30
66
|
} catch {
|
|
31
67
|
// Skip unreadable/corrupt manifests — best-effort.
|
|
32
68
|
}
|
|
33
69
|
}
|
|
34
|
-
return
|
|
70
|
+
return groups;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
export function runArtifactCleanup(cwd: string): void {
|
|
@@ -49,9 +85,14 @@ export function runArtifactCleanup(cwd: string): void {
|
|
|
49
85
|
markerFile: CLEANUP_MARKER_FILE,
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
// ST-10:
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
// ST-10 + R44-N1: enforce retention/expiresAt from manifest metadata,
|
|
89
|
+
// per-run and containment-anchored — a run's descriptors can only prune
|
|
90
|
+
// files under that run's OWN host-derived artifacts root.
|
|
91
|
+
for (const scope of [userCrewRoot(), projectCrewRoot(cwd)]) {
|
|
92
|
+
for (const group of collectArtifactDescriptors(path.join(scope, DEFAULT_PATHS.state.runsSubdir))) {
|
|
93
|
+
pruneExpiredArtifacts(group.descriptors, { artifactsRoot: group.artifactsRoot });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
55
96
|
} catch (error) {
|
|
56
97
|
logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
|
|
57
98
|
}
|
|
@@ -99,8 +99,10 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
99
99
|
description: "Open the pi-crew interactive menu (list/run/status/cleanup/manage resources/doctor)",
|
|
100
100
|
handler: handleTeamManagerCommand,
|
|
101
101
|
});
|
|
102
|
-
// Backward-compat alias:
|
|
103
|
-
//
|
|
102
|
+
// Backward-compat alias: the interactive menu originally shipped under a
|
|
103
|
+
// runId-targeted name; `/team-cleanup` itself was NEVER registered (R27-D1) —
|
|
104
|
+
// the live names are `/team-manager` and this `/team-cleanup-menu` alias, so
|
|
105
|
+
// docs must not advertise `/team-cleanup <runId>`. Keep both.
|
|
104
106
|
|
|
105
107
|
pi.registerCommand("team-cleanup-menu", {
|
|
106
108
|
description: "Alias for /team-manager (pi-crew interactive menu)",
|
|
@@ -218,6 +220,11 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
218
220
|
// LAZY: load withSessionId only when needed for skill-create command
|
|
219
221
|
const { withSessionId } = await import("../../team-tool/context.ts");
|
|
220
222
|
const sessionId = withSessionId(ctx);
|
|
223
|
+
// R49-F7: `ctx.workspaceFolder` is a PHANTOM read — it is a VS-Code-era
|
|
224
|
+
// field that pi's ExtensionCommandContext does not define (absent in the
|
|
225
|
+
// resolved 0.77/1.0.4 api literal), so the optional chain always falls back
|
|
226
|
+
// to `process.cwd()`. Harmless (the fallback is correct) but do not treat
|
|
227
|
+
// this branch as a real cwd override.
|
|
221
228
|
const cwd =
|
|
222
229
|
(
|
|
223
230
|
ctx as unknown as {
|
|
@@ -80,6 +80,9 @@ function installResourcesDiscoverHook(pi: ExtensionAPI, ctx: RegistrationContext
|
|
|
80
80
|
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
81
81
|
});
|
|
82
82
|
} catch {
|
|
83
|
+
// R49-F4: `pi.on()` does NOT throw for an unknown event name in pi 0.77/1.0.4
|
|
84
|
+
// (verified against the resolved api literal + dist) — this catch is defensive
|
|
85
|
+
// only. The real degradation on an older host is that the hook NEVER FIRES.
|
|
83
86
|
/* older Pi without resources_discover */
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -76,6 +76,8 @@ function installTurnReconcileHook(pi: ExtensionAPI, ctx: RegistrationContext): v
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
} catch {
|
|
79
|
+
// R49-F4: `pi.on()` never throws on an unknown event (pi 0.77/1.0.4) — this
|
|
80
|
+
// catch is defensive; the real older-host degradation is a NEVER-FIRING hook.
|
|
79
81
|
/* older Pi without before_agent_start — rely on the interval alone */
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
} from "../../extension/run-maintenance.ts";
|
|
28
28
|
import { type BrokerSpawnCredentials, setActiveBrokerIssuer, setActiveBrokerRevoker } from "../../runtime/broker/broker-issuer.ts";
|
|
29
29
|
import { CrewBroker } from "../../runtime/broker/crew-broker.ts";
|
|
30
|
+
import { setActiveDelegateAborter } from "../../runtime/broker/delegate/delegate-abort.ts";
|
|
31
|
+
import { DEFAULT_LOCK_BUSY_RETRY_DELAYS_MS, withRunLockBusyRetryAsync } from "../../runtime/broker/protocol/lock-busy.ts";
|
|
30
32
|
import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
|
|
31
33
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
32
34
|
import { configuredModelInfosFromPiConfig } from "../../runtime/model/model-fallback.ts";
|
|
@@ -40,7 +42,8 @@ import { tryRegisterSessionCleanup } from "../../runtime/session-resources.ts";
|
|
|
40
42
|
import { createSessionSnapshot } from "../../runtime/session-snapshot.ts";
|
|
41
43
|
import { applyCrewSettingsTiersToConfig, loadCrewSettingsTiers, scheduledJobsHiddenCountOf } from "../../runtime/settings-store.ts";
|
|
42
44
|
import type { ReconcileResult } from "../../runtime/stale-reconciler.ts";
|
|
43
|
-
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
45
|
+
import { loadRunManifestById, saveRunManifest } from "../../state/stores/state-store.ts";
|
|
46
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
44
47
|
import { installInlinePanel } from "../../ui/inline-panel/index.ts";
|
|
45
48
|
import { clearSessionSwitchInFlight, markSessionSwitchInFlight } from "../../ui/inline-panel/view-session-store.ts";
|
|
46
49
|
import { registerPiCrewPowerbarSegments, resetPowerbarDedupState, updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
@@ -532,6 +535,56 @@ async function runDeferredSessionCleanup(
|
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
537
|
|
|
538
|
+
/** Manifest keys the scheduler stamps for traceability. They are NOT declared
|
|
539
|
+
* on TeamRunManifest (carried as extra keys, exactly like the pre-fix inline
|
|
540
|
+
* write did) — the provenance stamp is advisory metadata, never read back by
|
|
541
|
+
* any consumer. */
|
|
542
|
+
type SchedulerProvenanceFields = { schedulerJobId: string; schedulerName: string };
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* M3-7 (R22-F2 / F22-2, fix-wave 6): stamp scheduler provenance on a scheduled
|
|
546
|
+
* run's manifest WITHOUT racing the runner.
|
|
547
|
+
*
|
|
548
|
+
* Pre-fix the executor did `atomicWriteJson(stateRoot + "/manifest.json",
|
|
549
|
+
* {...loaded.manifest, schedulerJobId, schedulerName})` — a RAW write that
|
|
550
|
+
* (a) bypassed run.lock, so it interleaved with team-runner's saveRunManifest /
|
|
551
|
+
* saveRunManifestAsync, and (b) bypassed the write-layer terminal-preserve
|
|
552
|
+
* guard, so a cancel/reconciler flip landing in the window was RESURRECTED to
|
|
553
|
+
* "running" while usage/artifacts were reverted to the pre-write snapshot. It
|
|
554
|
+
* fired on EVERY scheduled fire.
|
|
555
|
+
*
|
|
556
|
+
* Fix: re-read INSIDE run.lock (bounded busy-retry) and persist through
|
|
557
|
+
* `saveRunManifest`, which applies `preserveDiskTerminalStatus` (never erases a
|
|
558
|
+
* terminal disk status) and invalidates/repopulates the manifest cache. Read +
|
|
559
|
+
* write under the same lock closes the lost-update window by construction.
|
|
560
|
+
*
|
|
561
|
+
* Best-effort by contract (provenance must never break a scheduled fire): a
|
|
562
|
+
* pruned run, exhausted lock budget or write fault logs at warn and returns
|
|
563
|
+
* false.
|
|
564
|
+
*
|
|
565
|
+
* @internal exported for the M3-7 unit pin
|
|
566
|
+
* (test/unit/runtime/scheduling/scheduler-provenance-lock.test.ts).
|
|
567
|
+
*/
|
|
568
|
+
export async function stampSchedulerRunProvenance(cwd: string, runId: string, job: { id: string; name: string }): Promise<boolean> {
|
|
569
|
+
try {
|
|
570
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
571
|
+
if (!loaded) return false; // run pruned/unknown — nothing to stamp
|
|
572
|
+
const provenance: SchedulerProvenanceFields = { schedulerJobId: job.id, schedulerName: job.name };
|
|
573
|
+
await withRunLockBusyRetryAsync(loaded.manifest, DEFAULT_LOCK_BUSY_RETRY_DELAYS_MS, async () => {
|
|
574
|
+
// Fresh reload INSIDE the lock (respond.ts:42-43 discipline): outside
|
|
575
|
+
// would reintroduce the exact lost-update window being closed.
|
|
576
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, runId);
|
|
577
|
+
if (!fresh) return; // pruned between the outer load and the lock
|
|
578
|
+
const next: TeamRunManifest & SchedulerProvenanceFields = { ...fresh.manifest, ...provenance };
|
|
579
|
+
saveRunManifest(next);
|
|
580
|
+
});
|
|
581
|
+
return true;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
logInternalError("scheduler.stamp-provenance", error, `runId=${runId} jobId=${job.id}`, "warn");
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
535
588
|
/**
|
|
536
589
|
* Build a CrewScheduler wired to the current session. The scheduler's
|
|
537
590
|
* executor closure invokes handleTeamTool lazily — the heavy team-tool
|
|
@@ -596,22 +649,11 @@ export function setupCrewScheduler(
|
|
|
596
649
|
const runId = runResult?.details?.runId;
|
|
597
650
|
if (runId && typeof runId === "string") {
|
|
598
651
|
crewScheduler?.recordSpawnedRun(job.id, runId);
|
|
599
|
-
// Update run manifest with scheduler provenance for traceability
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
// LAZY: defer dynamic import of atomic-write.ts to its call site.
|
|
605
|
-
const { atomicWriteJson } = await import("../../state/atomic-write.ts");
|
|
606
|
-
atomicWriteJson(loaded.manifest.stateRoot + "/manifest.json", {
|
|
607
|
-
...loaded.manifest,
|
|
608
|
-
schedulerJobId: job.id,
|
|
609
|
-
schedulerName: job.name,
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
} catch {
|
|
613
|
-
/* best-effort provenance tracking */
|
|
614
|
-
}
|
|
652
|
+
// Update run manifest with scheduler provenance for traceability.
|
|
653
|
+
// M3-7 (R22-F2): lock-held + terminal-preserving (was a raw
|
|
654
|
+
// atomicWriteJson outside run.lock that could resurrect a
|
|
655
|
+
// terminal status). Best-effort by contract.
|
|
656
|
+
await stampSchedulerRunProvenance(extensionCtx.cwd ?? process.cwd(), runId, job);
|
|
615
657
|
}
|
|
616
658
|
// R6-2(b) + I6 (round-6 audit): update state FIRST, then persist the
|
|
617
659
|
// UPDATED job. The old order persisted the fire-time snapshot (still
|
|
@@ -690,7 +732,7 @@ export interface CrewBrokerLifecycleController {
|
|
|
690
732
|
}
|
|
691
733
|
|
|
692
734
|
function isRootSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
693
|
-
if (
|
|
735
|
+
if (getCrewEnv("PI_CREW_KIND", env) === "subagent") return false;
|
|
694
736
|
try {
|
|
695
737
|
return currentCrewDepth(env) === 0;
|
|
696
738
|
} catch {
|
|
@@ -847,6 +889,24 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
847
889
|
};
|
|
848
890
|
setActiveBrokerRevoker(revokeForTask);
|
|
849
891
|
|
|
892
|
+
// M3-5 / R43-N3: publish the delegate canceller alongside the revoker. The
|
|
893
|
+
// run-terminal paths (budget abort / cancellation → budget-enforcement.ts
|
|
894
|
+
// terminaliseRunWithDrain) call it so in-flight DELEGATE grandchildren are
|
|
895
|
+
// aborted instead of outliving the run to their own 24h deadline. Same
|
|
896
|
+
// self-gating as the revoker: no bound broker ⇒ nothing to cancel.
|
|
897
|
+
const abortDelegatesForRun = (runId: string, reason: string): number => {
|
|
898
|
+
if (!runId || typeof runId !== "string") return 0;
|
|
899
|
+
if (!isRootSession(process.env)) return 0;
|
|
900
|
+
if (!broker || brokerSessionId !== cachedSessionId) return 0;
|
|
901
|
+
try {
|
|
902
|
+
return broker.abortDelegatesForRun(runId, reason);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
logInternalError("broker.abort-delegates", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`, "warn");
|
|
905
|
+
return 0;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
setActiveDelegateAborter(abortDelegatesForRun);
|
|
909
|
+
|
|
850
910
|
// Publish this issuer as the process-local active issuer so runChildPi can
|
|
851
911
|
// default `brokerIssuer` without the registration context being threaded
|
|
852
912
|
// through every runner call site. The issuer self-gates (root + flag), so
|
|
@@ -858,6 +918,7 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
858
918
|
stop: async () => {
|
|
859
919
|
setActiveBrokerIssuer(undefined);
|
|
860
920
|
setActiveBrokerRevoker(undefined);
|
|
921
|
+
setActiveDelegateAborter(undefined);
|
|
861
922
|
if (broker) {
|
|
862
923
|
try {
|
|
863
924
|
await broker.stop();
|
|
@@ -19,7 +19,7 @@ import { forgetDetachedRun, hasDetachedRuns, peekFinishedDetachedRunResults } fr
|
|
|
19
19
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
20
20
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
21
21
|
import { loadTasksWithRecovery } from "../../state/stores/manifest-io.ts";
|
|
22
|
-
import type { TeamRunManifest } from "../../state/types.ts";
|
|
22
|
+
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
23
23
|
import { overlayFreshTaskStatuses, summarizeHeartbeats } from "../../ui/heartbeat-aggregator.ts";
|
|
24
24
|
import { requestRender, setExtensionWidget, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
|
|
25
25
|
import { requestPowerbarUpdate } from "../../ui/powerbar-publisher.ts";
|
|
@@ -27,6 +27,7 @@ import { RenderScheduler } from "../../ui/render-scheduler.ts";
|
|
|
27
27
|
import { runEventBus } from "../../ui/run-event-bus.ts";
|
|
28
28
|
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
29
29
|
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
30
|
+
import { readJsonFileCoalesced } from "../../utils/file-coalescer.ts";
|
|
30
31
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
31
32
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
32
33
|
import { RunWatcherRegistry } from "../../utils/run-watcher-registry.ts";
|
|
@@ -128,6 +129,30 @@ export function applyTerminalRunEventToManifests(manifests: TeamRunManifest[], e
|
|
|
128
129
|
return isTerminalRunEventType(event.type) ? evictRunFromManifests(manifests, event.runId) : manifests;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* R15-P1: GATE 3 used to call `loadTasksWithRecovery` — an uncached
|
|
134
|
+
* `readFileSync` + `JSON.parse` of the FULL task packet array (~8.4 KB/task) —
|
|
135
|
+
* once per running run on every 160 ms tick. Measured 14.78 ms/call at 200
|
|
136
|
+
* tasks, i.e. ~28% of the tick blocked on the event loop while runs were live.
|
|
137
|
+
*
|
|
138
|
+
* The read now goes through the in-tree coalesced reader (stat-gated on
|
|
139
|
+
* mtime+size, LRU-capped, short TTL): a tick that sees an unchanged file pays a
|
|
140
|
+
* syscall instead of a parse. The TTL is far below the stale (60 s) / dead
|
|
141
|
+
* (5 min) thresholds GATE 3 protects, and the quarantine/reconstruct path stays
|
|
142
|
+
* intact and self-invalidating — a recovered file is rewritten, so its mtime
|
|
143
|
+
* changes and the next tick misses the cache.
|
|
144
|
+
*/
|
|
145
|
+
const TASKS_READ_TTL_MS = 200;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read a run's tasks.json through the coalesced reader (GATE 3 truth source).
|
|
149
|
+
* Exported so the cache behaviour (and its freshness/quarantine invariants) can
|
|
150
|
+
* be pinned directly — see test/unit/extension/registration/render-loop-tasks-read-cache.test.ts.
|
|
151
|
+
*/
|
|
152
|
+
export function readTasksCoalesced(tasksPath: string, eventsPath: string, runId: string): TeamTaskState[] {
|
|
153
|
+
return readJsonFileCoalesced(tasksPath, TASKS_READ_TTL_MS, () => loadTasksWithRecovery(tasksPath, eventsPath, runId));
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
/**
|
|
132
157
|
* Build the render scheduler + preload loop + bounded run watchers.
|
|
133
158
|
*
|
|
@@ -385,7 +410,7 @@ export function setupRenderLoop(
|
|
|
385
410
|
// "dead worker" (live: team_20260923100114, 01_explore parked on
|
|
386
411
|
// ask). Overlay FRESH statuses before summarizing; divergence also
|
|
387
412
|
// schedules an in-place rebuild so the cache syncs off the tick.
|
|
388
|
-
const freshTasks =
|
|
413
|
+
const freshTasks = readTasksCoalesced(freshManifest.tasksPath, freshManifest.eventsPath, run.runId);
|
|
389
414
|
const overlaid = overlayFreshTaskStatuses(snapshot, freshTasks);
|
|
390
415
|
if (overlaid !== snapshot) refreshSnapshotInPlace(run.runId);
|
|
391
416
|
const summary = summarizeHeartbeats(overlaid, { now });
|
|
@@ -215,7 +215,9 @@ export function createLoopGuardState() {
|
|
|
215
215
|
/**
|
|
216
216
|
* Install the loop guard on a Pi instance. Both hooks are best-effort:
|
|
217
217
|
* older Pi versions without these events are tolerated (same pattern as
|
|
218
|
-
* installResourcesDiscoverHook).
|
|
218
|
+
* installResourcesDiscoverHook). R49-F4: `pi.on()` never THROWS on an unknown
|
|
219
|
+
* event name (pi 0.77/1.0.4 verified) — so the try/catch below is defensive
|
|
220
|
+
* only, and the real older-host degradation is a NEVER-FIRING hook.
|
|
219
221
|
*/
|
|
220
222
|
export function installToolLoopGuard(pi: ExtensionAPI): void {
|
|
221
223
|
const state = createLoopGuardState();
|
|
@@ -228,7 +230,7 @@ export function installToolLoopGuard(pi: ExtensionAPI): void {
|
|
|
228
230
|
return undefined;
|
|
229
231
|
});
|
|
230
232
|
} catch {
|
|
231
|
-
/*
|
|
233
|
+
/* R49-F4: defensive only — pi.on() does not throw on unknown events; the hook simply never fires */
|
|
232
234
|
}
|
|
233
235
|
try {
|
|
234
236
|
pi.on("tool_result", (event: { toolName: string; input?: unknown; content?: unknown }) => {
|
|
@@ -238,6 +240,6 @@ export function installToolLoopGuard(pi: ExtensionAPI): void {
|
|
|
238
240
|
return { content: [...existing, ...appends] };
|
|
239
241
|
});
|
|
240
242
|
} catch {
|
|
241
|
-
/*
|
|
243
|
+
/* R49-F4: defensive only — pi.on() does not throw on unknown events; the hook simply never fires */
|
|
242
244
|
}
|
|
243
245
|
}
|
|
@@ -33,7 +33,6 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
33
33
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
34
34
|
import {
|
|
35
35
|
DEFAULT_VERIFY_GATE_TIMEOUT_MS,
|
|
36
|
-
defaultVerifyGateChecks,
|
|
37
36
|
renderVerifyGateContext,
|
|
38
37
|
runVerifyGate,
|
|
39
38
|
type VerifyGateCheck,
|
|
@@ -59,7 +58,7 @@ const VerifyGateToolParams = Type.Object({
|
|
|
59
58
|
}),
|
|
60
59
|
{
|
|
61
60
|
description:
|
|
62
|
-
|
|
61
|
+
'Explicit checks — REQUIRED for a meaningful verdict (M1-7: the gate never derives commands from a worker-mutable package.json). Without checks the verdict is INCONCLUSIVE (escalate). Add grep evidence lines here (e.g. `grep -c "<marker>" dist/index.mjs`).',
|
|
63
62
|
},
|
|
64
63
|
),
|
|
65
64
|
),
|
|
@@ -152,7 +151,12 @@ export function registerVerifyGateTool(pi: ExtensionAPI, deps: VerifyGateToolDep
|
|
|
152
151
|
}),
|
|
153
152
|
async execute(_id, params: Static<typeof VerifyGateToolParams>, signal, _onUpdate, ctx) {
|
|
154
153
|
const cwd = params.cwd && params.cwd.trim().length > 0 ? params.cwd : ctx.cwd;
|
|
155
|
-
|
|
154
|
+
// M1-7 (R47-1): NO package.json fallback — the task/session cwd is
|
|
155
|
+
// worker-mutable, so deriving checks from it at call time let a worker
|
|
156
|
+
// grade its own work (tampered `scripts.typecheck: "exit 0"` ⇒ PASS).
|
|
157
|
+
// Callers must pass explicit checks; without them the gate is
|
|
158
|
+
// INCONCLUSIVE (escalate — never a PASS-shaped verdict).
|
|
159
|
+
const checks: VerifyGateCheck[] = params.checks ?? [];
|
|
156
160
|
// Nested-limit guards — validate BEFORE any executeTool call.
|
|
157
161
|
if (checks.length > VERIFY_GATE_TOOL_MAX_CHECKS) {
|
|
158
162
|
return {
|