pi-crew 0.11.23 → 0.11.25
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/explorer.md +3 -3
- package/agents/librarian.md +3 -3
- package/agents/orchestrator.md +17 -0
- package/dist/index.mjs +567 -555
- package/{workflows/chain.workflow.md → docs/chain-workflow.md} +8 -0
- package/docs/dynamic-workflows.md +37 -2
- 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 +6 -4
- package/skills/read-only-explorer/SKILL.md +15 -5
- package/skills/real-test-pi-crew/SKILL.md +7 -7
- package/src/agents/discover-agents.ts +8 -2
- package/src/config/config-validation.ts +61 -1
- package/src/config/config.ts +90 -5
- package/src/config/env-allowlist-registry.ts +5 -0
- package/src/config/env-vars.ts +23 -13
- package/src/config/role-tools.ts +14 -11
- package/src/config/types.ts +4 -1
- package/src/extension/crew-cleanup.ts +6 -0
- package/src/extension/management.ts +7 -0
- package/src/extension/project-init.ts +19 -1
- package/src/extension/register.ts +83 -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/run-index.ts +24 -1
- 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 +268 -0
- package/src/extension/team-tool/dispatch/manage.ts +9 -1
- package/src/extension/team-tool/goal-wrap.ts +11 -1
- package/src/extension/team-tool/goal.ts +13 -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 +13 -0
- package/src/i18n.ts +18 -14
- package/src/observability/exporters/otlp-exporter.ts +11 -1
- package/src/prompt/inbox-poll.ts +14 -2
- package/src/prompt/message-tool.ts +1 -1
- package/src/prompt/prompt-runtime.ts +127 -21
- package/src/prompt/scratchpad-lifecycle.ts +14 -13
- package/src/prompt/surface-worker.ts +35 -9
- package/src/runtime/broker/crew-broker-client.ts +7 -0
- package/src/runtime/broker/crew-broker.ts +216 -242
- 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/connection-state.ts +159 -1
- package/src/runtime/broker/protocol/events-replay.ts +50 -10
- 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/child-pi-constants.ts +9 -0
- package/src/runtime/child-pi/child-pi-rpc.ts +72 -7
- package/src/runtime/child-pi/child-pi-streams.ts +19 -3
- package/src/runtime/child-pi/mock-fixtures.ts +12 -5
- package/src/runtime/child-pi/postmortem-registry.ts +8 -2
- package/src/runtime/classifier/ambient-noise-gate.ts +5 -1
- package/src/runtime/compaction/tool-output-pruner.ts +28 -5
- package/src/runtime/crew-agent-records.ts +67 -4
- package/src/runtime/custom-tools/submit-result-tool.ts +17 -0
- package/src/runtime/deadletter.ts +5 -1
- package/src/runtime/delegate-spawn.ts +61 -8
- package/src/runtime/dispatch-batch.ts +61 -14
- package/src/runtime/effectiveness.ts +55 -4
- package/src/runtime/finalize-run.ts +12 -1
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +12 -3
- package/src/runtime/goal-workflow/goal-evaluator.ts +30 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +60 -3
- package/src/runtime/live-session/live-agent-control.ts +123 -20
- package/src/runtime/live-session/live-agent-manager.ts +114 -15
- package/src/runtime/live-session/live-session-runtime.ts +75 -83
- package/src/runtime/live-session/usage-bridge.ts +66 -2
- package/src/runtime/mcp-proxy.ts +10 -0
- package/src/runtime/model/model-fallback.ts +74 -9
- package/src/runtime/model/pi-args.ts +3 -3
- package/src/runtime/model/runtime-policy.ts +22 -6
- package/src/runtime/model/runtime-resolver.ts +11 -5
- package/src/runtime/orphan-worker-registry.ts +64 -24
- package/src/runtime/pi-spawn.ts +4 -1
- package/src/runtime/process/orphan-reaper.ts +143 -0
- package/src/runtime/process-status.ts +38 -16
- package/src/runtime/recovery/crash-recovery.ts +35 -2
- 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/batch-barrier.ts +42 -5
- package/src/runtime/scheduling/run-coalesced-task-group.ts +82 -28
- package/src/runtime/scheduling/scheduler.ts +74 -29
- package/src/runtime/spawn-policy.ts +190 -8
- package/src/runtime/stale-reconciler.ts +77 -6
- package/src/runtime/subagent-manager.ts +29 -0
- package/src/runtime/surface/herdr-provider.ts +85 -2
- package/src/runtime/surface/launch-script.ts +7 -1
- package/src/runtime/surface/resolve-surface.ts +4 -1
- package/src/runtime/surface/tmux-provider.ts +32 -2
- package/src/runtime/task-output-context.ts +58 -11
- package/src/runtime/task-runner/child-executor.ts +3 -2
- package/src/runtime/task-runner/post-execution.ts +144 -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/runtime/workspace-lock.ts +52 -4
- package/src/state/atomic-write.ts +73 -4
- package/src/state/contracts.ts +9 -0
- package/src/state/coordination/mailbox.ts +40 -11
- package/src/state/coordination/process-lock.ts +50 -0
- package/src/state/event-log/cursor.ts +10 -1
- package/src/state/event-log/event-log-rotation.ts +54 -8
- package/src/state/event-log/poison-marker.ts +16 -1
- package/src/state/event-log/worker-atomic-writer.ts +5 -0
- package/src/state/stores/active-run-registry.ts +56 -5
- package/src/state/stores/artifact-store.ts +181 -6
- package/src/state/stores/manifest-io.ts +126 -8
- package/src/state/stores/ownership-map.ts +26 -7
- package/src/state/stores/plan-store.ts +30 -1
- package/src/state/stores/sqlite-run-state.ts +91 -1
- package/src/state/stores/state-store.ts +30 -31
- package/src/state/types.ts +18 -0
- package/src/state/usage.ts +41 -0
- package/src/ui/dashboard-panes/agents-pane.ts +4 -3
- package/src/ui/dashboard-panes/plan-pane.ts +5 -3
- package/src/ui/dashboard-panes/transcript-pane.ts +2 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +5 -3
- package/src/ui/run-dashboard.ts +9 -7
- package/src/ui/run-snapshot-cache.ts +3 -2
- package/src/ui/settings-overlay.ts +87 -11
- package/src/ui/theme-discovery.ts +26 -7
- package/src/ui/transcript-viewer.ts +5 -3
- 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/utils/visual.ts +84 -0
- package/src/workflows/discover-workflows.ts +9 -1
- package/src/worktree/worktree-manager.ts +128 -18
package/src/config/env-vars.ts
CHANGED
|
@@ -78,7 +78,10 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
78
78
|
name: "PI_CREW_DEPTH",
|
|
79
79
|
mirror: "PI_TEAMS_DEPTH",
|
|
80
80
|
mirrorPrecedence: "crew",
|
|
81
|
-
|
|
81
|
+
// wave-2b: default was the STRING "0" on an int parser — getCrewEnvParsed
|
|
82
|
+
// returned the raw string from the fallback, so typed readers (getCrewEnvInt)
|
|
83
|
+
// saw undefined instead of 0. Numeric default routes through the parser.
|
|
84
|
+
default: 0,
|
|
82
85
|
parser: "int",
|
|
83
86
|
doc: "current crew depth (pi-args.ts:72 reads CREW ?? TEAMS ?? '0')",
|
|
84
87
|
},
|
|
@@ -86,7 +89,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
86
89
|
name: "PI_TEAMS_DEPTH",
|
|
87
90
|
mirror: "PI_CREW_DEPTH",
|
|
88
91
|
mirrorPrecedence: "crew",
|
|
89
|
-
default: "0"
|
|
92
|
+
default: 0, // wave-2b: numeric default on int parser (was string "0")
|
|
90
93
|
parser: "int",
|
|
91
94
|
doc: "crew depth mirror (fallback of PI_CREW_DEPTH)",
|
|
92
95
|
},
|
|
@@ -274,7 +277,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
274
277
|
},
|
|
275
278
|
PI_CREW_DEBUG: {
|
|
276
279
|
name: "PI_CREW_DEBUG",
|
|
277
|
-
doc: "truthiness enables background debug logging (background-runner.ts:63)",
|
|
280
|
+
doc: "truthiness enables background debug logging (background-runner.ts:63); RAW truthiness — '0'/'false' still ENABLE, unset to disable (R45-F4)",
|
|
278
281
|
},
|
|
279
282
|
PI_CREW_DEBUG_BUDGET: {
|
|
280
283
|
name: "PI_CREW_DEBUG_BUDGET",
|
|
@@ -368,7 +371,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
368
371
|
},
|
|
369
372
|
PI_CREW_TRUST_PROJECT_DWF: {
|
|
370
373
|
name: "PI_CREW_TRUST_PROJECT_DWF",
|
|
371
|
-
doc: "'1' allows project-sourced .dwf.ts workflows (dynamic-workflow-runner.ts:
|
|
374
|
+
doc: "'1' allows project-sourced .dwf.ts workflows (dynamic-workflow-runner.ts:189)",
|
|
372
375
|
},
|
|
373
376
|
PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS: {
|
|
374
377
|
name: "PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS",
|
|
@@ -377,7 +380,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
377
380
|
},
|
|
378
381
|
PI_TEAMS_DEBUG: {
|
|
379
382
|
name: "PI_TEAMS_DEBUG",
|
|
380
|
-
doc: "truthiness gates debug-severity internal-error logging (internal-error.ts:
|
|
383
|
+
doc: "truthiness gates debug-severity internal-error logging (internal-error.ts:10); RAW truthiness — '0'/'false' still ENABLE, unset to disable (R45-F4)",
|
|
381
384
|
},
|
|
382
385
|
PI_TEAMS_MOCK_CHILD_PI: {
|
|
383
386
|
name: "PI_TEAMS_MOCK_CHILD_PI",
|
|
@@ -508,7 +511,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
508
511
|
PI_CREW_VERIFY_GATE_TIMEOUT_MS: {
|
|
509
512
|
name: "PI_CREW_VERIFY_GATE_TIMEOUT_MS",
|
|
510
513
|
parser: "int",
|
|
511
|
-
default: "120000"
|
|
514
|
+
default: 120_000, // wave-2b: numeric default (was string "120000" — fallback bypassed the parser)
|
|
512
515
|
doc: "U13 per-check timeout in ms for the deterministic verify_gate pre-gate commands. Invalid/≤0 → 120000, mirroring runPhaseGates (runtime/verification/verify-gate.ts resolveVerifyGateTimeoutMs)",
|
|
513
516
|
},
|
|
514
517
|
PI_CREW_VERIFY_DETERMINISM: {
|
|
@@ -523,7 +526,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
523
526
|
PI_CREW_RPC_TURN_TIMEOUT_MS: {
|
|
524
527
|
name: "PI_CREW_RPC_TURN_TIMEOUT_MS",
|
|
525
528
|
parser: "int",
|
|
526
|
-
default: "600000"
|
|
529
|
+
default: 600_000, // wave-2b: numeric default (was string "600000" — fallback bypassed the parser)
|
|
527
530
|
doc: "W7/DR1 belt-and-suspenders turn timeout for the EXPERIMENTAL RPC worker transport: max ms a prompt-accepted turn may run without agent_settled before the transport fails with the early-failure shape (DR5 guard still blocks the stdio fallback once a session event was seen). Env-only while RPC stays experimental. Invalid/≤0 → 600000 (rpc-worker.ts resolveRpcTurnTimeoutMs)",
|
|
528
531
|
},
|
|
529
532
|
PI_CREW_AUTO_EXIT: {
|
|
@@ -533,13 +536,13 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
533
536
|
PI_CREW_AUTO_PRUNE_KEEP: {
|
|
534
537
|
name: "PI_CREW_AUTO_PRUNE_KEEP",
|
|
535
538
|
parser: "int",
|
|
536
|
-
default: "10"
|
|
539
|
+
default: 10, // wave-2b: numeric default (was string "10" — fallback bypassed the parser)
|
|
537
540
|
doc: "DP-01: number of most-recent finished runs the session-start auto-prune retains (was hard-coded 10). Invalid/negative → 10 with a warning (run-maintenance.ts:resolveAutoPruneKeep)",
|
|
538
541
|
},
|
|
539
542
|
PI_CREW_AUTO_PRUNE_AGE_FLOOR_HOURS: {
|
|
540
543
|
name: "PI_CREW_AUTO_PRUNE_AGE_FLOOR_HOURS",
|
|
541
544
|
parser: "int",
|
|
542
|
-
default: "24"
|
|
545
|
+
default: 24, // wave-2b: numeric default (was string "24" — fallback bypassed the parser)
|
|
543
546
|
doc: "DP-01: auto-prune never deletes a finished run younger than this many hours, even beyond top-keep — evidence/incident runs survive a session restart (0 disables; run-maintenance.ts:resolveAutoPruneAgeFloorMs)",
|
|
544
547
|
},
|
|
545
548
|
PI_CREW_SURFACE: {
|
|
@@ -763,18 +766,25 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
763
766
|
* Raw crew-env lookup. Returns the raw string (undefined when unset) with
|
|
764
767
|
* mirror-alias fallback applied per the entry's `mirrorPrecedence`. This is
|
|
765
768
|
* the behavior-preserving replacement for `process.env.PI_CREW_X` reads.
|
|
769
|
+
*
|
|
770
|
+
* R26-F1 (deep-review round 26): `env` is an OPTIONAL source override. Reads
|
|
771
|
+
* that receive an env object as a parameter (`isRootSession(env)`,
|
|
772
|
+
* `currentCrewDepth(env)`, …) must still route through the registry — passing
|
|
773
|
+
* the object here keeps the registry invariant intact for those sites instead
|
|
774
|
+
* of leaving them as gate-blind `env.PI_CREW_X` dot reads. Defaults to
|
|
775
|
+
* `process.env`, so existing call sites are unchanged.
|
|
766
776
|
*/
|
|
767
|
-
export function getCrewEnv(name: string): string | undefined {
|
|
777
|
+
export function getCrewEnv(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
768
778
|
const spec = CREW_ENV_VARS[name];
|
|
769
779
|
if (!spec?.mirror || spec.mirrorPrecedence === "or") {
|
|
770
|
-
return
|
|
780
|
+
return env[name];
|
|
771
781
|
}
|
|
772
782
|
const crewName = name.startsWith("PI_TEAMS_") ? spec.mirror : name;
|
|
773
783
|
const teamsName = name.startsWith("PI_TEAMS_") ? name : spec.mirror;
|
|
774
784
|
if (spec.mirrorPrecedence === "teams") {
|
|
775
|
-
return
|
|
785
|
+
return env[teamsName] ?? env[crewName];
|
|
776
786
|
}
|
|
777
|
-
return
|
|
787
|
+
return env[crewName] ?? env[teamsName];
|
|
778
788
|
}
|
|
779
789
|
|
|
780
790
|
/**
|
package/src/config/role-tools.ts
CHANGED
|
@@ -17,18 +17,21 @@ export interface RoleToolConfig {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
20
|
-
// Explorer - Read-only exploration
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// Explorer - Read-only exploration. Wave-3 (Chain A cheapest cut, deep review
|
|
21
|
+
// 2026-10-16/17): bash REMOVED from the enforced allowlist — the advertised
|
|
22
|
+
// read-only contract is now the enforceable one (READ_ONLY_ROLES state-mutation
|
|
23
|
+
// gating cannot stop host-level persistence via arbitrary commands, e.g.
|
|
24
|
+
// `git config --global` or writes to ~/.pi/agent). Frontmatter mirrors this.
|
|
24
25
|
explorer: {
|
|
25
|
-
tools: ["read", "grep", "find", "ls", "glob", "
|
|
26
|
-
excludeTools: ["edit", "write", "web"],
|
|
26
|
+
tools: ["read", "grep", "find", "ls", "glob", "ask"],
|
|
27
|
+
excludeTools: ["edit", "write", "bash", "web"],
|
|
27
28
|
},
|
|
28
29
|
|
|
29
|
-
// Analyst - Read and analyze, limited execution
|
|
30
|
+
// Analyst - Read and analyze, limited execution. (ask_question removed
|
|
31
|
+
// 2026-10-18: the host tool is `ask` — excluding a nonexistent name was a
|
|
32
|
+
// dead entry; the live `ask` tool stays allowed for analyst.)
|
|
30
33
|
analyst: {
|
|
31
|
-
excludeTools: ["edit", "write"
|
|
34
|
+
excludeTools: ["edit", "write"],
|
|
32
35
|
},
|
|
33
36
|
|
|
34
37
|
// Planner - Read-only planning; emits plans as TEXT (runner persists result).
|
|
@@ -40,7 +43,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
40
43
|
// default/implementation workflows.
|
|
41
44
|
planner: {
|
|
42
45
|
tools: ["read", "grep", "find", "ls", "glob", "ask"],
|
|
43
|
-
excludeTools: ["edit", "write", "bash", "web"
|
|
46
|
+
excludeTools: ["edit", "write", "bash", "web"],
|
|
44
47
|
},
|
|
45
48
|
|
|
46
49
|
// Critic - Read-only plan/design critique (F2: was missing from the map,
|
|
@@ -66,7 +69,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
66
69
|
// Writer - Documentation focused
|
|
67
70
|
writer: {
|
|
68
71
|
tools: ["read", "edit", "write", "ls", "ask"],
|
|
69
|
-
excludeTools: ["bash", "web"
|
|
72
|
+
excludeTools: ["bash", "web"],
|
|
70
73
|
},
|
|
71
74
|
|
|
72
75
|
// Security Reviewer - Strict restrictions
|
|
@@ -75,7 +78,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
75
78
|
// resolved at runtime (returned {}), silently dropping enforcement.
|
|
76
79
|
"security-reviewer": {
|
|
77
80
|
tools: ["read", "grep", "find", "ask"],
|
|
78
|
-
excludeTools: ["edit", "write", "bash", "web"
|
|
81
|
+
excludeTools: ["edit", "write", "bash", "web"],
|
|
79
82
|
},
|
|
80
83
|
|
|
81
84
|
// Verifier - Runs tests (needs bash) but must NOT edit source (F4: moved
|
package/src/config/types.ts
CHANGED
|
@@ -386,7 +386,10 @@ export interface CrewReliabilityConfig {
|
|
|
386
386
|
retryPolicy?: CrewRetryPolicyConfig;
|
|
387
387
|
autoRecover?: boolean;
|
|
388
388
|
deadletterThreshold?: number;
|
|
389
|
-
/** Interval (ms) for periodic stale-run auto-repair. Default
|
|
389
|
+
/** Interval (ms) for periodic stale-run auto-repair. Default 300_000 (5min) —
|
|
390
|
+
* the canonical value lives in the `?? 300_000` fallback at
|
|
391
|
+
* `src/extension/registration/observability.ts:256` (RT-F2 raised it from 60s;
|
|
392
|
+
* R45-F2). Set to 0 to disable. */
|
|
390
393
|
autoRepairIntervalMs?: number;
|
|
391
394
|
/** Remove /tmp/pi-crew-* directories after their orphaned runs are reconciled. Default: true. */
|
|
392
395
|
cleanupOrphanedTempDirs?: boolean;
|
|
@@ -79,6 +79,12 @@ export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTermina
|
|
|
79
79
|
// suppression; on REAL shutdowns (quit/reload) the flag is unset
|
|
80
80
|
// and this cleanup still drains the registry, as does the
|
|
81
81
|
// process-exit hook (tryRegisterSessionCleanup) when pi dies.
|
|
82
|
+
// R49-F6: this gate is a SWITCH FLAG (`isSessionSwitchInFlight()`), not the
|
|
83
|
+
// reason-aware gate used by lifecycle-handlers.ts:165-166. On a `reload` shutdown
|
|
84
|
+
// the flag can still read true (it is cleared on the next session_start), so
|
|
85
|
+
// this handler may skip a cleanup the reason-gate would have run — the
|
|
86
|
+
// process-exit hook (tryRegisterSessionCleanup) is the backstop. Divergence
|
|
87
|
+
// is deliberate (never kill workers on a navigational switch) but not obvious.
|
|
82
88
|
if (isSessionSwitchInFlight()) return;
|
|
83
89
|
// Kill all child-pi processes
|
|
84
90
|
await cleanupChildProcesses();
|
|
@@ -15,6 +15,7 @@ import { projectCrewRoot, userPiRoot } from "../utils/paths.ts";
|
|
|
15
15
|
import { allWorkflows, discoverWorkflows, invalidateWorkflowDiscoveryCache } from "../workflows/discover-workflows.ts";
|
|
16
16
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
17
17
|
import { serializeWorkflow } from "../workflows/workflow-serializer.ts";
|
|
18
|
+
import { assertRootOnlyAction } from "./team-tool/destructive-gate.ts";
|
|
18
19
|
import { enforceDestructiveIntent } from "./team-tool/intent-policy.ts";
|
|
19
20
|
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
20
21
|
import { type PiTeamsToolResult, toolResult } from "./tool-result.ts";
|
|
@@ -415,6 +416,12 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
415
416
|
if (parsed.error) return result(parsed.error, "error", true);
|
|
416
417
|
const cfg = parsed.value!;
|
|
417
418
|
const current = resolved.resource!;
|
|
419
|
+
// Wave-3 root-gate (R5-M-B2): the update path resolves its write scope from
|
|
420
|
+
// the MATCHED resource (params.scope may be undefined and match either
|
|
421
|
+
// tier), so the facade gate cannot decide — gate HERE on the resolved
|
|
422
|
+
// source. Non-root sessions may only rewrite project-tier resources.
|
|
423
|
+
const updateGate = assertRootOnlyAction("update", undefined, current.source === "project" ? "project" : "user");
|
|
424
|
+
if (updateGate) return result(updateGate.message, "error", true);
|
|
418
425
|
const nextName = hasOwn(cfg, "name") ? sanitizeName(String(cfg.name ?? "")) : current.name;
|
|
419
426
|
if (!nextName) return result("config.name is invalid after sanitization.", "error", true);
|
|
420
427
|
const source = current.source === "project" ? "project" : "user";
|
|
@@ -21,6 +21,13 @@ export interface ProjectInitResult {
|
|
|
21
21
|
configScope: "global" | "project" | "none";
|
|
22
22
|
configCreated: boolean;
|
|
23
23
|
configSkipped: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* F13-A: set when an EXPLICIT `overwrite: true` replaced an existing config
|
|
26
|
+
* file. The previous content is preserved at this path — an overwrite of the
|
|
27
|
+
* global (`~/.pi/agent/pi-crew.json`) or project config is destructive and may
|
|
28
|
+
* discard user-customized settings, so it must never be silent.
|
|
29
|
+
*/
|
|
30
|
+
configBackupPath?: string;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
function ensureDir(dir: string, createdDirs: string[]): void {
|
|
@@ -112,10 +119,20 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
|
|
|
112
119
|
|
|
113
120
|
let configCreated = false;
|
|
114
121
|
let configSkipped = false;
|
|
122
|
+
let configBackupPath: string | undefined;
|
|
115
123
|
if (configPath) {
|
|
116
124
|
if (configScope === "project") ensureDir(path.dirname(configPath), createdDirs);
|
|
117
125
|
else fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
118
|
-
|
|
126
|
+
const configExists = fs.existsSync(configPath);
|
|
127
|
+
if (!configExists) {
|
|
128
|
+
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
|
|
129
|
+
configCreated = true;
|
|
130
|
+
} else if (options.overwrite === true) {
|
|
131
|
+
// F13-A: overwriting an existing config is destructive — it may hold the
|
|
132
|
+
// user's own settings (global scope writes ~/.pi/agent/pi-crew.json).
|
|
133
|
+
// Keep the previous content and report it instead of replacing silently.
|
|
134
|
+
configBackupPath = `${configPath}.bak-${Date.now()}`;
|
|
135
|
+
fs.copyFileSync(configPath, configBackupPath);
|
|
119
136
|
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
|
|
120
137
|
configCreated = true;
|
|
121
138
|
} else {
|
|
@@ -166,5 +183,6 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
|
|
|
166
183
|
configScope,
|
|
167
184
|
configCreated,
|
|
168
185
|
configSkipped,
|
|
186
|
+
...(configBackupPath ? { configBackupPath } : {}),
|
|
169
187
|
};
|
|
170
188
|
}
|
|
@@ -24,6 +24,7 @@ import { startRuntimeWarmup } from "../runtime/model/runtime-warmup.ts";
|
|
|
24
24
|
import { primePeerDep } from "../runtime/peer-dep.ts";
|
|
25
25
|
import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
|
|
26
26
|
import { installChildProcessAbortShield } from "../utils/child-process-shield.ts";
|
|
27
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
27
28
|
import { resetTimings, time } from "../utils/timings.ts";
|
|
28
29
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
29
30
|
import { registerContextStatusInjection } from "./context-status-injection.ts";
|
|
@@ -75,6 +76,21 @@ export async function registerPiTeams(pi: ExtensionAPI): Promise<void> {
|
|
|
75
76
|
);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Wave-3 (attack-chain A / R2-M-B1 hardening): hermeticWorkers is DEFAULT
|
|
80
|
+
// ON; runtime.hermeticWorkers=false is a deliberate trust downgrade (worker
|
|
81
|
+
// pi sessions load ambient extensions/skills — the parity state where a
|
|
82
|
+
// repo-planted worker can reach the `team` tool's user-tier persistence).
|
|
83
|
+
// Emit ONE prominent warning at extension init so the operator sees it.
|
|
84
|
+
try {
|
|
85
|
+
if (loadConfig(process.cwd()).config.runtime?.hermeticWorkers === false) {
|
|
86
|
+
console.warn(
|
|
87
|
+
"[pi-crew] ⚠ SECURITY: runtime.hermeticWorkers=false — workers will load ambient extensions/skills (non-default trust posture). Worker sessions can then persist user-tier config/agents (every future session). Re-enable hermetic spawns unless parity mode is specifically required.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
/* config read failures surface through their own channel */
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
startRuntimeWarmup();
|
|
79
95
|
primePeerDep().catch(() => undefined);
|
|
80
96
|
deployBundledThemes();
|
|
@@ -82,56 +98,93 @@ export async function registerPiTeams(pi: ExtensionAPI): Promise<void> {
|
|
|
82
98
|
const ctx = buildRegistrationContext(pi);
|
|
83
99
|
ctx.importCrashRecovery = importCrashRecovery;
|
|
84
100
|
ctx.purgeStaleActiveRunIndexSyncIfLoaded = purgeStaleActiveRunIndexSyncIfLoaded;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
|
|
102
|
+
// R49-F5 (wave-6 M4-R2) — registrar guard. The registrars below run in a
|
|
103
|
+
// flat sequence; before this guard ANY throw aborted the remaining
|
|
104
|
+
// registrars and left a PARTIAL extension with no diagnostic (Pi's loader
|
|
105
|
+
// discards the rejection). Guard each call: on throw, record the subsystem
|
|
106
|
+
// name, log it through the internal-error channel, and CONTINUE — extension
|
|
107
|
+
// load must never break the session, but a failed subsystem must be
|
|
108
|
+
// visible. ONE consolidated warning is surfaced before the skill check.
|
|
109
|
+
const registrarFailures: string[] = [];
|
|
110
|
+
const withRegistrar = <T>(name: string, fn: () => T): T | undefined => {
|
|
111
|
+
try {
|
|
112
|
+
return fn();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
115
|
+
registrarFailures.push(`${name} (${detail})`);
|
|
116
|
+
logInternalError(`register.${name}`, err, "extension registration subsystem");
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
withRegistrar("runtimeCleanup", () => installRuntimeCleanup(pi, ctx));
|
|
122
|
+
withRegistrar("foregroundRunController", () => installForegroundRunController(pi, ctx));
|
|
123
|
+
withRegistrar("subagentManager", () => installSubagentManager(pi, ctx));
|
|
124
|
+
withRegistrar("lazyConfigurers", () => installLazyConfigurers(pi, ctx));
|
|
125
|
+
withRegistrar("crossExtensionWiring", () => installCrossExtensionWiring(pi, ctx));
|
|
90
126
|
|
|
91
127
|
time("register.policy");
|
|
92
|
-
registerAutonomousPolicy(pi);
|
|
93
|
-
registerKnowledgeInjection(pi);
|
|
128
|
+
withRegistrar("autonomousPolicy", () => registerAutonomousPolicy(pi));
|
|
129
|
+
withRegistrar("knowledgeInjection", () => registerKnowledgeInjection(pi));
|
|
94
130
|
|
|
95
|
-
registerPiTools(pi, ctx);
|
|
96
|
-
registerPiCommands(pi, ctx);
|
|
97
|
-
installPiHooks(pi, ctx);
|
|
98
|
-
installSessionLifecycleHandlers(pi, ctx);
|
|
131
|
+
withRegistrar("piTools", () => registerPiTools(pi, ctx));
|
|
132
|
+
withRegistrar("piCommands", () => registerPiCommands(pi, ctx));
|
|
133
|
+
withRegistrar("piHooks", () => installPiHooks(pi, ctx));
|
|
134
|
+
withRegistrar("sessionLifecycleHandlers", () => installSessionLifecycleHandlers(pi, ctx));
|
|
99
135
|
// Phase 0 inter-pi broker: install the lifecycle controller immediately
|
|
100
136
|
// after the session handlers. The controller's gate (broker.enabled AND
|
|
101
137
|
// root-session only) decides whether anything is actually done; for
|
|
102
138
|
// subagents or when the flag is off, it returns a no-op controller.
|
|
103
|
-
ctx.brokerController = installCrewBrokerLifecycleController(pi, ctx);
|
|
139
|
+
ctx.brokerController = withRegistrar("crewBrokerLifecycle", () => installCrewBrokerLifecycleController(pi, ctx));
|
|
104
140
|
|
|
105
141
|
// M3-1 (UI-AUDIT P0-2/P1-1): wire the tab-title + Ghostty progress
|
|
106
142
|
// controller to runEventBus. Before this the controller was never
|
|
107
143
|
// constructed (dead since v0.8.3). Registers the dispose hook that the
|
|
108
144
|
// SIGTERM/SIGHUP handler calls (crew-cleanup.ts).
|
|
109
|
-
installTerminalStatus(pi, ctx);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
withRegistrar("terminalStatus", () => installTerminalStatus(pi, ctx));
|
|
146
|
+
withRegistrar("compactionGuard", () =>
|
|
147
|
+
registerCompactionGuard(pi, {
|
|
148
|
+
foregroundControllers: ctx.foregroundControllers,
|
|
149
|
+
foregroundTeamRunControllers: ctx.foregroundTeamRunControllers,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
114
152
|
|
|
115
153
|
if (process.env.CREW_RESILIENT_EDIT === "1") {
|
|
154
|
+
// R49-F5: do NOT swallow the outcome. `wrapEditWithResilientReplace`
|
|
155
|
+
// returns false when the hook was NOT registered (pi-diff present or the
|
|
156
|
+
// host lacks the tool_result seam) and the dynamic import can reject;
|
|
157
|
+
// both were previously discarded, so an operator who set
|
|
158
|
+
// CREW_RESILIENT_EDIT=1 got a silent no-op.
|
|
116
159
|
// LAZY: resilient-edit only loads when the opt-in env var is set.
|
|
117
160
|
import("../runtime/resilient-edit.ts")
|
|
118
|
-
.then(({ wrapEditWithResilientReplace }) =>
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
.then(({ wrapEditWithResilientReplace }) => {
|
|
162
|
+
if (!wrapEditWithResilientReplace(pi)) {
|
|
163
|
+
console.warn(
|
|
164
|
+
"[pi-crew] CREW_RESILIENT_EDIT=1 but the resilient-edit wrapper did NOT register " +
|
|
165
|
+
"(pi-diff detected or the host lacks the tool_result seam) — the opt-in is a no-op this session.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch((err: unknown) => {
|
|
170
|
+
logInternalError("register.resilientEdit", err, "CREW_RESILIENT_EDIT=1");
|
|
171
|
+
console.warn(`[pi-crew] resilient-edit failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
121
172
|
});
|
|
122
173
|
}
|
|
123
174
|
|
|
124
|
-
registerCrewMessageRenderers(pi);
|
|
125
|
-
registerCrewInputRouter(pi);
|
|
126
|
-
registerCrewShortcuts(pi);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
withRegistrar("crewMessageRenderers", () => registerCrewMessageRenderers(pi));
|
|
176
|
+
withRegistrar("crewInputRouter", () => registerCrewInputRouter(pi));
|
|
177
|
+
withRegistrar("crewShortcuts", () => registerCrewShortcuts(pi));
|
|
178
|
+
withRegistrar("contextStatusInjection", () =>
|
|
179
|
+
registerContextStatusInjection(pi, {
|
|
180
|
+
enabled: loadConfig(process.cwd()).config.reliability?.ambientStatusInjection !== false,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
130
183
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
console.warn(
|
|
184
|
+
withRegistrar("crewVibes", () => registerCrewVibes(pi));
|
|
185
|
+
|
|
186
|
+
if (registrarFailures.length > 0) {
|
|
187
|
+
console.warn(`[pi-crew] ${registrarFailures.length} subsystem(s) failed to register: ${registrarFailures.join(", ")}`);
|
|
135
188
|
}
|
|
136
189
|
|
|
137
190
|
const skillCheck = await runPostInitSkillCheck(process.cwd());
|
|
@@ -5,33 +5,69 @@ import { CLEANUP_MARKER_FILE, cleanupOldArtifacts, pruneExpiredArtifacts } from
|
|
|
5
5
|
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
6
6
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
7
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
8
|
+
import { resolveContainedRelativePath } from "../../utils/safe-paths.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* ST-10:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* ST-10 + R44-N1: artifact descriptors for ONE run, paired with the run's
|
|
12
|
+
* HOST-DERIVED artifacts root (containment anchor). The anchor is computed
|
|
13
|
+
* from the runs directory being scanned (`<crewRoot>/state/runs/<runId>` →
|
|
14
|
+
* `<crewRoot>/artifacts/<runId>` — the same convention `createRunPaths`
|
|
15
|
+
* derives), NEVER from the worker-writable manifest itself: a malicious
|
|
16
|
+
* manifest must not be able to nominate its own prune boundary.
|
|
13
17
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
export interface RunArtifactGroup {
|
|
19
|
+
runId: string;
|
|
20
|
+
/** Host-derived per-run artifacts root (prune containment anchor). */
|
|
21
|
+
artifactsRoot: string;
|
|
22
|
+
descriptors: ArtifactDescriptor[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ST-10: Load artifact descriptors from all manifests in a runs directory,
|
|
27
|
+
* grouped per run with the host-derived artifacts root for each run.
|
|
28
|
+
* Best-effort: unreadable or corrupt manifests are silently skipped. Runs
|
|
29
|
+
* whose DIRECTORY NAME cannot form a safe artifacts root (path-escape) are
|
|
30
|
+
* skipped with a warn (fail-closed — their descriptors are never pruned).
|
|
31
|
+
*/
|
|
32
|
+
export function collectArtifactDescriptors(runsDir: string): RunArtifactGroup[] {
|
|
33
|
+
const groups: RunArtifactGroup[] = [];
|
|
16
34
|
let dirs: fs.Dirent[];
|
|
17
35
|
try {
|
|
18
36
|
dirs = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
19
37
|
} catch {
|
|
20
|
-
return
|
|
38
|
+
return groups;
|
|
21
39
|
}
|
|
40
|
+
// runsDir = <crewRoot>/<state.runsSubdir> → the crew root sits two levels up
|
|
41
|
+
// (mirrors state-store.ts validateRunManifestPaths' baseRoot derivation).
|
|
42
|
+
const crewRoot = path.resolve(runsDir, "..", "..");
|
|
43
|
+
const artifactsParent = path.join(crewRoot, DEFAULT_PATHS.state.artifactsSubdir);
|
|
22
44
|
for (const dir of dirs) {
|
|
23
45
|
if (!dir.isDirectory()) continue;
|
|
46
|
+
let artifactsRoot: string;
|
|
47
|
+
try {
|
|
48
|
+
artifactsRoot = resolveContainedRelativePath(artifactsParent, dir.name, "runId");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// R44-N1 fail-closed: an unsafe run-dir name must not reach the prune
|
|
51
|
+
// path at all — its descriptors are dropped, not anchored elsewhere.
|
|
52
|
+
logInternalError(
|
|
53
|
+
"artifact-cleanup.run-anchor",
|
|
54
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
55
|
+
`runsDir=${runsDir} runId=${dir.name}`,
|
|
56
|
+
"warn",
|
|
57
|
+
);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
24
60
|
const manifestPath = path.join(runsDir, dir.name, DEFAULT_PATHS.state.manifestFile);
|
|
25
61
|
try {
|
|
26
62
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest;
|
|
27
63
|
if (Array.isArray(manifest.artifacts)) {
|
|
28
|
-
|
|
64
|
+
groups.push({ runId: dir.name, artifactsRoot, descriptors: manifest.artifacts });
|
|
29
65
|
}
|
|
30
66
|
} catch {
|
|
31
67
|
// Skip unreadable/corrupt manifests — best-effort.
|
|
32
68
|
}
|
|
33
69
|
}
|
|
34
|
-
return
|
|
70
|
+
return groups;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
export function runArtifactCleanup(cwd: string): void {
|
|
@@ -49,9 +85,14 @@ export function runArtifactCleanup(cwd: string): void {
|
|
|
49
85
|
markerFile: CLEANUP_MARKER_FILE,
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
// ST-10:
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
// ST-10 + R44-N1: enforce retention/expiresAt from manifest metadata,
|
|
89
|
+
// per-run and containment-anchored — a run's descriptors can only prune
|
|
90
|
+
// files under that run's OWN host-derived artifacts root.
|
|
91
|
+
for (const scope of [userCrewRoot(), projectCrewRoot(cwd)]) {
|
|
92
|
+
for (const group of collectArtifactDescriptors(path.join(scope, DEFAULT_PATHS.state.runsSubdir))) {
|
|
93
|
+
pruneExpiredArtifacts(group.descriptors, { artifactsRoot: group.artifactsRoot });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
55
96
|
} catch (error) {
|
|
56
97
|
logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
|
|
57
98
|
}
|
|
@@ -99,8 +99,10 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
99
99
|
description: "Open the pi-crew interactive menu (list/run/status/cleanup/manage resources/doctor)",
|
|
100
100
|
handler: handleTeamManagerCommand,
|
|
101
101
|
});
|
|
102
|
-
// Backward-compat alias:
|
|
103
|
-
//
|
|
102
|
+
// Backward-compat alias: the interactive menu originally shipped under a
|
|
103
|
+
// runId-targeted name; `/team-cleanup` itself was NEVER registered (R27-D1) —
|
|
104
|
+
// the live names are `/team-manager` and this `/team-cleanup-menu` alias, so
|
|
105
|
+
// docs must not advertise `/team-cleanup <runId>`. Keep both.
|
|
104
106
|
|
|
105
107
|
pi.registerCommand("team-cleanup-menu", {
|
|
106
108
|
description: "Alias for /team-manager (pi-crew interactive menu)",
|
|
@@ -218,6 +220,11 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
218
220
|
// LAZY: load withSessionId only when needed for skill-create command
|
|
219
221
|
const { withSessionId } = await import("../../team-tool/context.ts");
|
|
220
222
|
const sessionId = withSessionId(ctx);
|
|
223
|
+
// R49-F7: `ctx.workspaceFolder` is a PHANTOM read — it is a VS-Code-era
|
|
224
|
+
// field that pi's ExtensionCommandContext does not define (absent in the
|
|
225
|
+
// resolved 0.77/1.0.4 api literal), so the optional chain always falls back
|
|
226
|
+
// to `process.cwd()`. Harmless (the fallback is correct) but do not treat
|
|
227
|
+
// this branch as a real cwd override.
|
|
221
228
|
const cwd =
|
|
222
229
|
(
|
|
223
230
|
ctx as unknown as {
|
|
@@ -80,6 +80,9 @@ function installResourcesDiscoverHook(pi: ExtensionAPI, ctx: RegistrationContext
|
|
|
80
80
|
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
81
81
|
});
|
|
82
82
|
} catch {
|
|
83
|
+
// R49-F4: `pi.on()` does NOT throw for an unknown event name in pi 0.77/1.0.4
|
|
84
|
+
// (verified against the resolved api literal + dist) — this catch is defensive
|
|
85
|
+
// only. The real degradation on an older host is that the hook NEVER FIRES.
|
|
83
86
|
/* older Pi without resources_discover */
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -76,6 +76,8 @@ function installTurnReconcileHook(pi: ExtensionAPI, ctx: RegistrationContext): v
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
} catch {
|
|
79
|
+
// R49-F4: `pi.on()` never throws on an unknown event (pi 0.77/1.0.4) — this
|
|
80
|
+
// catch is defensive; the real older-host degradation is a NEVER-FIRING hook.
|
|
79
81
|
/* older Pi without before_agent_start — rely on the interval alone */
|
|
80
82
|
}
|
|
81
83
|
}
|