pi-crew 0.9.40 → 0.9.42

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 (150) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/build-meta.json +7089 -6728
  3. package/dist/index.mjs +45845 -45192
  4. package/dist/index.mjs.map +4 -4
  5. package/docs/migration/atomic-write-v2-migration.md +2 -0
  6. package/docs/optimization-plan-2026-07.md +366 -0
  7. package/docs/phase4-triage.md +58 -0
  8. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
  9. package/package.json +4 -2
  10. package/scripts/README.md +6 -0
  11. package/scripts/check-all-skills.ts +3 -3
  12. package/scripts/dev-runner.mjs +106 -0
  13. package/scripts/postinstall.mjs +6 -2
  14. package/scripts/verify-skill.ts +2 -2
  15. package/src/config/config.ts +19 -0
  16. package/src/config/drift-detector.ts +97 -15
  17. package/src/config/resilient-parser.ts +4 -4
  18. package/src/extension/crew-cleanup.ts +17 -3
  19. package/src/extension/crew-vibes/index.ts +1 -1
  20. package/src/extension/cross-extension-rpc.ts +18 -5
  21. package/src/extension/management.ts +6 -5
  22. package/src/extension/register.ts +58 -1573
  23. package/src/extension/registration/command-registration.ts +57 -0
  24. package/src/extension/registration/commands.ts +2 -1
  25. package/src/extension/registration/context-builder.ts +131 -0
  26. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  27. package/src/extension/registration/foreground-run-controller.ts +210 -0
  28. package/src/extension/registration/hook-registration.ts +119 -0
  29. package/src/extension/registration/lazy-configurers.ts +124 -0
  30. package/src/extension/registration/lifecycle-handlers.ts +731 -0
  31. package/src/extension/registration/observability.ts +33 -4
  32. package/src/extension/registration/registration-types.ts +149 -0
  33. package/src/extension/registration/runtime-cleanup.ts +230 -0
  34. package/src/extension/registration/subagent-manager-setup.ts +211 -0
  35. package/src/extension/registration/tool-registration.ts +50 -0
  36. package/src/extension/registration/wire-cross-extension.ts +37 -0
  37. package/src/extension/rpc-hmac.ts +21 -1
  38. package/src/extension/run-import.ts +3 -3
  39. package/src/extension/team-tool/auto-summarize.ts +4 -4
  40. package/src/extension/team-tool/cancel.ts +5 -5
  41. package/src/extension/team-tool/chain-executor.ts +5 -4
  42. package/src/extension/team-tool/context.ts +11 -1
  43. package/src/extension/team-tool/doctor.ts +2 -1
  44. package/src/extension/team-tool/goal-wrap.ts +2 -2
  45. package/src/extension/team-tool/goal.ts +1 -1
  46. package/src/extension/team-tool/handle-schedule.ts +25 -36
  47. package/src/extension/team-tool/orchestrate.ts +0 -1
  48. package/src/extension/team-tool/parallel-dispatch.ts +5 -0
  49. package/src/extension/team-tool/run.ts +220 -214
  50. package/src/extension/team-tool/workflow-manage.ts +1 -1
  51. package/src/extension/team-tool.ts +64 -16
  52. package/src/hooks/registry.ts +7 -2
  53. package/src/observability/correlation.ts +4 -3
  54. package/src/observability/event-bus.ts +10 -1
  55. package/src/observability/event-to-metric.ts +8 -7
  56. package/src/observability/exporters/adapter.ts +6 -9
  57. package/src/observability/exporters/otlp-exporter.ts +81 -0
  58. package/src/observability/metric-retention.ts +4 -0
  59. package/src/observability/metric-sink.ts +14 -4
  60. package/src/runtime/adaptive-plan.ts +8 -8
  61. package/src/runtime/async-runner.ts +13 -4
  62. package/src/runtime/background-runner.ts +32 -23
  63. package/src/runtime/capability-inventory.ts +1 -1
  64. package/src/runtime/chain-runner.ts +5 -5
  65. package/src/runtime/checkpoint.ts +6 -16
  66. package/src/runtime/child-pi.ts +157 -71
  67. package/src/runtime/coalesce-tasks.ts +1 -1
  68. package/src/runtime/crash-recovery.ts +63 -47
  69. package/src/runtime/delivery-coordinator.ts +1 -1
  70. package/src/runtime/deterministic-ast.ts +2 -2
  71. package/src/runtime/diagnostic-export.ts +2 -1
  72. package/src/runtime/foreground-control.ts +3 -2
  73. package/src/runtime/goal-loop-runner.ts +6 -2
  74. package/src/runtime/goal-state-store.ts +1 -1
  75. package/src/runtime/handoff-manager.ts +0 -2
  76. package/src/runtime/heartbeat-watcher.ts +5 -0
  77. package/src/runtime/intercom-bridge.ts +0 -2
  78. package/src/runtime/live-agent-manager.ts +12 -32
  79. package/src/runtime/live-extension-bridge.ts +0 -2
  80. package/src/runtime/live-session-runtime.ts +65 -57
  81. package/src/runtime/manifest-cache.ts +59 -0
  82. package/src/runtime/mcp-proxy.ts +1 -1
  83. package/src/runtime/orphan-worker-registry.ts +3 -3
  84. package/src/runtime/output-validator.ts +1 -1
  85. package/src/runtime/path-overlap.ts +1 -1
  86. package/src/runtime/pi-args.ts +3 -3
  87. package/src/runtime/pi-json-output.ts +2 -2
  88. package/src/runtime/pipeline-runner.ts +1 -4
  89. package/src/runtime/post-checks.ts +0 -1
  90. package/src/runtime/retry-runner.ts +1 -1
  91. package/src/runtime/run-coalesced-task-group.ts +0 -13
  92. package/src/runtime/scheduler.ts +2 -0
  93. package/src/runtime/session-resources.ts +1 -1
  94. package/src/runtime/settings-store.ts +31 -1
  95. package/src/runtime/skill-effectiveness.ts +2 -2
  96. package/src/runtime/stale-reconciler.ts +23 -53
  97. package/src/runtime/subagent-manager.ts +19 -3
  98. package/src/runtime/task-output-context.ts +2 -1
  99. package/src/runtime/task-runner/prompt-builder.ts +1 -1
  100. package/src/runtime/task-runner/run-projection.ts +3 -1
  101. package/src/runtime/task-runner/state-helpers.ts +13 -1
  102. package/src/runtime/task-runner.ts +31 -6
  103. package/src/runtime/team-runner.ts +3 -2
  104. package/src/runtime/verification-gates.ts +54 -5
  105. package/src/schema/config-schema.ts +19 -1
  106. package/src/schema/team-tool-schema.ts +2 -0
  107. package/src/skills/discover-skills.ts +25 -15
  108. package/src/state/active-run-registry.ts +0 -1
  109. package/src/state/atomic-write.ts +37 -11
  110. package/src/state/blob-store.ts +3 -9
  111. package/src/state/contracts.ts +5 -1
  112. package/src/state/crew-init.ts +3 -2
  113. package/src/state/decision-ledger.ts +2 -2
  114. package/src/state/event-log-rotation.ts +1 -1
  115. package/src/state/event-log.ts +3 -3
  116. package/src/state/gitignore-manager.ts +2 -1
  117. package/src/state/health-store.ts +5 -2
  118. package/src/state/instinct-store.ts +11 -2
  119. package/src/state/mailbox.ts +3 -4
  120. package/src/state/observation-store.ts +1 -13
  121. package/src/state/run-cache.ts +12 -12
  122. package/src/state/run-graph.ts +2 -1
  123. package/src/state/run-metrics.ts +1 -1
  124. package/src/state/schedule.ts +7 -1
  125. package/src/state/state-store.ts +9 -3
  126. package/src/state/tiered-eval.ts +1 -1
  127. package/src/ui/dynamic-border.ts +13 -3
  128. package/src/ui/live-conversation-overlay.ts +7 -27
  129. package/src/ui/live-duration.ts +2 -2
  130. package/src/ui/live-run-sidebar.ts +56 -32
  131. package/src/ui/mascot.ts +19 -13
  132. package/src/ui/powerbar-publisher.ts +244 -178
  133. package/src/ui/render-coalescer.ts +44 -3
  134. package/src/ui/render-diff.ts +2 -2
  135. package/src/ui/run-dashboard.ts +122 -21
  136. package/src/ui/run-event-bus.ts +66 -0
  137. package/src/ui/run-snapshot-cache.ts +84 -81
  138. package/src/ui/settings-overlay.ts +3 -3
  139. package/src/ui/status-colors.ts +24 -0
  140. package/src/ui/tool-progress-formatter.ts +1 -1
  141. package/src/ui/tool-render.ts +3 -3
  142. package/src/ui/transcript-cache.ts +5 -10
  143. package/src/ui/transcript-entries.ts +68 -72
  144. package/src/ui/transcript-viewer.ts +54 -14
  145. package/src/ui/widget/index.ts +26 -15
  146. package/src/ui/widget/widget-model.ts +18 -9
  147. package/src/ui/widget/widget-renderer.ts +2 -5
  148. package/src/utils/fingerprint.ts +1 -1
  149. package/src/utils/project-detector.ts +1 -1
  150. package/src/state/atomic-write-v2.ts +0 -108
