pi-crew 0.11.5 → 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 (51) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/dist/index.mjs +493 -488
  4. package/docs/dynamic-workflows.md +37 -0
  5. package/package.json +12 -8
  6. package/scripts/analyze-run.mjs +95 -11
  7. package/skills/model-routing-context/SKILL.md +4 -3
  8. package/skills/real-test-pi-crew/SKILL.md +9 -6
  9. package/src/config/env-vars.ts +6 -0
  10. package/src/extension/crew-vibes/index.ts +29 -10
  11. package/src/extension/pi-api.ts +1 -0
  12. package/src/extension/registration/lifecycle-handlers.ts +65 -3
  13. package/src/extension/team-tool/respond.ts +54 -0
  14. package/src/extension/team-tool/status.ts +10 -0
  15. package/src/extension/team-tool.ts +393 -3
  16. package/src/extension/webhook-notify.ts +23 -1
  17. package/src/observability/exporters/otlp-exporter.ts +93 -11
  18. package/src/prompt/prompt-runtime.ts +13 -4
  19. package/src/runtime/agent-control.ts +1 -15
  20. package/src/runtime/background-runner.ts +3 -0
  21. package/src/runtime/broker/crew-broker.ts +12 -103
  22. package/src/runtime/broker/protocol/request-parsers.ts +9 -1
  23. package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
  24. package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
  25. package/src/runtime/dispatch-batch.ts +3 -1
  26. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
  27. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
  28. package/src/runtime/live-session/live-agent-manager.ts +35 -0
  29. package/src/runtime/live-session/live-session-runtime.ts +34 -5
  30. package/src/runtime/mcp-proxy.ts +110 -7
  31. package/src/runtime/model/pi-args.ts +14 -11
  32. package/src/runtime/model/provider-extensions.ts +67 -1
  33. package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
  34. package/src/runtime/stale-reconciler.ts +47 -0
  35. package/src/runtime/task-runner/child-executor.ts +130 -4
  36. package/src/runtime/task-runner/output-splitter.ts +48 -5
  37. package/src/runtime/task-runner/post-execution.ts +0 -2
  38. package/src/runtime/task-runner/pre-execution.ts +8 -3
  39. package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
  40. package/src/schema/team-tool-schema.ts +25 -0
  41. package/src/state/contracts.ts +16 -0
  42. package/src/state/crew-init.ts +114 -0
  43. package/src/state/stores/active-run-registry.ts +13 -6
  44. package/src/state/types.ts +0 -8
  45. package/src/ui/goal-flag.ts +44 -0
  46. package/src/ui/live-run-sidebar.ts +5 -1
  47. package/src/ui/pi-ui-compat.ts +20 -3
  48. package/src/ui/run-dashboard.ts +2 -1
  49. package/src/ui/widget/widget-model.ts +6 -2
  50. package/src/utils/ip-literal.ts +145 -0
  51. package/src/workflows/workflow-config.ts +6 -0
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.5",
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,10 +133,10 @@
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.52",
140
140
  "acorn": "^8.18.0",
141
141
  "ajv": "^8.20.0",
142
142
  "cli-highlight": "^2.1.11",
@@ -147,10 +147,11 @@
147
147
  },
