pi-crew 0.9.68 → 0.10.2
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 +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- 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 +68113 -60774
- 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 +4 -2
- package/schema.json +503 -91
- 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 +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -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 +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- 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 +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- 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-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- 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/detached-run-results.ts +90 -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 +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- 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 +32 -10
- 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 +36 -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/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- 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/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 +132 -1673
- 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 +66 -26
- 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 +139 -232
- package/src/state/types.ts +199 -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/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- 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/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- 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/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -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/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
|
@@ -147,9 +147,16 @@ function readProcStatus(pid) {
|
|
|
147
147
|
|
|
148
148
|
// ---------- fallback via ps (non-Linux) ----------
|
|
149
149
|
function readPs(pid) {
|
|
150
|
-
|
|
150
|
+
// state= makes zombie detection possible on BSD ps (macOS): a dead child
|
|
151
|
+
// stays listed by ps until reaped — without the state check, watch-parent
|
|
152
|
+
// (R4) never sees the watched PID die and live-warn (proc_died) never fires
|
|
153
|
+
// (CI incident: resource-sampler-audit.test.ts on macos-latest). Linux ps
|
|
154
|
+
// also supports state=. Z/X (and empty) = dead.
|
|
155
|
+
const res = spawnSync("ps", ["-o", "rss=,pcpu=,state=", "-p", String(pid)], { encoding: "utf8" });
|
|
151
156
|
if (res.status !== 0 || !res.stdout.trim()) return null;
|
|
152
157
|
const parts = res.stdout.trim().split(/\s+/);
|
|
158
|
+
const state = parts.length >= 3 ? parts[2] : "";
|
|
159
|
+
if (state.startsWith("Z") || state.startsWith("X")) return null;
|
|
153
160
|
return {
|
|
154
161
|
pid,
|
|
155
162
|
ppid: 0,
|
|
@@ -163,7 +170,34 @@ function readPs(pid) {
|
|
|
163
170
|
// ---------- child discovery ----------
|
|
164
171
|
function findDescendants(rootPid) {
|
|
165
172
|
// BFS over /proc to find all PIDs whose ppid chain leads to rootPid.
|
|
166
|
-
|
|
173
|
+
// Non-Linux (macOS/BSD): no /proc — build the ppid map from `ps -eo pid=,ppid=`
|
|
174
|
+
// instead (R3 on macos-latest CI: /proc-less platforms previously returned
|
|
175
|
+
// [rootPid] only, so descendants were never sampled).
|
|
176
|
+
if (!existsSync("/proc")) {
|
|
177
|
+
try {
|
|
178
|
+
const res = spawnSync("ps", ["-eo", "pid=,ppid="], { encoding: "utf8" });
|
|
179
|
+
if (res.status !== 0 || !res.stdout.trim()) return [rootPid];
|
|
180
|
+
const ppidOf = new Map();
|
|
181
|
+
for (const line of res.stdout.trim().split("\n")) {
|
|
182
|
+
const parts = line.trim().split(/\s+/);
|
|
183
|
+
if (parts.length >= 2) ppidOf.set(Number.parseInt(parts[0], 10), Number.parseInt(parts[1], 10));
|
|
184
|
+
}
|
|
185
|
+
const result = new Set([rootPid]);
|
|
186
|
+
let grew = true;
|
|
187
|
+
while (grew) {
|
|
188
|
+
grew = false;
|
|
189
|
+
for (const [pid, ppid] of ppidOf) {
|
|
190
|
+
if (result.has(ppid) && !result.has(pid)) {
|
|
191
|
+
result.add(pid);
|
|
192
|
+
grew = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [...result];
|
|
197
|
+
} catch {
|
|
198
|
+
return [rootPid];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
167
201
|
const all = [];
|
|
168
202
|
try {
|
|
169
203
|
for (const name of readdirSync("/proc")) {
|
|
@@ -62,6 +62,32 @@ Use observable checks:
|
|
|
62
62
|
- compatibility requirements such as Windows paths or Pi CLI flags;
|
|
63
63
|
- rollback notes.
|
|
64
64
|
|
|
65
|
+
## Spec Pairs (pi-crew v0.10.1+, ADR-6)
|
|
66
|
+
|
|
67
|
+
When the target runs pi-crew, author the **SpecRecord + TaskPacket pair** instead of prose-only acceptance:
|
|
68
|
+
|
|
69
|
+
1. **SpecRecord** (workspace `state/specs/<id>.json`): `requirements[]` with
|
|
70
|
+
`must|should|could` priority + stable ids; `acceptance[]` entries each tied to
|
|
71
|
+
a `requirementId`. Machine-checkable acceptances carry `command`,
|
|
72
|
+
`expectedDigest` (sha-256 hex of stdout) or `expectedExitCode`, and
|
|
73
|
+
`idempotent: true` — only idempotent commands are ever re-run.
|
|
74
|
+
2. **PROVENANCE — critical**: specs you (an agent/skill path) write are persisted
|
|
75
|
+
`generated` and NEVER re-executed by the orchestrator. `manual`+`trusted`
|
|
76
|
+
(the only specs strict mode re-runs) are minted exclusively by USER-facing
|
|
77
|
+
import actions — a worker cannot author a command the root re-executes.
|
|
78
|
+
3. **Wire the workflow**: add `specRefs: [<spec ids>]` to steps held to the spec;
|
|
79
|
+
`specStrict: true` in frontmatter opts the whole workflow into strict mode
|
|
80
|
+
(requires a `verifier` role step — the run rejects at start otherwise).
|
|
81
|
+
4. **Executor footer contract**: workers must END results with
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
SPEC-EVIDENCE:
|
|
85
|
+
<acceptanceId>: <one-line evidence>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Non-strict = mechanical coverage only (`unverified` badge on gaps, never
|
|
89
|
+
blocks). Strict = coverage AND machine-check; failures fail the run.
|
|
90
|
+
|
|
65
91
|
## Enforcement — Requirements to Task Packet Gate
|
|
66
92
|
|
|
67
93
|
**Before dispatching workers, verify task packet has:**
|
|
@@ -32,16 +32,16 @@ In-memory map from `live-agent-manager.ts`. Provides:
|
|
|
32
32
|
|
|
33
33
|
**When NOT used:** After `evictStaleLiveAgentHandles()` removes a handle, widget falls back to agent records on disk.
|
|
34
34
|
|
|
35
|
-
### 2. Snapshot cache (
|
|
35
|
+
### 2. Snapshot cache (1500ms TTL)
|
|
36
36
|
|
|
37
|
-
`RunSnapshotCache` from `run-snapshot-cache.ts` caches parsed manifests and agents for
|
|
37
|
+
`RunSnapshotCache` from `run-snapshot-cache.ts` caches parsed manifests and agents for 1500ms. Reduces disk reads during rapid refresh.
|
|
38
38
|
|
|
39
39
|
**When used:** As the fallback when no live handle exists. Prevents excessive disk reads on every render tick.
|
|
40
40
|
|
|
41
41
|
**Invalidation:** Cache is invalidated when:
|
|
42
42
|
- `invalidate()` is called on a specific run
|
|
43
43
|
- An empty result is returned (forces refresh on next tick)
|
|
44
|
-
- TTL expires (
|
|
44
|
+
- TTL expires (1500ms)
|
|
45
45
|
|
|
46
46
|
### 3. `agents.json` on disk (durables, lowest priority)
|
|
47
47
|
|
|
@@ -156,7 +156,7 @@ Every render cycle (`renderTick` / `requestAnimationFrame`) must complete in <16
|
|
|
156
156
|
|
|
157
157
|
### TTL interactions
|
|
158
158
|
|
|
159
|
-
- Snapshot cache TTL =
|
|
159
|
+
- Snapshot cache TTL = 1500ms
|
|
160
160
|
- Preload interval must be < TTL to avoid render-time gaps
|
|
161
161
|
- If preload interval ≥ TTL, the cache always has fresh data for render
|
|
162
162
|
|
|
@@ -248,7 +248,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
|
|
|
248
248
|
## Anti-patterns
|
|
249
249
|
|
|
250
250
|
- **Blocking render with fs calls**: Every `readFileSync`, `readdirSync`, `fs.statSync` in the render path causes frame drops. Preload everything async.
|
|
251
|
-
- **Stale cache in hot path**: If snapshot cache TTL is too long, widget shows outdated state. Keep TTL at
|
|
251
|
+
- **Stale cache in hot path**: If snapshot cache TTL is too long, widget shows outdated state. Keep TTL at 1500ms or less (the code default; lower only with cause).
|
|
252
252
|
- **No invalidation on empty**: When `readCrewAgents` returns `[]` (no agents yet), the cache must be invalidated on next tick to prevent showing empty for too long.
|
|
253
253
|
- **Expired handles accumulating**: Without `evictStaleLiveAgentHandles`, the Map grows indefinitely. Call it on every refresh.
|
|
254
254
|
- **Widget showing stale health warnings**: Completed/cancelled/failed runs should not show health warnings. Filter by status.
|
|
@@ -258,7 +258,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
|
|
|
258
258
|
## Source patterns
|
|
259
259
|
|
|
260
260
|
- `src/ui/crew-widget.ts` — render, refresh, activeWidgetRuns, evictStaleLiveAgentHandles, agentActivity, describeLiveActivity
|
|
261
|
-
- `src/ui/run-snapshot-cache.ts` — SnapshotCache, get, refreshIfStale, TTL=
|
|
261
|
+
- `src/ui/run-snapshot-cache.ts` — SnapshotCache, get, refreshIfStale, TTL=1500ms
|
|
262
262
|
- `src/runtime/crew-agent-records.ts` — readCrewAgents, agents.json
|
|
263
263
|
- `src/runtime/process-status.ts` — hasStaleAsyncProcess, isDisplayActiveRun
|
|
264
264
|
- `src/runtime/background-runner.ts` — active run filtering with async PID check
|
|
@@ -275,7 +275,7 @@ Render-path performance for the widget is non-negotiable. Treat every `render(wi
|
|
|
275
275
|
- **Prefer `snapshotCache.get(runId)`** on render paths. If a synchronous fallback is genuinely unavoidable, classify it as first-load/rare and document why it can't be preloaded.
|
|
276
276
|
- **Keep panes pure.** Dashboard panes must accept a snapshot/model and format strings only. Never call `fs.readFileSync`, `fs.readdirSync`, `fs.statSync`, network APIs, or large JSON parsing from pane render methods.
|
|
277
277
|
- **Stay non-blocking at 60fps.** Each render cycle must complete in under ~16ms. Anything that can't finish synchronously (reads, fetches, directory scans) belongs in the async preload path, not `renderTick()`.
|
|
278
|
-
- **Respect the snapshot-cache TTL of ≤
|
|
278
|
+
- **Respect the snapshot-cache TTL of ≤1500ms.** Keep the `RunSnapshotCache` TTL at 1500ms or less (the code default; lower only with cause) so the widget never shows stale state. Watch TTL interactions: the preload interval must be shorter than the cache TTL, otherwise render-time refresh gaps appear.
|
|
279
279
|
- **Guard session switches.** On a session switch, cancel timers and ensure in-flight async preloads cannot update a now-stale session's UI.
|
|
280
280
|
- **Filter stale warnings by terminal status.** Do not surface health warnings for completed/failed/cancelled runs.
|
|
281
281
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
1
2
|
import type { RoleToolConfig } from "../config/role-tools.ts";
|
|
2
3
|
import { getToolConfig, isScratchpadEnabledForRole } from "../config/role-tools.ts";
|
|
3
4
|
|
|
@@ -196,7 +197,7 @@ export function resolveToolPolicy(agent: AgentConfig, role?: string): ResolvedTo
|
|
|
196
197
|
* live-session filterActiveTools) via the unified policy.
|
|
197
198
|
*/
|
|
198
199
|
function shouldDemoteBashForScratchpad(role: string | undefined, agent: AgentConfig): boolean {
|
|
199
|
-
if (
|
|
200
|
+
if (getCrewEnv("PI_CREW_SCRATCHPAD_DEMOTE_BASH") !== "1") return false;
|
|
200
201
|
if (!role) return false;
|
|
201
202
|
return isScratchpadEnabledForRole(role, { scratchpad: agent.scratchpad });
|
|
202
203
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { type LoadedPiTeamsConfig, loadConfig } from "../config/config.ts";
|
|
4
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
4
5
|
import { discoverProviderExtensionPaths } from "../runtime/model/provider-extensions.ts";
|
|
5
6
|
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
|
|
6
7
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
@@ -136,19 +137,13 @@ function logSecurityEvent(event: SecurityEvent): void {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
|
-
* Get recent security events
|
|
140
|
+
* Get recent security events. Production reader: the team doctor report
|
|
141
|
+
* surfaces a compact summary (R7-13); tests assert on the full list.
|
|
140
142
|
*/
|
|
141
143
|
export function getSecurityEventLog(): readonly SecurityEvent[] {
|
|
142
144
|
return securityEventLog;
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
/**
|
|
146
|
-
* Clear security event log (for testing).
|
|
147
|
-
*/
|
|
148
|
-
export function clearSecurityEventLog(): void {
|
|
149
|
-
securityEventLog.length = 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
147
|
/**
|
|
153
148
|
* Security check: throws if the agent name is protected.
|
|
154
149
|
*
|
|
@@ -373,7 +368,11 @@ export function sanitizeAgentSystemPrompt(content: string, source: ResourceSourc
|
|
|
373
368
|
* emitted the `contextMode: fork` warn-only notice. Avoids spam when
|
|
374
369
|
* the discovery cache reloads or the same agent is parsed multiple
|
|
375
370
|
* times in a session. Exported for test reset.
|
|
371
|
+
* R5-L5: FIFO cap so the set stays bounded in long-lived sessions
|
|
372
|
+
* (Set preserves insertion order; bounded naturally <50, cap is a
|
|
373
|
+
* safety net).
|
|
376
374
|
*/
|
|
375
|
+
const MAX_WARNED_FORK_AGENTS = 128;
|
|
377
376
|
const warnedForkAgents = new Set<string>();
|
|
378
377
|
export function __test_resetForkWarnings(): void {
|
|
379
378
|
warnedForkAgents.clear();
|
|
@@ -406,14 +405,19 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
406
405
|
// so the agent will behave as `fresh` regardless of the setting.
|
|
407
406
|
// We warn (not throw) so existing configs that predate live-session
|
|
408
407
|
// keep working. Deduped per-filePath so we don't spam on cache
|
|
409
|
-
// reload.
|
|
410
|
-
//
|
|
411
|
-
// logInternalError
|
|
408
|
+
// reload. User-facing config notice (kept on console.warn deliberately
|
|
409
|
+
// so the user sees it in the interactive session; internal failures
|
|
410
|
+
// below use logInternalError).
|
|
412
411
|
if (contextMode === "fork" && !warnedForkAgents.has(filePath)) {
|
|
413
412
|
console.warn(
|
|
414
413
|
"contextMode: 'fork' is only effective in live-session runtime; current default child-process will behave as 'fresh'. See docs/runtime-flow.md.",
|
|
415
414
|
);
|
|
416
415
|
warnedForkAgents.add(filePath);
|
|
416
|
+
// R5-L5: FIFO eviction — drop the oldest entry past the cap.
|
|
417
|
+
if (warnedForkAgents.size > MAX_WARNED_FORK_AGENTS) {
|
|
418
|
+
const oldest = warnedForkAgents.values().next().value;
|
|
419
|
+
if (oldest !== undefined) warnedForkAgents.delete(oldest);
|
|
420
|
+
}
|
|
417
421
|
}
|
|
418
422
|
|
|
419
423
|
return {
|
|
@@ -442,7 +446,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
442
446
|
// code. Bypass only when PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS=1 is
|
|
443
447
|
// explicitly set. buildPiWorkerArgs also enforces this as
|
|
444
448
|
// defense-in-depth.
|
|
445
|
-
...((source === "project" || source === "project-pi") &&
|
|
449
|
+
...((source === "project" || source === "project-pi") && getCrewEnv("PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS") !== "1"
|
|
446
450
|
? { extensions: [], excludeExtensions: [] }
|
|
447
451
|
: {
|
|
448
452
|
extensions: frontmatter.extensions === "" ? [] : parseCsv(frontmatter.extensions),
|
|
@@ -494,7 +498,12 @@ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
|
|
|
494
498
|
try {
|
|
495
499
|
const stat = fs.statSync(fullPath);
|
|
496
500
|
if (stat.size > MAX_AGENT_FILE_BYTES) {
|
|
497
|
-
|
|
501
|
+
logInternalError(
|
|
502
|
+
"discover-agents",
|
|
503
|
+
new Error(`Skipping oversized agent file (${stat.size} > ${MAX_AGENT_FILE_BYTES} bytes): ${fullPath}`),
|
|
504
|
+
undefined,
|
|
505
|
+
"warn",
|
|
506
|
+
);
|
|
498
507
|
return undefined;
|
|
499
508
|
}
|
|
500
509
|
} catch {
|
|
@@ -536,7 +545,7 @@ function applyAgentOverrides(agents: AgentConfig[], cwd: string, loadedConfig?:
|
|
|
536
545
|
// user-trusted config knob (provider extensions like pi-commandcode-provider)
|
|
537
546
|
// and applies to builtin / user agents only.
|
|
538
547
|
const isUntrustedProject = (agent: AgentConfig) =>
|
|
539
|
-
(agent.source === "project" || agent.source === "project-pi") &&
|
|
548
|
+
(agent.source === "project" || agent.source === "project-pi") && getCrewEnv("PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS") !== "1";
|
|
540
549
|
const withGlobalExtensions = (agent: AgentConfig): AgentConfig => {
|
|
541
550
|
if (isUntrustedProject(agent)) return agent;
|
|
542
551
|
return deduped.length > 0 || agent.extensions !== undefined
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { DANGEROUS_OBJECT_KEYS } from "./config-validation.ts";
|
|
2
|
+
import type { AgentOverrideConfig, PiTeamsConfig } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
5
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
|
|
9
|
+
const warnings: string[] = [];
|
|
10
|
+
const merged: PiTeamsConfig = {
|
|
11
|
+
...base,
|
|
12
|
+
...withoutUndefined(override as Record<string, unknown>),
|
|
13
|
+
};
|
|
14
|
+
if (base.autonomous || override.autonomous) {
|
|
15
|
+
merged.autonomous = {
|
|
16
|
+
...(base.autonomous ?? {}),
|
|
17
|
+
...withoutUndefined((override.autonomous ?? {}) as Record<string, unknown>),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (base.broker || override.broker) {
|
|
21
|
+
// WP-2/R2 fix (B1 battery 2026-08-18): `broker` was previously merged by
|
|
22
|
+
// the top-level spread only — a user-side broker object (defaults or a
|
|
23
|
+
// user config file with ANY broker block) replaced the project config's
|
|
24
|
+
// block WHOLESALE, silently dropping keys like waitMethodsEnabled that
|
|
25
|
+
// the user side did not set (enabled:true survived only because it
|
|
26
|
+
// matched DEFAULT_BROKER). Per-key user-wins, same as runtime/ui/….
|
|
27
|
+
merged.broker = {
|
|
28
|
+
...(base.broker ?? {}),
|
|
29
|
+
...withoutUndefined((override.broker ?? {}) as Record<string, unknown>),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (base.nesting || override.nesting) {
|
|
33
|
+
// ADR-5: per-key user-wins deep merge — a partial user-side nesting block
|
|
34
|
+
// (e.g. just maxSlots) must not erase DEFAULT_NESTING.enabled=false.
|
|
35
|
+
merged.nesting = {
|
|
36
|
+
...(base.nesting ?? {}),
|
|
37
|
+
...withoutUndefined((override.nesting ?? {}) as Record<string, unknown>),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (base.limits || override.limits) {
|
|
41
|
+
merged.limits = {
|
|
42
|
+
...(base.limits ?? {}),
|
|
43
|
+
...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (base.runtime || override.runtime) {
|
|
47
|
+
merged.runtime = {
|
|
48
|
+
...(base.runtime ?? {}),
|
|
49
|
+
...withoutUndefined((override.runtime ?? {}) as Record<string, unknown>),
|
|
50
|
+
};
|
|
51
|
+
// F19-1 (Round 19 parity): deep-merge modelFallback like
|
|
52
|
+
// reliability.retryPolicy so a partial override cannot erase base fields
|
|
53
|
+
// (user-wins precedence per key). Assigned only when a side defines it —
|
|
54
|
+
// unlike retryPolicy we avoid a stray `modelFallback: undefined` key so
|
|
55
|
+
// the merged runtime shape stays byte-identical for configs without it.
|
|
56
|
+
if (base.runtime?.modelFallback || override.runtime?.modelFallback) {
|
|
57
|
+
merged.runtime.modelFallback = {
|
|
58
|
+
...(base.runtime?.modelFallback ?? {}),
|
|
59
|
+
...withoutUndefined((override.runtime?.modelFallback ?? {}) as Record<string, unknown>),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (base.control || override.control) {
|
|
64
|
+
merged.control = {
|
|
65
|
+
...(base.control ?? {}),
|
|
66
|
+
...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (base.worktree || override.worktree) {
|
|
70
|
+
merged.worktree = {
|
|
71
|
+
...(base.worktree ?? {}),
|
|
72
|
+
...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (base.ui || override.ui) {
|
|
76
|
+
merged.ui = {
|
|
77
|
+
...(base.ui ?? {}),
|
|
78
|
+
...withoutUndefined((override.ui ?? {}) as Record<string, unknown>),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (base.agents || override.agents) {
|
|
82
|
+
merged.agents = {
|
|
83
|
+
...(base.agents ?? {}),
|
|
84
|
+
...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
|
|
85
|
+
overrides: {
|
|
86
|
+
...(base.agents?.overrides ?? {}),
|
|
87
|
+
...(withoutUndefined((override.agents?.overrides ?? {}) as Record<string, unknown>) as Record<string, AgentOverrideConfig>),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (base.tools || override.tools) {
|
|
92
|
+
merged.tools = {
|
|
93
|
+
...(base.tools ?? {}),
|
|
94
|
+
...withoutUndefined((override.tools ?? {}) as Record<string, unknown>),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (base.telemetry || override.telemetry) {
|
|
98
|
+
merged.telemetry = {
|
|
99
|
+
...(base.telemetry ?? {}),
|
|
100
|
+
...withoutUndefined((override.telemetry ?? {}) as Record<string, unknown>),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (base.policy || override.policy) {
|
|
104
|
+
merged.policy = {
|
|
105
|
+
...(base.policy ?? {}),
|
|
106
|
+
...withoutUndefined((override.policy ?? {}) as Record<string, unknown>),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (base.notifications || override.notifications) {
|
|
110
|
+
merged.notifications = {
|
|
111
|
+
...(base.notifications ?? {}),
|
|
112
|
+
...withoutUndefined((override.notifications ?? {}) as Record<string, unknown>),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (base.observability || override.observability) {
|
|
116
|
+
merged.observability = {
|
|
117
|
+
...(base.observability ?? {}),
|
|
118
|
+
...withoutUndefined((override.observability ?? {}) as Record<string, unknown>),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (base.reliability || override.reliability) {
|
|
122
|
+
merged.reliability = {
|
|
123
|
+
...(base.reliability ?? {}),
|
|
124
|
+
...withoutUndefined((override.reliability ?? {}) as Record<string, unknown>),
|
|
125
|
+
retryPolicy:
|
|
126
|
+
base.reliability?.retryPolicy || override.reliability?.retryPolicy
|
|
127
|
+
? {
|
|
128
|
+
...(base.reliability?.retryPolicy ?? {}),
|
|
129
|
+
...withoutUndefined((override.reliability?.retryPolicy ?? {}) as Record<string, unknown>),
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (base.otlp || override.otlp) {
|
|
135
|
+
merged.otlp = {
|
|
136
|
+
...(base.otlp ?? {}),
|
|
137
|
+
...withoutUndefined((override.otlp ?? {}) as Record<string, unknown>),
|
|
138
|
+
headers: {
|
|
139
|
+
...(base.otlp?.headers ?? {}),
|
|
140
|
+
...(override.otlp?.headers ?? {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
if (Object.keys(merged.otlp.headers ?? {}).length === 0) delete merged.otlp.headers;
|
|
144
|
+
// Validate OTLP headers for injection attacks:
|
|
145
|
+
// - Check top-level keys for dangerous prototype pollution patterns
|
|
146
|
+
// - Block ALL control characters except tab (0x09) to prevent header
|
|
147
|
+
// injection via CR/LF/zero-byte/etc.
|
|
148
|
+
// BUG (Round 28, CRLF injection): the previous range
|
|
149
|
+
// /[\x00-\x08\x0b\x0c\x0e-\x1f]/ left THREE chars unblocked: tab (0x09,
|
|
150
|
+
// intentionally allowed), LF (0x0A) AND CR (0x0D). The comment claimed to
|
|
151
|
+
// "prevent header injection via CR/LF" but CR was never matched, and LF
|
|
152
|
+
// was explicitly allowed — both are CRLF injection vectors that can split
|
|
153
|
+
// HTTP headers. Fix: block 0x00-0x08 and 0x0A-0x1F, allowing only tab.
|
|
154
|
+
const invalidHeaders: string[] = [];
|
|
155
|
+
for (const [k, v] of Object.entries(merged.otlp.headers ?? {})) {
|
|
156
|
+
// Check top-level key for dangerous names (only top-level keys are checked)
|
|
157
|
+
const checkKey = (key: string): boolean => {
|
|
158
|
+
const lowerKey = key.toLowerCase();
|
|
159
|
+
if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) return true;
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
if (checkKey(k)) {
|
|
163
|
+
invalidHeaders.push(k);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Block any control characters except tab (0x09) in values.
|
|
167
|
+
// Round 28 fix: /[\x00-\x08\x0a-\x1f]/ blocks LF (0x0A) and CR (0x0D) too.
|
|
168
|
+
const valStr = String(v);
|
|
169
|
+
if (/[\x00-\x08\x0a-\x1f]/.test(valStr)) {
|
|
170
|
+
invalidHeaders.push(k);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (invalidHeaders.length > 0) {
|
|
174
|
+
delete merged.otlp.headers;
|
|
175
|
+
warnings.push(`OTLP headers blocked due to invalid characters: ${invalidHeaders.join(", ")}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
|
|
179
|
+
return merged;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** @internal — direct-test seam for Phase 2.2 extraction target (refactor-plan step 1.9c). */
|
|
183
|
+
export const __test__mergeConfig = mergeConfig;
|