@@ -676,6 +676,7 @@ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
676
676
  maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
677
677
  maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
678
678
  heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
679
+ serializeOnPathOverlap: parseWithSchema(Type.Boolean(), obj.serializeOnPathOverlap),
679
680
  };
680
681
  return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
681
682
  }
@@ -717,6 +718,17 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
717
718
  Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
718
719
  obj.effectivenessGuard,
719
720
  ),
721
+ yield: (() => {
722
+ const y = asRecord(obj.yield);
723
+ if (!y) return undefined;
724
+ const parsed: NonNullable<CrewRuntimeConfig["yield"]> = {
725
+ enabled: parseWithSchema(Type.Boolean(), y.enabled),
726
+ maxReminders: parseWithSchema(Type.Integer({ minimum: 0 }), y.maxReminders),
727
+ reminderPrompt: parseWithSchema(Type.String({ maxLength: 1000 }), y.reminderPrompt),
728
+ };
729
+ return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
730
+ })(),
731
+ excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
720
732
  isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
721
733
  };
722
734
  return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
@@ -815,6 +827,7 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
815
827
  dashboardLiveRefreshMs: parseIntegerInRange(obj.dashboardLiveRefreshMs, 250, 60_000),
816
828
  autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
817
829
  autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
830
+ autoCloseDashboardMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoCloseDashboardMs),
818
831
  showModel: parseWithSchema(Type.Boolean(), obj.showModel),
