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
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { removeGuidance } from "../../config/markers.ts";
|
|
4
4
|
import { appendHookEvent, executeHook } from "../../hooks/registry.ts";
|
|
5
|
+
import { abortRunDelegates } from "../../runtime/broker/delegate/delegate-abort.ts";
|
|
5
6
|
import { killProcessPid } from "../../runtime/child-pi/child-pi.ts";
|
|
6
7
|
import { terminateLiveAgentsForRun } from "../../runtime/live-session/live-agent-manager.ts";
|
|
7
8
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
@@ -315,6 +316,12 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
315
316
|
// Without this, forgetting a running run orphans those processes (separate
|
|
316
317
|
// process groups via setsid) which keep executing and consuming tokens after
|
|
317
318
|
// their state directory is gone. Mirrors handleCancel's cleanup.
|
|
319
|
+
// M4-R1(c) (fix-wave 6 residual, M3-A / R43-N3): the in-flight DELEGATE
|
|
320
|
+
// grandchildren too — they live in the broker's abort registry (own
|
|
321
|
+
// process-local lifetime) and would otherwise keep spending against a state
|
|
322
|
+
// directory this call is about to delete. Must run before the fs.rmSync
|
|
323
|
+
// calls below. Sync + never throws (0 when no broker aborter is installed).
|
|
324
|
+
abortRunDelegates(loaded.manifest.runId, "run-forgotten");
|
|
318
325
|
await terminateLiveAgentsForRun(loaded.manifest.runId, "cancelled", appendEvent, loaded.manifest.eventsPath);
|
|
319
326
|
const asyncPid = loaded.manifest.async?.pid;
|
|
320
327
|
if (asyncPid !== undefined && asyncPid > 0) {
|
|
@@ -529,23 +536,36 @@ function dryRunRemovedIds(guidancePath: string): string[] {
|
|
|
529
536
|
* - NEVER removes user-authored agent files (`~/.pi/agent/agents/*.md`)
|
|
530
537
|
* because pi-crew cannot tell which were user-created vs test-copied —
|
|
531
538
|
* only the timestamped `.bak-*` backups (clearly pi-crew test junk).
|
|
532
|
-
* -
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
539
|
+
* - F13-B: the user state dir is NOT removed on `confirm` alone. It holds run
|
|
540
|
+
* history/artifacts for the whole user tier, so removal is gated on an
|
|
541
|
+
* explicit `force: true` — parity with the global config below and with
|
|
542
|
+
* project-scope `.crew/` removal (`force` gate). Without force the state dir
|
|
543
|
+
* is reported and preserved.
|
|
544
|
+
* - dryRun=true previews without writing (and always states the force gate).
|
|
536
545
|
*/
|
|
537
546
|
function handleUserCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
538
547
|
const dryRun = params.dryRun === true;
|
|
539
548
|
const force = params.force === true;
|
|
540
549
|
const lines: string[] = ["User-scope cleanup for pi-crew:"];
|
|
541
550
|
|
|
542
|
-
// 1. pi-crew user state dir (~/.pi/agent/extensions/pi-crew/) —
|
|
543
|
-
//
|
|
544
|
-
//
|
|
551
|
+
// 1. pi-crew user state dir (~/.pi/agent/extensions/pi-crew/) — run history +
|
|
552
|
+
// artifacts for the user tier. F13-B: force-gated (was confirm-only, which
|
|
553
|
+
// let a routine `cleanup scope=user` wipe the whole user state dir).
|
|
545
554
|
const crewStateDir = userCrewRoot();
|
|
546
555
|
lines.push(`pi-crew user state dir (${crewStateDir}):`);
|
|
547
556
|
let crewStateBytes = 0;
|
|
548
|
-
if (fs.existsSync(crewStateDir)) {
|
|
557
|
+
if (!fs.existsSync(crewStateDir)) {
|
|
558
|
+
lines.push(" - (not present — nothing to do)");
|
|
559
|
+
} else if (!force) {
|
|
560
|
+
try {
|
|
561
|
+
crewStateBytes = dirSize(crewStateDir);
|
|
562
|
+
} catch {
|
|
563
|
+
/* best-effort size */
|
|
564
|
+
}
|
|
565
|
+
lines.push(
|
|
566
|
+
` - ${dryRun ? "would preserve" : "preserved"}: ${crewStateDir} (${formatBytes(crewStateBytes)}) — holds run history/artifacts; pass force: true to remove`,
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
549
569
|
try {
|
|
550
570
|
crewStateBytes = dirSize(crewStateDir);
|
|
551
571
|
} catch {
|
|
@@ -559,8 +579,6 @@ function handleUserCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTea
|
|
|
559
579
|
}
|
|
560
580
|
}
|
|
561
581
|
lines.push(` - ${dryRun ? "would remove" : "removed"}: ${crewStateDir} (${formatBytes(crewStateBytes)})`);
|
|
562
|
-
} else {
|
|
563
|
-
lines.push(" - (not present — nothing to do)");
|
|
564
582
|
}
|
|
565
583
|
|
|
566
584
|
// 2. pi-crew global config (~/.pi/agent/pi-crew.json) — gated on force=true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderAskAnswer } from "../../prompt/prompt-runtime.ts";
|
|
2
|
+
import { repointWaitState } from "../../runtime/broker/protocol/wait-resolve.ts";
|
|
2
3
|
import { readCrewAgents, recordFromTask, saveCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
3
4
|
import { isWaitingWorkerAlive } from "../../runtime/dispatch-batch.ts";
|
|
4
5
|
import { checkProcessLiveness } from "../../runtime/process-status.ts";
|
|
@@ -243,12 +244,18 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
243
244
|
let manifest = fresh.manifest;
|
|
244
245
|
if (requeuedIds.length > 0) {
|
|
245
246
|
saveRunTasks(fresh.manifest, updatedTasks);
|
|
246
|
-
// WP-2 review round 1 (P3)
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
247
|
+
// WP-2 review round 1 (P3) + M4-R1(b) (fix-wave 6 residual): when the slot
|
|
248
|
+
// pointed at a REQUEUED park, scoped-forget it — but never by blind
|
|
249
|
+
// clearing. Another task may still be parked and this single slot is the
|
|
250
|
+
// only thing shielding it (isWaitAnswerPending reads just this field), so
|
|
251
|
+
// re-point it at the surviving park (latest deadline wins, mirroring
|
|
252
|
+
// wait.resolve's repointedWaitState); it is cleared only when this was
|
|
253
|
+
// the last live park. The clear remains the correct outcome there — the
|
|
254
|
+
// pre-fix guard left the stale pointer shielding the run for the full
|
|
255
|
+
// 24h TTL with a stale marker in the manifest/UI.
|
|
250
256
|
if (manifest.waitState && requeuedIds.includes(manifest.waitState.taskId)) {
|
|
251
|
-
|
|
257
|
+
const waitState = repointWaitState(updatedTasks, manifest.waitState, (candidateId) => requeuedIds.includes(candidateId));
|
|
258
|
+
manifest = { ...manifest, waitState, updatedAt: new Date().toISOString() };
|
|
252
259
|
saveRunManifest(manifest);
|
|
253
260
|
}
|
|
254
261
|
if (
|
|
@@ -2,6 +2,7 @@ import type { PiTeamsConfig } from "../../config/config.ts";
|
|
|
2
2
|
import { loadConfig } from "../../config/config.ts";
|
|
3
3
|
import { isSessionSwitchInFlight } from "../../ui/inline-panel/view-session-store.ts";
|
|
4
4
|
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
5
|
+
import { clampTimerDelayMs, MAX_TIMER_DELAY_MS } from "../../utils/timer-limits.ts";
|
|
5
6
|
import type { TeamContext } from "./context.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -22,6 +23,28 @@ export interface RunDeadline {
|
|
|
22
23
|
/** Fallback deadline when no config or param override is available: 1 hour. */
|
|
23
24
|
export const DEFAULT_RUN_DEADLINE_MS = 3_600_000;
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* R37-F1: re-export the timer ceiling so callers/tests of this module can pin
|
|
28
|
+
* the clamp without reaching into utils/timer-limits.ts.
|
|
29
|
+
*/
|
|
30
|
+
export { MAX_TIMER_DELAY_MS };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* R37-F1: the resolved deadline MUST stay inside the `setTimeout` range.
|
|
34
|
+
*
|
|
35
|
+
* `params.timeoutMs` is read off the raw tool params (`additionalProperties:
|
|
36
|
+
* true`, so it is not schema-validated) and `maxRunMinutes * 60_000` comes from
|
|
37
|
+
* config. Either can exceed 2^31-1, and Node's timer then fires after **1 ms**
|
|
38
|
+
* — the run aborted at birth (deep-review round 37, runtime-proven).
|
|
39
|
+
*
|
|
40
|
+
* Exported so the `waitForRun` mirror (run-tracker.ts) and tests share ONE
|
|
41
|
+
* boundary. Returns undefined for absent/invalid values so the config →
|
|
42
|
+
* default chain still applies.
|
|
43
|
+
*/
|
|
44
|
+
export function clampRunDeadlineMs(value: unknown): number | undefined {
|
|
45
|
+
return clampTimerDelayMs(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
/** Options for resolveRunDeadline. */
|
|
26
49
|
export interface RunDeadlineOptions {
|
|
27
50
|
/** "abort" (default): arm the deadline timer that ABORTS the controller —
|
|
@@ -69,10 +92,14 @@ export function resolveRunDeadline<T extends object>(
|
|
|
69
92
|
config?: PiTeamsConfig,
|
|
70
93
|
options?: RunDeadlineOptions,
|
|
71
94
|
): RunDeadline {
|
|
72
|
-
|
|
95
|
+
// R37-F1: BOTH inputs are caller-controlled and neither is schema-validated
|
|
96
|
+
// (params: additionalProperties; config: may be passed in directly).
|
|
97
|
+
// Validate (finite, > 0) and clamp to the timer ceiling before arming.
|
|
98
|
+
const timeoutMs = clampRunDeadlineMs((params as { timeoutMs?: number }).timeoutMs);
|
|
73
99
|
const effectiveConfig = config ?? loadConfig(ctx.cwd).config;
|
|
74
100
|
const maxRunMinutes = effectiveConfig?.limits?.maxRunMinutes;
|
|
75
|
-
const
|
|
101
|
+
const configDeadlineMs = clampRunDeadlineMs(typeof maxRunMinutes === "number" ? maxRunMinutes * 60_000 : undefined);
|
|
102
|
+
const deadlineMs = timeoutMs ?? configDeadlineMs ?? DEFAULT_RUN_DEADLINE_MS;
|
|
76
103
|
|
|
77
104
|
const controller = new AbortController();
|
|
78
105
|
// Propagate caller abort (ctx.signal) to the shared controller.
|
|
@@ -27,6 +27,7 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
|
|
|
27
27
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
28
28
|
import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/model/runtime-resolver.ts";
|
|
29
29
|
import { captureRunModelContext, resolveParentModel } from "../../runtime/model/session-model.ts";
|
|
30
|
+
import { resolveHostVerifyGateSpec } from "../../runtime/verification/verify-gate.ts";
|
|
30
31
|
import { appendEventAsync, readEventsCursor } from "../../state/event-log/event-log.ts";
|
|
31
32
|
import type { RunMetrics } from "../../state/stores/run-metrics.ts";
|
|
32
33
|
import type { RuntimeResolutionState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
@@ -495,6 +496,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
495
496
|
);
|
|
496
497
|
}
|
|
497
498
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
499
|
+
// M1-7 (R47-1): pin the host-owned verify-gate ground truth AT DISPATCH —
|
|
500
|
+
// this host process read of the project manifests happens BEFORE any
|
|
501
|
+
// worker can run, so the snapshot is pristine. The run manifest carries it
|
|
502
|
+
// to the verifier pre-gate (task-runner), which re-hashes the task cwd
|
|
503
|
+
// against it and never re-derives checks from the worker-mutable
|
|
504
|
+
// task-cwd package.json. Undefined when the project has neither manifests
|
|
505
|
+
// nor a check preset — the gate then resolves no checks (fail-closed).
|
|
506
|
+
const hostVerifyGateSpec = await resolveHostVerifyGateSpec(resolvedCtx.cwd);
|
|
498
507
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
499
508
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
500
509
|
// DEBUG: log what we received (gated to avoid stdout pollution in production)
|
|
@@ -514,6 +523,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
514
523
|
...updatedManifest,
|
|
515
524
|
runtimeResolution,
|
|
516
525
|
runConfig: executedConfig,
|
|
526
|
+
// M1-7 (R47-1): host-owned verify-gate spec (see hostVerifyGateSpec above).
|
|
527
|
+
...(hostVerifyGateSpec ? { verifyGateSpec: hostVerifyGateSpec } : {}),
|
|
517
528
|
// Background/async runs re-enter through background-runner in a detached
|
|
518
529
|
// process with no ExtensionContext. Snapshot the model routing inputs so
|
|
519
530
|
// they survive the hand-off instead of being rediscovered from models.json.
|
|
@@ -3,15 +3,17 @@ import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from
|
|
|
3
3
|
import { extractCommandTrace } from "../../runtime/command-trace.ts";
|
|
4
4
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
5
5
|
import { deadletterStatusLine } from "../../runtime/deadletter.ts";
|
|
6
|
-
import { evaluateRunEffectiveness } from "../../runtime/effectiveness.ts";
|
|
6
|
+
import { delegateCompletedTaskIdsFromEvents, evaluateRunEffectiveness } from "../../runtime/effectiveness.ts";
|
|
7
7
|
import { computePhaseProgress } from "../../runtime/phase-progress.ts";
|
|
8
8
|
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
9
|
+
import { stripWorkerEscapes } from "../../runtime/sanitize/worker-text.ts";
|
|
9
10
|
import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
|
|
10
11
|
import { verifyTaskCompletion } from "../../runtime/verification/completion-guard.ts";
|
|
11
12
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
12
13
|
import { withRunLockSync } from "../../state/coordination/locks.ts";
|
|
13
14
|
import { readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
|
|
14
15
|
import { appendEvent, readEventsCursor } from "../../state/event-log/event-log.ts";
|
|
16
|
+
import { getTasksCorruptionReport } from "../../state/stores/manifest-io.ts";
|
|
15
17
|
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
16
18
|
import { aggregateUsage, formatCost, formatUsage } from "../../state/usage.ts";
|
|
17
19
|
import { formatDuration } from "../../ui/format-helpers.ts";
|
|
@@ -19,6 +21,7 @@ import { goalAchievedStatusLabel } from "../../ui/goal-flag.ts";
|
|
|
19
21
|
import { locateRunCwd } from "../team-tool.ts";
|
|
20
22
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
21
23
|
import { result, type TeamContext } from "./context.ts";
|
|
24
|
+
import { stuckTasksFromRecords } from "./health-monitor.ts";
|
|
22
25
|
import { paramRequired } from "./param-error.ts";
|
|
23
26
|
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
24
27
|
|
|
@@ -108,6 +111,31 @@ export function transitionStaleAsyncUnderLock(
|
|
|
108
111
|
return transitioned;
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
/**
|
|
115
|
+
* R46-1 / EP1 (unfenced-worker-text rollout): worker-authored values reach the
|
|
116
|
+
* orchestrator-facing status text from several places — child stderr flows
|
|
117
|
+
* VERBATIM into `task.error` / `failureCause` / `agent.error`
|
|
118
|
+
* (child-executor.ts attemptErrorFor), tool names/activity states arrive from
|
|
119
|
+
* worker JSON events, and event/attention messages are appended by crew code
|
|
120
|
+
* that embeds worker text. Interpolated raw, any of them can forge a fresh
|
|
121
|
+
* column-0 status row (e.g. a fake `✓ verifier [completed]` line, fabricated
|
|
122
|
+
* task/agent rows or `Progress: 100%`) and smuggle terminal escapes
|
|
123
|
+
* (ANSI color, OSC-52 clipboard writes) into the LEADER's status view.
|
|
124
|
+
*
|
|
125
|
+
* Shape: these are inline `key=value` fields inside dense single-line rows, so
|
|
126
|
+
* a multi-line provenance fence would break the row grammar. Route each value
|
|
127
|
+
* through the SHARED strip cascade (`stripWorkerEscapes`, not reimplemented)
|
|
128
|
+
* and collapse newlines/CRs to a single space — the R46-1 "newline-collapse"
|
|
129
|
+
* fix (the finding prescribes "newline-collapse or structural quoting in both
|
|
130
|
+
* status builders"). The stripped value can no longer start a new line, so
|
|
131
|
+
* forged rows are structurally impossible and escapes never reach the session.
|
|
132
|
+
*/
|
|
133
|
+
function sanitizeStatusField(value: string): string {
|
|
134
|
+
return stripWorkerEscapes(value)
|
|
135
|
+
.replace(/[\n\r]+/g, " ")
|
|
136
|
+
.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
112
140
|
if (!params.runId)
|
|
113
141
|
return result(
|
|
@@ -225,21 +253,43 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
225
253
|
tasks,
|
|
226
254
|
executeWorkers: manifest.runtimeResolution?.kind !== "scaffold",
|
|
227
255
|
runtimeConfig: cfg.runtime,
|
|
256
|
+
// EP2 parity rider (wave-6 M4-R2): finalize-run passes the delegate.completed
|
|
257
|
+
// id set so a grandchild delegate task (real output delivered via the broker
|
|
258
|
+
// mailbox, no usage/transcript of its own) is NOT misread as no-observed-work.
|
|
259
|
+
// status omitted it, so the SAME run rendered a phantom `noObservedWork` row
|
|
260
|
+
// here while finalize counted it as work. `allEvents` is the bounded
|
|
261
|
+
// cursor/tail read above (not the full readEvents call finalize uses) —
|
|
262
|
+
// best-effort parity: a delegate.completed event that has already scrolled
|
|
263
|
+
// out of the tail window is not recoverable here, matching the reader's
|
|
264
|
+
// bounded contract.
|
|
265
|
+
delegateCompleted: delegateCompletedTaskIdsFromEvents(allEvents),
|
|
228
266
|
});
|
|
229
267
|
const noObservedWorkTasks = effectiveness.noObservedWorkTaskIds
|
|
230
268
|
.map((id) => tasks.find((task) => task.id === id))
|
|
231
269
|
.filter((task): task is (typeof tasks)[number] => task !== undefined);
|
|
232
|
-
|
|
270
|
+
// R48-N1 (fix-wave 6, EP3): fold an EXTERNAL liveness signal into the attention
|
|
271
|
+
// surface. `effectiveness.needsAttentionTaskIds` is worker-SELF-REPORTED
|
|
272
|
+
// (activityState === "needs_attention"); a dead sync host reports nothing and
|
|
273
|
+
// the run renders "running / needsAttention=none / 0%" forever. Consult the
|
|
274
|
+
// timestamp-based stuck-task signal (heartbeat/activity age) for active runs so
|
|
275
|
+
// an unresponsive/frozen task is surfaced even with zero self-report.
|
|
276
|
+
const externallyStuckTasks = isActiveRunStatus(manifest.status) ? stuckTasksFromRecords(tasks, manifest.runId, Date.now()) : [];
|
|
277
|
+
const externallyStuckIds = new Set(externallyStuckTasks.map((stuck) => stuck.taskId));
|
|
278
|
+
const needsAttentionIds = [...new Set([...effectiveness.needsAttentionTaskIds, ...externallyStuckIds])];
|
|
279
|
+
const attentionTasks = needsAttentionIds
|
|
233
280
|
.map((id) => tasks.find((task) => task.id === id))
|
|
234
281
|
.filter((task): task is (typeof tasks)[number] => task !== undefined);
|
|
235
282
|
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
236
283
|
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
237
284
|
const waitingTasks = tasks.filter((task) => task.status === "queued" || task.status === "waiting");
|
|
238
285
|
const agentLine = (agent: (typeof crewAgents)[number]): string =>
|
|
239
|
-
`- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.usage?.cost ? ` cost=${formatCost(agent.usage.cost)}` : ""}${agent.progress?.activityState ? ` activityState=${agent.progress.activityState}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
286
|
+
`- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.usage?.cost ? ` cost=${formatCost(agent.usage.cost)}` : ""}${agent.progress?.activityState ? ` activityState=${sanitizeStatusField(agent.progress.activityState)}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${sanitizeStatusField(agent.progress.currentTool)}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${sanitizeStatusField(agent.error)}` : ""}`;
|
|
240
287
|
// G19 (W-E Phase 1): surface the goal-achievement verdict on terminal runs —
|
|
241
288
|
// ⚠ false-green warning line; silent while running, when achieved, or when
|
|
242
289
|
// the run predates the assessment (goalAchieved undefined).
|
|
290
|
+
// R39-M2 (fix-wave 6, EP3): if the tasks load above recovered from a corrupt
|
|
291
|
+
// tasks.json, surface the drop instead of a clean "completed 100%".
|
|
292
|
+
const corruptionReport = getTasksCorruptionReport(manifest.runId);
|
|
243
293
|
const goalLine = goalAchievedStatusLabel(manifest);
|
|
244
294
|
const lines = [
|
|
245
295
|
`Run: ${manifest.runId}`,
|
|
@@ -268,10 +318,10 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
268
318
|
...(tasks.length
|
|
269
319
|
? tasks.map(
|
|
270
320
|
(task) =>
|
|
271
|
-
`- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.agentProgress?.activityState ? ` activityState=${task.agentProgress.activityState}` : ""}${(() => {
|
|
321
|
+
`- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.agentProgress?.activityState ? ` activityState=${sanitizeStatusField(task.agentProgress.activityState)}` : ""}${(() => {
|
|
272
322
|
const t = extractCommandTrace(task.agentProgress?.recentTools);
|
|
273
323
|
return t.summary ? ` ${t.summary}` : "";
|
|
274
|
-
})()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}${task.failureCause ? ` failureCause=${task.failureCause}` : ""}`,
|
|
324
|
+
})()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${sanitizeStatusField(String(attentionByTask.get(task.id)?.data?.reason))}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${sanitizeStatusField(task.error)}` : ""}${task.failureCause ? ` failureCause=${sanitizeStatusField(task.failureCause)}` : ""}`,
|
|
275
325
|
)
|
|
276
326
|
: ["- (none)"]),
|
|
277
327
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
@@ -281,7 +331,13 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
281
331
|
`- observable=${effectiveness.observable}/${Math.max(1, effectiveness.completed)} completed tasks`,
|
|
282
332
|
`- workerExecution=${effectiveness.workerExecution} guard=${effectiveness.guardMode} severity=${effectiveness.severity}`,
|
|
283
333
|
`- noObservedWork=${effectiveness.noObservedWorkTaskIds.length ? effectiveness.noObservedWorkTaskIds.join(",") : "none"}`,
|
|
284
|
-
`- needsAttention=${
|
|
334
|
+
`- needsAttention=${needsAttentionIds.length ? needsAttentionIds.join(",") : "none"}`,
|
|
335
|
+
`- unresponsive=${externallyStuckTasks.length ? externallyStuckTasks.map((stuck) => `${stuck.taskId}(${stuck.detail})`).join(",") : "none"}`,
|
|
336
|
+
...(corruptionReport
|
|
337
|
+
? [
|
|
338
|
+
`⚠ State recovery: tasks.json was corrupt; reconstructed ${corruptionReport.reconstructedCount} task(s) from events${corruptionReport.droppedIds.length ? `, ${corruptionReport.droppedIds.length} DROPPED (${corruptionReport.droppedIds.join(",")})` : ""} — run is DEGRADED.`,
|
|
339
|
+
]
|
|
340
|
+
: []),
|
|
285
341
|
"Completion verification",
|
|
286
342
|
...(tasks.filter((t) => t.status === "completed").length
|
|
287
343
|
? tasks
|
|
@@ -316,7 +372,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
316
372
|
...(events.length
|
|
317
373
|
? events.map(
|
|
318
374
|
(event) =>
|
|
319
|
-
`- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`,
|
|
375
|
+
`- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${sanitizeStatusField(event.message)}` : ""}`,
|
|
320
376
|
)
|
|
321
377
|
: ["- (none)"]),
|
|
322
378
|
];
|
|
@@ -382,7 +438,7 @@ export function buildCompactStatus(
|
|
|
382
438
|
if (failedOrAttention.length > 0) {
|
|
383
439
|
lines.push("Issues:");
|
|
384
440
|
for (const t of failedOrAttention) {
|
|
385
|
-
lines.push(`- ${t.id} [${t.status}] ${t.role}: ${t.error
|
|
441
|
+
lines.push(`- ${t.id} [${t.status}] ${t.role}: ${t.error ? sanitizeStatusField(t.error) : "(no error detail)"}`);
|
|
386
442
|
}
|
|
387
443
|
}
|
|
388
444
|
lines.push("Tip: pass details=true for full output (task graph, agents, effectiveness, events).");
|
|
@@ -89,7 +89,11 @@ export function handleWorkflowCreate(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
89
89
|
return result(`workflow-create rejected: ${validationError}`, { action: "workflow-create", status: "error" }, true);
|
|
90
90
|
}
|
|
91
91
|
try {
|
|
92
|
-
|
|
92
|
+
// R11-F1 (fix-wave 6, M1-2): scope reads the canonical carrier
|
|
93
|
+
// `config.scope` (same source as the root-gate and every other
|
|
94
|
+
// scope-bearing handler), defaulting to project like handleImport —
|
|
95
|
+
// top-level `params.scope` no longer diverges from the gate's read.
|
|
96
|
+
const scope = (params.config?.scope === "user" ? "user" : "project") as "user" | "project";
|
|
93
97
|
const filePath = resolveWorkflowWritePath(ctx.cwd, name, scope);
|
|
94
98
|
writeFileSync(filePath, script, "utf-8");
|
|
95
99
|
return result(
|
|
@@ -1230,6 +1230,9 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
1230
1230
|
// actions (schedule mutations, goal start/force, user-scope config/settings/
|
|
1231
1231
|
// create/update, autonomy, settings theme) are reserved for the user's
|
|
1232
1232
|
// ROOT session. Non-root callers (workers) can still write project scope.
|
|
1233
|
+
// Wave-6 (R11-F1/R11-S1): scope resolves from config.scope only, and
|
|
1234
|
+
// workflow-create/save/delete are ALWAYS worker-gated (.dwf.ts ACE),
|
|
1235
|
+
// import/export at user scope.
|
|
1233
1236
|
// Single chokepoint: every domain router dispatch passes through here.
|
|
1234
1237
|
const rootGateReason = rootGateBlockReason(params);
|
|
1235
1238
|
if (rootGateReason) {
|
package/src/i18n.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isDangerousKey, stripDangerousKeysDeep } from "./config/config-validation.ts";
|
|
2
3
|
import { logInternalError } from "./utils/internal-error.ts";
|
|
3
4
|
|
|
4
5
|
type Params = Record<string, string | number>;
|
|
@@ -166,22 +167,17 @@ export function t(key: Key, params?: Params): string {
|
|
|
166
167
|
* @example
|
|
167
168
|
* addTranslations("vi", { "agent.requiresPrompt": "Agent cần prompt." })
|
|
168
169
|
*/
|
|
169
|
-
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
170
|
-
|
|
171
|
-
function stripDangerousKeys<T extends Record<string, unknown>>(obj: T): T {
|
|
172
|
-
const safe: Record<string, unknown> = {};
|
|
173
|
-
for (const key of Object.keys(obj)) {
|
|
174
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
175
|
-
safe[key] = obj[key];
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return safe as T;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
170
|
export function addTranslations(locale: string, bundle: Partial<Record<Key, string>>): void {
|
|
182
171
|
if (!locale) return;
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
// R46-7 (M1-1): the locale axis reaches `translations[locale]` — a
|
|
173
|
+
// reserved name (`__proto__`) resolves through the [[Prototype]] getter to
|
|
174
|
+
// Object.prototype, so the Object.assign branch would pollute it and the
|
|
175
|
+
// assignment branch would silently reassign [[Prototype]]. Reject reserved
|
|
176
|
+
// locale names and use own-property lookup (shared proto-safe helpers in
|
|
177
|
+
// config-validation.ts).
|
|
178
|
+
if (isDangerousKey(locale)) return;
|
|
179
|
+
const safeBundle = stripDangerousKeysDeep(bundle as Record<string, unknown>) as Partial<Record<Key, string>>;
|
|
180
|
+
const existing = Object.hasOwn(translations, locale) ? translations[locale] : undefined;
|
|
185
181
|
if (existing) {
|
|
186
182
|
Object.assign(existing, safeBundle);
|
|
187
183
|
} else {
|
|
@@ -199,6 +195,14 @@ export function listLocales(): string[] {
|
|
|
199
195
|
// --- Initialization ---
|
|
200
196
|
|
|
201
197
|
export function initI18n(pi: ExtensionAPI): () => void {
|
|
198
|
+
// R49-F3 (pi-core census): the `pi-core/i18n/*` channels below are a PHANTOM
|
|
199
|
+
// protocol in pi 0.77 and 1.0.4 — the resolved api literal + dist contain ZERO
|
|
200
|
+
// emitters/consumers for `pi-core/i18n/registerBundle`, `.../localeChanged` and
|
|
201
|
+
// `.../requestApi`, so `pi.events.on(...)` never fires and the locale stays
|
|
202
|
+
// locked to the default (`currentLocale` stays undefined → English/built-in
|
|
203
|
+
// fallback via t()). Harmless today (no runtime callers rely on a non-default
|
|
204
|
+
// locale); kept as forward-compat shims. Re-check before wiring any locale
|
|
205
|
+
// switch to this path.
|
|
202
206
|
try {
|
|
203
207
|
pi.events?.emit?.("pi-core/i18n/registerBundle", {
|
|
204
208
|
namespace,
|
package/src/prompt/inbox-poll.ts
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* `sinceTs` watermark — one message delivers once;
|
|
26
26
|
* - U5 (2026-10-10): dedup is ALSO durable across restarts — delivered
|
|
27
27
|
* rows are recorded in delivery.json (`messages[id]="acknowledged"` +
|
|
28
|
-
* `${direction}:${requestId}` index
|
|
28
|
+
* `${direction}:${recipientTaskId}:${requestId}` index — recipient-scoped,
|
|
29
|
+
* M3-9/R33-1) and re-read on the next poll, so a
|
|
29
30
|
* worker that restarts mid-run (fresh process, EMPTY seen-set) never
|
|
30
31
|
* re-delivers. A resend carrying the same requestId is dropped by the
|
|
31
32
|
* same index; duplicate rows under one requestId deliver exactly once.
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
readAllMailboxMessages,
|
|
45
46
|
readDeliveryState,
|
|
46
47
|
recordMailboxRequestIdDelivery,
|
|
48
|
+
requestIdDeliveryKey,
|
|
47
49
|
} from "../state/coordination/mailbox.ts";
|
|
48
50
|
import { appendEventFireAndForget } from "../state/event-log/event-log.ts";
|
|
49
51
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
@@ -201,7 +203,13 @@ export function pollWorkerInbox(pickup: WorkerInboxPickup, limit?: number): Mail
|
|
|
201
203
|
// was PREVENTED — audited via mailbox.pickup_deduped (once per key
|
|
202
204
|
// per process so the 500ms re-scan cannot spam the event log).
|
|
203
205
|
const requestId = messageIdempotencyKey(m);
|
|
204
|
-
|
|
206
|
+
// M3-9 / R33-1: the index key is RECIPIENT-scoped
|
|
207
|
+
// (`direction:recipient:requestId`). A group fan-out mints ONE requestId
|
|
208
|
+
// and reuses it for every recipient, so the pre-fix run-scoped key let the
|
|
209
|
+
// first acking recipient suppress every sibling at its own pickup
|
|
210
|
+
// (probe: 1 delivered / 0 / 0). Same-row duplicate protection is
|
|
211
|
+
// unaffected — that is the per-message `messages[id]` record below.
|
|
212
|
+
const dedupKey = requestId !== undefined ? requestIdDeliveryKey(m.direction, taskId, requestId) : undefined;
|
|
205
213
|
if (dedupKey !== undefined) {
|
|
206
214
|
// (direction, requestId) index — catches resends carrying the same
|
|
207
215
|
// requestId AND duplicate rows under one requestId.
|
|
@@ -258,6 +266,10 @@ export function acknowledgeWorkerInboxDelivery(
|
|
|
258
266
|
picked.map((m) => ({
|
|
259
267
|
direction: m.direction,
|
|
260
268
|
messageId: m.id,
|
|
269
|
+
// M3-9 / R33-1: record WHICH recipient consumed the row — the index
|
|
270
|
+
// key is recipient-scoped, so group fan-out siblings keep their own
|
|
271
|
+
// exactly-once records.
|
|
272
|
+
taskId,
|
|
261
273
|
requestId: messageIdempotencyKey(m),
|
|
262
274
|
})),
|
|
263
275
|
);
|
|
@@ -118,7 +118,7 @@ export interface MessageTool {
|
|
|
118
118
|
* the injected env object, never a raw process.env.PI_CREW_* member, so the
|
|
119
119
|
* check:env-vars gate stays green). */
|
|
120
120
|
export function shouldRegisterMessageTool(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
121
|
-
return
|
|
121
|
+
return getCrewEnv(PI_CREW_MSG_ENABLED_ENV, env) === "1";
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export function createMessageTool(deps: MessageToolDeps = {}): MessageTool {
|
|
@@ -385,7 +385,7 @@ export type AskToolDefinition = ToolDefinition<typeof AskParams, AskDetails>;
|
|
|
385
385
|
* reads the injected env object, never a raw process.env.PI_CREW_* member,
|
|
386
386
|
* so the check:env-vars gate stays green). */
|
|
387
387
|
export function shouldRegisterAskTool(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
388
|
-
return
|
|
388
|
+
return getCrewEnv(PI_CREW_ASK_ENABLED_ENV, env) === "1";
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
// ── T3/R5 (ADR-5 2026-08-17-governed-nesting §1): worker-side `delegate` tool ──
|
|
@@ -551,11 +551,33 @@ export function createDelegateTool(deps: DelegateToolDeps = {}): DelegateToolDef
|
|
|
551
551
|
const socketPath = get(PI_CREW_BROKER_SOCKET_ASK_ENV);
|
|
552
552
|
const token = get(PI_CREW_BROKER_TOKEN_ASK_ENV);
|
|
553
553
|
const runId = get(PI_CREW_BROKER_RUN_ID_ASK_ENV);
|
|
554
|
+
// delegate-auth: name the ACTUALLY-missing vars and distinguish the two
|
|
555
|
+
// no-creds shapes instead of a catch-all that blames all four:
|
|
556
|
+
// (a) identity present + socket/token absent — nested-host run: the
|
|
557
|
+
// spawning host is itself a child session, and child sessions
|
|
558
|
+
// never install a broker (issueForChild is root-session-only,
|
|
559
|
+
// lifecycle-handlers), so NO broker exists for this runId at any
|
|
560
|
+
// depth. (A root-spawned depth-2 worker DOES carry creds, so the
|
|
561
|
+
// old "depth-2 worker" label was wrong for this case.)
|
|
562
|
+
// (b) identity vars absent too — scaffold, mock, or partially-wired
|
|
563
|
+
// spawn; list exactly which vars are missing.
|
|
564
|
+
const missingVars: string[] = [];
|
|
565
|
+
if (!taskId) missingVars.push("PI_CREW_TASK_ID/PI_CREW_BROKER_TASK_ID");
|
|
566
|
+
if (!stateRoot) missingVars.push(PI_CREW_STATE_ROOT_ENV);
|
|
567
|
+
if (!runId) missingVars.push(PI_CREW_BROKER_RUN_ID_ASK_ENV);
|
|
568
|
+
if (!socketPath) missingVars.push(PI_CREW_BROKER_SOCKET_ASK_ENV);
|
|
569
|
+
if (!token) missingVars.push(PI_CREW_BROKER_TOKEN_ASK_ENV);
|
|
570
|
+
// direct-truthiness guard keeps TS narrowing for the client construction
|
|
571
|
+
// below (the missingVars[] indirection alone defeats control-flow analysis).
|
|
554
572
|
if (!taskId || !stateRoot || !socketPath || !token || !runId) {
|
|
573
|
+
const reason =
|
|
574
|
+
!socketPath && !token && taskId && stateRoot && runId
|
|
575
|
+
? "PI_CREW_BROKER_SOCKET / PI_CREW_BROKER_TOKEN absent while task-id/run-id/state-root are set — nested-host run: this worker's host is itself a child session, which never installs a broker, so no broker exists for this run"
|
|
576
|
+
: `${missingVars.join(" / ")} absent — scaffold, mock, or partially-wired spawn`;
|
|
555
577
|
return notice(
|
|
556
578
|
"unavailable",
|
|
557
579
|
"no-broker",
|
|
558
|
-
|
|
580
|
+
`[delegate] unavailable: no broker connection (${reason}) — do the work yourself; do not call delegate again.`,
|
|
559
581
|
);
|
|
560
582
|
}
|
|
561
583
|
const client = deps.makeBrokerClient
|
|
@@ -674,7 +696,7 @@ export function createDelegateTool(deps: DelegateToolDeps = {}): DelegateToolDef
|
|
|
674
696
|
* for EVERY role. Broker admission re-checks depth+slot from the task
|
|
675
697
|
* RECORD — the env is UX/hygiene, not the security boundary). */
|
|
676
698
|
export function shouldRegisterDelegateTool(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
677
|
-
return
|
|
699
|
+
return getCrewEnv(PI_CREW_DELEGATE_ENABLED_ENV, env) === "1";
|
|
678
700
|
}
|
|
679
701
|
|
|
680
702
|
/** Task 5 (§15.2): fence a sibling/group inbox message before it reaches the
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import * as fs from "node:fs";
|
|
31
31
|
import * as path from "node:path";
|
|
32
32
|
import { type Static, Type } from "@sinclair/typebox";
|
|
33
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
33
34
|
import { defineTool, type ExtensionAPI, type ToolDefinition } from "../extension/pi-api.ts";
|
|
34
35
|
import { EngineManager, type ExecuteResult } from "../runtime/scratchpad/engine.ts";
|
|
35
36
|
import { appendEventBuffered, appendEventFireAndForget } from "../state/event-log/event-log.ts";
|
|
@@ -77,8 +78,8 @@ function emitScratchpadMetric(
|
|
|
77
78
|
data: Record<string, unknown>,
|
|
78
79
|
appendEvent: typeof appendEventFireAndForget = appendEventFireAndForget,
|
|
79
80
|
): void {
|
|
80
|
-
const eventsPath = env
|
|
81
|
-
const runId = env
|
|
81
|
+
const eventsPath = getCrewEnv(PI_CREW_EVENTS_PATH_ENV, env);
|
|
82
|
+
const runId = getCrewEnv(PI_CREW_RUN_ID_ENV, env);
|
|
82
83
|
if (!eventsPath || !runId) return;
|
|
83
84
|
// Fire-and-forget semantics (H1): a throwing writer (sync or async) must
|
|
84
85
|
// never break the cell. The real appendEventFireAndForget already catches
|
|
@@ -87,7 +88,7 @@ function emitScratchpadMetric(
|
|
|
87
88
|
appendEventBuffered(eventsPath, {
|
|
88
89
|
type,
|
|
89
90
|
runId,
|
|
90
|
-
taskId:
|
|
91
|
+
taskId: getCrewEnv(PI_CREW_TASK_ID_ENV, env),
|
|
91
92
|
data,
|
|
92
93
|
}).catch((e) => logInternalError("scratchpad_lifecycle.buffered", e, `type=${type}`));
|
|
93
94
|
} catch {
|
|
@@ -224,10 +225,10 @@ export interface SnapshotEnvValidation {
|
|
|
224
225
|
* write to a guessed location.
|
|
225
226
|
*/
|
|
226
227
|
export function validateSnapshotEnv(env: NodeJS.ProcessEnv = process.env): SnapshotEnvValidation {
|
|
227
|
-
const taskId = env
|
|
228
|
-
const artifactsRoot = env
|
|
229
|
-
const snapshotPath = env
|
|
230
|
-
const attempt =
|
|
228
|
+
const taskId = getCrewEnv(PI_CREW_TASK_ID_ENV, env);
|
|
229
|
+
const artifactsRoot = getCrewEnv(PI_CREW_ARTIFACTS_ROOT_ENV, env);
|
|
230
|
+
const snapshotPath = getCrewEnv(PI_CREW_SCRATCHPAD_SNAPSHOT_ENV, env);
|
|
231
|
+
const attempt = getCrewEnv(PI_CREW_ATTEMPT_ENV, env) ?? "0";
|
|
231
232
|
|
|
232
233
|
if (!taskId) return { valid: false, reason: `missing ${PI_CREW_TASK_ID_ENV}`, attempt };
|
|
233
234
|
if (!artifactsRoot) return { valid: false, reason: `missing ${PI_CREW_ARTIFACTS_ROOT_ENV}`, attempt };
|
|
@@ -277,8 +278,8 @@ export interface RestoreEnvValidation {
|
|
|
277
278
|
* no restore, empty namespace, execute continues, message never leaks paths.
|
|
278
279
|
*/
|
|
279
280
|
export function validateRestoreEnv(env: NodeJS.ProcessEnv = process.env, restorePath: string): RestoreEnvValidation {
|
|
280
|
-
const taskId = env
|
|
281
|
-
const artifactsRoot = env
|
|
281
|
+
const taskId = getCrewEnv(PI_CREW_TASK_ID_ENV, env);
|
|
282
|
+
const artifactsRoot = getCrewEnv(PI_CREW_ARTIFACTS_ROOT_ENV, env);
|
|
282
283
|
if (!taskId) return { valid: false, reason: `missing ${PI_CREW_TASK_ID_ENV}` };
|
|
283
284
|
if (!artifactsRoot) return { valid: false, reason: `missing ${PI_CREW_ARTIFACTS_ROOT_ENV}` };
|
|
284
285
|
try {
|
|
@@ -299,7 +300,7 @@ export function validateRestoreEnv(env: NodeJS.ProcessEnv = process.env, restore
|
|
|
299
300
|
if (st.isSymbolicLink() || !st.isFile()) return { valid: false, reason: "restore-path-not-regular-file" };
|
|
300
301
|
if (st.size > SNAPSHOT_MAX_BYTES) return { valid: false, reason: "restore-path-over-cap" };
|
|
301
302
|
// (4) mtime pin (swap-detection hint — forgeable, never authn).
|
|
302
|
-
const pinned = env
|
|
303
|
+
const pinned = getCrewEnv(PI_CREW_SCRATCHPAD_RESTORE_MTIME_ENV, env);
|
|
303
304
|
if (pinned) {
|
|
304
305
|
const pinnedMs = Number.parseFloat(pinned);
|
|
305
306
|
if (Number.isFinite(pinnedMs) && Math.abs(st.mtimeMs - pinnedMs) > RESTORE_MTIME_TOLERANCE_MS) {
|
|
@@ -483,7 +484,7 @@ export function createExecuteTool(engine: EngineManager, deps: Partial<Scratchpa
|
|
|
483
484
|
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
484
485
|
const env = deps.env ?? process.env;
|
|
485
486
|
// 1. Dormant check (lớp 2 — defense in depth behind the registration gate).
|
|
486
|
-
if (
|
|
487
|
+
if (getCrewEnv(PI_CREW_SCRATCHPAD_ENV, env) !== "1") {
|
|
487
488
|
throw new Error("scratchpad is dormant");
|
|
488
489
|
}
|
|
489
490
|
// 2. F12: terminal shutdown state — a distinct system error, not "wedged".
|
|
@@ -650,7 +651,7 @@ export function registerScratchpadLifecycle(pi: ExtensionAPI, options: Scratchpa
|
|
|
650
651
|
// Reset first: each register is a fresh worker session (a session reload
|
|
651
652
|
// re-arms restore via the env — NIT-2a, module-scope is intentional).
|
|
652
653
|
const env = options.env ?? process.env;
|
|
653
|
-
const restorePath = env
|
|
654
|
+
const restorePath = getCrewEnv(PI_CREW_SCRATCHPAD_RESTORE_ENV, env);
|
|
654
655
|
if (restorePath) {
|
|
655
656
|
const m = restorePath.match(/\.attempt-(\d+)\.snapshot\.json$/);
|
|
656
657
|
restorePending = { path: restorePath, attempt: m ? Number.parseInt(m[1], 10) : 0 };
|
|
@@ -676,5 +677,5 @@ export function registerScratchpadLifecycle(pi: ExtensionAPI, options: Scratchpa
|
|
|
676
677
|
* the single source of truth per F15.
|
|
677
678
|
*/
|
|
678
679
|
export function shouldRegisterScratchpadTool(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
679
|
-
return
|
|
680
|
+
return getCrewEnv(PI_CREW_SCRATCHPAD_ENV, env) === "1" && getCrewEnv(PI_CREW_KIND_ENV, env) === "subagent";
|
|
680
681
|
}
|