pi-crew 0.9.41 → 0.9.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# atomic-write-v2 migration plan
|
|
2
2
|
|
|
3
|
+
> **Note:** `atomic-write-v2.ts` was consolidated into `atomic-write.ts` as of v0.9.42. This document is preserved as historical record — the migration was completed with v1 winning on simplicity + symlink-safety + link+unlink atomicity. The v2 class was never adopted; no v2 API surface ships with pi-crew.
|
|
4
|
+
|
|
3
5
|
> Status: **PROPOSED** — decision RECOMMEND MIGRATE (staged, feature-flagged).
|
|
4
6
|
> Source: performance review `docs/perf/performance-review-2026-07.md` (findings F3–F6).
|
|
5
7
|
> Scope: replace the standalone-function module `src/state/atomic-write.ts`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Fallow Patterns Adoption - Implementation Plan
|
|
2
2
|
|
|
3
|
+
> **Note:** `atomic-write-v2` / `AtomicWriter` mentioned in this plan was consolidated into `atomic-write.ts` as of v0.9.42. This document is preserved as historical record — the v2 class was never adopted; v1 won on simplicity + symlink-safety + link+unlink atomicity. See `docs/migration/atomic-write-v2-migration.md` for the decision rationale.
|
|
4
|
+
|
|
3
5
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
6
|
|
|
5
7
|
**Goal:** Adopt 5 high-value patterns from fallow (Rust static analyzer) into pi-crew: structured error codes, atomic write v2 with fsync, health score system, plugin registry, and stable task ID refinement.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.44",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
57
|
"check": "npm run ci",
|
|
58
|
-
"ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:lazy-imports && npm run check:bundle-staleness && npm test && npm pack --dry-run",
|
|
58
|
+
"ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:lazy-imports && npm run check:bundle-staleness && npm run build:bundle && npm run test:bundle && npm test && npm pack --dry-run",
|
|
59
59
|
"check:lazy-imports": "node scripts/check-lazy-imports.mjs",
|
|
60
60
|
"check:bundle-staleness": "node scripts/check-bundle-staleness.mjs",
|
|
61
61
|
"check:conflict-markers": "node scripts/check-conflict-markers.mjs",
|
|
@@ -68,8 +68,10 @@
|
|
|
68
68
|
"test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=300000 test/integration/*.test.ts",
|
|
69
69
|
"test:smoke": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=180000 test/smoke/*.smoke.ts",
|
|
70
70
|
"build:bundle": "node scripts/build-bundle.mjs",
|
|
71
|
+
"test:bundle": "node --experimental-strip-types --test --test-force-exit test/unit/bundle-load.test.ts",
|
|
71
72
|
"watch:bundle": "node scripts/watch-bundle.mjs",
|
|
72
73
|
"build:bundle:once": "node scripts/watch-bundle.mjs --once",
|
|
74
|
+
"dev": "node scripts/dev-runner.mjs",
|
|
73
75
|
"postinstall": "node scripts/postinstall.mjs",
|
|
74
76
|
"install:crew-font": "node scripts/install-crew-vibes-font.mjs",
|
|
75
77
|
"build:crew-font": "python scripts/build-crew-vibes-font.py",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Scripts
|
|
2
|
+
|
|
3
|
+
> **Note:** Many scripts in this directory are one-off fix scripts (e.g.,
|
|
4
|
+
> `fix-*.cjs`, `patch-*.cjs`) that were used for specific migrations or
|
|
5
|
+
> bugfixes and could be archived. Only `postinstall.mjs`, `build-bundle.mjs`,
|
|
6
|
+
> and `watch-bundle.mjs` are actively used by the project.
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* node scripts/check-all-skills.ts
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { spawnSync } from "child_process";
|
|
13
|
-
import * as fs from "fs";
|
|
14
|
-
import * as path from "path";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
15
|
|
|
16
16
|
interface SkillSummary {
|
|
17
17
|
name: string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dev runner — spawn `watch:bundle` and `test:watch` in parallel.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the `npm-run-all --parallel watch:bundle test:watch` form that
|
|
6
|
+
* would require an additional dev-dependency. Uses Node's stdlib
|
|
7
|
+
* `child_process.spawn` so we keep the dev tooling dependency-free.
|
|
8
|
+
*
|
|
9
|
+
* Behavior:
|
|
10
|
+
* - Spawns both child processes with stdio: inherit (the parent's TTY
|
|
11
|
+
* sees both streams interleaved).
|
|
12
|
+
* - Forwards Ctrl+C (SIGINT) and SIGTERM to both children so they shut
|
|
13
|
+
* down cleanly when the user aborts.
|
|
14
|
+
* - Exits with a non-zero code if EITHER child exits with a non-zero
|
|
15
|
+
* status (mimics `npm-run-all --parallel` semantics).
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* npm run dev
|
|
19
|
+
* node scripts/dev-runner.mjs
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
|
|
25
|
+
const tasks = [
|
|
26
|
+
{ name: "watch:bundle", script: "watch:bundle" },
|
|
27
|
+
{ name: "test:watch", script: "test:watch" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {{name: string, child: import("node:child_process").ChildProcess, exited: boolean, code: number|null, signal: NodeJS.Signals|null}} Task
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** @type {Task[]} */
|
|
35
|
+
const children = tasks.map((t) => ({
|
|
36
|
+
name: t.name,
|
|
37
|
+
child: spawnNpm(t.script),
|
|
38
|
+
exited: false,
|
|
39
|
+
code: null,
|
|
40
|
+
signal: null,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
let shuttingDown = false;
|
|
44
|
+
let exitCode = 0;
|
|
45
|
+
|
|
46
|
+
function spawnNpm(script) {
|
|
47
|
+
return spawn("npm", ["run", "--silent", script], {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
env: process.env,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function maybeExit() {
|
|
54
|
+
if (!shuttingDown) return;
|
|
55
|
+
if (children.some((c) => !c.exited)) return;
|
|
56
|
+
process.exit(exitCode);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shutdown(reason) {
|
|
60
|
+
if (shuttingDown) return;
|
|
61
|
+
shuttingDown = true;
|
|
62
|
+
process.stderr.write(`\n[dev-runner] received ${reason}, forwarding to children...\n`);
|
|
63
|
+
for (const c of children) {
|
|
64
|
+
if (c.exited) continue;
|
|
65
|
+
try {
|
|
66
|
+
c.child.kill("SIGTERM");
|
|
67
|
+
} catch {
|
|
68
|
+
/* already dead */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Hard-kill grace window: if children ignore SIGTERM for 3s, send SIGKILL.
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
for (const c of children) {
|
|
74
|
+
if (c.exited) continue;
|
|
75
|
+
try {
|
|
76
|
+
c.child.kill("SIGKILL");
|
|
77
|
+
} catch {
|
|
78
|
+
/* already dead */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, 3000).unref();
|
|
82
|
+
maybeExit();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const c of children) {
|
|
86
|
+
c.child.on("exit", (code, signal) => {
|
|
87
|
+
c.exited = true;
|
|
88
|
+
c.code = code;
|
|
89
|
+
c.signal = signal;
|
|
90
|
+
const note = signal ? `signal=${signal}` : `code=${code}`;
|
|
91
|
+
process.stderr.write(`[dev-runner] ${c.name} exited (${note})\n`);
|
|
92
|
+
// Non-zero is only meaningful when the child exited via code (not signal).
|
|
93
|
+
if (signal === null && code !== 0) exitCode = exitCode || (code ?? 1);
|
|
94
|
+
maybeExit();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
c.child.on("error", (err) => {
|
|
98
|
+
process.stderr.write(`[dev-runner] failed to spawn ${c.name}: ${err.message}\n`);
|
|
99
|
+
c.exited = true;
|
|
100
|
+
exitCode = exitCode || 1;
|
|
101
|
+
maybeExit();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
106
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -32,8 +32,12 @@ function main() {
|
|
|
32
32
|
// dist/index.mjs, so this best-effort build simply no-ops.
|
|
33
33
|
const bundleStatus = run("scripts/build-bundle.mjs");
|
|
34
34
|
if (bundleStatus !== 0) {
|
|
35
|
-
console.warn
|
|
36
|
-
|
|
35
|
+
// TB-11: Surface bundle failures loudly to stderr instead of console.warn,
|
|
36
|
+
// which can scroll off in noisy install logs. We still don't `process.exit(1)`
|
|
37
|
+
// here — the install itself must succeed via the strip-types fallback —
|
|
38
|
+
// but the failure must be obvious to whoever runs `npm install`.
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
"\u001b[31m[pi-crew] postinstall: bundle build FAILED\u001b[0m — using committed dist/ (or strip-types fallback). Run 'npm run build:bundle' to retry. See logs above for esbuild errors.\n",
|
|
37
41
|
);
|
|
38
42
|
}
|
|
39
43
|
// Font install is best-effort and must never fail the install.
|
package/scripts/verify-skill.ts
CHANGED
|
@@ -83,7 +83,7 @@ export interface AgentConfig {
|
|
|
83
83
|
contextMode?: "fresh" | "fork";
|
|
84
84
|
/** Maximum turns for this agent. Overrides runtime config if set. */
|
|
85
85
|
maxTurns?: number;
|
|
86
|
-
/** Cap on output tokens per API call. Set via
|
|
86
|
+
/** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT env in child. */
|
|
87
87
|
maxTokens?: number;
|
|
88
88
|
/** Effort level for this agent. Controls how much work the agent puts in. */
|
|
89
89
|
effort?: "low" | "medium" | "high";
|
package/src/config/config.ts
CHANGED
|
@@ -169,7 +169,7 @@ export function invalidateConfigCache(): void {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
function resolveHomeDir(): string {
|
|
172
|
-
const envValue = process.env.PI_TEAMS_HOME?.trim();
|
|
172
|
+
const envValue = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
|
|
173
173
|
const defaultHome = os.homedir();
|
|
174
174
|
if (!envValue) return defaultHome;
|
|
175
175
|
// FIX (Round 14): When PI_TEAMS_HOME is explicitly set, validate that
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
89
|
+
// Collect ALL errors for this field
|
|
90
90
|
const fieldErrors = [...Value.Errors(subSchema as TSchema, value)];
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const msg = typeof
|
|
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,
|
package/src/config/types.ts
CHANGED
|
@@ -15,8 +15,6 @@ export interface PiTeamsAutonomousConfig {
|
|
|
15
15
|
preferAsyncForLongTasks?: boolean;
|
|
16
16
|
allowWorktreeSuggestion?: boolean;
|
|
17
17
|
magicKeywords?: Record<string, string[]>;
|
|
18
|
-
/** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
|
|
19
|
-
excludeContextBash?: boolean;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export interface CrewLimitsConfig {
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -13,7 +13,7 @@ export const CAPACITY_STATUS_ID = "pi-crew-bar";
|
|
|
13
13
|
export const PROVIDER_STATUS_ID = "pi-crew-bar";
|
|
14
14
|
|
|
15
15
|
function resolveHome(): string {
|
|
16
|
-
return process.env.PI_TEAMS_HOME?.trim() || process.env.HOME || process.env.USERPROFILE || "";
|
|
16
|
+
return (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim() || process.env.HOME || process.env.USERPROFILE || "";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function configPath(): string {
|
|
@@ -33,18 +33,30 @@ export function hasCrewFontFile(): boolean {
|
|
|
33
33
|
return _hasFontFile;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
let _isWebTerminal: boolean | null = null;
|
|
37
|
+
|
|
36
38
|
/** Returns true when running in a web-based terminal (gotty, wetty, etc.)
|
|
37
39
|
* where system fonts are not available for PUA rendering. */
|
|
38
40
|
export function isWebTerminal(): boolean {
|
|
39
|
-
if (
|
|
40
|
-
if (process.env.
|
|
41
|
+
if (_isWebTerminal !== null) return _isWebTerminal;
|
|
42
|
+
if (process.env.GOTTY || process.env.WEBTERM) {
|
|
43
|
+
_isWebTerminal = true;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (process.env.TERM === "dumb") {
|
|
47
|
+
_isWebTerminal = true;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
41
50
|
try {
|
|
42
51
|
// Check current process and ancestors — gotty is often the grandparent
|
|
43
52
|
// (gotty → tmux → pi), so /proc/self/cgroup may not contain it.
|
|
44
53
|
let pid = process.pid;
|
|
45
54
|
for (let i = 0; i < 6 && pid > 1; i++) {
|
|
46
55
|
const cgroup = readFileSync(`/proc/${pid}/cgroup`, "utf8");
|
|
47
|
-
if (cgroup.includes("gotty") || cgroup.includes("wetty"))
|
|
56
|
+
if (cgroup.includes("gotty") || cgroup.includes("wetty")) {
|
|
57
|
+
_isWebTerminal = true;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
48
60
|
const match = cgroup.match(/\d+:.*:(.*)/);
|
|
49
61
|
// Read PPid from status
|
|
50
62
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
@@ -54,5 +66,6 @@ export function isWebTerminal(): boolean {
|
|
|
54
66
|
} catch {
|
|
55
67
|
// not Linux or /proc not available
|
|
56
68
|
}
|
|
69
|
+
_isWebTerminal = false;
|
|
57
70
|
return false;
|
|
58
71
|
}
|
|
@@ -19,7 +19,7 @@ function safeUiCall(scope: string, fn: () => void): void {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
import { type CrewVibesConfig, loadConfig,
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -269,10 +282,14 @@ export function registerPiCrewRpc(
|
|
|
269
282
|
}
|
|
270
283
|
}, "pi-crew:rpc:status"),
|
|
271
284
|
),
|
|
272
|
-
on(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
on(
|
|
286
|
+
events,
|
|
287
|
+
"pi-crew:live-control",
|
|
288
|
+
withHmacVerification((raw) => {
|
|
289
|
+
const request = parseLiveControlRealtimeMessage(raw);
|
|
290
|
+
if (request) publishLiveControlRealtime(request);
|
|
291
|
+
}, "pi-crew:live-control"),
|
|
292
|
+
),
|
|
276
293
|
on(
|
|
277
294
|
events,
|
|
278
295
|
"pi-crew:rpc:live-control",
|