pi-crew 0.11.4 → 0.11.6

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +2 -2
  3. package/dist/index.mjs +498 -492
  4. package/docs/actions-reference.md +22 -0
  5. package/docs/dynamic-workflows.md +37 -0
  6. package/index.ts +9 -5
  7. package/package.json +18 -14
  8. package/scripts/analyze-run.mjs +101 -14
  9. package/skills/model-routing-context/SKILL.md +4 -3
  10. package/skills/real-test-pi-crew/SKILL.md +10 -7
  11. package/src/config/env-vars.ts +6 -0
  12. package/src/extension/crew-vibes/index.ts +29 -10
  13. package/src/extension/pi-api.ts +1 -0
  14. package/src/extension/registration/lifecycle-handlers.ts +65 -3
  15. package/src/extension/team-tool/dispatch/index.ts +4 -3
  16. package/src/extension/team-tool/dispatch/status.ts +7 -3
  17. package/src/extension/team-tool/inspect.ts +90 -0
  18. package/src/extension/team-tool/respond.ts +54 -0
  19. package/src/extension/team-tool/status.ts +10 -0
  20. package/src/extension/team-tool.ts +393 -3
  21. package/src/extension/webhook-notify.ts +23 -1
  22. package/src/observability/exporters/otlp-exporter.ts +93 -11
  23. package/src/prompt/prompt-runtime.ts +13 -4
  24. package/src/runtime/agent-control.ts +1 -15
  25. package/src/runtime/background-runner.ts +3 -0
  26. package/src/runtime/broker/crew-broker.ts +12 -103
  27. package/src/runtime/broker/protocol/request-parsers.ts +9 -1
  28. package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
  29. package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
  30. package/src/runtime/dispatch-batch.ts +3 -1
  31. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
  32. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
  33. package/src/runtime/live-session/live-agent-manager.ts +35 -0
  34. package/src/runtime/live-session/live-session-runtime.ts +34 -5
  35. package/src/runtime/mcp-proxy.ts +110 -7
  36. package/src/runtime/model/pi-args.ts +14 -11
  37. package/src/runtime/model/provider-extensions.ts +67 -1
  38. package/src/runtime/recovery/crash-recovery.ts +6 -1
  39. package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
  40. package/src/runtime/stale-reconciler.ts +55 -0
  41. package/src/runtime/task-output-context.ts +11 -1
  42. package/src/runtime/task-runner/child-executor.ts +130 -4
  43. package/src/runtime/task-runner/output-splitter.ts +48 -5
  44. package/src/runtime/task-runner/post-execution.ts +2 -2
  45. package/src/runtime/task-runner/pre-execution.ts +37 -24
  46. package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
  47. package/src/schema/team-tool-schema.ts +27 -1
  48. package/src/state/contracts.ts +16 -0
  49. package/src/state/crew-init.ts +114 -0
  50. package/src/state/stores/active-run-registry.ts +13 -6
  51. package/src/state/types.ts +0 -8
  52. package/src/ui/goal-flag.ts +44 -0
  53. package/src/ui/live-run-sidebar.ts +5 -1
  54. package/src/ui/pi-ui-compat.ts +20 -3
  55. package/src/ui/run-dashboard.ts +2 -1
  56. package/src/ui/widget/widget-model.ts +6 -2
  57. package/src/utils/ip-literal.ts +145 -0
  58. package/src/workflows/workflow-config.ts +6 -0
@@ -48,6 +48,7 @@ Reads the versioned `PlanRecord` list at `<stateRoot>/plans/plans.json` (ADR-4).
48
48
  | `search` | BM25-ranked agent/team discovery | Smart search |
49
49
  | `events` | Read the event log | Debug/audit |
50
50
  | `artifacts` | List run artifacts | View outputs |
51
+ | `breakdown` | Read per-task prompt-token breakdown artifacts | Debug prompt bloat (requires `PI_CREW_PROMPT_BREAKDOWN=1`) |
51
52
  | `worktrees` | List run worktree metadata | Inspect worktrees |
52
53
  | `graph` | Load/save/list run graphs | Visualization |
53
54
  | `explain` | Explain a run or task structure | Understand a run |
@@ -77,6 +78,7 @@ Reads the versioned `PlanRecord` list at `<stateRoot>/plans/plans.json` (ADR-4).
77
78
  | **`workflow-list`** | **v0.9.0** List static + dynamic workflows | Discover workflows |
78
79
  | **`workflow-save`** | **v0.9.0** Overwrite `.dwf.ts` source (requires `confirm:true`) | Update dynamic workflow |
79
80
  | **`workflow-delete`** | **v0.9.0** Delete a `.dwf.ts` (requires `confirm:true`) | Remove dynamic workflow |
81
+ | `compare` | Compare two runs side by side (US-021) | Diff before/after a fix, or two attempts at one goal |
80
82
  | `help` | Display help text | Help |
81
83
 
82
84
  ---
@@ -420,6 +422,26 @@ Append-only JSONL events: task.started, task.completed, run.blocked, etc.
420
422
 
421
423
  ---
422
424
 
425
+ ### `breakdown` — Per-task prompt-token breakdown
426
+
427
+ G18 (opt-in measurement surface): reads the per-task
428
+ `metadata/<taskId>.prompt-breakdown.json` artifacts and aggregates per-task
429
+ `estTokens` totals plus the top-5 largest prompt sections. Artifacts are
430
+ recorded only when the host Pi session is started with
431
+ `PI_CREW_PROMPT_BREAKDOWN=1` (default off — zero cost when off).
432
+
433
+ ```json
434
+ {
435
+ "action": "breakdown",
436
+ "runId": "team_...",
437
+ "taskId": "01_plan"
438
+ }
439
+ ```
440
+
441
+ `taskId` is optional — omit it to aggregate every task with a breakdown.
442
+
443
+ ---
444
+
423
445
  ### `worktrees` — Worktree metadata
424
446
 
425
447
  ```json
@@ -163,6 +163,13 @@ child worker, so no tokens are wasted past the limit.
163
163
  - `spent()` accumulates each `ctx.agent()` run's reported `usage.input + usage.output`.
164
164
  - Set it via the workflow's `maxTokenBudget` field, or the run `tokenBudget` param
165
165
  (the param overrides the workflow value).
166
+ - **Content-based reserve (SDD-3 W-C G14):** before spawning, the call reserves
167
+ `max(512, estimateTokens(prompt + systemPrompt chars))` — the real in-tree
168
+ estimator (`chars/4`, same as `pre-execution.ts`) instead of the old flat
169
+ 4096. Short calls no longer over-reserve (more headroom for concurrent
170
+ siblings); very long prompts reserve proportionally. On completion the
171
+ reserve is adjusted to the reported usage; on failure it is refunded in
172
+ full, so `spent()` never drifts from reality.
166
173
 
167
174
  ```ts
