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
|
@@ -6,6 +6,14 @@ topology: dynamic
|
|
|
6
6
|
|
|
7
7
|
# Chain Workflow - Sequential execution with context passing
|
|
8
8
|
|
|
9
|
+
> **This file is DOCUMENTATION, not a runnable workflow (R28-M1 / wave-6 M3-24).**
|
|
10
|
+
> It lives in `docs/` because the framework discovers every
|
|
11
|
+
> `workflows/*.workflow.md` as a workflow resource; as a doc it parsed to 10
|
|
12
|
+
> phantom steps (its prose headings) and the advertised `chain` workflow could
|
|
13
|
+
> never run. Chain execution is implemented by the chain runner
|
|
14
|
+
> (`src/runtime/chain-runner.ts` + `src/extension/team-tool/chain-dispatch.ts`);
|
|
15
|
+
> there is no `chain` workflow resource.
|
|
16
|
+
|
|
9
17
|
**Source:** pi-boomerang integration plan
|
|
10
18
|
**Syntax:** `step1 -> step2 -> step3`
|
|
11
19
|
**Version:** 1.0.0
|
|
@@ -25,6 +25,9 @@ export default async function (ctx) {
|
|
|
25
25
|
const synth = await ctx.agent({ role: "analyst", prompt: "Merge + dedupe findings", inputs: reports.map(r => r.artifactPath) });
|
|
26
26
|
|
|
27
27
|
ctx.phase("Review");
|
|
28
|
+
// NOTE (R2-M-B5): the identical-call cache is RESUME-ONLY — every
|
|
29
|
+
// ctx.review / ctx.retry iteration really re-runs the task (fixed
|
|
30
|
+
// semantics; see the caveat under WorkflowCtx API below).
|
|
28
31
|
for (let i = 0; i < 3; i++) {
|
|
29
32
|
const review = await ctx.review(synth.taskId, "reviewer");
|
|
30
33
|
if (review.outcome === "accept") break;
|
|
@@ -52,8 +55,8 @@ Slash command: `/workflows` lists all workflows (static + dynamic).
|
|
|
52
55
|
| `ctx.agent({role, prompt, model?, skill?, maxTurns?, inputs?, schema?, worktree?})` | Spawn one agent, await `{ok, text, structured, artifactPath, usage}`. Concurrency enforced by `ctx.semaphore`. `schema?` (round-13) is a TypeBox schema — when set, output is validated and mismatch yields `ok:false`. `worktree?` (round-17) spawns the agent in an isolated git worktree (default false; falls back to normal cwd + warning in a non-git repo). |
|
|
53
56
|
| `ctx.fanOut(items, limit, fn)` | Bounded parallel fan-out (wraps `mapConcurrent`). |
|
|
54
57
|
| `ctx.pipeline(items, ...stages)` | **round-16.** Multi-stage pipeline: each item passes through all stages sequentially; different items run concurrently (bounded by `ctx.semaphore`). A failed stage yields `null` for that item (logged via `ctx.log`) and other items continue. Aborts propagate. Returns `(TResult\|null)[]`. |
|
|
55
|
-
| `ctx.review(taskId, reviewerRole?)` | Run a reviewer; parse `{outcome, feedback}`. |
|
|
56
|
-
| `ctx.retry(taskId, {feedback?})` | Re-run with feedback (wraps `executeWithRetry`). |
|
|
58
|
+
| `ctx.review(taskId, reviewerRole?)` | Run a reviewer; parse `{outcome, feedback}`. Re-runs the task every call (see the identical-call cache caveat below). |
|
|
59
|
+
| `ctx.retry(taskId, {feedback?})` | Re-run with feedback (wraps `executeWithRetry`). Always re-spawns (never cached). |
|
|
57
60
|
| `ctx.mail(to, body, opts?)` | Mailbox message to another agent/leader. |
|
|
58
61
|
| `ctx.gatherReplies(ids, deadlineMs)` | Block until N replies arrive or deadline. |
|
|
59
62
|
| `ctx.renderTemplate(name, vars)` | Render a built-in plan template. |
|
|
@@ -68,6 +71,15 @@ Slash command: `/workflows` lists all workflows (static + dynamic).
|
|
|
68
71
|
explicit `agent` name → `team.roles[].agent` → `discoverAgents` by name → synthesize
|
|
69
72
|
minimal (`source:'dynamic'`).
|
|
70
73
|
|
|
74
|
+
> **Identical-call cache caveat (R2-M-B5, fixed semantics).** The
|
|
75
|
+
> completed-calls cache is consulted ONLY for calls hydrated from a resumed
|
|
76
|
+
> checkpoint — identical call options within a LIVE run always re-spawn.
|
|
77
|
+
> Historically the cache also hit within a run, which made
|
|
78
|
+
> `while(true){review; if accept break; retry}` loops replay round-1 verdicts
|
|
79
|
+
> forever (stale verdicts until the agent-call cap aborted the run). If you
|
|
80
|
+
> need true idempotency within a run, structure the workflow to call each
|
|
81
|
+
> distinct step once and rely on resume for crash recovery.
|
|
82
|
+
|
|
71
83
|
### Pipeline (round-16 P2-1)
|
|
72
84
|
|
|
73
85
|
`ctx.pipeline(items, ...stages)` runs a **multi-stage transform** over a list of
|
|
@@ -263,6 +275,29 @@ script can reach `process`/`require` directly or via constructor walking. The
|
|
|
263
275
|
(e.g. `require('child'+'_process')`, `globalThis.process.mainModule.require`).
|
|
264
276
|
The real boundary is commit-review + the path-allowlist, not the content check.
|
|
265
277
|
|
|
278
|
+
### Project-sourced workflows are DEFAULT-DENY (F-01)
|
|
279
|
+
|
|
280
|
+
`.dwf.ts` files that resolve to a **project** source (`.crew/workflows/`,
|
|
281
|
+
`<proj>/.pi/teams/workflows/`) are refused at run time unless you explicitly opt
|
|
282
|
+
in with the env var `PI_CREW_TRUST_PROJECT_DWF=1`
|
|
283
|
+
(`src/runtime/goal-workflow/dynamic-workflow-runner.ts:189`). The rejected run
|
|
284
|
+
emits a `dwf.trust_denied` event and throws before the script is loaded:
|
|
285
|
+
|
|
286
|
+
```text
|
|
287
|
+
Project dynamic workflow requires explicit trust. Set PI_CREW_TRUST_PROJECT_DWF=1
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The gate compares against the literal string `"1"` — `true`, `yes`, or `on` all
|
|
291
|
+
FAIL closed. Cloning a repo and running its workflow is otherwise arbitrary code
|
|
292
|
+
execution, hence the opt-in.
|
|
293
|
+
|
|
294
|
+
**Scope of the gate (honest):** only `source === "project"` is gated. **Builtin**
|
|
295
|
+
workflows and **user-scope** workflows (`~/.pi/agent/extensions/pi-crew/workflows/`)
|
|
296
|
+
proceed with **no** trust prompt — they run as ordinary trusted local code. The
|
|
297
|
+
`PI_CREW_TRUST_PROJECT_DWF` gate is therefore not a blanket guarantee that every
|
|
298
|
+
`.dwf.ts` you execute was reviewed: it protects against *repo-supplied* authoring,
|
|
299
|
+
not against user-scope authoring.
|
|
300
|
+
|
|
266
301
|
## Determinism (round-13 P0-2)
|
|
267
302
|
|
|
268
303
|
Dynamic workflow scripts must be **deterministic** — the runner rejects
|
package/docs/publishing.md
CHANGED
|
@@ -24,8 +24,8 @@ Before publishing to npm:
|
|
|
24
24
|
npm run check
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
For a fast pre-publish smoke (
|
|
28
|
-
of the full
|
|
27
|
+
For a fast pre-publish smoke (124 broker/UI/config tests in ~15s, instead
|
|
28
|
+
of the full `npm run check` unit+integration suite, which takes minutes):
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm run test:critical && npm run typecheck && npm run build:bundle
|
|
@@ -94,7 +94,7 @@ lockfile drift):
|
|
|
94
94
|
- **push to `refs/heads/main`** — `check:conflict-markers` + `typecheck`,
|
|
95
95
|
then the **FULL unit suite** (`npm run test:unit`, the same gate CI
|
|
96
96
|
runs). Any failure blocks the push. This closes the 2026-10 gap where
|
|
97
|
-
local pushes ran only `test:critical` (
|
|
97
|
+
local pushes ran only `test:critical` (124 tests) while CI ran the full
|
|
98
98
|
suite — three waves shipped red that way.
|
|
99
99
|
- **push to any other ref** — fast gates only (`check:conflict-markers` +
|
|
100
100
|
`typecheck`).
|
|
@@ -128,8 +128,37 @@ The package exports:
|
|
|
128
128
|
./schema.json
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
Use this for editor validation of
|
|
131
|
+
Use this for editor validation of the **current** config path
|
|
132
|
+
(`src/config/config.ts:208`; the `~/.pi/agent/extensions/pi-crew/config.json`
|
|
133
|
+
path is legacy/read-only-for-migration — see `docs/usage.md`):
|
|
132
134
|
|
|
133
135
|
```text
|
|
134
|
-
~/.pi/agent/
|
|
136
|
+
~/.pi/agent/pi-crew.json
|
|
135
137
|
```
|
|
138
|
+
|
|
139
|
+
## Known tarball / consumer-side notes (R26/R29 census, 2026-10-19)
|
|
140
|
+
|
|
141
|
+
Verified with `npm pack --dry-run`. These are **declared defects**, not hidden:
|
|
142
|
+
|
|
143
|
+
- **`skills/research/scripts/__pycache__/safe_io.cpython-312.pyc` ships** in the
|
|
144
|
+
tarball (11.5 kB): the `files` entry `"skills/**"` plus the missing
|
|
145
|
+
`.npmignore` rule means compiled Python bytecode is published and it embeds
|
|
146
|
+
build-machine paths. Fix = add `__pycache__` to a `.npmignore` / tighten
|
|
147
|
+
`files`.
|
|
148
|
+
- **Dev tooling ships to consumers:** `scripts/analyze-run.mjs` (64.5 kB),
|
|
149
|
+
`scripts/resource-sampler.mjs`, `scripts/watch-agent-transcript.mjs`. Harmless
|
|
150
|
+
bulk; consider moving them behind a dev-only entry.
|
|
151
|
+
- **Consumer-side git hooks reference scripts that are NOT published:**
|
|
152
|
+
`scripts/install-hooks.mjs` (published) sets `core.hooksPath=.githooks`, and
|
|
153
|
+
`.githooks/pre-commit` runs `node scripts/check-lockfile-sync.mjs` — but
|
|
154
|
+
`files[]` ships neither `scripts/check-lockfile-sync.mjs` nor
|
|
155
|
+
`scripts/test-runner.mjs`, so an installed-from-npm consumer's hook fails.
|
|
156
|
+
- **Peer range on `pi-coding-agent` is unbounded** (`>=1.0.4`, no ceiling) while
|
|
157
|
+
the `pi-tui` peer IS bounded (`<2.0.0`); pi-crew consumes pi-tui symbols
|
|
158
|
+
re-exported through `pi-coding-agent`, so a future 2.x installs clean and fails
|
|
159
|
+
at load (R49-F2 — bounding it is prospective work).
|
|
160
|
+
- **Committed `dist/` must match the tag:** the `--committed-hash` gate reads the
|
|
161
|
+
committed blob (`dist/index.mjs` — the ONLY tracked dist artifact) from the git
|
|
162
|
+
object DB, so every commit that touches `src/` needs
|
|
163
|
+
`npm run build:bundle && git add -f dist/index.mjs` rerun and committed
|
|
164
|
+
(`dist/` is `.gitignore`d, hence `-f`).
|
package/docs/troubleshooting.md
CHANGED
|
@@ -18,8 +18,11 @@ run/process status. Start here.
|
|
|
18
18
|
**Symptom:** `team action='run'` returns a `blocked` status with a message like
|
|
19
19
|
"Child worker execution is disabled".
|
|
20
20
|
|
|
21
|
-
**Cause:** Worker execution is off.
|
|
22
|
-
|
|
21
|
+
**Cause:** Worker execution is off. When `executeWorkers` is **explicitly**
|
|
22
|
+
`false` (or `PI_CREW_EXECUTE_WORKERS=0`), pi-crew refuses to create no-op
|
|
23
|
+
scaffold subagents and returns `blocked`. Note the default is the **opposite**:
|
|
24
|
+
child-worker execution is enabled by default
|
|
25
|
+
(`src/extension/team-tool/run.ts:205`).
|
|
23
26
|
|
|
24
27
|
**Fix — pick one:**
|
|
25
28
|
- Set in your config (`~/.pi/agent/pi-crew.json`): `"executeWorkers": true`
|
|
@@ -149,6 +152,51 @@ code + a help hint inline. Common ones:
|
|
|
149
152
|
| E012 | RunStale | run reconciled as stale | see "Stale async process" above |
|
|
150
153
|
| E013 | ModelOutOfScope | requested model not in pi's `enabledModels` allowlist | pick a model listed in `enabledModels` (settings.json) or disable `reliability.scopeModels` |
|
|
151
154
|
|
|
155
|
+
## Doctor says OK but the run is dead (doctor blind spots, R48)
|
|
156
|
+
|
|
157
|
+
`team action='doctor'` is a **liveness sanity check, not a watchdog**. The
|
|
158
|
+
deep-review health/status/doctor census (round 48) found these classes are
|
|
159
|
+
**NOT** detected — status/health can print `running` for a dead host:
|
|
160
|
+
|
|
161
|
+
| Class | Why it is missed | What to do |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Host process dead, manifest frozen `running` | orphan detection keys on updatedAt/magic-string heuristics (`src/runtime/process-status.ts`) | check the host pid yourself; `team action='cancel'` then re-run |
|
|
164
|
+
| Disk full (`ENOSPC`) | no free-space probe in doctor | `df -h` the state root / TMPDIR |
|
|
165
|
+
| Bad state-file permissions (`EACCES`) | reads may classify as unreadable, not as a doctor failure | `ls -l <crewRoot>/state/runs/*` |
|
|
166
|
+
| Orphaned worker processes (reparented to PID 1) | only reaped when a run is reconciled | `ps -ef \| grep ' pi '`, then `team action='cleanup'` |
|
|
167
|
+
| Broken `node_modules` / missing `pi` resolve | spawn succeeds, worker dies instantly | run `pi --version` in the run cwd |
|
|
168
|
+
|
|
169
|
+
Only two classes are reliably caught: stale broker socket and orphan mux pane.
|
|
170
|
+
Treat a green `doctor` as "no *known* problem", not as "the run is alive".
|
|
171
|
+
|
|
172
|
+
## Debug logging: `PI_CREW_DEBUG` / `PI_TEAMS_DEBUG` are TRUTHY, not "=1" (R45-F4)
|
|
173
|
+
|
|
174
|
+
Internal debug logging (`logInternalError(..., "debug")`, `src/utils/internal-error.ts:10`)
|
|
175
|
+
is gated on a raw truthiness check, so **any non-empty value enables it** —
|
|
176
|
+
including `PI_CREW_DEBUG=0`, `PI_TEAMS_DEBUG=false`, and `PI_TEAMS_DEBUG=off`.
|
|
177
|
+
To actually silence debug output, **unset** the variable (do not set it to `0`).
|
|
178
|
+
|
|
179
|
+
## Mux surfaces (tmux / herdr) — known sharp edges (R42)
|
|
180
|
+
|
|
181
|
+
`runtime.surface.mode: 'tmux' | 'herdr'` (default `auto`) runs tier-1 workers in
|
|
182
|
+
real mux panes. Live-probed known limits:
|
|
183
|
+
|
|
184
|
+
- **Panic characters in commands:** tmux `send-keys -l` swallows a trailing `\n`,
|
|
185
|
+
while herdr `pane.send_text` treats `\n` as Enter — a boot line containing a
|
|
186
|
+
newline would be split/executed early. pi-crew's own callers pass
|
|
187
|
+
shell-escaped single-line paths, so this is latent, not live.
|
|
188
|
+
- **Pane/tab ids are not regex-validated before `-t`:** a malformed id in a
|
|
189
|
+
manifest (`kill-pane -t "-x"`) can act on the *wrong* pane (today every path is
|
|
190
|
+
liveness-gated, so it is defence-in-depth, not an open hole).
|
|
191
|
+
- **`capture-pane -p` trims trailing spaces** (cosmetic; only feeds doctor
|
|
192
|
+
liveness).
|
|
193
|
+
- **No tmux minimum-version gate:** old tmux silently falls back to a two-strike
|
|
194
|
+
headless degrade.
|
|
195
|
+
- **herdr is an external socket daemon** — pi-crew never spawns it, and the
|
|
196
|
+
server-wide subscription (no pane filter) amplifies traffic.
|
|
197
|
+
- Pane ids are unique only within one mux server lifetime; a stale manifest can
|
|
198
|
+
name an id now owned by an unrelated pane.
|
|
199
|
+
|
|
152
200
|
## Still stuck
|
|
153
201
|
|
|
154
202
|
- `team action='explain' runId=…` — structured per-task analysis (why, files,
|
package/docs/usage.md
CHANGED
|
@@ -42,7 +42,7 @@ Supported fields:
|
|
|
42
42
|
"requirePlanApproval": false
|
|
43
43
|
},
|
|
44
44
|
"ui": {
|
|
45
|
-
"widgetPlacement": "
|
|
45
|
+
"widgetPlacement": "bottom",
|
|
46
46
|
"widgetMaxLines": 8,
|
|
47
47
|
"powerbar": true,
|
|
48
48
|
"dashboardPlacement": "right",
|
|
@@ -239,6 +239,12 @@ Dirty worktrees are preserved unless `force: true` is provided.
|
|
|
239
239
|
|
|
240
240
|
## Slash commands
|
|
241
241
|
|
|
242
|
+
> **`/team-cleanup <runId>` does not exist** (R27-D1). The registered commands
|
|
243
|
+
> are `/team-manager` (and its alias `/team-cleanup-menu`) — runId-targeted
|
|
244
|
+
> cleanup is the `cleanup` action on the `team` tool (`team action='cleanup'
|
|
245
|
+
> runId=…`). Only `team-cleanup-menu` is registered
|
|
246
|
+
> (`src/extension/registration/commands/manage.ts:99-107`).
|
|
247
|
+
|
|
242
248
|
```text
|
|
243
249
|
/teams
|
|
244
250
|
/team-run default "Implement login with tests"
|
|
@@ -251,7 +257,6 @@ Dirty worktrees are preserved unless `force: true` is provided.
|
|
|
251
257
|
/team-events team_...
|
|
252
258
|
/team-artifacts team_...
|
|
253
259
|
/team-worktrees team_...
|
|
254
|
-
/team-cleanup team_...
|
|
255
260
|
/team-forget team_... --confirm
|
|
256
261
|
/team-export team_...
|
|
257
262
|
/team-import .crew/artifacts/team_.../export/run-export.json # or .pi/teams/artifacts/... on legacy layout
|
package/install.mjs
CHANGED
|
@@ -13,7 +13,8 @@ const legacyConfigPath = path.join(agentDir, "extensions", "pi-crew", "config.js
|
|
|
13
13
|
* Default config written to `~/.pi/agent/pi-crew.json` on fresh installs.
|
|
14
14
|
*
|
|
15
15
|
* WI-1b.2 (G17): this object is intentionally an embedded literal — install.mjs
|
|
16
|
-
* runs under bare `node` as the `pi-crew` bin (engines floor is >=22.
|
|
16
|
+
* runs under bare `node` as the `pi-crew` bin (engines floor is >=22.19.0 — the
|
|
17
|
+
* pre-R29-F3 comment said >=22.0.0; .ts
|
|
17
18
|
* imports would require >=22.18 or `--experimental-strip-types`, which the npm
|
|
18
19
|
* bin shim cannot pass). Runtime jiti/esbuild loading was rejected as a
|
|
19
20
|
* postinstall failure surface; build-time codegen would need package.json /
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.25",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"docs/troubleshooting.md",
|
|
58
58
|
"docs/usage.md",
|
|
59
59
|
"docs/architecture.md",
|
|
60
|
+
"docs/chain-workflow.md",
|
|
60
61
|
"docs/dynamic-workflows.md",
|
|
61
62
|
"docs/resource-formats.md",
|
|
62
63
|
"docs/publishing.md",
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
"smoke:pi": "pi install .",
|
|
117
118
|
"smoke:release": "node scripts/release-smoke.mjs",
|
|
118
119
|
"prepack": "node scripts/clean-strip-types.mjs",
|
|
120
|
+
"prepublishOnly": "npm run ci",
|
|
119
121
|
"test:fast": "npm run test:critical && npm run test:bundle",
|
|
120
122
|
"check:wc-gate": "node scripts/wc-gate.mjs"
|
|
121
123
|
},
|
|
@@ -134,9 +136,9 @@
|
|
|
134
136
|
]
|
|
135
137
|
},
|
|
136
138
|
"peerDependencies": {
|
|
137
|
-
"@earendil-works/pi-agent-core": "
|
|
138
|
-
"@earendil-works/pi-ai": "
|
|
139
|
-
"@earendil-works/pi-coding-agent": "
|
|
139
|
+
"@earendil-works/pi-agent-core": ">=1.0.4",
|
|
140
|
+
"@earendil-works/pi-ai": ">=1.0.4",
|
|
141
|
+
"@earendil-works/pi-coding-agent": ">=1.0.4 <2.0.0",
|
|
140
142
|
"@earendil-works/pi-tui": ">=1.0.4 <2.0.0",
|
|
141
143
|
"@sinclair/typebox": "*"
|
|
142
144
|
},
|
|
@@ -17,11 +17,21 @@ triggers:
|
|
|
17
17
|
|
|
18
18
|
Use this skill for explorer, analyst, reviewer, and source-audit roles. These roles must inspect code without modifying it.
|
|
19
19
|
|
|
20
|
+
> **Tool availability (enforced — R27-D6).** `explorer` (and `librarian`) ship
|
|
21
|
+
> **without a `bash` tool**: `src/config/role-tools.ts:25-29` sets
|
|
22
|
+
> `tools: ["read", "grep", "find", "ls", "glob", "ask"]` +
|
|
23
|
+
> `excludeTools: ["edit", "write", "bash", "web"]`. The shell syntax below
|
|
24
|
+
> (`rg …`, `find …`, `git status`) is therefore **reference syntax for the
|
|
25
|
+
> intent**, not runnable in an explorer session. Map each example to the tool you
|
|
26
|
+
> actually have: `rg` → `grep`, `find`/shell globbing → `find`/`glob`,
|
|
27
|
+
> `ls` → `ls`, `cat`/`sed -n` → `read` (with offset/limit). `analyst`/`reviewer`
|
|
28
|
+
> roles keep `bash` (`role-tools.ts:33-35`, `:64-68`) — the restriction is per-role.
|
|
29
|
+
|
|
20
30
|
## Core Contract
|
|
21
31
|
|
|
22
32
|
1. **Do not edit files** — no write, no edit, no delete
|
|
23
33
|
2. **Do not write generated artifacts** outside the run artifact directory
|
|
24
|
-
3. **Prefer read-only
|
|
34
|
+
3. **Prefer read-only tools**: `read`, `grep`, `find`, `ls`, `glob` (explorer has no shell — see the note above)
|
|
25
35
|
4. **Record exact files inspected** — include path and relevant line numbers
|
|
26
36
|
5. **Distinguish direct evidence from inference** — don't guess
|
|
27
37
|
6. **If implementation is needed, recommend** — don't modify code
|
|
@@ -242,10 +252,10 @@ If ANY answer is NO → Stop. Adhere to read-only contract.
|
|
|
242
252
|
|
|
243
253
|
## Source patterns
|
|
244
254
|
|
|
245
|
-
- `src/runtime/task-runner.ts` — task execution pipeline
|
|
246
|
-
- `src/runtime/child-pi
|
|
247
|
-
- `src/runtime/live-agent-manager.ts` — live agent lifecycle
|
|
248
|
-
- `src/state/event-log
|
|
255
|
+
- `src/runtime/task-runner.ts` + `src/runtime/task-runner/` — task execution pipeline
|
|
256
|
+
- `src/runtime/child-pi/` — worker spawning
|
|
257
|
+
- `src/runtime/live-session/live-agent-manager.ts` — live agent lifecycle
|
|
258
|
+
- `src/state/event-log/` — event logging system
|
|
249
259
|
- `src/extension/team-tool/` — API and tool handling
|
|
250
260
|
- `src/ui/` — widget and TUI rendering
|
|
251
261
|
|
|
@@ -275,7 +275,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
275
275
|
|
|
276
276
|
---
|
|
277
277
|
|
|
278
|
-
## Tier 1 — Critical unit tests (~
|
|
278
|
+
## Tier 1 — Critical unit tests (~13s, 124 tests, the only suite you need for broker/UI changes)
|
|
279
279
|
|
|
280
280
|
**What**: run the curated 15-file fast subset.
|
|
281
281
|
|
|
@@ -287,7 +287,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
287
287
|
time npm run test:critical
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
Expected output: `# tests
|
|
290
|
+
Expected output: `# tests 124 # pass 124 # fail 0 # cancelled 0 # duration_ms ~12800`. (Count was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 since 2026-09-20, 120 @ 2026-10-07, **124 @ 2026-10-18** (+`child-executor-heartbeat-pulse.test.ts` → 15 files) — verify with the actual run; the skill's hard-coded numbers drift between releases.) **Since RR-021 round-2 the runner itself fails loudly on `# cancelled N>0`** (dual-reporter TAP scrape — the round-1 unref incident shipped green with 2 cancelled tests), and **round-3 added the missing-file pre-check**: an explicitly-listed file that does not exist now exits 1 naming the path instead of being silently skipped by node:test.
|
|
291
291
|
|
|
292
292
|
**References**:
|
|
293
293
|
|
|
@@ -327,7 +327,7 @@ PI_CREW_BROKER=0 npm run test:critical
|
|
|
327
327
|
PI_CREW_BROKER=1 npm run test:critical
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
All three must show `# pass
|
|
330
|
+
All three must show `# pass 124 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
|
|
331
331
|
|
|
332
332
|
**References**:
|
|
333
333
|
|
|
@@ -609,7 +609,7 @@ If the two md5s match → session is on the latest code. If not → user must `/
|
|
|
609
609
|
|
|
610
610
|
**What**: drive the team tool + subagent tools through a spread of actions from the parent Pi session to prove the full surface works end-to-end, not just one smoke run.
|
|
611
611
|
|
|
612
|
-
**Why this exists**: Tier 7 proves one team run completes. But pi-crew has **
|
|
612
|
+
**Why this exists**: Tier 7 proves one team run completes. But pi-crew has **57 `team` actions across 5 domain dispatchers** (`RUN` 10: run/parallel/plan/plans/orchestrate/resume/retry/wait/steer/goal · `STATUS` 17: status/list/get/events/artifacts/breakdown/summary/graph/search/health/worktrees/checkpoint/cache/explain/onboard/recommend/help · `CONTROL` 7 · `MANAGE` 17 · `AUTOMATE` 6 — `src/schema/team-tool-schema.ts:437-489`) plus the subagent tools (`Agent`, `crew_agent`, `get_subagent_result`, `steer_subagent` — with `crew_agent_result`/`crew_agent_steer` aliases), the worker-side tools (`ask`, `delegate`, `message`), and the `team-settings` config surface, dispatched through several code paths (sync run, async run, chain, parallel, direct subagent). A schema or registration regression can break *some* paths while others still pass. The battery catches path-specific breakage.
|
|
613
613
|
|
|
614
614
|
**When required**: any change to `src/schema/team-tool-schema.ts`, `src/extension/registration/team-tool.ts`, `src/extension/team-tool/*.ts` (handler dispatch), or the subagent-tool registration. Optional but cheap for any change — the read-only actions are free.
|
|
615
615
|
|
|
@@ -1383,7 +1383,7 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
|
|
|
1383
1383
|
|
|
1384
1384
|
| Feature | Code entry | Verify via |
|
|
1385
1385
|
|---|---|---|
|
|
1386
|
-
| Team tool —
|
|
1386
|
+
| Team tool — 57 actions / 5 domains | `src/schema/team-tool-schema.ts:437-489`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
|
|
1387
1387
|
| Runtime mode `child-process` (default) | `src/runtime/child-pi/` | 9b sync run + T7 |
|
|
1388
1388
|
| Runtime mode `scaffold` (dry-run) | `src/runtime/task-runner/pre-execution.ts:176` | 9b `action='plan'`/`'plans'` (preview không spawn) hoặc run với `runtime.mode='scaffold'` |
|
|
1389
1389
|
| Runtime mode `live-session` (experimental) | `src/runtime/live-session/` | Run với `runtime.mode='live-session'` + irc tool xuất hiện trong worker (`src/runtime/custom-tools/irc-tool.ts`) |
|
|
@@ -1464,7 +1464,7 @@ The skill does NOT need to be updated for every commit — only when the cited l
|
|
|
1464
1464
|
## Quick reference — exact commands
|
|
1465
1465
|
|
|
1466
1466
|
```bash
|
|
1467
|
-
# Tier 1 (critical unit, ~
|
|
1467
|
+
# Tier 1 (critical unit, ~13s, 124 tests)
|
|
1468
1468
|
npm run test:critical
|
|
1469
1469
|
# Tier 2 (3-path proof, broker changes only)
|
|
1470
1470
|
PI_CREW_BROKER=0 npm run test:critical
|
|
@@ -1548,7 +1548,7 @@ PY
|
|
|
1548
1548
|
|
|
1549
1549
|
Before claiming "tested":
|
|
1550
1550
|
|
|
1551
|
-
- [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 @ 2026-09-20,
|
|
1551
|
+
- [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 @ 2026-09-20, 120 @ 2026-10-07, **124 @ 2026-10-18**; record the actual count in the report.
|
|
1552
1552
|
- [ ] Tier 2: 3-path proof all pass — **required if you touched `src/config/defaults.ts` or `src/extension/registration/lifecycle-handlers.ts`**
|
|
1553
1553
|
- [ ] Tier 3: `npm run typecheck` exit 0, `npm run build:bundle` exit 0
|
|
1554
1554
|
- [ ] Tier 4: bundle md5 matches what the session loaded (or user has `/quit`-ed + reopened)
|
|
@@ -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 { LIMIT_CEILINGS } from "../config/config-validation.ts";
|
|
4
5
|
import { getCrewEnv } from "../config/env-vars.ts";
|
|
5
6
|
import { discoverProviderExtensionPaths } from "../runtime/model/provider-extensions.ts";
|
|
6
7
|
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
|
|
@@ -462,11 +463,16 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
462
463
|
contextMode,
|
|
463
464
|
maxTurns: (() => {
|
|
464
465
|
const n = Number.parseInt(frontmatter.maxTurns, 10);
|
|
465
|
-
|
|
466
|
+
// R7-A3 (wave-2b): clamp to the config ceiling — a repo-sourced agent
|
|
467
|
+
// file with maxTurns: 1e9 became the effective run cap whenever
|
|
468
|
+
// runtime.maxTurns was unset (config paths already bound at 10k).
|
|
469
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, LIMIT_CEILINGS.runtimeMaxTurns) : undefined;
|
|
466
470
|
})(),
|
|
467
471
|
maxTokens: (() => {
|
|
468
472
|
const n = Number.parseInt(frontmatter.maxTokens, 10);
|
|
469
|
-
|
|
473
|
+
// R7-A3: same clamp class — budget-equivalent token counts use the
|
|
474
|
+
// handoff-budget ceiling (1M) as the family bound.
|
|
475
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, LIMIT_CEILINGS.runtimeHandoffBudgetTokens) : undefined;
|
|
470
476
|
})(),
|
|
471
477
|
effort:
|
|
472
478
|
frontmatter.effort === "low" || frontmatter.effort === "medium" || frontmatter.effort === "high"
|
|
@@ -63,7 +63,9 @@ function validateConfigWithWarnings(raw: unknown): string[] {
|
|
|
63
63
|
return [];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// wave-2b (R7-A3): exported so agent frontmatter parsing (discover-agents)
|
|
67
|
+
// clamps project-sourced values with the SAME ceilings the config paths use.
|
|
68
|
+
export const LIMIT_CEILINGS = {
|
|
67
69
|
maxConcurrentWorkers: 1024,
|
|
68
70
|
maxTaskDepth: 100,
|
|
69
71
|
maxChildrenPerTask: 1000,
|
|
@@ -108,6 +110,64 @@ export const DANGEROUS_OBJECT_KEYS = new Set([
|
|
|
108
110
|
"__lookupSetter__",
|
|
109
111
|
]);
|
|
110
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Case-normalized view of DANGEROUS_OBJECT_KEYS. The canonical set keeps the
|
|
115
|
+
* real member spellings (toString, valueOf, …), but key matching must be
|
|
116
|
+
* lowercase on BOTH sides — `"valueOf".toLowerCase()` is `"valueof"`, which
|
|
117
|
+
* would never match a camelCase entry.
|
|
118
|
+
*/
|
|
119
|
+
const DANGEROUS_KEYS_LOWER = new Set([...DANGEROUS_OBJECT_KEYS].map((key) => key.toLowerCase()));
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when `key` collides with a reserved Object.prototype member.
|
|
123
|
+
* Case-insensitive to catch `__Proto__`, `CONSTRUCTOR`, `TOSTRING`, etc.
|
|
124
|
+
*/
|
|
125
|
+
export function isDangerousKey(key: string): boolean {
|
|
126
|
+
return DANGEROUS_KEYS_LOWER.has(key.toLowerCase());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* True when ANY dot-separated segment of a dotted config path collides with a
|
|
131
|
+
* reserved Object.prototype member. This is the guard for dotted-path walkers
|
|
132
|
+
* (setNested/unsetPath): it catches the NESTED pollution vectors
|
|
133
|
+
* `__proto__.polluted` and `constructor.prototype.x` that single-key checks
|
|
134
|
+
* miss, because the walk hits `obj["__proto__"]` (the [[Prototype]] getter)
|
|
135
|
+
* long before the final segment is written.
|
|
136
|
+
*/
|
|
137
|
+
export function isDangerousPath(dottedPath: string): boolean {
|
|
138
|
+
return dottedPath.split(".").some((segment) => segment.length > 0 && isDangerousKey(segment));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Deeply strip dangerous prototype-pollution keys from an arbitrary value
|
|
143
|
+
* (objects, arrays, primitives pass through). Returns a fresh plain-object
|
|
144
|
+
* tree that is safe to merge/assign at a parse boundary.
|
|
145
|
+
*
|
|
146
|
+
* M1-1 shared boundary contract: every boundary that accepts a user-supplied
|
|
147
|
+
* config patch, dotted key, or translation registration (team-settings
|
|
148
|
+
* set/unset, updateConfig unsetPaths, config/autonomy patch parsing, i18n
|
|
149
|
+
* addTranslations) routes through isDangerousPath/isDangerousKey/
|
|
150
|
+
* stripDangerousKeysDeep here — ONE canonical drop rule. This is
|
|
151
|
+
* BOUNDARY-LEVEL defense only; it must not grow handler-specific predicates:
|
|
152
|
+
* carrier keys of tool payloads (`args`, `scope`, `unset`, …) are
|
|
153
|
+
* intentionally NOT dangerous keys — never add them (RT24-F1 lesson: mirrored
|
|
154
|
+
* handler predicates drifted and over-blocked reads).
|
|
155
|
+
*
|
|
156
|
+
* Case-insensitive superset of the retired per-module strippers (which covered
|
|
157
|
+
* only `__proto__`/`constructor`/`prototype` case-sensitively): this drops the
|
|
158
|
+
* full DANGEROUS_OBJECT_KEYS set at every level.
|
|
159
|
+
*/
|
|
160
|
+
export function stripDangerousKeysDeep<T>(value: T): T {
|
|
161
|
+
if (value === null || typeof value !== "object") return value;
|
|
162
|
+
if (Array.isArray(value)) return value.map((item) => stripDangerousKeysDeep(item)) as T;
|
|
163
|
+
const safe: Record<string, unknown> = {};
|
|
164
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
165
|
+
if (isDangerousKey(key)) continue;
|
|
166
|
+
safe[key] = stripDangerousKeysDeep(child);
|
|
167
|
+
}
|
|
168
|
+
return safe as T;
|
|
169
|
+
}
|
|
170
|
+
|
|
111
171
|
/**
|
|
112
172
|
* Strips dangerous Object.prototype keys from an object.
|
|
113
173
|
* Returns a new object built with Object.create(null) to prevent
|
package/src/config/config.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { withFileLockSync } from "../state/coordination/locks.ts";
|
|
|
6
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
7
|
import { isJunkPathLiteral, projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
|
|
8
8
|
import { mergeConfig } from "./config-merge.ts";
|
|
9
|
-
import { parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
|
|
9
|
+
import { isDangerousKey, isDangerousPath, parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
|
|
10
10
|
import { DEFAULT_BROKER, DEFAULT_NESTING, DEFAULT_PERSISTENCE, resolveBrokerEnvOverride } from "./defaults.ts";
|
|
11
11
|
import { getCrewEnv, getCrewEnvBool } from "./env-vars.ts";
|
|
12
12
|
import { sanitizeProjectConfig } from "./sanitize-project-config.ts";
|
|
@@ -258,6 +258,13 @@ function resolvePersistenceEnvOverrideAndDefaults(parsed: PersistenceConfig | un
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
|
|
261
|
+
// R24-H2 (M1-1): walking a raw dotted key through `target["__proto__"]`
|
|
262
|
+
// resolves to Object.prototype via the [[Prototype]] getter, so a key like
|
|
263
|
+
// `__proto__.toString` DELETES Object.prototype members. Refuse dangerous
|
|
264
|
+
// segments at the walker itself (shared proto-safe helpers in
|
|
265
|
+
// config-validation.ts) — this covers
|
|
266
|
+
// every unsetPaths consumer (team-settings unset, team `config` action).
|
|
267
|
+
if (isDangerousPath(dottedPath)) return;
|
|
261
268
|
const parts = dottedPath.split(".").filter(Boolean);
|
|
262
269
|
if (parts.length === 0) return;
|
|
263
270
|
let target: Record<string, unknown> = record;
|
|
@@ -305,6 +312,66 @@ function measureJsonDepth(root: unknown): number {
|
|
|
305
312
|
return maxDepth;
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
/**
|
|
316
|
+
* M3-13 (R45-F1): `parseConfig` is a strict WHITELIST builder (see
|
|
317
|
+
* config-validation.ts) — it returns an object literal of known keys only, so
|
|
318
|
+
* merging a patch into `parseConfig(current)` and writing the result back
|
|
319
|
+
* silently DELETES every unknown on-disk key (typos, forward-compat keys
|
|
320
|
+
* written by a newer version, keys owned by another tool). updateConfig used
|
|
321
|
+
* to do exactly that on every write.
|
|
322
|
+
*
|
|
323
|
+
* This helper re-attaches the raw on-disk shape underneath the normalized
|
|
324
|
+
* merge result: `parsed` wins for every key/value it defines (so mergeConfig's
|
|
325
|
+
* per-key semantics and its sanitizers keep applying to known keys), while raw
|
|
326
|
+
* keys that the whitelist dropped are carried through untouched.
|
|
327
|
+
*
|
|
328
|
+
* Dangerous prototype keys are NEVER carried through (M1-1/R24-H1: the parse
|
|
329
|
+
* boundary drops `__proto__`/`constructor`/`prototype` at every depth).
|
|
330
|
+
*
|
|
331
|
+
* @param parsed normalized merge result (what the whitelist understands)
|
|
332
|
+
* @param raw raw on-disk record, already with `unsetPaths` applied
|
|
333
|
+
* @returns a new record shaped like the on-disk file, preserving key order
|
|
334
|
+
*/
|
|
335
|
+
function preserveUnknownKeys(parsed: Record<string, unknown>, raw: Record<string, unknown>): Record<string, unknown> {
|
|
336
|
+
const target = cloneRawRecord(raw, 0);
|
|
337
|
+
return overlayKnownKeys(target, parsed, 0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const MAX_PRESERVED_DEPTH = 100;
|
|
341
|
+
|
|
342
|
+
function isPlainRecordObject(value: unknown): value is Record<string, unknown> {
|
|
343
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Depth-bounded clone that keeps raw key order and skips dangerous keys. */
|
|
347
|
+
function cloneRawRecord(source: Record<string, unknown>, depth: number): Record<string, unknown> {
|
|
348
|
+
const out: Record<string, unknown> = {};
|
|
349
|
+
if (depth > MAX_PRESERVED_DEPTH) return out;
|
|
350
|
+
for (const [key, value] of Object.entries(source)) {
|
|
351
|
+
if (isDangerousKey(key)) continue;
|
|
352
|
+
out[key] = isPlainRecordObject(value) ? cloneRawRecord(value, depth + 1) : value;
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Overlay `source` (normalized) onto `target` (raw clone), recursing into shared objects. */
|
|
358
|
+
function overlayKnownKeys(target: Record<string, unknown>, source: Record<string, unknown>, depth: number): Record<string, unknown> {
|
|
359
|
+
if (depth > MAX_PRESERVED_DEPTH) return target;
|
|
360
|
+
for (const [key, value] of Object.entries(source)) {
|
|
361
|
+
// `undefined` means "not set in this layer" (mergeConfig's withoutUndefined
|
|
362
|
+
// contract) — never write it over a preserved raw value.
|
|
363
|
+
if (value === undefined) continue;
|
|
364
|
+
if (isDangerousKey(key)) continue;
|
|
365
|
+
const existing = target[key];
|
|
366
|
+
if (isPlainRecordObject(value) && isPlainRecordObject(existing)) {
|
|
367
|
+
overlayKnownKeys(existing, value, depth + 1);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
target[key] = value;
|
|
371
|
+
}
|
|
372
|
+
return target;
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
function readConfigRecord(filePath: string): Record<string, unknown> {
|
|
309
376
|
if (!fs.existsSync(filePath)) return {};
|
|
310
377
|
// Defense-in-depth: reject config files larger than 10 MB before parsing.
|
|
@@ -462,7 +529,11 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
|
|
|
462
529
|
const message = error instanceof Error ? error.message : String(error);
|
|
463
530
|
throw new Error(`Could not update pi-crew config: ${message}`);
|
|
464
531
|
}
|
|
465
|
-
|
|
532
|
+
const normalizedCurrent = parseConfig(current);
|
|
533
|
+
// Known-key semantics (per-key merge + otlp header sanitization) stay in
|
|
534
|
+
// mergeConfig — see M3-13 below for why the WRITE target is rebuilt from
|
|
535
|
+
// the raw record instead of from this whitelist-normalized result.
|
|
536
|
+
let merged = mergeConfig(normalizedCurrent, patch);
|
|
466
537
|
if (options.unsetPaths?.length) {
|
|
467
538
|
const raw = JSON.parse(JSON.stringify(merged)) as Record<string, unknown>;
|
|
468
539
|
for (const unset of options.unsetPaths) unsetPath(raw, unset);
|
|
@@ -472,12 +543,26 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
|
|
|
472
543
|
// (e.g. `team action='config'` with an empty patch — read-only path).
|
|
473
544
|
// Both sides are parseConfig-normalized, so JSON.stringify key order is
|
|
474
545
|
// deterministic (same construction path); no key sorting needed.
|
|
475
|
-
const
|
|
476
|
-
|
|
546
|
+
const normalizedUnchanged = JSON.stringify(merged) === JSON.stringify(normalizedCurrent);
|
|
547
|
+
// M3-13 (R45-F1): parseConfig is a whitelist, so writing `merged` erased
|
|
548
|
+
// every unknown on-disk key (typos, forward-compat keys). Rebuild the
|
|
549
|
+
// written shape from the RAW record: known keys take the merged value,
|
|
550
|
+
// unknown keys survive untouched. `unsetPaths` are applied to the raw
|
|
551
|
+
// base too, so an explicit unset of an unknown key still removes it.
|
|
552
|
+
const rawBase = cloneRawRecord(current, 0);
|
|
553
|
+
for (const unset of options.unsetPaths ?? []) unsetPath(rawBase, unset);
|
|
554
|
+
const toWrite = preserveUnknownKeys(merged as unknown as Record<string, unknown>, rawBase);
|
|
555
|
+
// An unsetPaths entry may name a key the whitelist cannot see (an unknown
|
|
556
|
+
// key), in which case the normalized comparison above reports "unchanged"
|
|
557
|
+
// even though the raw record must change. Only that case overrides the
|
|
558
|
+
// skip: if applying the unsets left the raw base byte-identical, nothing
|
|
559
|
+
// outside the whitelist moved and the legacy skip stands.
|
|
560
|
+
const unsetsChangedRaw = JSON.stringify(rawBase) !== JSON.stringify(current);
|
|
561
|
+
if (normalizedUnchanged && !unsetsChangedRaw) {
|
|
477
562
|
return { path: filePath, config: merged, written: false }; // unchanged — skip write
|
|
478
563
|
}
|
|
479
564
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
480
|
-
atomicWriteFile(filePath, `${JSON.stringify(
|
|
565
|
+
atomicWriteFile(filePath, `${JSON.stringify(toWrite, null, 2)}\n`);
|
|
481
566
|
// (F16) Invalidate the loadConfig cache after a write — the next
|
|
482
567
|
// caller must see the new value, not a 0-2s stale snapshot.
|
|
483
568
|
invalidateConfigCache();
|
|
@@ -69,6 +69,11 @@ export const ESSENTIAL_PROCESS_ENV_VARS: readonly string[] = [
|
|
|
69
69
|
"XDG_DATA_HOME",
|
|
70
70
|
"XDG_CACHE_HOME",
|
|
71
71
|
"XDG_RUNTIME_DIR",
|
|
72
|
+
// R1-M1 (wave-2b): Node's fs.realpathSync.native / os.tmpdir / sqlite WAL
|
|
73
|
+
// all consult TMPDIR; a host with a non-default TMPDIR spawned children that
|
|
74
|
+
// diverged from it (temp paths under /tmp vs $TMPDIR) — pass it through so
|
|
75
|
+
// parent and children agree on the temp root.
|
|
76
|
+
"TMPDIR",
|
|
72
77
|
];
|
|
73
78
|
|
|
74
79
|
/** Node/npm toolchain vars (nvm prefixes, npm registry/config). */
|