148
148
  "devDependencies": {
149
149
  "@biomejs/biome": "^2.5.14",
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",
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",
154
155
  "@types/node": "^25.9.8",
155
156
  "tsx": "^4.23.15",
156
157
  "typescript": "^7.0.2"
@@ -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"
@@ -13,34 +13,39 @@
13
13
  *
14
14
  * Output:
15
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,
@@ -967,7 +1045,7 @@ async function main() {
967
1045
  for (const sa of report.subagents) {
968
1046
  const rows = eventTimeline ? eventTimeline.rows.filter((r) => r.taskId === sa.taskId) : [];
969
1047
  const mdSa = renderSubagentFile(sa, report, rows);
970
- writeFileSync(join(agentsDir, `${sa.taskId}.md`), mdSa + "\n");
1048
+ writeFileSync(join(agentsDir, `${sa.taskId}.md`), (args.ascii ? toAscii(mdSa) : mdSa) + "\n");
971
1049
  }
972
1050
  process.stderr.write(`[analyze-run] wrote ${report.subagents.length} per-agent files → ${agentsDir}/\n`);
973
1051
  }
@@ -975,7 +1053,7 @@ async function main() {
975
1053
  // ---------- emit markdown ----------
976
1054
  const md = renderMarkdown(report, ea, subagents, args.agents);
977
1055
  const mdPath = join(docsDir, `perf-report-${runId}.md`);
978
- writeFileSync(mdPath, md);
1056
+ writeFileSync(mdPath, args.ascii ? toAscii(md) : md);
979
1057
 
980
1058
  process.stderr.write(`[analyze-run] wrote ${mdPath}\n[analyze-run] wrote ${jsonPath}\n`);
981
1059
  }
@@ -1330,7 +1408,13 @@ function renderMarkdown(report, ea, subagents, perAgent = false) {
1330
1408
  return L.join("\n");
1331
1409
  }
1332
1410
 
1333
- main().catch((e) => {
1334
- process.stderr.write(`[analyze-run] fatal: ${e.stack || e}\n`);
1335
- process.exit(1);
1336
- });
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. |
@@ -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",
@@ -34,6 +34,13 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
34
34
  let providerTimer: ReturnType<typeof setInterval> | undefined;
35
35
  let lastProviderUsage: ProviderUsage | null = null;
36
36
  let currentProvider: string | undefined;
37
+ // Latest ctx of the CURRENT session, re-fetched at every publish/tick.
38
+ // Never hold a ctx captured from an earlier event across a session
39
+ // replacement (newSession/fork/switchSession/reload): pi invalidates every
40
+ // prior ctx, and even reading ctx.hasUI then throws the stale-ctx error.
41
+ // session_shutdown clears this so in-flight fetch continuations no-op
42
+ // instead of publishing into a dead session (WI-1, SDD-4).
43
+ let sessionCtx: ExtensionContext | undefined;
37
44
 
38
45
  function themeOf(ctx: ExtensionContext) {
39
46
  return asCrewTheme(ctx.hasUI ? ctx.ui.theme : undefined);
@@ -57,29 +64,37 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
57
64
  /** Fetch provider usage for currentProvider and publish the quota status.
58
65
  * Called on start, on each timer tick, and immediately when the
59
66
  * provider changes (model_select) so the quota reflects the new
60
- * provider without waiting for the next tick. */
61
- async function fetchProviderAndRefresh(ctx: ExtensionContext): Promise<void> {
67
+ * provider without waiting for the next tick. The ctx is resolved at
68
+ * call time (never captured) and re-checked after the await: if the
69
+ * session was replaced while the fetch was in flight, the continuation
70
+ * is dropped — the new session re-arms its own publish. */
71
+ async function fetchProviderAndRefresh(): Promise<void> {
72
+ const ctx = sessionCtx;
73
+ if (!ctx) return; // session already shut down or replaced — nothing to publish to
62
74
  if (!config.enabled || !config.capacity.providerUsage) {
63
75
  lastProviderUsage = null;
64
76
  publishQuotaStatus(ctx);
65
77
  return;
66
78
  }
79
+ let usage: ProviderUsage | null = null;
67
80
  try {
68
- lastProviderUsage = await fetchProviderUsage(config.capacity.providerRefreshMs, currentProvider);
81
+ usage = await fetchProviderUsage(config.capacity.providerRefreshMs, currentProvider);
69
82
  } catch {
70
83
  // Never crash on provider fetch failure
71
- lastProviderUsage = null;
84
+ usage = null;
72
85
  }
86
+ if (sessionCtx !== ctx) return; // replaced mid-fetch: old ctx must never be used
87
+ lastProviderUsage = usage;
73
88
  publishQuotaStatus(ctx);
74
89
  }
75
90
 
76
- function startProviderTimer(ctx: ExtensionContext): void {
91
+ function startProviderTimer(): void {
77
92
  if (providerTimer) return;
78
93
  if (!config.capacity.providerUsage) return;
79
94
  const interval = Math.max(10000, config.capacity.providerRefreshMs);
80
95
 
81
- fetchProviderAndRefresh(ctx); // Fetch immediately on start
82
- providerTimer = setInterval(() => fetchProviderAndRefresh(ctx), interval);
96
+ fetchProviderAndRefresh(); // Fetch immediately on start
97
+ providerTimer = setInterval(() => fetchProviderAndRefresh(), interval);
83
98
  providerTimer.unref?.();
84
99
  }
85
100
 
@@ -90,12 +105,13 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
90
105
  clearVibesStatus(ctx);
91
106
  return;
92
107
  }
93
- if (config.capacity.providerUsage) startProviderTimer(ctx);
108
+ if (config.capacity.providerUsage) startProviderTimer();
94
109
  else publishQuotaStatus(ctx); // clears the stale status when disabled
95
110
  }
96
111
 
97
112
  pi.on("session_start", (_event, ctx) => {
98
113
  stopProviderTimer();
114
+ sessionCtx = ctx; // fresh ctx of the current session — any prior ctx is invalid now
99
115
  config = loadConfig();
100
116
  clearProviderUsageCache();
101
117
  // Initialize provider from current model — model_select only fires on manual switch
@@ -104,19 +120,22 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
104
120
  clearVibesStatus(ctx);
105
121
  return;
106
122
  }
107
- startProviderTimer(ctx);
123
+ startProviderTimer();
108
124
  });
109
125
 
110
126
  pi.on("model_select", (event, ctx) => {
127
+ sessionCtx = ctx;
111
128
  currentProvider = (event as { model?: { provider?: string } }).model?.provider;
112
129
  clearProviderUsageCache();
113
130
  // Fetch immediately so the quota reflects the new provider without
114
131
  // waiting for the next timer tick.
115
- fetchProviderAndRefresh(ctx);
132
+ fetchProviderAndRefresh();
116
133
  });
117
134
 
118
135
  pi.on("session_shutdown", (_event, ctx) => {
119
136
  stopProviderTimer();
137
+ sessionCtx = undefined; // later continuations must no-op, never touch a disposed ctx
138
+ // ctx is still valid here: pi emits session_shutdown before dispose().
120
139
  clearVibesStatus(ctx);
121
140
  });
122
141
 
@@ -34,6 +34,7 @@ export type {
34
34
  ExtensionAPI,
35
35
  ExtensionCommandContext,
36
36
  ExtensionContext,
37
+ ExtensionToolContext,
37
38
  ToolDefinition,
38
39
  } from "@earendil-works/pi-coding-agent";
39
40