819
832
  showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
820
833
  showTools: parseWithSchema(Type.Boolean(), obj.showTools),
@@ -932,6 +945,11 @@ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefin
932
945
  autoRecover: parseWithSchema(Type.Boolean(), obj.autoRecover),
933
946
  deadletterThreshold: parsePositiveInteger(obj.deadletterThreshold),
934
947
  cleanupOrphanedTempDirs: parseWithSchema(Type.Boolean(), obj.cleanupOrphanedTempDirs),
948
+ autoRepairIntervalMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoRepairIntervalMs),
949
+ forcePreflight: parseWithSchema(Type.Boolean(), obj.forcePreflight),
950
+ ambientStatusInjection: parseWithSchema(Type.Boolean(), obj.ambientStatusInjection),
951
+ perWriteValidation: parseWithSchema(Type.Boolean(), obj.perWriteValidation),
952
+ scopeModels: parseWithSchema(Type.Boolean(), obj.scopeModels),
935
953
  };
936
954
  return Object.values(reliability).some((entry) => entry !== undefined) ? reliability : undefined;
937
955
  }
@@ -985,6 +1003,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
985
1003
  executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
986
1004
  notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
987
1005
  requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
1006
+ ignoreMethod: parseWithSchema(Type.Union([Type.Literal("gitignore"), Type.Literal("exclude")]), obj.ignoreMethod),
988
1007
  autonomous: parseAutonomousConfig(obj.autonomous),