168
175
  if (ctx.budget.total !== null && ctx.budget.remaining() < 500) {
@@ -170,6 +177,32 @@ if (ctx.budget.total !== null && ctx.budget.remaining() < 500) {
170
177
  }
171
178
  ```
172
179
 
180
+ ### Agent-call cap (SDD-3 W-C G13)
181
+
182
+ Every dynamic workflow is bounded by a **maxAgentCalls cap** — the maximum number
183
+ of `ctx.agent()` invocations per run (for DWF, steps ≡ agent calls: the loop
184
+ lives inside your script, so each spawned agent IS a step). This closes the
185
+ no-budget runaway gap where the only bounds were the semaphore (concurrency 4)
186
+ and the blind 30-minute script timeout.
187
+
188
+ - **Default: 200 calls** (`DEFAULT_MAX_AGENT_CALLS`) when nothing is configured —
189
+ a run is always bounded, even with no `tokenBudget`. 200 gives ≥5× headroom
190
+ over the largest real workflows (distill pipelines, adaptive plans use ~10-40).
191
+ - Override via the run `maxAgentCalls` param or `workflow.maxAgentCalls`
192
+ (frontmatter/`DynamicWorkflowConfig`); values < 1 or non-numeric fall back to
193
+ the default.
194
+ - The cap counts **every `ctx.agent()` invocation**, including cached replays
195
+ (PERS-1 hits) — a spin loop over the same prompt cannot bypass it.
196
+ - **Tripping the cap terminates the run with a structured reason**, not a blind
197
+ timeout: `ctx.agent()` throws `DwfAgentCallCapError`
198
+ (`"dynamic workflow agent-call cap reached (N/N calls; …)"`), the ctx signal is
199
+ aborted (in-flight children are killed; `ctx.pipeline()` rethrows instead of
200
+ swallowing the error into a `null` item), a `dwf.log` event records the trip,
201
+ and the runner fails the run with a `dwf.failed` event carrying the reason.
202
+ - Scripts that `catch` the cap error and keep looping will still make no further
203
+ spawns — every subsequent `ctx.agent()` re-trips the cap instantly (no cost).
204
+ Only the wall-clock script timeout bounds a deliberately-catching script.
205
+
173
206
  ### Typed args (round-14 P1-5)
174
207
 
175
208
  `ctx.args<T>()` returns typed workflow arguments (sourced from `manifest.args`,
@@ -214,6 +247,10 @@ script can reach `process`/`require` directly or via constructor walking. The
214
247
  - The path-allowlist (`resolveRealContainedPath`) limits **WHERE** scripts load from
215
248
  (`.crew/workflows/`, `<proj>/.pi/teams/workflows/`, `~/.pi/agent/extensions/pi-crew/workflows/`),
216
249
  not what they can do.
250
+ - **Resource bound (SDD-3 G13):** agent dispatches are capped at
251
+ `maxAgentCalls` (default 200) even without a token budget — a runaway script
252
+ terminates with a structured cap reason instead of burning unbounded worker
253
+ spawns until the script timeout.
217
254
  - `isolated-vm` (real V8 isolate) is planned for **v1.5**.
218
255
  - **Only place `.dwf.ts` files you have reviewed** in `.crew/workflows/`.
219
256
 
package/index.ts CHANGED
@@ -4,7 +4,10 @@
4
4
  * Resolution order:
5
5
  * 1. dist/index.mjs (bundle) when present AND not explicitly disabled.
6
6
  * Default since v0.9.17 close-out — benchmarks show bundle is
7
- * ~19% faster total cold-start (post-fsync) than strip-types.
7
+ * ~19% faster cold-start on a cold Node cache; with a warm
8
+ * NODE_COMPILE_CACHE the gap shrinks to ~2%. The bundle's primary
9
+ * value is correctness (vendored typebox) and deterministic startup,
10
+ * not raw speed.
8
11
  * See `scripts/bench-cold-start.mjs` for reproducible numbers.
9
12
  * 2. Inline strip-types loading — fallback when bundle is missing
10
13
  * (e.g. dev clone without `npm run build:bundle`) OR when
@@ -52,9 +55,9 @@ import { fileURLToPath } from "node:url";
52
55
  import { dirname, resolve } from "node:path";
53
56
 
54
57
  // IMPORTANT: src/ imports here MUST be dynamic (lazy `import()`), never
55
- // top-level static imports. Published npm packages ship only dist/index.mjs
56
- // in the tarball (src/ is excluded by the package.json "files" field — keeps
57
- // the package ~5MB instead of ~16MB). A top-level static
58
+ // top-level static imports. The npm tarball ships dist/index.mjs as the
59
+ // entry and ALSO includes src/ (see "files" in package.json) for source
60
+ // maps of future tooling. dist/index.mjs is ~1.6MB. A top-level static
58
61
  // `import { x } from "./src/..."` is resolved by the module loader BEFORE
59
62
  // this file's code runs, so it would break every fresh `npm install`
60
63
  // (Cannot find module './src/...'). Dynamic import() defers src/ resolution
@@ -105,7 +108,8 @@ if (!envForceOff) {
105
108
  // Lazy src/ fallback — only resolved when the bundle is unavailable
106
109
  // (dev clones without a built dist/). NEVER imported in published
107
110
  // installs, where the bundle always ships + loads above. This dynamic
108
- // import() is what lets us exclude src/ from the npm tarball.
111
+ // import() keeps startup lazy so the bundle loads only when the extension
112
+ // actually activates.
109
113
  let srcRegister:
110
114
  | typeof import("./src/extension/register.ts").registerPiTeams
111
115
  | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:integration",
87
87
  "test:full": "npm run test:unit && npm run test:integration && npm run test:integration:slow",
88
88
  "test:unit": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=180000 --test-force-exit 'test/unit/**/*.test.ts'",
89
- "test:critical": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/runtime/broker/crew-broker-handshake.test.ts test/unit/runtime/broker/crew-broker-stale-socket.test.ts test/unit/runtime/broker/crew-broker-feature-flag.test.ts test/unit/runtime/broker/crew-broker-server-gate.test.ts test/unit/runtime/broker/crew-broker-client-fallback.test.ts test/unit/runtime/broker/crew-broker-mailbox-observer.test.ts test/unit/runtime/broker/crew-broker-close-during-reconnect.test.ts test/unit/runtime/broker/crew-broker-steer-dedup.test.ts test/unit/runtime/broker/crew-broker-symlink-steering.test.ts test/unit/ui/keybinding-map.parity.test.ts test/unit/ui/pi-tui-dispatch-probe.test.ts test/unit/utils/session-utils-extract.test.ts test/unit/config/config-schema-sync.test.ts test/unit/runtime/child-pi/child-pi-env-spread.test.ts",
89
+ "test:critical": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/runtime/broker/crew-broker-handshake.test.ts test/unit/runtime/broker/crew-broker-stale-socket.test.ts test/unit/runtime/broker/crew-broker-feature-flag.test.ts test/unit/runtime/broker/crew-broker-server-gate.test.ts test/unit/runtime/broker/crew-broker-client-fallback.test.ts test/unit/runtime/broker/crew-broker-mailbox-observer.test.ts test/unit/runtime/broker/crew-broker-close-during-reconnect.test.ts test/unit/runtime/broker/crew-broker-steer-dedup.test.ts test/unit/runtime/broker/crew-broker-symlink-steering.test.ts test/unit/ui/keybinding-map.parity.test.ts test/unit/ui/pi-tui-dispatch-probe.test.ts test/unit/utils/session-utils-extract.test.ts test/unit/config/config-schema-sync.test.ts test/unit/runtime/child-pi/child-pi-env-spread.test.ts test/unit/runtime/task-runner/child-executor-heartbeat-pulse.test.ts",
90
90
  "test:watch": "node scripts/test-runner.mjs --watch --test-concurrency=4 --test-timeout=30000 'test/unit/**/*.test.ts'",
91
91
  "test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=300000 test/integration/*.test.ts",
92
92
  "test:integration:slow": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=600000 test/integration/slow/*.test.ts",
@@ -133,26 +133,27 @@
133
133
  "@earendil-works/pi-agent-core": "*",
134
134
  "@earendil-works/pi-ai": "*",
135
135
  "@earendil-works/pi-coding-agent": "*",
136
- "@earendil-works/pi-tui": "*"
136
+ "@earendil-works/pi-tui": "*",
137
+ "@sinclair/typebox": "*"
137
138
  },
138
139
  "dependencies": {
139
- "@sinclair/typebox": "^0.34.50",
140
- "acorn": "^8.17.0",
140
+ "acorn": "^8.18.0",
141
141
  "ajv": "^8.20.0",
142
142
  "cli-highlight": "^2.1.11",
143
143
  "diff": "^5.2.0",
144
- "esbuild": "^0.28.1",
144
+ "esbuild": "^0.28.2",
145
145
  "jiti": "^2.7.0",
146
- "yaml": "^2.9.0"
146
+ "yaml": "^2.9.1"
147
147
  },
148
148
  "devDependencies": {
149
- "@biomejs/biome": "^2.5.3",
150
- "@earendil-works/pi-agent-core": "^0.84.0",
151
- "@earendil-works/pi-ai": "^0.84.0",
152
- "@earendil-works/pi-coding-agent": "^0.84.0",
153
- "@earendil-works/pi-tui": "^0.84.0",
154
- "@types/node": "^25.9.5",
155
- "tsx": "^4.23.0",
149
+ "@biomejs/biome": "^2.5.14",
150
+ "@earendil-works/pi-agent-core": "^1.0.0",
151
+ "@earendil-works/pi-ai": "^1.0.0",
152
+ "@earendil-works/pi-coding-agent": "^1.0.0",
153
+ "@earendil-works/pi-tui": "^1.0.0",
154
+ "@sinclair/typebox": "^0.34.52",
155
+ "@types/node": "^25.9.8",
156
+ "tsx": "^4.23.15",
156
157
  "typescript": "^7.0.2"
157
158
  },
158
159
  "peerDependenciesMeta": {
@@ -167,6 +168,9 @@
167
168
  },
168
169
  "@earendil-works/pi-tui": {
169
170
  "optional": true
171
+ },
172
+ "@sinclair/typebox": {
173
+ "optional": true
170
174
  }
171
175
  },
172
176
  "readmeFilename": "README.md"
@@ -12,35 +12,40 @@
12
12
  * pathological multi-GB runs, which pi-crew does not produce.
13
13
  *
14
14
  * Output:
15
- * docs/perf-report-<runId>.md — báo cáo tiếng Việt, bảng + 🔴 highlight
15
+ * docs/perf/perf-report-<runId>.md — báo cáo tiếng Việt, bảng + 🔴 highlight
16
+ * (opt-in --ascii / PI_CREW_ASCII=1: glyph-free ASCII markers — GH-059)
16
17
  * bench/results/<runId>.json — structured JSON
17
18
  */
18
19
 
19
20
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, createReadStream } from "node:fs";
20
21
  import { join } from "node:path";
21
22
  import { createInterface } from "node:readline";
23
+ import { pathToFileURL } from "node:url";
22
24
 
23
25
  // ---------- CLI ----------
24
26
  function parseArgs(argv) {
25
- const args = { runId: null, crewRoot: null, resources: null, events: false, agents: false };
27
+ const args = { runId: null, crewRoot: null, resources: null, events: false, agents: false, ascii: false };
26
28
  for (let i = 2; i < argv.length; i++) {
27
29
  const a = argv[i];
28
30
  if (a === "--crew-root") args.crewRoot = argv[++i];
29
31
  else if (a === "--resources") args.resources = argv[++i];
30
32
  else if (a === "--events") args.events = true;
31
33
  else if (a === "--agents") args.agents = true;
34
+ else if (a === "--ascii") args.ascii = true;
32
35
  else if (a === "-h" || a === "--help") {
33
- process.stderr.write("Usage: analyze-run.mjs <runId> [--crew-root <path>] [--resources <path>] [--events]\n");
36
+ process.stderr.write("Usage: analyze-run.mjs <runId> [--crew-root <path>] [--resources <path>] [--events] [--agents] [--ascii]\n");
34
37
  process.exit(0);
35
38
  } else if (!a.startsWith("--")) {
36
39
  args.runId = a;
37
40
  }
38
41
  }
39
42
  if (!args.runId) {
40
- process.stderr.write("Error: runId required.\nUsage: analyze-run.mjs <runId> [--crew-root <path>]\n");
43
+ process.stderr.write("Error: runId required.\nUsage: analyze-run.mjs <runId> [--crew-root <path>] [--events] [--agents] [--ascii]\n");
41
44
  process.exit(1);
42
45
  }
43
46
  args.crewRoot = args.crewRoot || join(process.env.HOME || "/home/bom", ".crew");
47
+ // --ascii can also come from the environment (CI / vault workflows)
48
+ args.ascii = args.ascii || process.env.PI_CREW_ASCII === "1";
44
49
  return args;
45
50
  }
46
51
 
@@ -57,6 +62,47 @@ const fmtMs = (n) => {
57
62
  const money = (n) => (n ? `$${n.toFixed(4)}` : "$0");
58
63
  const esc = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 80);
59
64
 
65
+ // ---------- ASCII mode (--ascii / PI_CREW_ASCII=1) — issue #59, spec GH-059 ----------
66
+ // Post-process sweep over the FINAL markdown string, applied at the md write
67
+ // sites only (JSON/CSV outputs are already glyph-free). ORDER MATTERS:
68
+ // composite labels and duration-anchored slow markers first, VS16 sequences
69
+ // (⚠️/⏱️ = base + U+FE0F) before bare codepoints, variation-selector/ZWJ
70
+ // safety net LAST — a leftover U+FE0F is itself inside the consumer lint
71
+ // ranges (U+FE00-FE0F), so partial matches would still fail their gate.
72
+ const ASCII_RULES = [
73
+ // composite severity labels (sevLabel) — per-glyph would render "(!)(!)"
74
+ [/🔴🔴 Nghiêm trọng/g, "CRITICAL"],
75
+ [/🔴 Lỗi/g, "ERROR"],
76
+ [/🟡 Blocked/g, "BLOCKED"],
77
+ [/🟡 Retry/g, "RETRY"],
78
+ [/⚪ Warning/g, "WARN"],
79
+ // slow-phase markers: duration-formatted values followed by 🔴 (timeline
80
+ // cells "626.3s 🔴", bottlenecks "**626.3s** 🔴") + legend/table header
81
+ [/(\d+(?:\.\d+)?ms|\d+(?:\.\d+)?s|\d+m\d+s) 🔴/g, "$1 [SLOW]"],
82
+ [/🔴 = phase vượt/g, "[SLOW] = phase vượt"],
83
+ [/🔴>30s/g, "[SLOW]>30s"],
84
+ // VS16 sequences — MUST match the full sequence, never the bare codepoint
85
+ [/⚠️/g, "[WARN]"],
86
+ [/⏱️/g, ""],
87
+ // semantic markers
88
+ [/🔴/g, "(!)"],
89
+ [/🟡/g, "(~)"],
90
+ [/🔵/g, "(i)"],
91
+ [/⚪/g, "(~)"],
92
+ [/✅/g, "[OK]"],
93
+ [/❌/g, "[FAIL]"],
94
+ [/✓/g, "[ok]"],
95
+ // decorative heading emoji — dropped
96
+ [/[📋💰🐌🚨📊💡📈⚡🔍]/gu, ""],
97
+ // safety net: leftover variation selectors / ZWJ
98
+ [/[\uFE00-\uFE0F\u200D]/g, ""],
99
+ ];
100
+ function toAscii(md) {
101
+ const out = ASCII_RULES.reduce((s, [re, rep]) => s.replace(re, rep), md);
102
+ // collapse the double space left by dropped heading emoji ("## 📋 X" → "## X")
103
+ return out.replace(/^(#+)[ \t]+/gm, "$1 ");
104
+ }
105
+
60
106
  // ---------- line-by-line JSONL reader ----------
61
107
  async function readJsonl(path) {
62
108
  const out = [];
@@ -570,6 +616,37 @@ async function analyzeTranscripts(artifactsDir) {
570
616
  }
571
617
 
572
618
  // ---------- resources ----------
619
+ /**
620
+ * Max-decimation for per-agent trajectories: when samples exceed `cap`, split
621
+ * the index range into ≤cap buckets and keep each bucket's MAX-rssBytes
622
+ * point (first index wins ties — deterministic).
623
+ *
624
+ * The previous even-stride cap (keep every ceil(n/80)-th index) is
625
+ * aliasing-prone: a single-sample RSS spike (OOM, short-lived tool
626
+ * subprocess) landing between kept indices was dropped ENTIRELY — the
627
+ * report charted a flat line where the incident happened. Max-per-bucket
628
+ * guarantees the spike's bucket cannot select a smaller point, so peaks
629
+ * always survive downsampling.
630
+ *
631
+ * Output keeps input order (buckets are index-ordered) with ts strictly
632
+ * increasing (equal-ts samples across worker/tool PIDs collapse).
633
+ */
634
+ export function decimateTrajectory(points, cap = 80) {
635
+ if (!Array.isArray(points) || points.length <= cap) return points;
636
+ const n = points.length;
637
+ const kept = new Array(cap).fill(null);
638
+ for (let i = 0; i < n; i++) {
639
+ const bucket = Math.min(Math.floor((i * cap) / n), cap - 1);
640
+ const cur = kept[bucket];
641
+ if (cur === null || (points[i].rssBytes || 0) > (cur.rssBytes || 0)) kept[bucket] = points[i];
642
+ }
643
+ const out = [];
644
+ for (const p of kept) {
645
+ if (p && (!out.length || p.ts > out[out.length - 1].ts)) out.push(p);
646
+ }
647
+ return out;
648
+ }
649
+
573
650
  function analyzeResources(path, subagents) {
574
651
  if (!path || !existsSync(path)) return null;
575
652
  const samples = readJsonlSync(path);
@@ -657,8 +734,9 @@ function analyzeResources(path, subagents) {
657
734
  avgCpuPct: sa.ownCpuSamples ? Math.round((sa.ownCpuSum / sa.ownCpuSamples) * 10) / 10 : 0,
658
735
  attributed: sa.samples > 0,
659
736
  descendantPids: [...sa.descendantPids],
660
- // trajectory: cap to ~80 points (even stride) so per-agent files stay readable
661
- trajectory: sa.trajectory.length <= 80 ? sa.trajectory : sa.trajectory.filter((_, i) => i % Math.ceil(sa.trajectory.length / 80) === 0),
737
+ // trajectory: cap to ~80 points via max-decimation (per-bucket max RSS) so
738
+ // per-agent files stay readable WITHOUT dropping spikes to stride aliasing
739
+ trajectory: decimateTrajectory(sa.trajectory, 80),
662
740
  }));
663
741
  return {
664
742
  sampleCount: samples.length,
@@ -706,7 +784,7 @@ async function main() {
706
784
 
707
785
  // F1 (audit): validate runId to prevent path injection — runId flows into
708
786
  // join(crewRoot, "state", "runs", runId) AND into output filenames
709
- // (docs/perf-report-<runId>.md, bench/results/<runId>.json). Without this,
787
+ // (docs/perf/perf-report-<runId>.md, bench/results/<runId>.json). Without this,
710
788
  // runId="../../tmp/x" would read/write outside the intended dirs.
711
789
  if (!/^[A-Za-z0-9_.-]+$/.test(runId)) {
712
790
  process.stderr.write(`Error: invalid runId (must be alphanumeric/_/./-): ${runId}\n`);
@@ -939,7 +1017,10 @@ async function main() {
939
1017
 
940
1018
  const resultsDir = join(process.cwd(), "bench", "results");
941
1019
  mkdirSync(resultsDir, { recursive: true });
942
- const docsDir = join(process.cwd(), "docs");
1020
+ // Perf reports write to docs/perf/ (runtime output, gitignored via the
1021
+ // narrow `docs/perf/perf-report-team_*` pattern — curated perf docs in
1022
+ // docs/perf/ stay tracked). Legacy location was docs/ top level.
1023
+ const docsDir = join(process.cwd(), "docs", "perf");
943
1024
  mkdirSync(docsDir, { recursive: true });
944
1025
  const jsonPath = join(resultsDir, `${runId}.json`);
945
1026
  writeFileSync(jsonPath, JSON.stringify(report, null, 2));
@@ -964,7 +1045,7 @@ async function main() {
964
1045
  for (const sa of report.subagents) {
965
1046
  const rows = eventTimeline ? eventTimeline.rows.filter((r) => r.taskId === sa.taskId) : [];
966
1047
  const mdSa = renderSubagentFile(sa, report, rows);
967
- writeFileSync(join(agentsDir, `${sa.taskId}.md`), mdSa + "\n");
1048
+ writeFileSync(join(agentsDir, `${sa.taskId}.md`), (args.ascii ? toAscii(mdSa) : mdSa) + "\n");
968
1049
  }
969
1050
  process.stderr.write(`[analyze-run] wrote ${report.subagents.length} per-agent files → ${agentsDir}/\n`);
970
1051
  }
@@ -972,7 +1053,7 @@ async function main() {
972
1053
  // ---------- emit markdown ----------
973
1054
  const md = renderMarkdown(report, ea, subagents, args.agents);
974
1055
  const mdPath = join(docsDir, `perf-report-${runId}.md`);
975
- writeFileSync(mdPath, md);
1056
+ writeFileSync(mdPath, args.ascii ? toAscii(md) : md);
976
1057
 
977
1058
  process.stderr.write(`[analyze-run] wrote ${mdPath}\n[analyze-run] wrote ${jsonPath}\n`);
978
1059
  }
@@ -1327,7 +1408,13 @@ function renderMarkdown(report, ea, subagents, perAgent = false) {
1327
1408
  return L.join("\n");
1328
1409
  }
1329
1410
 
1330
- main().catch((e) => {
1331
- process.stderr.write(`[analyze-run] fatal: ${e.stack || e}\n`);
1332
- process.exit(1);
1333
- });
1411
+ // CLI main-guard: run main() only when executed directly (node scripts/analyze-run.mjs),
1412
+ // never when imported (analyze-run-decimation.test.ts imports the pure
1413
+ // decimateTrajectory). import.meta.url vs argv[1] through pathToFileURL so
1414
+ // relative invocations resolve identically to absolute ones.
1415
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1416
+ main().catch((e) => {
1417
+ process.stderr.write(`[analyze-run] fatal: ${e.stack || e}\n`);
1418
+ process.exit(1);
1419
+ });
1420
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: model-routing-context
3
3
  description: >
4
- Model routing, parent context, thinking level, and prompt construction workflow. Use when changing model fallback, child Pi args, inherited context, task prompts, or compact-read behavior.
4
+ Model routing, parent context, thinking level, and prompt construction workflow. Use when changing model fallback, child Pi args, inherited context, task prompts, or compaction/compact-string behavior.
5
5
  When NOT to use: project-level config (use resource-discovery-config); routing for non-pi-crew agents.
6
6
 
7
7
  origin: pi-crew
@@ -10,7 +10,7 @@ triggers:
10
10
  - "parent context"
11
11
  - "thinking level"
12
12
  - "task prompts"
13
- - "compact read"
13
+ - "compaction"
14
14
 
15
15
  ---
16
16
  # model-routing-context
@@ -27,7 +27,8 @@ Use this skill when working on model/context propagation.
27
27
  - Preserve parent model inheritance unless an agent/task/user explicitly provides a non-empty model override.
28
28
  - Treat empty strings and whitespace model values as absent.
29
29
  - Carry relevant parent conversation context as reference-only; do not let it override explicit task instructions or safety constraints.
30
- - Respect compact-read/compaction summaries when building context; avoid ballooning prompts with redundant transcript data.
30
+ - Distinguish the two compaction layers: host-Pi session compaction (`source/pi/.../session-manager.ts`, `agent-session.ts`, compaction modules) is a reference-only concept — it is not a pi-crew API.
31
+ - Respect pi-crew's write-side compaction when building context: `compactString`/`compactValue` transcript truncation in `src/runtime/child-pi/child-pi-transcript.ts:132-164` (`compactString` exported at :134; cuts marked `[truncated]` / `[pi-crew truncated N entries]`). Avoid ballooning prompts with redundant transcript data.
31
32
  - Avoid inline dynamic imports for model providers or prompt helpers.
32
33
  - When changing model precedence, add tests for undefined, empty, whitespace, agent, task, parent, and explicit tool override cases.
33
34
  - Redact secrets in context snippets and child prompts where logs/artifacts may persist them.
@@ -108,6 +108,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
108
108
  | `src/ui/` | T1 → (T5|T6) → T13 |
109
109
  | `src/config/` (kể cả migration-validator) | T1 → T11c |
110
110
  | `src/runtime/child-pi/` (spawn/kill/steer) | T7 → T9g |
111
+ | `src/runtime/mcp-proxy.ts` / `src/runtime/model/pi-args.ts` (worker argv build + MCP role-gate — G2/G3 surface, SDD-2) | T7 (`argv-flags.smoke.ts` với `PI_CREW_SMOKE=1`) → T9b-W loadout theo role (post-G2: read-only KHÔNG có MCP trên live-session path là ĐÚNG — xem mục full-loadout sanity 9b) + pinned suites `mcp-proxy.test.ts` / `phase6-runtime-hardening.test.ts` / `child-pi-timeout`+`child-pi-surface` (argv/spill) |
111
112
  | `src/runtime/async-runner.ts` / `src/runtime/background-runner.ts` (spawn, allowlist, entry-guard, `executeBackgroundRun` core) | T1 → T9b async (event `async.spawned` KHÔNG được có `data.inline:true` — battery phải chứng minh runner DETACHED thật) → T7; đụng `BACKGROUND_RUNNER_ENV_ALLOWLIST` → thêm T10b/T10c (quirk F4-2026-08-30: mux env bị strip ⇒ async headless âm thầm) |
112
113
  | `src/runtime/surface/` | T10a → T10b → T10c (dò herdr bằng `HERDR_ENV=1`, KHÔNG phải `$TMUX`) |
113
114
  | `src/prompt/` (ask/message/delegate/surface-worker) | T9b-W → T9g |
@@ -124,7 +125,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
124
125
  | `dist/` (bundle) | T3 → T4 → T8 → T11j |
125
126
  | bất kỳ đường dẫn trên, trước khi commit | gates đầy đủ: critical + unit + tsc + biome + md5 |
126
127
 
127
- **When to use**: after any change to `src/runtime/broker/*.ts` (broker + tokens + issuer), `src/ui/`, `src/config/` (incl. `src/config/migration-validator.ts`), `src/extension/registration/lifecycle-handlers.ts`, `src/runtime/child-pi/*.ts` (worker spawn/kill/steering), `src/runtime/async-runner.ts` + `src/runtime/background-runner.ts` (detached spawn + allowlist + entry-guarded `executeBackgroundRun` core — async dispatch, xem quy tắc tier ở decision table), `src/runtime/heartbeat/*` + `src/ui/heartbeat-aggregator.ts` (ambient heartbeat — guest `gc-*` skip), `src/runtime/surface/*.ts` (MuxSurface providers, degrade, launch script), `src/prompt/*.ts` (worker-side tools: ask / message / delegate / surface-worker recorder), `src/runtime/goal-workflow/plan-templates.ts`, `src/runtime/team-runner.ts` or `src/runtime/task-runner/**` (scheduler / execution — Tier 7 smoke), `src/state/**` (durable state — Tier 7 + 9a events/status + **Tier 11a read-your-writes**), `src/utils/paths.ts` + `src/utils/project-markers.ts` + `src/runtime/stale-reconciler.ts` + `src/extension/team-tool/health-monitor.ts` (run-state layout + symlink defense — pinned suites listed in the 2026-09-20 update), `src/runtime/live-session/**` + `src/runtime/custom-tools/*` (live-session mode + worker custom tools), `src/schema/team-tool-schema.ts` (or any `Type.Unsafe({...})` schema definition), `src/extension/registration/team-tool.ts`, `workflows/*.workflow.md`, `.github/workflows/*.yml` (CI env — Tier 11e), `scripts/wc-gate.mjs` (Tier 11b), or before any commit touching these paths. Schema changes additionally require Tier 9 (feature battery) because the team tool's TypeBox schema is validated by pi-ai BEFORE the handler runs — a too-strict or malformed schema breaks every action silently. Surface changes additionally require Tier 10 (surface-mode battery) because surface is fail-closed: every failure degrades to headless and the run still goes green — only pane-level evidence proves the panes engaged. Resource `.md` changes (agent bodies/frontmatter, skill metadata, discovery, frontmatter parsing) additionally require **Tier 12** (resource-contract battery) because the agent/team/workflow frontmatter parser is line-based, not YAML — a folded scalar parses as `">"` for every consumer while all other tiers stay green.
128
+ **When to use**: after any change to `src/runtime/broker/*.ts` (broker + tokens + issuer), `src/ui/`, `src/config/` (incl. `src/config/migration-validator.ts`), `src/extension/registration/lifecycle-handlers.ts`, `src/runtime/child-pi/*.ts` (worker spawn/kill/steering), `src/runtime/mcp-proxy.ts` + `src/runtime/model/pi-args.ts` (worker argv build + MCP role-gate — G2/G3, SDD-2), `src/runtime/async-runner.ts` + `src/runtime/background-runner.ts` (detached spawn + allowlist + entry-guarded `executeBackgroundRun` core — async dispatch, xem quy tắc tier ở decision table), `src/runtime/heartbeat/*` + `src/ui/heartbeat-aggregator.ts` (ambient heartbeat — guest `gc-*` skip), `src/runtime/surface/*.ts` (MuxSurface providers, degrade, launch script), `src/prompt/*.ts` (worker-side tools: ask / message / delegate / surface-worker recorder), `src/runtime/goal-workflow/plan-templates.ts`, `src/runtime/team-runner.ts` or `src/runtime/task-runner/**` (scheduler / execution — Tier 7 smoke), `src/state/**` (durable state — Tier 7 + 9a events/status + **Tier 11a read-your-writes**), `src/utils/paths.ts` + `src/utils/project-markers.ts` + `src/runtime/stale-reconciler.ts` + `src/extension/team-tool/health-monitor.ts` (run-state layout + symlink defense — pinned suites listed in the 2026-09-20 update), `src/runtime/live-session/**` + `src/runtime/custom-tools/*` (live-session mode + worker custom tools), `src/schema/team-tool-schema.ts` (or any `Type.Unsafe({...})` schema definition), `src/extension/registration/team-tool.ts`, `workflows/*.workflow.md`, `.github/workflows/*.yml` (CI env — Tier 11e), `scripts/wc-gate.mjs` (Tier 11b), or before any commit touching these paths. Schema changes additionally require Tier 9 (feature battery) because the team tool's TypeBox schema is validated by pi-ai BEFORE the handler runs — a too-strict or malformed schema breaks every action silently. Surface changes additionally require Tier 10 (surface-mode battery) because surface is fail-closed: every failure degrades to headless and the run still goes green — only pane-level evidence proves the panes engaged. Resource `.md` changes (agent bodies/frontmatter, skill metadata, discovery, frontmatter parsing) additionally require **Tier 12** (resource-contract battery) because the agent/team/workflow frontmatter parser is line-based, not YAML — a folded scalar parses as `">"` for every consumer while all other tiers stay green.
128
129
 
129
130
  > **Path map (2026-08-26 reorg + A1)**: `src/runtime/crew-broker*.ts` → `src/runtime/broker/`; `src/runtime/child-pi*.ts` → `src/runtime/child-pi/`; `src/runtime/plan-templates.ts` (flat) → `src/runtime/goal-workflow/plan-templates.ts`; NEW dirs `src/runtime/surface/` and `src/prompt/`. Test files moved with them (`test/unit/crew-broker-*.test.ts` → `test/unit/runtime/broker/`, `test/unit/keybinding-map.parity.test.ts` → `test/unit/ui/`, ...).
130
131
 
@@ -138,6 +139,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
138
139
 
139
140
  > **2026-09-29 update (RR-021 optimization wave + 4 review rounds, baseline `79a758d2`..`4393858c`, 37 commits; story `docs/stories/RR-021.md`; audit `pi-crew-optimization-audit.md`)**: (1) **Correctness/hang-risks** — `classifyReviewOutcome` word-boundary fix (the `\baccept\b` string-literal bug + negation guard), git/rg probe timeouts, `withRunLockSync`→`withRunLock` rerouting (lock family v0.9.26 intact), `reconcileAllStaleRuns` concurrent (`mapConcurrent` bound 4, per-run error isolation), sleeps→waitFor. (2) **Bundle hygiene** — esbuild/acorn/diff load via lazy sync-createRequire shims, ajv external, dead code −767 lines: bundle 3360 KB → **1575.2 KB**; NEW gates `check:lazy-imports` (ci.yml:142; src-side `// LAZY:` marker enforcement + dist-side no-hoisted-heavy-import assertion) and `check:bundle-size` (ci.yml:169). (3) **Test-gate integrity (review rounds 2–4)** — the runner now fails LOUDLY on `# cancelled N>0` (dual reporter: stdout UX unchanged, full TAP to a temp file, scraped in BOTH single-run and batch paths — `parseCancelledCount` + 21-test `test-runner-exit.test.ts`), on explicitly-listed files that do not exist (node:test v22 silently skipped them → exit 0; round-3 MAJOR B1), and `test:bundle` routes through the guarded runner (was the only CI gate bypassing it). Incremental tsc: typecheck ~20s → **~3.5s warm**. Suite anchors @ 2026-09-29: unit **8204 tests / 8201 pass / 0 fail / 3 skipped** (909 files), integration **136 / 132 pass / 4 skipped** (35 files incl. 3 slow), `test:critical` **116** (~18s), bundle **1575.2 KB** md5 `35ca335a90da01ac631493f220aaf121` (`--committed-hash` OK). Line refs moved: `PI_CREW_TEST_RUNNER_TIMEOUT_MS` now `scripts/test-runner.mjs:419`; crash-recovery design comment `:228`.
140
141
 
142
+ > **2026-10-01 update (SDD-2 W-B — G2 MCP role-gate + G3 spill-always + Phase D bump 0.99.1, commits `66f37875`→`f0b343ed`, execution record §13 `pi-crew-sdd-2026-09-30-buoi1.md`)**: (1) **G2 flips full-loadout sanity semantics** — MCP presence in workers is now ROLE- and PATH-dependent: the live-session SDK path strips MCP (`DefaultResourceLoader({ extensionsOverride: stripMcpExtensions })`) cho read-only roles (`mcpPermittedForRole` — explorer/reviewer/security-reviewer/analyst/critic/planner + unknown default-deny); a read-only worker WITHOUT MCP is CORRECT, a write-role worker without MCP is a regression; nếu strip không arm được (SDK hết export `DefaultResourceLoader`…) → fail-open + warning event `task.mcp_enforcement_degraded` (registry 168 → 169, `check:event-types --enforce` clean). CLI child-process path CHƯA cover (WI-4 defer: `~/.pi/agent/mcp.json` trống + `-ne` giết provider `oc-go` — read-only roles vẫn discover adapter MCP trên đường đó). (2) **G3 spill-always**: MỌI task text qua `task.md` 0600 — argv worker KHÔNG bao giờ chứa task text; probe đọc argv phải expect `@<temp>/task.md`, task ngắn mất prefix `Task: `; `TASK_ARG_LIMIT` đã xóa. (3) Phase D bump `@earendil-works/*` → 0.99.x: SDK bỏ option `enableMCP`, tool seam cần `ExtensionToolContext` (export mới `src/extension/pi-api.ts`). Decision table thêm row `mcp-proxy.ts`/`model/pi-args.ts`; D5 loadout line refs dời `:283-330` → `:280-336`.
141
143
  > **2026-09-21 update (F-L2 — steer boot-window loss, found by the live feature battery)**: a steer written to `<artifactsRoot>/steering/<taskId>.jsonl` while the worker extension is still LOADING was silently lost forever — `registerPiTeamsPromptRuntime` starts the file poll at load time, pi's loader gates action methods until bind (`sendMessage: notInitialized` THROWS), and the old poll advanced `lastOffset` to `stat.size` BEFORE the line loop while its per-line catch swallowed the throw as "malformed line" (run `team_20260921035840`, root-caused by watcher-driven bisection probes R7-R9: +0.65s post-boot = lost, +2.5s+ = delivered). Fix (`src/prompt/prompt-runtime.ts`): walk the buffer by byte offset, advance only past terminal verdicts (delivered/non-steer/rejected/malformed), rewind to the failing line on a DELIVERY failure, `createSeenSteerIdSet.unmark()` releases the dedup id so the retry is not skipped. Regression suite `test/unit/prompt/prompt-runtime-steer-boot-window.test.ts` (4 tests) — mutation-checked both layers (offset-revert → 4 fail; unmark-removal → exactly the id-bearing test fails). Note: `dist/index.mjs` is UNCHANGED by this fix — child workers load prompt-runtime from the SOURCE path (`pi-args.ts PROMPT_RUNTIME_EXTENSION_PATH`), so the fix is live without a bundle bump.
142
144
  >
143
145
  > **F-L2 live re-verification (post-restart, 2026-09-21 afternoon)** — two independent proofs: (a) **lab**: pre-write the steer, then `PI_CREW_STEERING_FILE=<file> pi --extension <repo>/src/prompt/prompt-runtime.ts -p '<long task>'` — stderr shows `[pi-crew:prompt-runtime.steer-delivery-failed] Extension runtime not initialized…` ×2 (pre-bind ticks now LOGGED instead of swallowed) then the post-bind tick delivers and the model complies; (b) **real team run**: watcher writes at **+0.51s** after the steering dir appears (the previously-lost window) → worker session log gains the `custom_message` and the probe token lands in `results/<taskId>.txt`. Suite anchors @ 2026-09-21 (post-fix): unit **8006 tests / 8003 pass / 0 fail / 3 skipped** (875 files; 1088s under load), integration **134 / 130 pass / 4 skipped**, `test:critical` **116**, bundle **3360.1 KB** md5 `2f45df91ed0580ee5bb5b9b57a8a8c3f` (UNCHANGED — see the source-path note above).
@@ -204,7 +206,7 @@ The skill maps to existing CI gates as follows:
204
206
  | `PI_CREW_SMOKE=1` env | Tier 11e | set ONLY in `weekly-smoke.yml` (auth-gated); nightly.yml deliberately does NOT (comment at `:24`) |
205
207
  | `npm run check:decision-drift` | Tier 11 (doc↔code) | **build-blocking in `.github/workflows/ci.yml` since 2026-09-17** (review-remediation baseline): every `PI_CREW_*` token cited in `docs/decisions/*.md` must exist in `src/` |
206
208
  | `npm run check:env-vars` | Tier 11 | **build-blocking in `ci.yml` since 2026-09-17**: every `PI_CREW_*` read must be routed via `getCrewEnv` + registered |
207
- | `npm run check:event-types -- --enforce` | Tier 9a/11 | **build-blocking in `ci.yml` since 2026-09-17**: event registry (`src/state/contracts.ts`) must cover every emitted type — 168 registered / 0 drift @ 2026-09-17 (was 89, drift silent) |
209
+ | `npm run check:event-types -- --enforce` | Tier 9a/11 | **build-blocking in `ci.yml` since 2026-09-17**: event registry (`src/state/contracts.ts`) must cover every emitted type — 168 registered / 0 drift @ 2026-09-17 (was 89, drift silent); **169 @ 2026-10-01** sau SDD-2 (+`task.mcp_enforcement_degraded`) |
208
210
  | `npm run check:lazy-imports` | Tier 3 companion (RR-021 WI-2.3 + review rounds 2-4) | `scripts/check-lazy-imports.mjs` — src-side: every runtime `import()` position needs a `// LAZY:` marker (known gaps: ternary-else / object-property / case-label / runtime member-access after same-line colon — documented in-script); dist-side: no module-scope hoisted heavy import (esbuild/acorn/diff/jiti/cli-highlight/yaml/ajv). Build-blocking at `ci.yml:142` |
209
211
  | `npm run check:bundle-size` | Tier 3 companion | `scripts/check-bundle-size.mjs` — committed bundle ≤ budget (Sprint 6 gate, `ci.yml:169`; budget re-anchored when RR-021 shrank the bundle 3360→1575 KB) |
210
212
  | `PI_CREW_TEST_RUNNER_TIMEOUT_MS` env | Tier 1 companion | overrides the test-runner spawn deadline (`scripts/test-runner.mjs:419` since RR-021; was :206); default **1_500_000 ms** since 2026-09-17 (was 900_000 — sized for a 5800-test suite; the full suite outgrew it and the ETIMEDOUT became a false green pre-F05). Since RR-021 rounds 2-3 the runner ALSO fail-closes on `# cancelled N>0` (TAP dual-reporter scrape) and on explicitly-listed missing files — see `test/unit/scripts/test-runner-exit.test.ts` (21 tests) |
@@ -593,7 +595,7 @@ If the two md5s match → session is on the latest code. If not → user must `/
593
595
  - **message notify**: goal says "use the `message` tool to notify the parent when done". Proves `msg.send` (non-blocking) + the broker `from`-override (anti-spoof) + the wake pattern on the orchestrator session. Rate-limit 10 msg/60s per worker — a burst probe should hit the limit, not hang.
594
596
  - **message DM/group**: goal says "DM task `<sibling taskId>` / send to group `x`" — proves `to:` routing + inbox pickup (delivered as fenced `<inbox-message>` DATA, not instructions).
595
597
  - **delegate nesting**: goal says "use the `delegate` tool to spawn a child agent". Proves the role gate is open for every role (D8, default-on), the depth cap (`nesting.maxDepth: 4` — a depth-5 attempt must reject with the structured policy message + `delegate.rejected` event, never silently), and the nested-slot budget. Kill switch: `nesting.enabled: false` in **user** config only (sensitive — project config cannot flip it).
596
- - **full loadout sanity** (D5): in any 9b run, have the worker report its loaded extensions/skills/tools. Workers are FULL pi sessions by default — no `--no-extensions`, no `--tools` allowlist, `--no-skills` only when the agent frontmatter says `inheritSkills: false`. A worker missing MCP tools/skills means the loadout policy regressed (see Anti-patterns, armed-role row).
598
+ - **full loadout sanity** (D5): in any 9b run, have the worker report its loaded extensions/skills/tools. Workers are FULL pi sessions by default — no `--no-extensions`, no `--tools` allowlist, `--no-skills` only when the agent frontmatter says `inheritSkills: false`. **Post-G2 (SDD-2, `8dab9e49` 2026-10-01): MCP presence is ROLE- and PATH-dependent on the live-session SDK path** — read-only roles (explorer/reviewer/security-reviewer/analyst/critic/planner + unknown/undefined, per `mcpPermittedForRole`) are INTENTIONALLY stripped of MCP (`DefaultResourceLoader({ extensionsOverride: stripMcpExtensions })` drops `builtin:mcp` + `pi-mcp-adapter`) — a read-only worker missing MCP tools is CORRECT, not a regression; a WRITE-role worker (executor/verifier/writer/test-engineer…) missing MCP still means the loadout policy regressed. If the strip could not arm (SDK stopped exporting `DefaultResourceLoader` / agent dir unresolvable), a non-permitted role fails open with a `task.mcp_enforcement_degraded` warning event — grep it before calling a read-only worker's MCP absence "enforced". The CLI child-process path is NOT yet covered (WI-4 deferred — read-only roles there still discover adapter MCP); see Anti-patterns, armed-role row.
597
599
  4. **Acceptance**: every action returns without `Unknown type` / `Validation failed for tool team` / empty error text; every spawn path completes with `consistency=1` and the expected probe token in the agent output.
598
600
 
599
601
  **Real measured outcome** (this session, after the v0.9.57 schema fix): 9a (15 team actions) + 9b (4 subagent tools / 3 run paths) exercised; all green; the two silent-failure modes that motivated this tier (`Unknown type` from `Type.Unsafe` without Kind, and `Validation failed for tool team` from empty-string-strict schema) were caught ONLY by this battery — Tier 1-8 all passed while the team tool was broken live. The session also surfaced the unauthorized-agent-edit anti-pattern (a chain-run agent edited `chain-runner.ts` mid-smoke) — see Anti-patterns.
@@ -1093,7 +1095,7 @@ PY
1093
1095
  | `makeFakeCtx({ flagOn: false })` without `brokerEnv: "0"` | `makeFakeCtx` deletes `PI_CREW_BROKER` env if `brokerEnv` is undefined | `612e18b` (test fix) | `test/unit/runtime/broker/crew-broker-server-gate.test.ts:78` — pass `brokerEnv: "0"` to preserve env |
1094
1096
  | Trust green CI on one OS | macOS/Windows regressions slip through | n/a (permanent) | `.crew/knowledge.md` — "CI runs 3 OSes ... A flake on one OS IS a real bug" |
1095
1097
  | Trusting a team-run agent not to edit the repo under test | Agents spawned by `team`/`Agent`/`crew_agent` inherit the session cwd and have `edit`/`write` tools — a proactive LLM (observed with deepseek) will make **unauthorized source edits** to pi-crew during a trivial smoke run (e.g. "improving" `chain-runner.ts` while parsing a chain string). The edit can be correct + green-tested yet still be unintended scope creep that silently lands in your commit. **Sharpened by D5 (2026-08-26)**: workers used to be tool-allowlisted (`read,grep,bash,...` by role); since full-loadout default EVERY worker has `edit`/`write` + extensions, so this risk now applies to ANY role, not just armed ones. | n/a (permanent) | After EVERY team/subagent run: `git status` and verify each changed file was authored by you. Diff + review any surprise change before staging. Consider `workspaceMode: 'worktree'` for parallel/risky runs to isolate mutations. |
1096
- | **Armed-role tool-surface bug (found live 2026-08-11; INVERTED by D5 2026-08-26)**: originally an opt-in tool (e.g. `scratchpad`) armed via `ROLE_TOOL_CONFIGS[role]` + env never appeared in the worker surface — the builtin `agents/*.md` frontmatter `tools:` allowlist hard-filtered it via `--tools`. **Since D5 (`bcb9dd5d`, spec v0.7 §10) workers are FULL pi sessions by default: no `--no-extensions`, no `--tools`, no `--no-skills` unless the agent frontmatter declares them (`src/runtime/model/pi-args.ts:283-330`) — so the default failure mode flipped.** Now a tool missing from a worker means either (a) the agent's frontmatter declares a restrictive `tools:` list (opt-in) that doesn't include it, or (b) `inheritSkills: false` / SEC-1 declaration-strip on a dynamic/project agent source. Control tools (`ask`, `delegate`) are auto-added to any declared list. Reproduce: `pi -p --tools read,bash "list tools"` → restricted; plain `pi -p` → full set. | `f753be30` → `bcb9dd5d` | **Fix**: for agents that OPT IN to restrictions, keep `agents/*.md` frontmatter `tools:` in sync with `ROLE_TOOL_CONFIGS` (add new tools to BOTH for pinned roles). A worker claiming a tool is "not available" is a REAL signal — check the worker's actual argv (`--tools` present?) and frontmatter, not just env vars. |
1098
+ | **Armed-role tool-surface bug (found live 2026-08-11; INVERTED by D5 2026-08-26)**: originally an opt-in tool (e.g. `scratchpad`) armed via `ROLE_TOOL_CONFIGS[role]` + env never appeared in the worker surface — the builtin `agents/*.md` frontmatter `tools:` allowlist hard-filtered it via `--tools`. **Since D5 (`bcb9dd5d`, spec v0.7 §10) workers are FULL pi sessions by default: no `--no-extensions`, no `--tools`, no `--no-skills` unless the agent frontmatter declares them (`src/runtime/model/pi-args.ts:280-336`) — so the default failure mode flipped.** Now a tool missing from a worker means either (a) the agent's frontmatter declares a restrictive `tools:` list (opt-in) that doesn't include it, or (b) `inheritSkills: false` / SEC-1 declaration-strip on a dynamic/project agent source. Control tools (`ask`, `delegate`) are auto-added to any declared list. Reproduce: `pi -p --tools read,bash "list tools"` → restricted; plain `pi -p` → full set. | `f753be30` → `bcb9dd5d` | **Fix**: for agents that OPT IN to restrictions, keep `agents/*.md` frontmatter `tools:` in sync with `ROLE_TOOL_CONFIGS` (add new tools to BOTH for pinned roles). A worker claiming a tool is "not available" is a REAL signal — check the worker's actual argv (`--tools` present?) and frontmatter, not just env vars. |
1097
1099
  | **Surface test that never engages surface**: `runtime.surface.visibleAgents` defaults to `[]` (nobody). A test setting only `mode:"auto"` (already default) passes green with ZERO panes — surface's fail-closed degrade makes the headless path indistinguishable from success in the run result. | n/a (process) | Always set `visibleAgents` when testing surface, and require pane-level evidence (events `worker.surface_spawned`/`worker.surface_closed`, `tmux list-panes` during the run, sentinel PID). See Tier 10. |
1098
1100
  | **Reading `manifest.surface.panes == {}` at run END as "zero panes engaged"** (observed 2026-08-27, full-10tier report): `releaseSurfacePane` deletes the pane entry the moment the pane closes, so a FULLY SUCCESSFUL surface run also ends with `panes:{}` — the report flipped a live herdr engagement (pane `w6:pW`, `worker.surface_spawned` seq 99) into "by-design headless, gate short-circuited". Same trap, other direction: the executor worker re-derived the gate trace from ITS OWN env (`PI_CREW_DEPTH=1` — the CORRECT and EXPECTED depth for a tier-1 worker) instead of the HOST env the gate actually reads (`child-pi.ts` passes `depthEnv ?? process.env`), concluding "headless" while literally running inside a herdr pane (`PI_CREW_SURFACE_PANE=w6:pW` sat unread in its own env). | n/a (process) | Engage-evidence = `events.jsonl` (`worker.surface_spawned` + `worker.surface_closed`, no `surface.degraded`) + `manifest.surface.provider`/`workerPids` (only the surface branch writes `workerPids`). A worker's self-report of "which path taken" is a HYPOTHESIS — workers cannot see the host's gate inputs; trust events over worker prose. |
1099
1101
  | **Reporting "session is loading the latest code" from FILE-md5 equality alone** (disk vs symlink): a live report (2026-08-27) did exactly this — md5 disk = md5 symlink → "Tier 4 PASS" — while the parent pi process had started BEFORE the bundle rebuild and was still running the PRE-A1 bundle in memory. Every downstream anomaly then got misread as a code bug (a false "config parser drops surface" finding + root-cause misread). File equality only proves the FILES match, not what the PROCESS loaded — extension code loads at cold-start only. | n/a (permanent) | Tier 4/8 needs PROCESS-level liveness: after any rebuild, the session must `/quit` + reopen, then prove the new code is live via a behavior probe (e.g. `team action='settings' config={args:'get runtime.surface.visibleAgents'}` must recognize the key; any new run's worker env shows `PI_CREW_MAX_DEPTH=4`). Corroborate with `ps -eo pid,lstart,args | grep pi` — a session started before the rebuild mtime is stale, full stop. |
@@ -1290,7 +1292,7 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
1290
1292
 
1291
1293
  | Feature | Code entry | Verify via |
1292
1294
  |---|---|---|
1293
- | Team tool — 55 actions / 5 domains | `src/schema/team-tool-schema.ts:391-437`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
1295
+ | Team tool — 56 actions / 5 domains | `src/schema/team-tool-schema.ts:391-437`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
1294
1296
  | Runtime mode `child-process` (default) | `src/runtime/child-pi/` | 9b sync run + T7 |
1295
1297
  | 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'` |
1296
1298
  | 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`) |
@@ -1298,7 +1300,8 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
1298
1300
  | Worker tool `ask` (blocking Q→parent) | `src/prompt/prompt-runtime.ts:639`, broker wait.* | 9b-W ask round-trip |
1299
1301
  | Worker tool `message` (notify/DM/group) | `src/prompt/message-tool.ts` | 9b-W message probes |
1300
1302
  | Worker tool `delegate` (nested spawning) | `src/prompt/prompt-runtime.ts:414` | 9b-W delegate + depth-cap reject |
1301
- | Full loadout (D5) | `src/runtime/model/pi-args.ts:283-330` | 9b-W full-loadout sanity |
1303
+ | Full loadout (D5) | `src/runtime/model/pi-args.ts:280-336` | 9b-W full-loadout sanity |
1304
+ | MCP role-gate + task spill (G2/G3, SDD-2 2026-10-01) | `src/runtime/mcp-proxy.ts`, `src/runtime/model/pi-args.ts`, `src/runtime/live-session/live-session-runtime.ts` | `mcp-proxy.test.ts` (15, đỏ-trước flip), `phase6-runtime-hardening.test.ts` (spill 3 case + hardened-default), `child-pi-timeout`/`child-pi-surface` argv assertions, T7 `argv-flags.smoke.ts` (`PI_CREW_SMOKE=1`); live-session strip fail-open → event `task.mcp_enforcement_degraded` |
1302
1305
  | Surface panes tmux/herdr (A1) | `src/runtime/surface/` | T10 (10a E2E + 10b live + 10c herdr) |
1303
1306
  | Broker (mailbox, steer, tokens) | `src/runtime/broker/` | T1/T2 + 9c steer/respond + T10a test #2 |
1304
1307
  | Dashboard + keybindings + overlays | `src/ui/`, commands `src/extension/registration/commands/` | T5/T6 probe + parity golden test |
@@ -1491,7 +1494,7 @@ Source files (critical paths):
1491
1494
  - `src/runtime/broker/crew-broker-child.ts` — child-side broker client wiring
1492
1495
  - `src/ui/key-utils.ts:37-42` — `keyOf()` using pi-tui `matchesKey()`
1493
1496
  - `src/ui/keybinding-map.ts` — dispatch using `matchesKey()` (commit `f05a10d`)
1494
- - `src/runtime/model/pi-args.ts:283-330` — D5 loadout: `--tools`/`--no-skills` ONLY khi agent frontmatter khai báo; `DEFAULT_MAX_CREW_DEPTH = 4`
1497
+ - `src/runtime/model/pi-args.ts:280-336` — D5 loadout: `--tools`/`--no-skills` ONLY khi agent frontmatter khai báo; `DEFAULT_MAX_CREW_DEPTH = 4`
1495
1498
  - `src/extension/registration/subagent-tools.ts` — `Agent` (:70), `get_subagent_result` (:359), `steer_subagent` (:475, alias `crew_agent*`)
1496
1499
  - `src/runtime/live-session/live-session-runtime.ts` + `src/runtime/custom-tools/irc-tool.ts` — live-session mode + peer-to-peer irc (experimental)
1497
1500
 
@@ -290,6 +290,12 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
290
290
  name: "PI_CREW_DWF_SKIP_DETERMINISM_CHECK",
291
291
  doc: "'1' disables the DWF determinism check (deterministic-ast.ts:60)",
292
292
  },
293
+ PI_CREW_HEARTBEAT_PULSE_MS: {
294
+ name: "PI_CREW_HEARTBEAT_PULSE_MS",
295
+ parser: "int",
296
+ default: 15_000,
297
+ doc: "child-executor liveness-pulse interval (ms): touches the worker heartbeat while the attempt's child process is alive even with no stdout events (NEW-4/G25 silent-turn freeze); ≤0 disables (child-executor.ts)",
298
+ },
293
299
  PI_CREW_INTERRUPT_GUARD_INTERVAL_MS: {
294
300
  name: "PI_CREW_INTERRUPT_GUARD_INTERVAL_MS",
295
301
  parser: "int",