pi-crew 0.9.67 → 0.10.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/CHANGELOG.md +172 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65847 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +9 -6
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +36 -14
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/scratchpad/README.md +10 -5
- package/src/runtime/scratchpad/guest.ts +103 -2
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -19,13 +19,15 @@ import * as path from "node:path";
|
|
|
19
19
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { loadConfig } from "../../config/config.ts";
|
|
21
21
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
22
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
22
23
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
|
|
23
24
|
import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker/broker-issuer.ts";
|
|
24
25
|
import { CrewBroker } from "../../runtime/broker/crew-broker.ts";
|
|
25
26
|
import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
|
|
26
27
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
27
28
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
28
|
-
import {
|
|
29
|
+
import { configuredModelInfosFromPiConfig } from "../../runtime/model/model-fallback.ts";
|
|
30
|
+
import { cleanupLegacyOrphanTempDirs, cleanupOrphanTempDirs, currentCrewDepth, resolveCrewMaxDepth } from "../../runtime/model/pi-args.ts";
|
|
29
31
|
import { clearProviderQuotaCache, noteProviderResponse } from "../../runtime/model/provider-quota.ts";
|
|
30
32
|
import { noteSessionModel, noteSessionThinking, resolveProviderForResponse } from "../../runtime/model/session-model.ts";
|
|
31
33
|
import { cleanupOrphanWorkers } from "../../runtime/orphan-worker-registry.ts";
|
|
@@ -33,7 +35,7 @@ import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts"
|
|
|
33
35
|
import { CrewScheduler, type ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
34
36
|
import { tryRegisterSessionCleanup } from "../../runtime/session-resources.ts";
|
|
35
37
|
import { createSessionSnapshot } from "../../runtime/session-snapshot.ts";
|
|
36
|
-
import {
|
|
38
|
+
import { applyCrewSettingsTiersToConfig, loadCrewSettingsTiers } from "../../runtime/settings-store.ts";
|
|
37
39
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
38
40
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
39
41
|
import { summarizeHeartbeats } from "../../ui/heartbeat-aggregator.ts";
|
|
@@ -215,8 +217,18 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
215
217
|
}, 0);
|
|
216
218
|
|
|
217
219
|
const loadedConfig = loadConfig(extensionCtx.cwd);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
// Wave 2B (P1 security): crew settings load as TIERS — the project-tier
|
|
221
|
+
// <cwd>/.pi/crew-settings.json is untrusted (a cloned repo can ship it)
|
|
222
|
+
// and now goes through sanitizeProjectConfig + tighten-only guard
|
|
223
|
+
// tiering instead of being applied raw over the sanitized config (the
|
|
224
|
+
// pre-2B `loadCrewSettings` + `applyCrewSettingsToConfig` bypass).
|
|
225
|
+
// See src/runtime/settings-store.ts for the tier pipeline + the
|
|
226
|
+
// scheduledJobs/schedulingEnabled boundary decision.
|
|
227
|
+
const crewSettingsTiers = loadCrewSettingsTiers(extensionCtx.cwd);
|
|
228
|
+
const settingsWarnings = applyCrewSettingsTiersToConfig(loadedConfig.config, crewSettingsTiers);
|
|
229
|
+
if (settingsWarnings.length > 0) {
|
|
230
|
+
(loadedConfig.warnings ??= []).push(...settingsWarnings);
|
|
231
|
+
}
|
|
220
232
|
|
|
221
233
|
// Start scheduler with event-based executor
|
|
222
234
|
const sessionId =
|
|
@@ -229,14 +241,20 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
229
241
|
// Wire scheduler into handle-schedule.ts so handlers can add/list jobs.
|
|
230
242
|
// EXT-9: module-scoped setter (was globalThis[Symbol.for(...)]).
|
|
231
243
|
registerCrewScheduler(ctx.crewScheduler);
|
|
232
|
-
// Load scheduled jobs from settings if present
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
// Load scheduled jobs from settings if present.
|
|
245
|
+
// BOUNDARY (Wave B2): project-tier scheduledJobs are OPT-IN GATED — the
|
|
246
|
+
// registration loop reads the gated `effectiveScheduledJobs` view (user
|
|
247
|
+
// jobs always; project jobs ONLY when the user-tier global file has BOTH
|
|
248
|
+
// schedulingEnabled:true AND allowProjectScheduledJobs:true).
|
|
249
|
+
// <cwd>/.pi/crew-settings.json remains the persistence store for the
|
|
250
|
+
// user's own `crew schedule add/update/remove` commands (handle-schedule.ts),
|
|
251
|
+
// so crew-schedule users must set both flags in ~/.pi/crew-settings.json.
|
|
252
|
+
// `schedulingEnabled` is user-tier-only (project values always dropped).
|
|
253
|
+
for (const job of crewSettingsTiers.effectiveScheduledJobs) {
|
|
254
|
+
try {
|
|
255
|
+
ctx.crewScheduler.add(job as ScheduledJob);
|
|
256
|
+
} catch {
|
|
257
|
+
/* skip invalid */
|
|
240
258
|
}
|
|
241
259
|
}
|
|
242
260
|
ctx.autoRecoveryLast.clear();
|
|
@@ -540,6 +558,44 @@ export function filterManifestsForHealthNotifications(
|
|
|
540
558
|
return manifests.filter((run) => !run.ownerSessionId || run.ownerSessionId === currentSessionId);
|
|
541
559
|
}
|
|
542
560
|
|
|
561
|
+
/**
|
|
562
|
+
* bug-026 sub-issue C: runEventBus event types that mark a run TERMINAL,
|
|
563
|
+
* across BOTH type namespaces seen on the bus — dotted `run.*` strings (kept
|
|
564
|
+
* for parity with classifyEventChannel's WORKER_LIFECYCLE_TYPES, see
|
|
565
|
+
* src/ui/run-event-bus.ts:31-44) and underscore `run_*` (the native
|
|
566
|
+
* RunEventType namespace actually emitted by team-runner, e.g.
|
|
567
|
+
* `run_completed`). Exported for unit testing.
|
|
568
|
+
*/
|
|
569
|
+
export const TERMINAL_RUN_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
570
|
+
"run.completed",
|
|
571
|
+
"run.failed",
|
|
572
|
+
"run.cancelled",
|
|
573
|
+
"run_completed",
|
|
574
|
+
"run_failed",
|
|
575
|
+
"run_cancelled",
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
/** True when a runEventBus event type marks the run terminal (either namespace). */
|
|
579
|
+
export function isTerminalRunEventType(type: string): boolean {
|
|
580
|
+
return TERMINAL_RUN_EVENT_TYPES.has(type);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Pure filter: drop `runId` from a preloaded-manifest frame (bug-026 sub-issue C eviction). */
|
|
584
|
+
export function evictRunFromManifests(manifests: TeamRunManifest[], runId: string): TeamRunManifest[] {
|
|
585
|
+
return manifests.filter((m) => m.runId !== runId);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Apply a runEventBus payload to a preloaded-manifest frame: evict the run on
|
|
590
|
+
* terminal events, pass through unchanged otherwise. This is the exact logic
|
|
591
|
+
* wired into the setupRenderLoop `runEventBus.onAny` subscription (bug-026
|
|
592
|
+
* sub-issue C); exported so tests can exercise it end-to-end against the
|
|
593
|
+
* real bus without spinning up the full render loop.
|
|
594
|
+
*/
|
|
595
|
+
export function applyTerminalRunEventToManifests(manifests: TeamRunManifest[], event: { type: string; runId: string }): TeamRunManifest[] {
|
|
596
|
+
return isTerminalRunEventType(event.type) ? evictRunFromManifests(manifests, event.runId) : manifests;
|
|
597
|
+
}
|
|
598
|
+
|
|
543
599
|
function setupRenderLoop(
|
|
544
600
|
pi: ExtensionAPI,
|
|
545
601
|
ctx: RegistrationContext,
|
|
@@ -803,6 +859,12 @@ function setupRenderLoop(
|
|
|
803
859
|
// re-renders within debounceMs of any agent lifecycle event.
|
|
804
860
|
const sched = ctx.renderScheduler;
|
|
805
861
|
const unsubscribeRunEvents = runEventBus.onAny((event) => {
|
|
862
|
+
// bug-026 sub-issue C: evict terminal runs from the preloaded manifest
|
|
863
|
+
// frame so a stale "running" entry cannot persist for the session
|
|
864
|
+
// lifetime. Additive to the renderTick GATE 1 / FIX #1 / GATE 2 snapshot
|
|
865
|
+
// purges (which invalidate the snapshot cache) — those gates do not
|
|
866
|
+
// touch lastPreloadedManifests itself.
|
|
867
|
+
lastPreloadedManifests = applyTerminalRunEventToManifests(lastPreloadedManifests, event);
|
|
806
868
|
sched.schedule({
|
|
807
869
|
runId: event.runId,
|
|
808
870
|
source: "runEventBus",
|
|
@@ -905,7 +967,7 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
905
967
|
|
|
906
968
|
function effectiveEnabled(): boolean {
|
|
907
969
|
// Env wins over config. PI_CREW_BROKER=1 forces on, =0 forces off.
|
|
908
|
-
const envOverride =
|
|
970
|
+
const envOverride = getCrewEnv("PI_CREW_BROKER");
|
|
909
971
|
if (envOverride === "0") return false;
|
|
910
972
|
// Config block: read fresh so a runtime config update takes effect.
|
|
911
973
|
try {
|
|
@@ -934,7 +996,23 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
934
996
|
starting = (async () => {
|
|
935
997
|
const cfg = (() => {
|
|
936
998
|
try {
|
|
937
|
-
|
|
999
|
+
// B1 battery 2026-08-18 (third config-layer bug): loadConfig()
|
|
1000
|
+
// WITHOUT cwd reads only the user config — the workspace
|
|
1001
|
+
// .crew/config.json (where broker.waitMethodsEnabled:true would
|
|
1002
|
+
// live) was never merged, so the flag stayed default-false even
|
|
1003
|
+
// after the parser + merge fixes. Pass the SAME cwd the broker
|
|
1004
|
+
// itself uses below.
|
|
1005
|
+
return loadConfig(process.cwd()).config.broker;
|
|
1006
|
+
} catch {
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
})();
|
|
1010
|
+
// T3/R5 (ADR-5 §10): governed-nesting + limits config for the delegate
|
|
1011
|
+
// surface — same cwd discipline as the broker block above.
|
|
1012
|
+
const nestingCfg = (() => {
|
|
1013
|
+
try {
|
|
1014
|
+
const c = loadConfig(process.cwd()).config;
|
|
1015
|
+
return { nesting: c.nesting, limits: c.limits };
|
|
938
1016
|
} catch {
|
|
939
1017
|
return undefined;
|
|
940
1018
|
}
|
|
@@ -944,6 +1022,32 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
944
1022
|
socketPath: getBrokerSocketPath(sessionId),
|
|
945
1023
|
maxFrameBytes: cfg?.maxFrameBytes ?? 262144,
|
|
946
1024
|
outboundQueueCap: cfg?.outboundQueueCap ?? 256,
|
|
1025
|
+
// WP-2 review round 1 (P1): thread the capability gate into the
|
|
1026
|
+
// PRODUCTION broker — the constructor default (false) made
|
|
1027
|
+
// config.broker.waitMethodsEnabled a dead knob and the ADR-0
|
|
1028
|
+
// "then true" flip a silent no-op. Fail-closed when unset.
|
|
1029
|
+
waitMethodsEnabled: cfg?.waitMethodsEnabled ?? false,
|
|
1030
|
+
// T3/R5 (ADR-5 §10): governed-nesting capability gate — fail-closed
|
|
1031
|
+
// default; production threads config.nesting (sensitive: user config
|
|
1032
|
+
// only). Nested-slot sizing + admission-time model catalog (ADR-5 §7 —
|
|
1033
|
+
// the production wiring MUST supply it) + workspace gate mirror.
|
|
1034
|
+
nestingEnabled: nestingCfg?.nesting?.enabled ?? false,
|
|
1035
|
+
...(nestingCfg?.nesting?.maxSlots !== undefined ? { nestingMaxSlots: nestingCfg.nesting.maxSlots } : {}),
|
|
1036
|
+
...(nestingCfg?.nesting?.maxDepth !== undefined ? { nestingMaxDepth: nestingCfg.nesting.maxDepth } : {}),
|
|
1037
|
+
// ADR-5 §12: enabling the sensitive USER-config-only flag IS the manual
|
|
1038
|
+
// trust decision for the escalation surface.
|
|
1039
|
+
nestingTrustedEscalation: nestingCfg?.nesting?.enabled === true,
|
|
1040
|
+
...(nestingCfg?.limits?.maxConcurrentWorkers !== undefined
|
|
1041
|
+
? { globalWorkerSemaphore: nestingCfg.limits.maxConcurrentWorkers }
|
|
1042
|
+
: {}),
|
|
1043
|
+
serializeOnPathOverlap: nestingCfg?.limits?.serializeOnPathOverlap ?? false,
|
|
1044
|
+
modelCatalog: () => {
|
|
1045
|
+
try {
|
|
1046
|
+
return configuredModelInfosFromPiConfig(process.cwd()).map((info) => info.fullId);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
947
1051
|
enabled: true,
|
|
948
1052
|
cwd: process.cwd(),
|
|
949
1053
|
});
|
|
@@ -960,10 +1064,17 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
960
1064
|
return starting!;
|
|
961
1065
|
}
|
|
962
1066
|
|
|
963
|
-
const issueForChild = async (runId: string, taskId?: string): Promise<BrokerSpawnCredentials | undefined> => {
|
|
1067
|
+
const issueForChild = async (runId: string, taskId?: string, childDepth?: number): Promise<BrokerSpawnCredentials | undefined> => {
|
|
964
1068
|
if (!runId || typeof runId !== "string") return undefined;
|
|
965
1069
|
if (!isRootSession(process.env)) return undefined;
|
|
966
1070
|
if (!effectiveEnabled()) return undefined;
|
|
1071
|
+
// ADR-5 §4 (governed nesting): tokens are minted ONLY for children that
|
|
1072
|
+
// may themselves delegate — childDepth < resolved maxDepth. At the default
|
|
1073
|
+
// maxDepth=2 a delegate-spawned depth-2 grandchild gets NO credentials
|
|
1074
|
+
// (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at depth 2; identity
|
|
1075
|
+
// routing via PI_CREW_BROKER_RUN_ID/TASK_ID is threaded unconditionally
|
|
1076
|
+
// elsewhere). Undefined childDepth = legacy worker spawn (depth 1).
|
|
1077
|
+
if (childDepth !== undefined && childDepth >= resolveCrewMaxDepth(undefined)) return undefined;
|
|
967
1078
|
const sessionId = cachedSessionId;
|
|
968
1079
|
if (!sessionId) return undefined;
|
|
969
1080
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "@sinclair/typebox";
|
|
3
4
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
@@ -28,14 +29,18 @@ import type { BatchBarrier } from "../../runtime/scheduling/batch-barrier.ts";
|
|
|
28
29
|
import {
|
|
29
30
|
readPersistedSubagentRecord,
|
|
30
31
|
type SubagentManager,
|
|
32
|
+
type SubagentRecord,
|
|
31
33
|
type SubagentSpawnOptions,
|
|
32
34
|
savePersistedSubagentRecord,
|
|
33
35
|
} from "../../runtime/subagent-manager.ts";
|
|
36
|
+
import { TEAM_TERMINAL_TASK_STATUSES } from "../../state/contracts.ts";
|
|
37
|
+
import { resolveEntryBySubagentId, upsertOwnershipEntry } from "../../state/stores/ownership-map.ts";
|
|
34
38
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
35
39
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
36
40
|
import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
|
|
37
41
|
import { agentToolRenderer, type ToolRenderContext } from "../../ui/tool-renderers/index.ts";
|
|
38
42
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
43
|
+
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
39
44
|
import {
|
|
40
45
|
__test__subagentSpawnParams,
|
|
41
46
|
formatSubagentRecord,
|
|
@@ -140,8 +145,55 @@ export function registerSubagentTools(
|
|
|
140
145
|
// by the Agent tool have proper session ownership for isolation.
|
|
141
146
|
const ctxWithSession = withSessionId(ctx);
|
|
142
147
|
spawnOptions.ownerSessionId = ctxWithSession.sessionId;
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
// WP-1/R1 (H6): the one-shot identity link. taskId is NOT knowable at
|
|
149
|
+
// spawn() time — handleTeamTool(action:"run") resolves only after the
|
|
150
|
+
// run is dispatched (scaffold/mock returns immediately; real runs at
|
|
151
|
+
// completion), and runId comes from the result details. So the link is
|
|
152
|
+
// written in the runner wrapper once the manifest resolves: the record
|
|
153
|
+
// gains taskId/depth, the persisted copy is refreshed, and the
|
|
154
|
+
// ownership-map entry (task ⇄ subagentId ⇄ artifactsDir) is upserted
|
|
155
|
+
// under the run lock. Best-effort: a failure logs but never throws
|
|
156
|
+
// into the spawn path. `spawnedRecord` is assigned right after spawn()
|
|
157
|
+
// returns; the runner's continuation runs on a later microtask, so the
|
|
158
|
+
// assignment is always visible when the identity-link block executes.
|
|
159
|
+
let spawnedRecord: SubagentRecord | undefined;
|
|
160
|
+
const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => {
|
|
161
|
+
// WP-1/R1 (H6 fix, review round): mid-run steer requires the identity
|
|
162
|
+
// link AT DISPATCH TIME, not after completion. `handleTeamTool` awaits
|
|
163
|
+
// the full run; the only mid-run signal we have is `onRunStarted`
|
|
164
|
+
// (fired in run.ts with updatedManifest.runId as the run starts). Hook
|
|
165
|
+
// it so the subagentId leg of the ownership map lands while the worker
|
|
166
|
+
// is live — steer_subagent can then resolve taskId mid-run instead of
|
|
167
|
+
// saying "not linked". The completion-time block below remains as a
|
|
168
|
+
// fallback for runs that complete between dispatch and this hook.
|
|
169
|
+
const linkAtDispatch = (runId?: string): void => {
|
|
170
|
+
if (typeof runId !== "string" || !spawnedRecord || spawnedRecord.taskId) return;
|
|
171
|
+
try {
|
|
172
|
+
const loaded = loadRunManifestById(currentOptions.cwd, runId);
|
|
173
|
+
const taskId = loaded?.tasks[0]?.id;
|
|
174
|
+
if (taskId && loaded) {
|
|
175
|
+
spawnedRecord.taskId = taskId;
|
|
176
|
+
// runId too: the manager only back-fills record.runId from the
|
|
177
|
+
// awaited result (waitForRun — i.e. at COMPLETION for background
|
|
178
|
+
// runs). Linking it here keeps taskId+runId landing together at
|
|
179
|
+
// dispatch so mid-run consumers (steer) resolve both immediately.
|
|
180
|
+
spawnedRecord.runId = runId;
|
|
181
|
+
spawnedRecord.depth = currentOptions.depth ?? 0;
|
|
182
|
+
savePersistedSubagentRecord(currentOptions.cwd, spawnedRecord);
|
|
183
|
+
upsertOwnershipEntry(loaded.manifest, {
|
|
184
|
+
taskId,
|
|
185
|
+
runId,
|
|
186
|
+
subagentId: spawnedRecord.id,
|
|
187
|
+
artifactsDir: loaded.manifest.artifactsRoot,
|
|
188
|
+
depth: spawnedRecord.depth,
|
|
189
|
+
updatedAt: new Date().toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logInternalError("subagent-tools.identity-dispatch", err, `runId=${runId ?? "(unknown)"}`);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const result = await handleTeamTool(
|
|
145
197
|
{
|
|
146
198
|
action: "run",
|
|
147
199
|
agent: currentOptions.type,
|
|
@@ -153,6 +205,7 @@ export function registerSubagentTools(
|
|
|
153
205
|
} as TeamToolParamsValue,
|
|
154
206
|
{
|
|
155
207
|
...ctxWithSession,
|
|
208
|
+
onRunStarted: (runId: string) => linkAtDispatch(runId), // mid-run capture (FIX)
|
|
156
209
|
signal: childSignal,
|
|
157
210
|
...(options.startForegroundRun
|
|
158
211
|
? {
|
|
@@ -162,7 +215,64 @@ export function registerSubagentTools(
|
|
|
162
215
|
: {}),
|
|
163
216
|
},
|
|
164
217
|
);
|
|
218
|
+
// WP-1/R1 (H6): identity link — record taskId/depth + ownership entry
|
|
219
|
+
// once the run manifest resolves. Direct-agent one-shot runs have
|
|
220
|
+
// exactly one task (direct-agent workflow); resolve from the manifest
|
|
221
|
+
// rather than hardcoding the id. The ownership upsert is verified and
|
|
222
|
+
// retried: a concurrently-completing run holds the run lock during its
|
|
223
|
+
// terminal writes, so a single upsert can lose the lock race
|
|
224
|
+
// (acquireLockWithRetry throws on a fresh foreign lock) and the
|
|
225
|
+
// store swallows that best-effort. taskId is already on the record at
|
|
226
|
+
// that point, so steer still resolves — but the map's subagentId leg
|
|
227
|
+
// would be missing for widget/status attribution. Ride out the
|
|
228
|
+
// contention window, then verify; still never throw.
|
|
229
|
+
let runId: string | undefined;
|
|
230
|
+
try {
|
|
231
|
+
const detailsRunId = (result.details as { runId?: unknown } | undefined)?.runId;
|
|
232
|
+
runId = typeof detailsRunId === "string" ? detailsRunId : undefined;
|
|
233
|
+
if (typeof runId === "string" && spawnedRecord) {
|
|
234
|
+
const loaded = loadRunManifestById(currentOptions.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
235
|
+
const taskId = loaded?.tasks[0]?.id;
|
|
236
|
+
// P3 hardening (T1 review round 1): base the completion-time retry on
|
|
237
|
+
// the MAP LEG, not on record.taskId. A dispatch-time upsert
|
|
238
|
+
// (linkAtDispatch) that lost the lock race would leave the map's
|
|
239
|
+
// subagentId leg missing; the old `!spawnedRecord.taskId` guard then
|
|
240
|
+
// skipped the verify+retry entirely. Now: write record fields once
|
|
241
|
+
// (idempotent), then verify+retry the MAP until the subagentId leg
|
|
242
|
+
// is present or budget exhausted. Upsert is merge-idempotent, so
|
|
243
|
+
// double-writing is safe.
|
|
244
|
+
if (taskId && loaded) {
|
|
245
|
+
if (!spawnedRecord.taskId) {
|
|
246
|
+
spawnedRecord.taskId = taskId;
|
|
247
|
+
spawnedRecord.depth = currentOptions.depth ?? 0;
|
|
248
|
+
savePersistedSubagentRecord(currentOptions.cwd, spawnedRecord);
|
|
249
|
+
}
|
|
250
|
+
const entry = {
|
|
251
|
+
taskId,
|
|
252
|
+
runId,
|
|
253
|
+
subagentId: spawnedRecord.id,
|
|
254
|
+
artifactsDir: loaded.manifest.artifactsRoot,
|
|
255
|
+
depth: spawnedRecord.depth,
|
|
256
|
+
updatedAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
259
|
+
try {
|
|
260
|
+
upsertOwnershipEntry(loaded.manifest, entry);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
logInternalError("subagent-tools.identity-link", err, `taskId=${taskId}, attempt=${attempt}`);
|
|
263
|
+
}
|
|
264
|
+
if (resolveEntryBySubagentId(loaded.manifest, spawnedRecord.id)?.taskId === taskId) break;
|
|
265
|
+
if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
logInternalError("subagent-tools.identity-link", err, `runId=${runId ?? "(unknown)"}`);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
165
274
|
const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
|
|
275
|
+
spawnedRecord = record;
|
|
166
276
|
// Rule 1: register batch membership so completions can be coalesced.
|
|
167
277
|
if (spawnOptions.batchId && spawnOptions.background) {
|
|
168
278
|
options.batchBarrier?.register(spawnOptions.batchId, record.id, { description: record.description, type: record.type });
|
|
@@ -365,28 +475,127 @@ export function registerSubagentTools(
|
|
|
365
475
|
name: "steer_subagent",
|
|
366
476
|
label: "Steer Agent",
|
|
367
477
|
description:
|
|
368
|
-
"Send a steering note to a running pi-crew subagent.
|
|
478
|
+
"Send a steering note to a running pi-crew subagent. Resolves the subagent record → ownership map → appends artifacts/steering/<taskId>.jsonl, which the child worker polls at its next turn boundary. Use team action=steer (runId+taskId+message) for run-level steering, or team cancel for interruption.",
|
|
369
479
|
parameters: Type.Object({
|
|
370
480
|
agent_id: Type.String(),
|
|
371
481
|
message: Type.String(),
|
|
372
482
|
}) as never,
|
|
373
483
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
374
484
|
const p = params as { agent_id?: string; message?: string };
|
|
485
|
+
if (!p.message?.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
|
|
375
486
|
const record = p.agent_id
|
|
376
487
|
? (subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id))
|
|
377
488
|
: undefined;
|
|
378
489
|
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
|
|
490
|
+
// P2.3: Cross-session ownership check — refuse to steer a record owned by
|
|
491
|
+
// a different session. Legacy records (no ownerSessionId) still pass.
|
|
492
|
+
const currentSessionId = withSessionId(ctx).sessionId;
|
|
493
|
+
if (record.ownerSessionId && record.ownerSessionId !== currentSessionId) {
|
|
494
|
+
return subagentToolResult("Agent belongs to another session.", {}, true);
|
|
495
|
+
}
|
|
496
|
+
// Resolve the live run's taskId + artifactsRoot: prefer the record's
|
|
497
|
+
// taskId link (set by the Agent-tool spawn route), falling back to the
|
|
498
|
+
// per-run ownership map (resolve by subagentId) when the record predates
|
|
499
|
+
// the link.
|
|
500
|
+
let taskId = record.taskId;
|
|
501
|
+
let artifactsRoot: string | undefined;
|
|
502
|
+
let loaded: ReturnType<typeof loadRunManifestById> | undefined;
|
|
503
|
+
if (record.runId) {
|
|
504
|
+
loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
505
|
+
if (loaded) {
|
|
506
|
+
artifactsRoot = loaded.manifest.artifactsRoot;
|
|
507
|
+
taskId ??= resolveEntryBySubagentId(loaded.manifest, record.id)?.taskId;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Back-compat: no taskId/link (legacy record or run never dispatched) →
|
|
511
|
+
// the existing "not linked" structured message, NO throw, NO file write.
|
|
512
|
+
if (!taskId || !artifactsRoot) {
|
|
513
|
+
return subagentToolResult(
|
|
514
|
+
[t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined]
|
|
515
|
+
.filter((line): line is string => Boolean(line))
|
|
516
|
+
.join("\n"),
|
|
517
|
+
{
|
|
518
|
+
agentId: record.id,
|
|
519
|
+
runId: record.runId,
|
|
520
|
+
status: record.status,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
// Real steer — append to artifacts/steering/<taskId>.jsonl, same writer util
|
|
525
|
+
// + JSONL schema as `team steer` (handleSteer, team-tool.ts):
|
|
526
|
+
// {type:"steer", message, ts}. The child worker polls this file every
|
|
527
|
+
// 500ms and picks the message up at its next turn boundary.
|
|
528
|
+
// Security review (T1/WP-1 round 1, security 1-3): mirror handleSteer's
|
|
529
|
+
// hardening — (a) T-S1 terminal-task refusal + taskId-belongs-to-manifest
|
|
530
|
+
// validation; (b) cap steering-file growth (unbounded files force a
|
|
531
|
+
// Buffer.alloc(fileSize) on the next worker's first poll and re-deliver
|
|
532
|
+
// every line to each incarnation — see P2 replay finding); (c) no throw.
|
|
533
|
+
if (!loaded) return subagentToolResult("Run manifest unavailable for steer.", {}, true);
|
|
534
|
+
const manifestTask = loaded.tasks.find((t) => t.id === taskId);
|
|
535
|
+
if (!manifestTask) {
|
|
536
|
+
return subagentToolResult(
|
|
537
|
+
`Task '${taskId}' not found in the owning run; cannot steer.`,
|
|
538
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
539
|
+
true,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (TEAM_TERMINAL_TASK_STATUSES.has(manifestTask.status)) {
|
|
543
|
+
return subagentToolResult(
|
|
544
|
+
`Task '${taskId}' is ${manifestTask.status}; cannot steer.`,
|
|
545
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
546
|
+
true,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const steeringDir = `${artifactsRoot}/steering`;
|
|
551
|
+
fs.mkdirSync(steeringDir, { recursive: true });
|
|
552
|
+
const safeSteeringPath = resolveRealContainedPath(steeringDir, `${taskId}.jsonl`);
|
|
553
|
+
// Security 3: cap file growth — refuse when the file exceeds a byte
|
|
554
|
+
// threshold (drop-oldest semantics are not feasible for a plain JSONL
|
|
555
|
+
// append; refuse loudly instead of poisoning the next incarnation).
|
|
556
|
+
const MAX_STEERING_BYTES = 256 * 1024;
|
|
557
|
+
let existingBytes = 0;
|
|
558
|
+
try {
|
|
559
|
+
existingBytes = fs.statSync(safeSteeringPath).size;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
562
|
+
}
|
|
563
|
+
const line = JSON.stringify({ type: "steer", message: p.message, ts: new Date().toISOString() }) + "\n";
|
|
564
|
+
if (existingBytes + Buffer.byteLength(line) > MAX_STEERING_BYTES) {
|
|
565
|
+
return subagentToolResult(
|
|
566
|
+
`Steering file for task '${taskId}' has reached its size cap (${(MAX_STEERING_BYTES / 1024).toFixed(0)}KiB); steer refused.`,
|
|
567
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
568
|
+
true,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
fs.appendFileSync(safeSteeringPath, line);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
// B1 battery 2026-08-18 (F7): this catch previously logged at DEBUG
|
|
574
|
+
// level (invisible unless PI_TEAMS_DEBUG) and then STILL returned the
|
|
575
|
+
// success message — a live probe showed "Steer delivered to task …"
|
|
576
|
+
// while the steering file stayed 0 bytes, undiagnosable from the
|
|
577
|
+
// outside. Fail LOUD: error-level log + isError result.
|
|
578
|
+
logInternalError(
|
|
579
|
+
"subagent-tools.steer-write-failed",
|
|
580
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
581
|
+
`taskId=${taskId} runId=${record.runId} artifactsRoot=${artifactsRoot}`,
|
|
582
|
+
"error",
|
|
583
|
+
);
|
|
584
|
+
return subagentToolResult(
|
|
585
|
+
`Steer write failed for task '${taskId}': ${err instanceof Error ? err.message : String(err)} (see pi-crew error log)`,
|
|
586
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
587
|
+
true,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
379
590
|
return subagentToolResult(
|
|
380
591
|
[
|
|
381
592
|
t("steer.noted", { id: record.id }),
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
]
|
|
385
|
-
.filter((line): line is string => Boolean(line))
|
|
386
|
-
.join("\n"),
|
|
593
|
+
`Steer delivered to task '${taskId}'; it will be picked up at the next turn boundary.`,
|
|
594
|
+
].join("\n"),
|
|
387
595
|
{
|
|
388
596
|
agentId: record.id,
|
|
389
597
|
runId: record.runId,
|
|
598
|
+
taskId,
|
|
390
599
|
status: record.status,
|
|
391
600
|
},
|
|
392
601
|
);
|
|
@@ -410,7 +619,7 @@ export function registerSubagentTools(
|
|
|
410
619
|
...steerSubagentTool,
|
|
411
620
|
name: "crew_agent_steer",
|
|
412
621
|
label: "Steer Crew Agent",
|
|
413
|
-
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
|
|
622
|
+
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name (same behavior as steer_subagent).",
|
|
414
623
|
};
|
|
415
624
|
const toolConfig = loadConfig(process.cwd()).config.tools;
|
|
416
625
|
const enableSteer = toolConfig?.enableSteer !== false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* HMAC-based origin signing for cross-extension RPC.
|
|
@@ -23,7 +24,7 @@ const SIGNATURE_VALIDITY_MS = 10 * 60 * 1000; // 10 min
|
|
|
23
24
|
|
|
24
25
|
/** Get the shared secret. Returns undefined if not configured. */
|
|
25
26
|
export function getRpcSecret(): string | undefined {
|
|
26
|
-
return
|
|
27
|
+
return getCrewEnv(SECRET_ENV_VAR);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Set the shared secret (for testing or programmatic setup). */
|
|
@@ -38,7 +39,8 @@ export function clearRpcSecret(): void {
|
|
|
38
39
|
|
|
39
40
|
/** Whether HMAC authentication is currently enabled. */
|
|
40
41
|
export function isHmacEnabled(): boolean {
|
|
41
|
-
|
|
42
|
+
const secret = getCrewEnv(SECRET_ENV_VAR);
|
|
43
|
+
return typeof secret === "string" && secret.length > 0;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// --- Types -------------------------------------------------------------------
|
|
@@ -142,7 +144,7 @@ export function withHmacVerification<P extends { requestId: string }>(
|
|
|
142
144
|
// Emit a soft info note (once per process) only when an actual RPC
|
|
143
145
|
// request is being processed — the prior registration-time warning
|
|
144
146
|
// was too noisy for users who never wire cross-extension RPC.
|
|
145
|
-
if (
|
|
147
|
+
if (getCrewEnv("PI_CREW_SUPPRESS_RPC_WARNING") !== "1") {
|
|
146
148
|
rpcHmacSoftInfoOnce();
|
|
147
149
|
}
|
|
148
150
|
return handler(params);
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
|
|
9
|
+
import { isTerminalTaskStatus } from "../../../state/contracts.ts";
|
|
9
10
|
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
10
11
|
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
11
|
-
import { saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
12
|
+
import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
13
|
+
import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
|
|
12
14
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
13
15
|
|
|
14
16
|
export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
@@ -31,28 +33,63 @@ export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
|
31
33
|
);
|
|
32
34
|
try {
|
|
33
35
|
return withRunLockSync(loaded.manifest, () => {
|
|
36
|
+
// R13-S1: fresh re-read INSIDE the lock (respond.ts:43 canonical pattern).
|
|
37
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, loaded.manifest.runId);
|
|
38
|
+
if (!fresh)
|
|
39
|
+
return result(
|
|
40
|
+
`Run '${loaded.manifest.runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
41
|
+
{
|
|
42
|
+
action: "api",
|
|
43
|
+
status: "error",
|
|
44
|
+
runId: loaded.manifest.runId,
|
|
45
|
+
},
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
const freshTask = fresh.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
49
|
+
if (!freshTask)
|
|
50
|
+
return result(
|
|
51
|
+
`Task '${taskId}' not found in run '${fresh.manifest.runId}'.`,
|
|
52
|
+
{
|
|
53
|
+
action: "api",
|
|
54
|
+
status: "error",
|
|
55
|
+
runId: fresh.manifest.runId,
|
|
56
|
+
},
|
|
57
|
+
true,
|
|
58
|
+
);
|
|
59
|
+
// R13-S1: never resurrect a task that became terminal on disk — a completed/
|
|
60
|
+
// failed task must not be flipped back to running by a stale heartbeat.
|
|
61
|
+
if (isTerminalTaskStatus(freshTask.status))
|
|
62
|
+
return result(
|
|
63
|
+
`Cannot write heartbeat for task '${freshTask.id}': task is in terminal state '${freshTask.status}'.`,
|
|
64
|
+
{
|
|
65
|
+
action: "api",
|
|
66
|
+
status: "error",
|
|
67
|
+
runId: fresh.manifest.runId,
|
|
68
|
+
},
|
|
69
|
+
true,
|
|
70
|
+
);
|
|
34
71
|
const heartbeat = touchWorkerHeartbeat(
|
|
35
|
-
|
|
36
|
-
workerId:
|
|
72
|
+
freshTask.heartbeat ?? {
|
|
73
|
+
workerId: freshTask.id,
|
|
37
74
|
lastSeenAt: new Date().toISOString(),
|
|
38
75
|
},
|
|
39
76
|
{
|
|
40
77
|
alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined,
|
|
41
78
|
},
|
|
42
79
|
);
|
|
43
|
-
const tasks =
|
|
44
|
-
saveRunTasks(
|
|
45
|
-
appendEvent(
|
|
80
|
+
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? { ...item, heartbeat } : item));
|
|
81
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
82
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
46
83
|
type: "worker.heartbeat",
|
|
47
|
-
runId:
|
|
48
|
-
taskId:
|
|
84
|
+
runId: fresh.manifest.runId,
|
|
85
|
+
taskId: freshTask.id,
|
|
49
86
|
data: { ...heartbeat },
|
|
50
87
|
});
|
|
51
88
|
return result(JSON.stringify(heartbeat, null, 2), {
|
|
52
89
|
action: "api",
|
|
53
90
|
status: "ok",
|
|
54
|
-
runId:
|
|
55
|
-
artifactsRoot:
|
|
91
|
+
runId: fresh.manifest.runId,
|
|
92
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
56
93
|
});
|
|
57
94
|
});
|
|
58
95
|
} catch (error) {
|
|
@@ -9,6 +9,7 @@ import { terminateLiveAgentsForRun } from "../../../runtime/live-session/live-ag
|
|
|
9
9
|
import { currentCrewRole, permissionForRole } from "../../../runtime/role-permission.ts";
|
|
10
10
|
import { withRunLock } from "../../../state/coordination/locks.ts";
|
|
11
11
|
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
12
|
+
import { getCurrentPlanRecord, setPlanApproval } from "../../../state/stores/plan-store.ts";
|
|
12
13
|
import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../../../state/stores/state-store.ts";
|
|
13
14
|
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
14
15
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
@@ -63,6 +64,10 @@ export const handleApprovePlan: ApiOperationHandler = async (hctx) => {
|
|
|
63
64
|
},
|
|
64
65
|
};
|
|
65
66
|
await saveRunManifestAsync(manifest);
|
|
67
|
+
// T2/R4 (ADR-4 §8): dual-write the record side — approval names the
|
|
68
|
+
// plan id+version. Pre-v2 runs without a PlanRecord skip silently.
|
|
69
|
+
const currentRecord = getCurrentPlanRecord(manifest);
|
|
70
|
+
if (currentRecord) setPlanApproval(manifest, { status: "approved", planVersion: currentRecord.version, by: "api" });
|
|
66
71
|
appendEvent(manifest.eventsPath, {
|
|
67
72
|
type: "plan.approved",
|
|
68
73
|
runId: manifest.runId,
|
|
@@ -140,6 +145,10 @@ export const handleCancelPlan: ApiOperationHandler = async (hctx) => {
|
|
|
140
145
|
},
|
|
141
146
|
};
|
|
142
147
|
await saveRunManifestAsync(manifest);
|
|
148
|
+
// T2/R4 (ADR-4 §8 vocabulary mapping): manifest gets "cancelled", the
|
|
149
|
+
// record side gets "rejected" (isPlanApprovalDenied accepts both).
|
|
150
|
+
const denyRecord = getCurrentPlanRecord(manifest);
|
|
151
|
+
if (denyRecord) setPlanApproval(manifest, { status: "rejected", planVersion: denyRecord.version, by: "api" });
|
|
143
152
|
saveRunTasks(manifest, tasks);
|
|
144
153
|
appendEvent(manifest.eventsPath, {
|
|
145
154
|
type: "plan.cancelled",
|