989
1008
  limits: parseLimitsConfig(obj.limits),
990
1009
  runtime: parseRuntimeConfig(obj.runtime),
@@ -50,10 +50,92 @@ export interface ConfigReferences {
50
50
  workflows: Set<string>;
51
51
  }
52
52
 
53
+ const MAX_CONFIG_KEY_DEPTH = 3;
54
+
55
+ type ConfigSchemaNode = Record<string, unknown>;
56
+
57
+ interface SchemaKeyResolution {
58
+ allowed: boolean;
59
+ childSchema?: unknown;
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function schemaAlternatives(schema: unknown): ConfigSchemaNode[] {
67
+ if (!isRecord(schema)) return [];
68
+ const alternatives = Array.isArray(schema.anyOf) ? schema.anyOf.flatMap((entry) => schemaAlternatives(entry)) : [];
69
+ return [schema, ...alternatives];
70
+ }
71
+
72
+ function resolveSchemaKey(schema: unknown, key: string): SchemaKeyResolution {
73
+ let sawObjectSchema = false;
74
+ let allowsAdditionalProperties = false;
75
+ const childSchemas: unknown[] = [];
76
+
77
+ for (const candidate of schemaAlternatives(schema)) {
78
+ const properties = isRecord(candidate.properties) ? candidate.properties : undefined;
79
+ const patternProperties = isRecord(candidate.patternProperties) ? candidate.patternProperties : undefined;
80
+ if (candidate.type !== "object" && !properties && !patternProperties) continue;
81
+ sawObjectSchema = true;
82
+
83
+ if (properties && Object.hasOwn(properties, key)) {
84
+ childSchemas.push(properties[key]);
85
+ continue;
86
+ }
87
+
88
+ let matchedPattern = false;
89
+ if (patternProperties) {
90
+ for (const [pattern, childSchema] of Object.entries(patternProperties)) {
91
+ try {
92
+ if (new RegExp(pattern).test(key)) {
93
+ childSchemas.push(childSchema);
94
+ matchedPattern = true;
95
+ break;
96
+ }
97
+ } catch {
98
+ // Schema patterns are trusted constants; an invalid one simply cannot match.
99
+ }
100
+ }
101
+ }
102
+ if (!matchedPattern && candidate.additionalProperties !== false) {
103
+ allowsAdditionalProperties = true;
104
+ }
105
+ }
106
+
107
+ return {
108
+ allowed: !sawObjectSchema || childSchemas.length > 0 || allowsAdditionalProperties,
109
+ childSchema: childSchemas[0],
110
+ };
111
+ }
112
+
53
113
  /**
54
- * Known top-level config keys allowed by the schema.
114
+ * Recursively compare object keys with their TypeBox schema nodes. Root is
115
+ * depth 0; nested objects are traversed through depth 3, which covers the
116
+ * deepest current config shapes (`goalWrap.<workflow>.verification` and
117
+ * `agents.overrides.<agent>`) without allowing attacker-controlled recursion.
55
118
  */
56
- const SCHEMA_KEYS: ReadonlySet<string> = new Set<string>(Object.keys(PiTeamsConfigSchema.properties ?? {}));
119
+ function findUnknownConfigKeyPaths(value: unknown, schema: unknown, depth = 0, path: readonly string[] = []): string[][] {
120
+ if (!isRecord(value) || depth > MAX_CONFIG_KEY_DEPTH) return [];
121
+ const unknownPaths: string[][] = [];
122
+ for (const [key, childValue] of Object.entries(value)) {
123
+ const childPath = [...path, key];
124
+ const resolution = resolveSchemaKey(schema, key);
125
+ if (!resolution.allowed) {
126
+ unknownPaths.push(childPath);
127
+ continue;
128
+ }
129
+ if (resolution.childSchema !== undefined && depth < MAX_CONFIG_KEY_DEPTH) {
130
+ unknownPaths.push(...findUnknownConfigKeyPaths(childValue, resolution.childSchema, depth + 1, childPath));
131
+ }
132
+ }
133
+ return unknownPaths;
134
+ }
135
+
136
+ function isAgentOverrideKeyPath(path: readonly string[]): boolean {
137
+ return path.length === 4 && path[0] === "agents" && path[1] === "overrides";
138
+ }
57
139
 
58
140
  /**
59
141
  * Extract all agent, team, and workflow names referenced in a raw config object.
@@ -94,7 +176,7 @@ function extractConfigReferences(config: Record<string, unknown>): ConfigReferen
94
176
  * 1. **Missing**: resource referenced in config (e.g. agents.overrides) but not discovered → error
95
177
  * 2. **Extra**: discovered resource not referenced anywhere in config → warning
96
178
  * 3. **Mismatched**: resource exists in both but with structural differences → warning
97
- * 4. **Config keys**: config keys not present in the schema → warning
179
+ * 4. **Config keys**: config keys not present in the schema, recursively through depth 3 → warning
98
180
  *
99
181
  * For agents: compares against `agents.overrides` in config. Discovered agents not in overrides
100
182
  * are NOT considered extra (they may be built-in). Only agents referenced in overrides but not
@@ -140,18 +222,18 @@ export function detectDrift(discovered: DiscoveredResources, config: PiTeamsConf
140
222
  // No extra/missing checks for workflows from config alone.
141
223
 
142
224
  // --- Config-key drift ---
143
- const configKeys = Object.keys(configRaw);
144
- for (const key of configKeys) {
145
- if (!SCHEMA_KEYS.has(key)) {
146
- items.push({
147
- kind: "config-key",
148
- name: key,
149
- status: "extra",
150
- expected: `key not in schema`,
151
- actual: `present in config`,
152
- severity: "warning",
153
- });
154
- }
225
+ for (const keyPath of findUnknownConfigKeyPaths(configRaw, PiTeamsConfigSchema)) {
226
+ // Preserve the existing, more specific agent/mismatched finding for
227
+ // agents.overrides.<agent>.<key> instead of reporting the same key twice.
228
+ if (isAgentOverrideKeyPath(keyPath)) continue;
229
+ items.push({
230
+ kind: "config-key",
231
+ name: keyPath.join("."),
232
+ status: "extra",
233
+ expected: `key not in schema`,
234
+ actual: `present in config`,
235
+ severity: "warning",
236
+ });
155
237
  }
156
238
 
157
239
  // --- Mismatched config-key check ---
@@ -86,11 +86,11 @@ export function parseConfigResilient(raw: unknown): ResilientParseResult {
86
86
  const value = raw[key];
87
87
  const subSchema = (PiTeamsConfigSchema.properties as Record<string, unknown>)[key];
88
88
  if (subSchema && !Value.Check(subSchema as TSchema, value)) {
89
- // Collect the first error for this field
89
+ // Collect ALL errors for this field
90
90
  const fieldErrors = [...Value.Errors(subSchema as TSchema, value)];
91
- if (fieldErrors.length > 0) {
92
- const first = fieldErrors[0] as unknown as Record<string, unknown>;
93
- const msg = typeof first.message === "string" ? first.message : "invalid value";
91
+ for (const err of fieldErrors) {
92
+ const errObj = err as unknown as Record<string, unknown>;
93
+ const msg = typeof errObj.message === "string" ? errObj.message : "invalid value";
94
94
  errors.push({
95
95
  field: key,
96
96
  message: msg,
@@ -93,11 +93,25 @@ export function registerCleanupHandler(pi: ExtensionAPI): void {
93
93
 
94
94
  async function cleanupChildProcesses(): Promise<void> {
95
95
  const pids = childProcessRegistry.getAllPids();
96
-
96
+ if (pids.length === 0) return;
97
+ // Lazy-import to avoid eagerly pulling the heavy runtime/child-pi chain into the
98
+ // extension load path (check-lazy-imports) AND to avoid a static import cycle
99
+ // (child-pi.ts already imports registerChildProcess from this module).
100
+ // LAZY: defer child-pi.ts load (heavy runtime chain) + avoid static import cycle.
101
+ const { killProcessPid } = await import("../runtime/child-pi.ts");
97
102
  for (const pid of pids) {
103
+ // killProcessPid signals the whole process GROUP (-pid on POSIX, taskkill /T on
104
+ // Windows) and escalates to SIGKILL after HARD_KILL_MS. A bare
105
+ // process.kill(pid, "SIGTERM") only reaches the direct child — grandchildren
106
+ // (bash/MCP the worker spawned) in the child's group survive as orphans, and
107
+ // a child that lags SIGTERM would linger forever with no escalation.
108
+ // NOTE: on graceful session_shutdown (reason=quit/reload) register.ts ALSO runs
109
+ // terminateActiveChildPiProcesses(); both are idempotent for a dying pid. This
110
+ // signal handler (SIGTERM/SIGHUP) is the SOLE teardown path when the Pi process
111
+ // is killed by signal, so it must be robust on its own.
98
112
  try {
99
- process.kill(pid, "SIGTERM");
100
- console.log(`[pi-crew] Sent SIGTERM to child process ${pid}`);
113
+ killProcessPid(pid);
114
+ console.log(`[pi-crew] Sent group SIGTERM (+SIGKILL escalation) to child process ${pid}`);
101
115
  } catch (error: unknown) {
102
116
  // Process may already be dead or not exist
103
117
  const err = error as NodeJS.ErrnoException;
@@ -19,7 +19,7 @@ function safeUiCall(scope: string, fn: () => void): void {
19
19
  }
20
20
  }
21
21
 
22
- import { type CrewVibesConfig, loadConfig, PROVIDER_STATUS_ID, saveConfig } from "./config.ts";
22
+ import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
23
23
  import { intervalForSpeed } from "./figures.ts";
24
24
  import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
25
25
  import { clearProviderUsageCache, fetchProviderUsage, type ProviderUsage } from "./provider-usage.ts";
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
- import { resolveContainedPath } from "../utils/safe-paths.ts";
3
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
4
4
  import { withHmacVerification } from "./rpc-hmac.ts";
5
5
  // Lazy-loaded to avoid pulling team-tool.ts (and its entire runtime chain) into module load.
6
6
  import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
@@ -109,7 +109,10 @@ export function resetRpcRateLimit(): void {
109
109
  rpcRunTimestamps.length = 0;
110
110
  }
111
111
 
112
- function isAllowedRpcRunParams(params: TeamToolParamsValue): {
112
+ export function isAllowedRpcRunParams(
113
+ params: TeamToolParamsValue,
114
+ ctx: ExtensionContext,
115
+ ): {
113
116
  ok: boolean;
114
117
  error?: string;
115
118
  } {
@@ -123,10 +126,16 @@ function isAllowedRpcRunParams(params: TeamToolParamsValue): {
123
126
  error: "RPC run requires config.intent (a non-empty intent string)",
124
127
  };
125
128
  }
126
- // SECURITY: Validate cwd is within the project directory if provided.
129
+ // SECURITY: Validate cwd, if provided, resolves WITHIN the project directory (ctx.cwd).
130
+ // Arg order is (baseDir=ctx.cwd, targetPath=params.cwd): the project dir is the container,
131
+ // the caller-supplied cwd is the contained target. (Previously the args were swapped —
132
+ // resolveContainedPath(params.cwd, ".") — which validates that "." is inside params.cwd,
133
+ // trivially true for ANY path, so the check never rejected anything.)
134
+ // NOTE: defense-in-depth — handleTeamTool currently uses ctx.cwd and ignores params.cwd,
135
+ // but a correct guard here prevents a future regression from silently enabling cwd escape.
127
136
  if (params.cwd && typeof params.cwd === "string") {
128
137
  try {
129
- resolveContainedPath(params.cwd, ".");
138
+ resolveRealContainedPath(ctx.cwd, params.cwd);
130
139
  } catch {
131
140
  return {
132
141
  ok: false,
@@ -152,6 +161,10 @@ export function registerPiCrewRpc(
152
161
  getCtx: () => ExtensionContext | undefined,
153
162
  ): PiCrewRpcHandle | undefined {
154
163
  if (!events) return undefined;
164
+ // Note: the prior loud startup warning has been replaced by a soft, opt-out
165
+ // info note emitted lazily by withHmacVerification only when an actual RPC
166
+ // request is processed (see rpc-hmac.ts). This avoids noisy output during
167
+ // every pi load for users who never wire cross-extension RPC.
155
168
  const unsubs = [
156
169
  on(
157
170
  events,
@@ -214,7 +227,7 @@ export function registerPiCrewRpc(
214
227
  } else {
215
228
  params = { action: "run" };
216
229
  }
217
- const permission = isAllowedRpcRunParams(params);
230
+ const permission = isAllowedRpcRunParams(params, ctx);
218
231
  if (!permission.ok) {
219
232
  reply(events, "pi-crew:rpc:run", id, {
220
233
  success: false,
@@ -6,6 +6,7 @@ import { serializeAgent } from "../agents/agent-serializer.ts";
6
6
  import { discoverAgents, invalidateAgentDiscoveryCache } from "../agents/discover-agents.ts";
7
7
  import type { PiTeamsConfig } from "../config/config.ts";
8
8
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
9
+ import { atomicWriteFile } from "../state/atomic-write.ts";
9
10
  import { allTeams, discoverTeams, invalidateTeamDiscoveryCache } from "../teams/discover-teams.ts";
10
11
  import type { TeamConfig, TeamRole } from "../teams/team-config.ts";
11
12
  import { serializeTeam } from "../teams/team-serializer.ts";
@@ -258,7 +259,7 @@ function updateReferencesForRename(
258
259
  changed.push(team.filePath);
259
260
  if (!dryRun) {
260
261
  backupFile(team.filePath);
261
- fs.writeFileSync(team.filePath, serializeTeam(nextTeam), "utf-8");
262
+ atomicWriteFile(team.filePath, serializeTeam(nextTeam));
262
263
  }
263
264
  }
264
265
  // L12 fix: also update workflow step role references when renaming agents.
@@ -276,7 +277,7 @@ function updateReferencesForRename(
276
277
  changed.push(workflow.filePath);
277
278
  if (!dryRun) {
278
279
  backupFile(workflow.filePath);
279
- fs.writeFileSync(workflow.filePath, serializeWorkflow({ ...workflow, steps: newSteps }), "utf-8");
280
+ atomicWriteFile(workflow.filePath, serializeWorkflow({ ...workflow, steps: newSteps }));
280
281
  }
281
282
  }
282
283
  // L12 fix: update agent references in test fixtures.
@@ -290,7 +291,7 @@ function updateReferencesForRename(
290
291
  if (newContent !== content) {
291
292
  changed.push(fixture);
292
293
  if (!dryRun) {
293
- fs.writeFileSync(fixture, newContent, "utf-8");
294
+ atomicWriteFile(fixture, newContent);
294
295
  }
295
296
  }
296
297
  }
@@ -388,7 +389,7 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
388
389
 
389
390
  if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
390
391
  try {
391
- fs.writeFileSync(filePath, content, "utf-8");
392
+ atomicWriteFile(filePath, content);
392
393
  } catch (writeError) {
393
394
  return result(
394
395
  `Failed to create ${params.resource}: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
@@ -513,7 +514,7 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
513
514
  }
514
515
  }
515
516
  }
516
- fs.writeFileSync(nextPath, content, "utf-8");
517
+ atomicWriteFile(nextPath, content);
517
518
  } catch (updateError) {
518
519
  return result(
519
520
  `Failed to update ${params.resource}: ${updateError instanceof Error ? updateError.message : String(updateError)}`,