pi-crew 0.10.4 → 0.10.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.
- package/CHANGELOG.md +233 -0
- package/agents/analyst.md +37 -2
- package/agents/cold-verifier.md +10 -1
- package/agents/councillor-critic.md +39 -0
- package/agents/councillor-pragmatist.md +39 -0
- package/agents/councillor-skeptic.md +41 -0
- package/agents/critic.md +40 -2
- package/agents/designer.md +58 -0
- package/agents/executor.md +39 -2
- package/agents/explorer.md +38 -2
- package/agents/librarian.md +49 -0
- package/agents/oracle.md +54 -0
- package/agents/orchestrator.md +48 -0
- package/agents/planner.md +41 -2
- package/agents/reviewer.md +39 -2
- package/agents/security-reviewer.md +43 -2
- package/agents/test-engineer.md +48 -2
- package/agents/verifier.md +14 -1
- package/agents/writer.md +32 -2
- package/dist/index.mjs +1297 -853
- package/package.json +1 -1
- package/skills/async-worker-recovery/SKILL.md +4 -1
- package/skills/child-pi-spawning/SKILL.md +4 -1
- package/skills/context-artifact-hygiene/SKILL.md +4 -1
- package/skills/council/SKILL.md +24 -45
- package/skills/delegation-patterns/SKILL.md +18 -1
- package/skills/distill-persona/SKILL.md +4 -1
- package/skills/distill-software/SKILL.md +4 -1
- package/skills/event-log-tracing/SKILL.md +4 -1
- package/skills/git-master/SKILL.md +4 -1
- package/skills/iterative-audit/SKILL.md +4 -1
- package/skills/live-agent-lifecycle/SKILL.md +4 -1
- package/skills/mailbox-interactive/SKILL.md +4 -1
- package/skills/model-routing-context/SKILL.md +10 -1
- package/skills/multi-perspective-review/SKILL.md +18 -1
- package/skills/observability-reliability/SKILL.md +4 -1
- package/skills/orchestration/SKILL.md +18 -1
- package/skills/ownership-session-security/SKILL.md +4 -1
- package/skills/pi-extension-lifecycle/SKILL.md +4 -1
- package/skills/post-mortem/SKILL.md +4 -1
- package/skills/read-only-explorer/SKILL.md +4 -1
- package/skills/real-test-pi-crew/SKILL.md +165 -12
- package/skills/requirements-to-task-packet/SKILL.md +10 -1
- package/skills/research/SKILL.md +4 -1
- package/skills/resource-discovery-config/SKILL.md +10 -1
- package/skills/runtime-state-reader/SKILL.md +4 -1
- package/skills/safe-bash/SKILL.md +4 -1
- package/skills/scrutinize/SKILL.md +24 -1
- package/skills/secure-agent-orchestration-review/SKILL.md +4 -1
- package/skills/state-mutation-locking/SKILL.md +4 -1
- package/skills/systematic-debugging/SKILL.md +4 -1
- package/skills/verification-before-done/SKILL.md +18 -1
- package/skills/widget-rendering/SKILL.md +4 -1
- package/skills/workspace-isolation/SKILL.md +4 -1
- package/skills/worktree-isolation/SKILL.md +4 -1
- package/src/config/config-validation.ts +1 -0
- package/src/config/types.ts +8 -0
- package/src/errors.ts +1 -1
- package/src/extension/context-status-injection.ts +2 -2
- package/src/extension/knowledge-injection.ts +19 -7
- package/src/extension/post-init-skill-check.ts +32 -0
- package/src/extension/register.ts +9 -1
- package/src/extension/registration/hook-registration.ts +20 -3
- package/src/extension/registration/tool-loop-guard.ts +243 -0
- package/src/extension/team-tool/handle-settings.ts +10 -0
- package/src/extension/team-tool/run.ts +42 -1
- package/src/extension/team-tool-types.ts +6 -0
- package/src/prompt/prompt-runtime.ts +25 -6
- package/src/runtime/async-runner.ts +75 -11
- package/src/runtime/background-runner.ts +73 -7
- package/src/runtime/broker/crew-broker-client.ts +45 -2
- package/src/runtime/broker/crew-broker.ts +22 -27
- package/src/runtime/broker/protocol/request-parsers.ts +10 -2
- package/src/runtime/broker/stdin-handshake.ts +87 -0
- package/src/runtime/broker/wait-push.ts +45 -0
- package/src/runtime/detached-run-results.ts +25 -1
- package/src/runtime/foreground-watchdog.ts +24 -5
- package/src/runtime/live-session/live-session-runtime.ts +1 -1
- package/src/runtime/model/model-scope.ts +2 -2
- package/src/runtime/run-tracker.ts +74 -19
- package/src/runtime/skill-instructions.ts +20 -4
- package/src/runtime/task-runner/child-executor.ts +1 -1
- package/src/runtime/task-runner/prompt-builder.ts +22 -9
- package/src/schema/config-schema.ts +1 -0
- package/src/skills/discover-skills.ts +2 -2
- package/src/ui/settings-overlay.ts +40 -0
- package/src/utils/frontmatter.ts +7 -1
- package/src/utils/ndjson.ts +9 -1
|
@@ -69,6 +69,13 @@ export function startForegroundWatchdog(opts: WatchdogOptions): void {
|
|
|
69
69
|
// Don't stack watchdogs for the same run
|
|
70
70
|
if (activeWatchdogs.has(runId)) return;
|
|
71
71
|
|
|
72
|
+
// ARCH-5: cap consecutive no-progress wake notices at 2, then send one
|
|
73
|
+
// final guidance message and go quiet — a ~24-notices/2h drip trains the
|
|
74
|
+
// user to ignore the watchdog. Reset on any sign of progress (run ends or
|
|
75
|
+
// leaves the hung state).
|
|
76
|
+
let consecutiveHungNotices = 0;
|
|
77
|
+
const HUNG_NOTICE_CAP = 2;
|
|
78
|
+
|
|
72
79
|
const check = (): void => {
|
|
73
80
|
// Check if max monitor time exceeded
|
|
74
81
|
if (Date.now() - startTime > maxMonitorMs) {
|
|
@@ -105,15 +112,27 @@ export function startForegroundWatchdog(opts: WatchdogOptions): void {
|
|
|
105
112
|
const now = Date.now();
|
|
106
113
|
if (isLikelyOrphanedActiveRun(manifest, agents, now)) {
|
|
107
114
|
const detail = `status=${manifest.status}, updatedAt=${manifest.updatedAt}, agents=${agents.length}`;
|
|
115
|
+
consecutiveHungNotices += 1;
|
|
108
116
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
if (consecutiveHungNotices <= HUNG_NOTICE_CAP) {
|
|
118
|
+
pi.sendUserMessage(
|
|
119
|
+
`pi-crew watchdog: run ${runId} appears hung (${detail}). Consider running team action='cancel' runId='${runId}' or team action='doctor'.`,
|
|
120
|
+
{ deliverAs: "followUp" },
|
|
121
|
+
);
|
|
122
|
+
} else if (consecutiveHungNotices === HUNG_NOTICE_CAP + 1) {
|
|
123
|
+
// One-time handoff, then silence — still monitoring, no more drips.
|
|
124
|
+
pi.sendUserMessage(
|
|
125
|
+
`pi-crew watchdog: run ${runId} is still hung after ${consecutiveHungNotices} checks — going quiet now. Intervene via team action='cancel' runId='${runId}', team action='doctor', or leave it; a completion notice will still fire.`,
|
|
126
|
+
{ deliverAs: "followUp" },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
113
129
|
} catch {
|
|
114
130
|
/* non-critical */
|
|
115
131
|
}
|
|
116
|
-
//
|
|
132
|
+
// Keep monitoring (ARCH-5: silently past the cap — no more notices).
|
|
133
|
+
} else {
|
|
134
|
+
// Run is alive and progressing — reset the no-progress wake counter.
|
|
135
|
+
consecutiveHungNotices = 0;
|
|
117
136
|
}
|
|
118
137
|
} catch {
|
|
119
138
|
// Non-critical — skip this check
|
|
@@ -273,7 +273,7 @@ function numberField(obj: Record<string, unknown> | undefined, keys: string[]):
|
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
275
|
* F7: resolve the enabledModels allowlist for the current project, but only
|
|
276
|
-
* if the `
|
|
276
|
+
* if the `reliability.scopeModels` toggle is ON. Returns an empty
|
|
277
277
|
* array when the toggle is off or no allowlist is configured — the routing
|
|
278
278
|
* gate treats empty patterns as "no enforcement" (no-op). Best-effort:
|
|
279
279
|
* any failure to read the toggle or the allowlist silently disables the gate
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* model-scope.ts — Opt-in model-scope enforcement (F7).
|
|
3
3
|
*
|
|
4
|
-
* When `
|
|
4
|
+
* When `reliability.scopeModels` is enabled, subagent model choices
|
|
5
5
|
* that fall outside the user's pi `enabledModels` allowlist are flagged:
|
|
6
6
|
* - Caller-supplied (per-spawn override / step / team role) out-of-scope
|
|
7
7
|
* → HARD ERROR to orchestrator (fail fast before spawn).
|
|
@@ -128,7 +128,7 @@ export function checkModelScope(
|
|
|
128
128
|
* Read the user's `enabledModels` allowlist from pi's SettingsManager.
|
|
129
129
|
* Returns an empty array when the SettingsManager export is unavailable, the
|
|
130
130
|
* allowlist is unset, or any error occurs (best-effort, never throws). The
|
|
131
|
-
* caller should still gate on `
|
|
131
|
+
* caller should still gate on `reliability.scopeModels` — an empty
|
|
132
132
|
* patterns array is a no-op (nothing to enforce against).
|
|
133
133
|
*
|
|
134
134
|
* @internal Only the runtime spawn layers should call this. Pure module: pure
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import { loadRunManifestById } from "../state/stores/state-store.ts";
|
|
2
|
+
import { createRunPaths, loadRunManifestById } from "../state/stores/state-store.ts";
|
|
4
3
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
|
-
import { projectCrewRoot } from "../utils/paths.ts";
|
|
6
4
|
import { isFinishedRunStatus } from "./process-status.ts";
|
|
7
5
|
|
|
8
6
|
export interface RunWaitResult {
|
|
@@ -11,6 +9,19 @@ export interface RunWaitResult {
|
|
|
11
9
|
/** True when the waiter was released early by `detachRunPromise` while the
|
|
12
10
|
* run itself keeps executing (see that function). */
|
|
13
11
|
detached?: boolean;
|
|
12
|
+
/** F1 (2026-09-12 live battery): set when the waiter was released early
|
|
13
|
+
* because a task PARKED on `ask` — the broker pushes this via
|
|
14
|
+
* `resolveRunPromise` so the sync caller's tool call returns with the
|
|
15
|
+
* question instead of blocking until the watchdog kills the worker. The
|
|
16
|
+
* run keeps executing; the leader answers via `team action='respond'`
|
|
17
|
+
* then re-blocks via `team action='wait'`. */
|
|
18
|
+
waiting?: {
|
|
19
|
+
taskId: string;
|
|
20
|
+
questionId: string;
|
|
21
|
+
question: string;
|
|
22
|
+
deadline: number;
|
|
23
|
+
options?: string[];
|
|
24
|
+
};
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export interface ActiveRunPromise {
|
|
@@ -32,7 +43,22 @@ const activeRunPromises = new Map<string, ActiveRunPromise>();
|
|
|
32
43
|
*/
|
|
33
44
|
const detachRequests = new Set<string>();
|
|
34
45
|
|
|
46
|
+
/** F1 tombstones (2026-09-12): resolveRunPromise deletes the live entry after
|
|
47
|
+
* resolving it, so a register+resolve landing BETWEEN two poll ticks of a
|
|
48
|
+
* slow-path waiter orphaned the payload (the waiter held no reference to the
|
|
49
|
+
* entry). A waiter on the polling path consumes the tombstone instead.
|
|
50
|
+
* Bounded — oldest evicted past the limit (detached runs never wait). */
|
|
51
|
+
const resolvedRunResults = new Map<string, RunWaitResult>();
|
|
52
|
+
const RESOLVED_TOMBSTONE_LIMIT = 32;
|
|
53
|
+
|
|
35
54
|
export function registerRunPromise(runId: string): ActiveRunPromise {
|
|
55
|
+
// Idempotent (F1 live-probe fix, 2026-09-12): run.ts pre-registers BEFORE
|
|
56
|
+
// startForegroundRun so the waitForRun that runs immediately after can hit
|
|
57
|
+
// the medium path; executeTeamRunCore's later `void registerRunPromise(...)`
|
|
58
|
+
// must NOT overwrite the entry (an overwrite strands waiters holding the
|
|
59
|
+
// old promise — the waiting-push would resolve a promise nobody awaits).
|
|
60
|
+
const existing = activeRunPromises.get(runId);
|
|
61
|
+
if (existing) return existing;
|
|
36
62
|
detachRequests.delete(runId);
|
|
37
63
|
let resolve!: (value: RunWaitResult) => void;
|
|
38
64
|
let reject!: (reason: unknown) => void;
|
|
@@ -85,6 +111,13 @@ export function resolveRunPromise(runId: string, result: RunWaitResult): void {
|
|
|
85
111
|
entry.resolve(result);
|
|
86
112
|
activeRunPromises.delete(runId);
|
|
87
113
|
}
|
|
114
|
+
// F1 tombstone: a slow-path waiter (register/await race, or register+resolve
|
|
115
|
+
// between two ticks) must still see the push. Cheap Map.set; bounded above.
|
|
116
|
+
resolvedRunResults.set(runId, result);
|
|
117
|
+
if (resolvedRunResults.size > RESOLVED_TOMBSTONE_LIMIT) {
|
|
118
|
+
const oldest = resolvedRunResults.keys().next().value;
|
|
119
|
+
if (oldest !== undefined) resolvedRunResults.delete(oldest);
|
|
120
|
+
}
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
export function rejectRunPromise(runId: string, reason: unknown): void {
|
|
@@ -95,6 +128,17 @@ export function rejectRunPromise(runId: string, reason: unknown): void {
|
|
|
95
128
|
}
|
|
96
129
|
}
|
|
97
130
|
|
|
131
|
+
function raceRunPromise(entry: ActiveRunPromise, timeoutMs: number, deadline: number): Promise<RunWaitResult> {
|
|
132
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
133
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
134
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
135
|
+
timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), remaining);
|
|
136
|
+
});
|
|
137
|
+
return Promise.race([entry.promise, timeoutPromise]).finally(() => {
|
|
138
|
+
if (timer) clearTimeout(timer);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
/**
|
|
99
143
|
* Wait for a team run to reach a terminal status.
|
|
100
144
|
* - If the run is already finished on disk, returns immediately.
|
|
@@ -124,17 +168,7 @@ export async function waitForRun(
|
|
|
124
168
|
|
|
125
169
|
// Medium path: foreground promise registered in this process
|
|
126
170
|
const entry = activeRunPromises.get(runId);
|
|
127
|
-
if (entry)
|
|
128
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
129
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
130
|
-
timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
131
|
-
});
|
|
132
|
-
try {
|
|
133
|
-
return await Promise.race([entry.promise, timeoutPromise]);
|
|
134
|
-
} finally {
|
|
135
|
-
if (timer) clearTimeout(timer);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
171
|
+
if (entry) return await raceRunPromise(entry, timeoutMs, deadline);
|
|
138
172
|
|
|
139
173
|
// Slow path: background run — poll with exponential backoff capped at pollIntervalMs.
|
|
140
174
|
// This path is ALSO taken by a foreground run whose executeTeamRun has not
|
|
@@ -147,13 +181,24 @@ export async function waitForRun(
|
|
|
147
181
|
const current = loadRunManifestById(cwd, runId);
|
|
148
182
|
if (current) return { ...current, detached: true };
|
|
149
183
|
}
|
|
184
|
+
// F1 live-probe fix (2026-09-12): a foreground promise may appear AFTER
|
|
185
|
+
// this waiter started (register/await race — the waiting-push from the
|
|
186
|
+
// broker resolves the ENTRY, which the polling loop would otherwise
|
|
187
|
+
// never observe). Re-check each tick and switch to the promise path with
|
|
188
|
+
// the REMAINING budget; evidence team_20260912053049 (parked 05:31:14,
|
|
189
|
+
// push no-op'd, waiter polled until the watchdog killed the worker).
|
|
190
|
+
const entryNow = activeRunPromises.get(runId);
|
|
191
|
+
if (entryNow) return await raceRunPromise(entryNow, timeoutMs, deadline);
|
|
150
192
|
if (attempt === 0) {
|
|
151
193
|
// Early exit: if the run directory doesn't exist, don't waste time polling.
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
194
|
+
// Resolve through createRunPaths (scopeBaseRoot) so the probe matches where
|
|
195
|
+
// runs are CREATED: project scope — incl. the .pi/teams/ fallback for
|
|
196
|
+
// .pi-based projects (issue #29) — for cwds under a repo root, and USER
|
|
197
|
+
// scope for markerless cwds (issue #54). The previous projectCrewRoot(cwd)
|
|
198
|
+
// join always looked at <cwd>/.crew/state/runs for a markerless cwd, so
|
|
199
|
+
// RUN/WAIT instantly threw "Run not found" while the user-scope crew kept
|
|
200
|
+
// running. createRunPaths is pure path math (no mkdir), so it is safe here.
|
|
201
|
+
const runDir = createRunPaths(cwd, runId).stateRoot;
|
|
157
202
|
if (!fs.existsSync(runDir)) {
|
|
158
203
|
throw new Error(`Run ${runId} not found. No run directory at ${runDir}`);
|
|
159
204
|
}
|
|
@@ -162,6 +207,15 @@ export async function waitForRun(
|
|
|
162
207
|
if (fresh && isFinishedRunStatus(fresh.manifest.status)) {
|
|
163
208
|
return fresh;
|
|
164
209
|
}
|
|
210
|
+
// F1 tombstone: a push that resolved (and evicted) the live entry between
|
|
211
|
+
// ticks lands here — consume it so the leader sees the question, not a
|
|
212
|
+
// 600s block. Terminal-on-disk above still wins (a finished run outranks
|
|
213
|
+
// a stale waiting payload).
|
|
214
|
+
const tombstone = resolvedRunResults.get(runId);
|
|
215
|
+
if (tombstone) {
|
|
216
|
+
resolvedRunResults.delete(runId);
|
|
217
|
+
return tombstone;
|
|
218
|
+
}
|
|
165
219
|
const delay = Math.min(pollIntervalMs, 50 * 2 ** Math.min(attempt, 6)); // max ~3.2s
|
|
166
220
|
await new Promise((r) => setTimeout(r, delay));
|
|
167
221
|
attempt++;
|
|
@@ -176,6 +230,7 @@ export function hasActiveRunPromise(runId: string): boolean {
|
|
|
176
230
|
|
|
177
231
|
export function clearRunPromisesForTest(): void {
|
|
178
232
|
detachRequests.clear();
|
|
233
|
+
resolvedRunResults.clear();
|
|
179
234
|
for (const entry of activeRunPromises.values()) {
|
|
180
235
|
entry.reject(new Error("Cleared by test"));
|
|
181
236
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
4
|
import type { TeamRole } from "../teams/team-config.ts";
|
|
6
5
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
|
+
import { packageRoot } from "../utils/paths.ts";
|
|
7
7
|
import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
8
8
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
9
9
|
import { CONFIDENCE_THRESHOLDS, getWeightedSkillsForRole, registerSkillEffectivenessHooks } from "./skill-effectiveness.ts";
|
|
10
10
|
|
|
11
|
-
const PACKAGE_SKILLS_DIR = path.
|
|
11
|
+
const PACKAGE_SKILLS_DIR = path.join(packageRoot(), "skills");
|
|
12
12
|
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
// peer-dep.ts resolves @earendil-works/pi-coding-agent robustly across install
|
|
@@ -121,8 +121,24 @@ function collectTaskSkillNames(input: ResolveTaskSkillsInput | undefined): strin
|
|
|
121
121
|
if (input.agent?.skills?.length) names.push(...input.agent.skills);
|
|
122
122
|
if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
|
|
123
123
|
if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// SKILL-HYGIENE-2: support wildcard (`*`) and denylist (`!name`) syntax in override.
|
|
125
|
+
// - `*` is a marker (no-op; defaults already included via defaultSkillsForRole).
|
|
126
|
+
// - `!name` removes `name` from the final selection.
|
|
127
|
+
// - Other names are added (existing additive behavior).
|
|
128
|
+
const denylist = new Set<string>();
|
|
129
|
+
if (Array.isArray(input.override)) {
|
|
130
|
+
for (const item of input.override) {
|
|
131
|
+
if (item === "*") {
|
|
132
|
+
// wildcard marker; defaults already in names via defaultSkillsForRole
|
|
133
|
+
} else if (item.startsWith("!")) {
|
|
134
|
+
denylist.add(item.slice(1));
|
|
135
|
+
} else {
|
|
136
|
+
names.push(item);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (denylist.size === 0) return unique(names);
|
|
141
|
+
return unique(names).filter((n) => !denylist.has(n));
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
|
|
@@ -107,7 +107,7 @@ async function appendBackgroundLogAsync(bgLogPath: string, eventLine: string): P
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* F7: resolve the enabledModels allowlist for the child-process spawn path,
|
|
110
|
-
* but only if `
|
|
110
|
+
* but only if `reliability.scopeModels` is ON. Returns [] (no-op)
|
|
111
111
|
* when the toggle is off or the allowlist is empty. Best-effort: any failure
|
|
112
112
|
* to read config or the allowlist silently disables the gate so spawn is
|
|
113
113
|
* never blocked by a misconfiguration.
|
|
@@ -35,10 +35,11 @@ function readOnlyRoleInstructions(role: string): string {
|
|
|
35
35
|
].join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
38
|
+
export function coordinationBridgeInstructions(task: TeamTaskState, opts?: { includeMailboxTarget?: boolean }): string {
|
|
39
|
+
const includeMailboxTarget = opts?.includeMailboxTarget ?? true;
|
|
39
40
|
return [
|
|
40
41
|
"# Crew Coordination Channel",
|
|
41
|
-
`Mailbox target for this task: ${task.id}
|
|
42
|
+
...(includeMailboxTarget ? [`Mailbox target for this task: ${task.id}`] : []),
|
|
42
43
|
"Use the run mailbox contract for coordination with the leader/orchestrator:",
|
|
43
44
|
"- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
|
|
44
45
|
"- Never guess implementation details that materially affect decisions. If the `ask` tool is available and you need a clarification, a decision, or a missing requirement before you can proceed safely, call `ask` and wait — a parked question is cheaper than a wrong build.",
|
|
@@ -262,7 +263,10 @@ export async function renderTaskPrompt(
|
|
|
262
263
|
// computation for parallel siblings in the same batch.
|
|
263
264
|
const stableComponents = precomputedStableComponents ?? (await computeStablePrefixComponents(manifest, step, task, agent));
|
|
264
265
|
|
|
265
|
-
// Stable prefix: role instructions, coordination, workspace tree — rarely changes
|
|
266
|
+
// Stable prefix: role instructions, coordination, workspace tree — rarely changes.
|
|
267
|
+
// ARCH-3 (byte-stable worker prefix): per-task values (Task ID, Task cwd, mailbox
|
|
268
|
+
// target) live in dynamicSuffix so siblings sharing a run+role produce a
|
|
269
|
+
// byte-identical prefix and hit provider KV-cache across the batch.
|
|
266
270
|
const stablePrefix = [
|
|
267
271
|
"# pi-crew Worker Runtime Context",
|
|
268
272
|
`Run ID: ${manifest.runId}`,
|
|
@@ -271,8 +275,6 @@ export async function renderTaskPrompt(
|
|
|
271
275
|
`State root: ${manifest.stateRoot}`,
|
|
272
276
|
`Artifacts root: ${manifest.artifactsRoot}`,
|
|
273
277
|
`Events path: ${manifest.eventsPath}`,
|
|
274
|
-
`Task ID: ${task.id}`,
|
|
275
|
-
`Task cwd: ${task.cwd}`,
|
|
276
278
|
`Workspace mode: ${manifest.workspaceMode}`,
|
|
277
279
|
"",
|
|
278
280
|
"Protocol:",
|
|
@@ -280,10 +282,14 @@ export async function renderTaskPrompt(
|
|
|
280
282
|
"- Report blockers and verification evidence in the final result.",
|
|
281
283
|
"- Do not claim completion without evidence.",
|
|
282
284
|
"- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
|
|
285
|
+
// PROMPT-2 (port of OMO-slim task-rejection, improved phrasing): a
|
|
286
|
+
// universal lane-guard for every role — complements the per-agent reject
|
|
287
|
+
// sections in agents/*.md with a scaffold-level instruction.
|
|
288
|
+
"- If a task falls outside your role, do not attempt partial work. Return a concise rejection to the leader naming the lane that should own it.",
|
|
283
289
|
"",
|
|
284
290
|
readOnlyRoleInstructions(task.role),
|
|
285
291
|
"",
|
|
286
|
-
coordinationBridgeInstructions(task),
|
|
292
|
+
coordinationBridgeInstructions(task, { includeMailboxTarget: false }),
|
|
287
293
|
"",
|
|
288
294
|
stableComponents.treeBlock,
|
|
289
295
|
"",
|
|
@@ -291,9 +297,13 @@ export async function renderTaskPrompt(
|
|
|
291
297
|
"",
|
|
292
298
|
toolGuidanceBlock(agent),
|
|
293
299
|
"",
|
|
294
|
-
// O4: project knowledge (.crew/knowledge.md)
|
|
295
|
-
// pi-crew extension (
|
|
296
|
-
//
|
|
300
|
+
// O4 (ARCH-2 corrected): project knowledge (.crew/knowledge.md). Builtin
|
|
301
|
+
// workers don't load the pi-crew extension (agents declare no `extensions:`
|
|
302
|
+
// in frontmatter), so before_agent_start knowledge injection doesn't fire
|
|
303
|
+
// for them — and the knowledge-injection hook now early-returns on
|
|
304
|
+
// PI_CREW_KIND=subagent, so even agents that DO declare the extension
|
|
305
|
+
// can't double-inject. This prompt-builder fragment is the single source
|
|
306
|
+
// of worker project knowledge.
|
|
297
307
|
stableComponents.knowledgeFragment,
|
|
298
308
|
]
|
|
299
309
|
.filter(Boolean)
|
|
@@ -301,6 +311,9 @@ export async function renderTaskPrompt(
|
|
|
301
311
|
|
|
302
312
|
// Dynamic suffix: goal, step, skills, task packet, dependency context, memory — changes per task
|
|
303
313
|
const dynamicSuffix = [
|
|
314
|
+
`Task ID: ${task.id}`,
|
|
315
|
+
`Task cwd: ${task.cwd}`,
|
|
316
|
+
`Mailbox target: ${task.id}`,
|
|
304
317
|
`Goal:\n${manifest.goal}`,
|
|
305
318
|
"",
|
|
306
319
|
`Step: ${step.id}`,
|
|
@@ -270,6 +270,7 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
|
|
|
270
270
|
ambientStatusInjection: Type.Optional(Type.Boolean()),
|
|
271
271
|
perWriteValidation: Type.Optional(Type.Boolean()),
|
|
272
272
|
scopeModels: Type.Optional(Type.Boolean()),
|
|
273
|
+
loopGuard: Type.Optional(Type.Boolean()),
|
|
273
274
|
},
|
|
274
275
|
{ additionalProperties: false },
|
|
275
276
|
);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
4
|
// peer-dep.ts resolves @earendil-works/pi-coding-agent robustly across install
|
|
6
5
|
// layouts. See src/runtime/peer-dep.ts (split-scope install fix).
|
|
7
6
|
import { getAgentDir } from "../runtime/peer-dep.ts";
|
|
8
7
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
import { packageRoot } from "../utils/paths.ts";
|
|
9
9
|
import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
10
10
|
import { parseSkillFrontmatter, type SkillValidationError, validateSkillFrontmatter } from "./validate.ts";
|
|
11
11
|
|
|
12
|
-
const PACKAGE_SKILLS_DIR = path.
|
|
12
|
+
const PACKAGE_SKILLS_DIR = path.join(packageRoot(), "skills");
|
|
13
13
|
|
|
14
14
|
const CACHE_TTL_MS = 30_000; // 30 seconds
|
|
15
15
|
let cache: { skills: SkillDescriptor[]; cachedAt: number; cwd: string } | null = null;
|
|
@@ -304,6 +304,41 @@ const SETTINGS: SettingDef[] = [
|
|
|
304
304
|
tab: "advanced",
|
|
305
305
|
description: "Remove /tmp/pi-crew-* directories after reconciliation (1h age threshold).",
|
|
306
306
|
},
|
|
307
|
+
{
|
|
308
|
+
id: "reliability.loopGuard",
|
|
309
|
+
label: "Tool Loop Guard",
|
|
310
|
+
type: "boolean",
|
|
311
|
+
tab: "advanced",
|
|
312
|
+
description: "Warn at 3 / block at 5 identical consecutive read-only tool results (ARCH-1).",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "reliability.perWriteValidation",
|
|
316
|
+
label: "Per-Write Validation",
|
|
317
|
+
type: "boolean",
|
|
318
|
+
tab: "advanced",
|
|
319
|
+
description: "Validate state writes as they are made (default on).",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: "reliability.ambientStatusInjection",
|
|
323
|
+
label: "Ambient Status Injection",
|
|
324
|
+
type: "boolean",
|
|
325
|
+
tab: "advanced",
|
|
326
|
+
description: "Inject a compact crew-status note into context on every LLM call while runs are in-flight.",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "reliability.forcePreflight",
|
|
330
|
+
label: "Force Preflight (audit override)",
|
|
331
|
+
type: "boolean",
|
|
332
|
+
tab: "advanced",
|
|
333
|
+
description: "Skip usage-threshold BLOCK/WARN preflight. Default false (enforce). Audit/debug only.",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "reliability.scopeModels",
|
|
337
|
+
label: "Scope Models (F7)",
|
|
338
|
+
type: "boolean",
|
|
339
|
+
tab: "advanced",
|
|
340
|
+
description: "Enforce user enabledModels allowlist on subagent model choices. Default false.",
|
|
341
|
+
},
|
|
307
342
|
{
|
|
308
343
|
id: "telemetry.enabled",
|
|
309
344
|
label: "Telemetry",
|
|
@@ -359,6 +394,11 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
359
394
|
"reliability.autoRetry": false,
|
|
360
395
|
"reliability.autoRecover": false,
|
|
361
396
|
"reliability.cleanupOrphanedTempDirs": true,
|
|
397
|
+
"reliability.loopGuard": true,
|
|
398
|
+
"reliability.perWriteValidation": true,
|
|
399
|
+
"reliability.ambientStatusInjection": true,
|
|
400
|
+
"reliability.forcePreflight": false,
|
|
401
|
+
"reliability.scopeModels": false,
|
|
362
402
|
"telemetry.enabled": false,
|
|
363
403
|
"notifications.enabled": false,
|
|
364
404
|
};
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -35,7 +35,13 @@ function parseLines(raw: string): Record<string, string> {
|
|
|
35
35
|
const separator = trimmed.indexOf(":");
|
|
36
36
|
if (separator === -1) continue;
|
|
37
37
|
const key = trimmed.slice(0, separator).trim();
|
|
38
|
-
|
|
38
|
+
let value = trimmed.slice(separator + 1).trim();
|
|
39
|
+
// Strip one pair of symmetric surrounding double quotes so quoted
|
|
40
|
+
// frontmatter values (required by strict YAML when the value contains
|
|
41
|
+
// ": ") behave identically to unquoted ones for every consumer.
|
|
42
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
43
|
+
value = value.slice(1, -1);
|
|
44
|
+
}
|
|
39
45
|
if (key) frontmatter[key] = value;
|
|
40
46
|
}
|
|
41
47
|
return frontmatter;
|
package/src/utils/ndjson.ts
CHANGED
|
@@ -17,7 +17,15 @@ export const MAX_BROKER_FRAME_BYTES = 256 * 1024;
|
|
|
17
17
|
// BrokerError — typed protocol errors
|
|
18
18
|
// ============================================================================
|
|
19
19
|
|
|
20
|
-
export type BrokerErrorCode =
|
|
20
|
+
export type BrokerErrorCode =
|
|
21
|
+
| "oversize-frame"
|
|
22
|
+
| "auth"
|
|
23
|
+
| "protocol"
|
|
24
|
+
| "timeout"
|
|
25
|
+
| "request-timeout"
|
|
26
|
+
| "close"
|
|
27
|
+
| "not-implemented"
|
|
28
|
+
| "rate-limit";
|
|
21
29
|
|
|
22
30
|
export class BrokerError extends Error {
|
|
23
31
|
readonly code: BrokerErrorCode;
|