pi-crew 0.9.17 → 0.9.19
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 +113 -0
- package/dist/build-meta.json +163 -164
- package/dist/index.mjs +939 -768
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +297 -0
- package/docs/perf/performance-review-2026-07.md +287 -0
- package/package.json +1 -1
- package/src/config/config.ts +137 -1
- package/src/extension/register.ts +1 -1
- package/src/extension/team-onboard.ts +1 -3
- package/src/extension/team-tool/anchor.ts +1 -1
- package/src/extension/team-tool/auto-summarize.ts +1 -1
- package/src/extension/team-tool/run.ts +98 -2
- package/src/runtime/async-runner.ts +12 -1
- package/src/runtime/child-pi.ts +1 -1
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/dynamic-workflow-context.ts +1 -1
- package/src/runtime/hidden-handoff.ts +1 -1
- package/src/runtime/mcp-proxy.ts +2 -2
- package/src/runtime/result-extractor.ts +1 -4
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +8 -8
- package/src/runtime/task-id.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +22 -7
- package/src/runtime/team-runner.ts +3 -3
- package/src/runtime/verification-integrity.ts +1 -3
- package/src/schema/team-tool-schema.ts +21 -0
- package/src/state/atomic-write.ts +1 -1
- package/src/state/state-store.ts +8 -2
- package/src/ui/terminal-status.ts +1 -3
- package/src/ui/tool-renderers/index.ts +1 -1
- package/src/utils/safe-paths.ts +1 -5
- package/workflows/plan-execute.workflow.md +30 -0
package/src/config/config.ts
CHANGED
|
@@ -66,6 +66,108 @@ import type {
|
|
|
66
66
|
UpdateConfigOptions,
|
|
67
67
|
} from "./types.ts";
|
|
68
68
|
|
|
69
|
+
// (F16) loadConfig was called 1 Hz idle / 6 Hz active with 0 cache — added 2s TTL+mtime cache following the manifestCache pattern in state-store.ts:75-130.
|
|
70
|
+
const CONFIG_CACHE_TTL_MS = 2000;
|
|
71
|
+
|
|
72
|
+
interface ConfigCacheEntry {
|
|
73
|
+
value: LoadedPiTeamsConfig;
|
|
74
|
+
mtimes: Record<string, number>;
|
|
75
|
+
cachedAt: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ConfigCacheKeyParts {
|
|
79
|
+
filePath: string;
|
|
80
|
+
legacyPath: string;
|
|
81
|
+
projectPath: string;
|
|
82
|
+
projectPiCrewJsonPath: string;
|
|
83
|
+
cwd: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const configCache = new Map<string, ConfigCacheEntry>();
|
|
87
|
+
|
|
88
|
+
/** @internal — TTL override for unit tests (matches __test__setManifestCache pattern in state-store.ts:82). */
|
|
89
|
+
export function __test__setConfigCacheTtlMs(ttlMs: number): void {
|
|
90
|
+
configCacheTtlMsOverride = ttlMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @internal — read the effective TTL in use by the cache. */
|
|
94
|
+
export function __test__getConfigCacheTtlMs(): number {
|
|
95
|
+
return configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @internal — peek at the cached entry for a given key shape. */
|
|
99
|
+
export function __test__getConfigCacheEntry(parts: ConfigCacheKeyParts): ConfigCacheEntry | undefined {
|
|
100
|
+
return configCache.get(buildConfigCacheKey(parts));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @internal — number of cached entries (for tests/diagnostics). */
|
|
104
|
+
export function __test__configCacheSize(): number {
|
|
105
|
+
return configCache.size;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let configCacheTtlMsOverride: number | null = null;
|
|
109
|
+
|
|
110
|
+
function buildConfigCacheKey(parts: ConfigCacheKeyParts): string {
|
|
111
|
+
return JSON.stringify([parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath, parts.cwd]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function statMtimeMs(filePath: string): number | undefined {
|
|
115
|
+
try {
|
|
116
|
+
return fs.statSync(filePath).mtimeMs;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readCachedConfigParts(filePath: string, legacyPath: string, cwd: string | undefined): ConfigCacheKeyParts {
|
|
124
|
+
const projectPath = cwd ? projectConfigPath(cwd) : "";
|
|
125
|
+
const piCrewJsonPath = cwd ? projectPiCrewJsonPath(cwd) : "";
|
|
126
|
+
return {
|
|
127
|
+
filePath,
|
|
128
|
+
legacyPath,
|
|
129
|
+
projectPath,
|
|
130
|
+
projectPiCrewJsonPath: piCrewJsonPath,
|
|
131
|
+
cwd: cwd ?? null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readCacheMtimes(parts: ConfigCacheKeyParts): Record<string, number> {
|
|
136
|
+
const mtimes: Record<string, number> = {};
|
|
137
|
+
for (const p of [parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath]) {
|
|
138
|
+
if (!p) continue; // skip empty (cwd-undefined project paths)
|
|
139
|
+
const m = statMtimeMs(p);
|
|
140
|
+
if (m !== undefined) mtimes[p] = m;
|
|
141
|
+
}
|
|
142
|
+
return mtimes;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function matchesCachedMtimes(cached: Record<string, number>, current: Record<string, number>): boolean {
|
|
146
|
+
const cachedKeys = Object.keys(cached);
|
|
147
|
+
const currentKeys = Object.keys(current);
|
|
148
|
+
if (cachedKeys.length !== currentKeys.length) return false;
|
|
149
|
+
for (const key of cachedKeys) {
|
|
150
|
+
if (current[key] !== cached[key]) return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setConfigCache(key: string, value: LoadedPiTeamsConfig, mtimes: Record<string, number>): void {
|
|
156
|
+
if (configCache.has(key)) configCache.delete(key);
|
|
157
|
+
configCache.set(key, { value, mtimes, cachedAt: Date.now() });
|
|
158
|
+
const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
|
|
159
|
+
// TTL eviction on insert (mirrors manifestCache eviction in state-store.ts:108-117)
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
for (const [k, entry] of configCache.entries()) {
|
|
162
|
+
if (now - entry.cachedAt > ttlMs) configCache.delete(k);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Drop all cached loadConfig results. Call after config writes or from tests. */
|
|
167
|
+
export function invalidateConfigCache(): void {
|
|
168
|
+
configCache.clear();
|
|
169
|
+
}
|
|
170
|
+
|
|
69
171
|
function resolveHomeDir(): string {
|
|
70
172
|
const envValue = process.env.PI_TEAMS_HOME?.trim();
|
|
71
173
|
const defaultHome = os.homedir();
|
|
@@ -972,6 +1074,27 @@ function readOptionalConfig(filePath: string): {
|
|
|
972
1074
|
export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
973
1075
|
const filePath = configPath();
|
|
974
1076
|
const legacyPath = legacyConfigPath();
|
|
1077
|
+
|
|
1078
|
+
// (F16) Quick-win cache: skip the expensive full-reparse on hot paths
|
|
1079
|
+
// (preload tick 1 Hz, per-write validation, subagent completion) when the
|
|
1080
|
+
// on-disk config files are unchanged. See CONFIG_CACHE_TTL_MS at the
|
|
1081
|
+
// top of this file for the rationale and the manifestCache pattern in
|
|
1082
|
+
// state-store.ts:75-130 for the canonical implementation we mirror.
|
|
1083
|
+
const cacheParts = readCachedConfigParts(filePath, legacyPath, cwd);
|
|
1084
|
+
const cacheKey = buildConfigCacheKey(cacheParts);
|
|
1085
|
+
const cached = configCache.get(cacheKey);
|
|
1086
|
+
const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
|
|
1087
|
+
if (cached && Date.now() - cached.cachedAt <= ttlMs) {
|
|
1088
|
+
const currentMtimes = readCacheMtimes(cacheParts);
|
|
1089
|
+
if (matchesCachedMtimes(cached.mtimes, currentMtimes)) {
|
|
1090
|
+
// Refresh insertion order so a frequently-accessed key isn't
|
|
1091
|
+
// evicted from the Map on the next eviction sweep.
|
|
1092
|
+
configCache.delete(cacheKey);
|
|
1093
|
+
configCache.set(cacheKey, cached);
|
|
1094
|
+
return cached.value;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
975
1098
|
const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
|
|
976
1099
|
const warnings: string[] = [];
|
|
977
1100
|
const legacyConfig = readOptionalConfig(legacyPath);
|
|
@@ -1015,12 +1138,20 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
1015
1138
|
paths.push(piCrewJsonPath);
|
|
1016
1139
|
}
|
|
1017
1140
|
}
|
|
1018
|
-
|
|
1141
|
+
const result: LoadedPiTeamsConfig = {
|
|
1019
1142
|
path: filePath,
|
|
1020
1143
|
paths,
|
|
1021
1144
|
config,
|
|
1022
1145
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
1023
1146
|
};
|
|
1147
|
+
// Only cache when at least one of the watched paths exists — this avoids
|
|
1148
|
+
// pinning stale empty results when a user later creates one of these files
|
|
1149
|
+
// in the cache window. mtime stat below picks up the new file (it appears
|
|
1150
|
+
// in currentMtimes but not in cached.mtimes) and triggers a re-parse.
|
|
1151
|
+
if (Object.keys(readCacheMtimes(cacheParts)).length > 0) {
|
|
1152
|
+
setConfigCache(cacheKey, result, readCacheMtimes(cacheParts));
|
|
1153
|
+
}
|
|
1154
|
+
return result;
|
|
1024
1155
|
}
|
|
1025
1156
|
|
|
1026
1157
|
export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions = {}): SavedPiTeamsConfig {
|
|
@@ -1042,6 +1173,9 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
|
|
|
1042
1173
|
}
|
|
1043
1174
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1044
1175
|
atomicWriteFile(filePath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
1176
|
+
// (F16) Invalidate the loadConfig cache after a write — the next
|
|
1177
|
+
// caller must see the new value, not a 0-2s stale snapshot.
|
|
1178
|
+
invalidateConfigCache();
|
|
1045
1179
|
return { path: filePath, config: merged };
|
|
1046
1180
|
});
|
|
1047
1181
|
}
|
|
@@ -1063,6 +1197,8 @@ export function updateAutonomousConfig(patch: PiTeamsAutonomousConfig): SavedPiT
|
|
|
1063
1197
|
: {};
|
|
1064
1198
|
current.autonomous = { ...currentAutonomous, ...patch };
|
|
1065
1199
|
atomicWriteFile(filePath, `${JSON.stringify(current, null, 2)}\n`);
|
|
1200
|
+
// (F16) Invalidate the loadConfig cache after a write — see updateConfig.
|
|
1201
|
+
invalidateConfigCache();
|
|
1066
1202
|
return { path: filePath, config: parseConfig(current) };
|
|
1067
1203
|
});
|
|
1068
1204
|
}
|
|
@@ -67,7 +67,7 @@ import { registerCrewShortcuts } from "./crew-shortcuts.ts";
|
|
|
67
67
|
import { type PiCrewRpcHandle, registerPiCrewRpc } from "./cross-extension-rpc.ts";
|
|
68
68
|
import { registerKnowledgeInjection } from "./knowledge-injection.ts";
|
|
69
69
|
import { registerCrewMessageRenderers } from "./message-renderers.ts";
|
|
70
|
-
import {
|
|
70
|
+
import type { NotificationDescriptor } from "./notification-router.ts";
|
|
71
71
|
import { runArtifactCleanup } from "./registration/artifact-cleanup.ts";
|
|
72
72
|
import { registerTeamCommands } from "./registration/commands.ts";
|
|
73
73
|
import { registerCompactionGuard } from "./registration/compaction-guard.ts";
|
|
@@ -67,9 +67,7 @@ function loadRunSummaries(cwd: string, options: OnboardingOptions = {}): RunSumm
|
|
|
67
67
|
completedAt: raw.completedAt ?? raw.updatedAt,
|
|
68
68
|
taskCount: 0, // tasks stored separately, not in manifest
|
|
69
69
|
});
|
|
70
|
-
} catch {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
70
|
+
} catch {}
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
return summaries;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides set/clear/status commands for anchor points.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { AnchorManager, AnchorNotFoundError, createAnchorManager, NoHandoffsError } from "../../runtime/anchor-manager.ts";
|
|
6
|
+
import { type AnchorManager, AnchorNotFoundError, createAnchorManager, NoHandoffsError } from "../../runtime/anchor-manager.ts";
|
|
7
7
|
import type { HandoffSummary } from "../../runtime/handoff-manager.ts";
|
|
8
8
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
9
9
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides on/off/status commands for auto-summarization.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { AutoSummarizeService, createAutoSummarizeService, DEFAULT_AUTO_SUMMARIZE_CONFIG } from "../../runtime/auto-summarize.ts";
|
|
6
|
+
import { type AutoSummarizeService, createAutoSummarizeService, DEFAULT_AUTO_SUMMARIZE_CONFIG } from "../../runtime/auto-summarize.ts";
|
|
7
7
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
8
8
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
9
9
|
import { result, type TeamContext } from "./context.ts";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
2
2
|
import { loadConfig } from "../../config/config.ts";
|
|
3
3
|
import { PipelineRunner, type PipelineWorkflow } from "../../runtime/pipeline-runner.ts";
|
|
4
|
+
import { sanitizeTaskText } from "../../runtime/task-packet.ts";
|
|
4
5
|
// Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
|
|
5
6
|
import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
|
|
6
7
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
@@ -17,7 +18,7 @@ import { assertCleanLeader, findGitRoot } from "../../worktree/worktree-manager.
|
|
|
17
18
|
const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
|
|
18
19
|
|
|
19
20
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
20
|
-
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
21
|
+
import { resolveContainedPath, resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
21
22
|
|
|
22
23
|
let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
|
|
23
24
|
async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
|
|
@@ -127,6 +128,76 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
|
|
|
127
128
|
timer.unref();
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Max analysis size in bytes — matches the `maxLength: 100_000` schema cap on
|
|
133
|
+
* the inline `analysis` param, so the file channel can't smuggle in a larger
|
|
134
|
+
* payload than the inline channel allows (prompt-size blowup guard).
|
|
135
|
+
*/
|
|
136
|
+
const MAX_ANALYSIS_BYTES = 100_000;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the analysis channel (round-X Y1): inline `analysis` or `analysisPath` file.
|
|
140
|
+
*
|
|
141
|
+
* Called BEFORE `createRunManifest` so validation failures fail-fast and no orphan
|
|
142
|
+
* run state is left on disk. Sanitizes the parsed content with the same
|
|
143
|
+
* {@link sanitizeTaskText} machinery used in `buildTaskPacket` (SEC-007), since the
|
|
144
|
+
* resulting text is injected into worker prompts via standard sharedReads.
|
|
145
|
+
*
|
|
146
|
+
* Mutual exclusivity mirrors the `budgetTotal`/`budgetUnlimited` pattern in
|
|
147
|
+
* `goal.ts` — a cold-review #2 blocking fix pattern.
|
|
148
|
+
*/
|
|
149
|
+
function resolveAnalysisText(
|
|
150
|
+
params: TeamToolParamsValue,
|
|
151
|
+
cwd: string,
|
|
152
|
+
): { text?: string; error?: string; source: "inline" | "path" | "none" } {
|
|
153
|
+
const hasInline = typeof params.analysis === "string" && params.analysis.length > 0;
|
|
154
|
+
const hasPath = typeof params.analysisPath === "string" && params.analysisPath.length > 0;
|
|
155
|
+
|
|
156
|
+
if (hasInline && hasPath) {
|
|
157
|
+
return {
|
|
158
|
+
error: "`analysis` and `analysisPath` are mutually exclusive. Set exactly one.",
|
|
159
|
+
source: "none",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!hasInline && !hasPath) return { source: "none" };
|
|
163
|
+
|
|
164
|
+
if (hasPath) {
|
|
165
|
+
let resolved: string;
|
|
166
|
+
try {
|
|
167
|
+
resolved = resolveContainedPath(cwd, params.analysisPath as string);
|
|
168
|
+
} catch {
|
|
169
|
+
return {
|
|
170
|
+
error: `analysisPath must be within project directory: ${params.analysisPath}`,
|
|
171
|
+
source: "none",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!fs.existsSync(resolved)) {
|
|
175
|
+
return {
|
|
176
|
+
error: `Analysis file not found: ${resolved}`,
|
|
177
|
+
source: "none",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Size cap BEFORE reading: mirror the inline schema cap (maxLength 100_000)
|
|
181
|
+
// so a large file can't blow up worker prompts via the sharedReads channel.
|
|
182
|
+
const { size } = fs.statSync(resolved);
|
|
183
|
+
if (size > MAX_ANALYSIS_BYTES) {
|
|
184
|
+
return {
|
|
185
|
+
error: `Analysis file too large: ${size} bytes (max ${MAX_ANALYSIS_BYTES}). Trim the analysis or pass a summary inline.`,
|
|
186
|
+
source: "none",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const raw = fs.readFileSync(resolved, "utf-8");
|
|
190
|
+
const sanitized = sanitizeTaskText(raw);
|
|
191
|
+
if (!sanitized) return { source: "none" };
|
|
192
|
+
return { text: sanitized, source: "path" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// hasInline
|
|
196
|
+
const sanitized = sanitizeTaskText(params.analysis as string);
|
|
197
|
+
if (!sanitized) return { source: "none" };
|
|
198
|
+
return { text: sanitized, source: "inline" };
|
|
199
|
+
}
|
|
200
|
+
|
|
130
201
|
export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
131
202
|
// CHAIN DISPATCH: runs before goal validation since a chain has no top-level
|
|
132
203
|
// goal. The injected handleRun reference breaks the run.ts ↔ chain-dispatch.ts
|
|
@@ -234,11 +305,18 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
234
305
|
role: params.role ?? "agent",
|
|
235
306
|
task: "{goal}",
|
|
236
307
|
model: params.model,
|
|
308
|
+
reads: params.analysis || params.analysisPath ? ["analysis.md"] : undefined,
|
|
237
309
|
},
|
|
238
310
|
],
|
|
239
311
|
}
|
|
240
312
|
: workflows.find((item) => item.name === workflowName);
|
|
241
313
|
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
314
|
+
|
|
315
|
+
// ANALYSIS CHANNEL (round-X Y1): resolve analysis text from inline or file BEFORE
|
|
316
|
+
// createRunManifest so validation errors fail-fast (no orphan run state).
|
|
317
|
+
const analysisParam = resolveAnalysisText(params, resolvedCtx.cwd);
|
|
318
|
+
if (analysisParam.error) return result(analysisParam.error, { action: "run", status: "error" }, true);
|
|
319
|
+
|
|
242
320
|
// LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { expandParallelResearchWorkflow }` above (issue #28, RFC 17). At call time the module body has fully evaluated, so the dynamic import returns a live binding. Multi-line form breaks scripts/check-lazy-imports.mjs (which does `lines[lineNum - 2]`), so keep destructuring + await import on one line.
|
|
243
321
|
const { expandParallelResearchWorkflow: expandParallelResearch } = await import("../../runtime/parallel-research.ts");
|
|
244
322
|
const workflow = directAgent ? baseWorkflow : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
|
|
@@ -276,6 +354,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
276
354
|
if (!directAgent && workflow.source === "builtin" && isGoalWrapEnabled(resolvedCtx.cwd, workflow.name)) {
|
|
277
355
|
const decision = shouldGoalWrap(resolvedCtx.cwd, workflow);
|
|
278
356
|
if (decision.enabled) {
|
|
357
|
+
if (analysisParam.text) {
|
|
358
|
+
console.warn(
|
|
359
|
+
`[team-tool.run] analysis param is ignored by goal-wrapped run (workflow=${workflow.name}). The analysis artifact will not be written.`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
279
362
|
return await startGoalWrappedRun(params, ctx, workflow, goal);
|
|
280
363
|
}
|
|
281
364
|
// goal-wrap disabled for this workflow — fall through silently to normal
|
|
@@ -364,10 +447,23 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
364
447
|
content: `${goal}\n`,
|
|
365
448
|
producer: "team-tool",
|
|
366
449
|
});
|
|
450
|
+
// ANALYSIS CHANNEL (round-X Y1): if analysis was provided, persist as a shared artifact
|
|
451
|
+
// so workflow steps declaring reads: analysis.md receive it via the standard
|
|
452
|
+
// dependency-context injection (collectDependencyOutputContext → renderDependencyOutputContext).
|
|
453
|
+
const analysisArtifacts = analysisParam.text
|
|
454
|
+
? [
|
|
455
|
+
writeArtifact(paths.artifactsRoot, {
|
|
456
|
+
kind: "prompt",
|
|
457
|
+
relativePath: "shared/analysis.md",
|
|
458
|
+
content: `${analysisParam.text}\n`,
|
|
459
|
+
producer: "team-tool",
|
|
460
|
+
}),
|
|
461
|
+
]
|
|
462
|
+
: [];
|
|
367
463
|
const updatedManifest = {
|
|
368
464
|
...manifest,
|
|
369
465
|
...(skillOverride !== undefined ? { skillOverride } : {}),
|
|
370
|
-
artifacts: [goalArtifact],
|
|
466
|
+
artifacts: [goalArtifact, ...analysisArtifacts],
|
|
371
467
|
summary: "Run manifest created; worker execution is not implemented yet.",
|
|
372
468
|
};
|
|
373
469
|
atomicWriteJson(paths.manifestPath, updatedManifest);
|
|
@@ -8,6 +8,7 @@ import type { TeamRunManifest } from "../state/types.ts";
|
|
|
8
8
|
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
9
9
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
10
10
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
11
|
+
import { packageRoot } from "../utils/paths.ts";
|
|
11
12
|
import { registerWorker, unregisterWorker } from "./orphan-worker-registry.ts";
|
|
12
13
|
import { PEER_DEP_DIR_ENV, resolvePeerDepDir } from "./peer-dep.ts";
|
|
13
14
|
|
|
@@ -223,7 +224,17 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
|
|
|
223
224
|
];
|
|
224
225
|
|
|
225
226
|
export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise<SpawnBackgroundTeamRunResult> {
|
|
226
|
-
|
|
227
|
+
// FIX (2026-07-02, perf review F-critical): use packageRoot() instead of
|
|
228
|
+
// import.meta.url-relative path. The previous path.resolve walks
|
|
229
|
+
// <bundleDir>/background-runner.ts, which is correct in src/ but BROKEN
|
|
230
|
+
// in the bundle: esbuild's __esm helper does not preserve per-module
|
|
231
|
+
// import.meta.url, so the resolve lands at <pi-crew>/background-runner.ts
|
|
232
|
+
// (missing `src/runtime/`). The background-runner then ENOENTs at spawn
|
|
233
|
+
// and 3 explorer agents failed during the verification run. packageRoot()
|
|
234
|
+
// walks up to find pi-crew's package.json and works correctly from both
|
|
235
|
+
// src/ and dist/ entry points. Mirrors the fix in pi-args.ts:10 (commit
|
|
236
|
+
// 0dd93e0).
|
|
237
|
+
const runnerPath = path.join(packageRoot(), "src", "runtime", "background-runner.ts");
|
|
227
238
|
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
228
239
|
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
229
240
|
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -993,7 +993,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
993
993
|
};
|
|
994
994
|
|
|
995
995
|
let softLimitReached = false;
|
|
996
|
-
|
|
996
|
+
const steerInjectionFailed = false;
|
|
997
997
|
const maxTurns = input.maxTurns;
|
|
998
998
|
// FIX (Issue #1): Bound graceTurns to prevent the hard abort condition from
|
|
999
999
|
// never triggering when an arbitrarily large value is passed.
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
* grouping many micro-tasks into one worker prompt.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { permissionForRole } from "./role-permission.ts";
|
|
31
30
|
import type { TeamTaskState } from "../state/types.ts";
|
|
32
31
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
32
|
+
import { permissionForRole } from "./role-permission.ts";
|
|
33
33
|
|
|
34
34
|
/** Default cap on group size to prevent context-budget overflow. */
|
|
35
35
|
export const DEFAULT_MAX_GROUP_SIZE = 5;
|
|
@@ -254,7 +254,7 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
254
254
|
// round-12 P0-1: in-memory phase state, exposed via non-enumerable getter like __finalResult.
|
|
255
255
|
// The events log is the durable source of truth for phase boundaries.
|
|
256
256
|
// round-18 P2-3: hydrate phaseState from a resumed checkpoint (backward compatible when unset).
|
|
257
|
-
|
|
257
|
+
const phaseState: { currentPhase: string | undefined; phases: string[] } = opts.resumedState
|
|
258
258
|
? {
|
|
259
259
|
currentPhase: opts.resumedState.currentPhase,
|
|
260
260
|
phases: [...opts.resumedState.phases],
|
|
@@ -172,7 +172,7 @@ export class HiddenHandoffService {
|
|
|
172
172
|
|
|
173
173
|
const priority = options.priority ?? this.inferPriority(summary);
|
|
174
174
|
const content = this.buildContent(summary);
|
|
175
|
-
|
|
175
|
+
const recipient = options.to ?? this.getParentAgentId();
|
|
176
176
|
|
|
177
177
|
if (!recipient) {
|
|
178
178
|
// No parent to send to, but we still emit the event
|
package/src/runtime/mcp-proxy.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* when proxying from the parent.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { Static, TSchema } from "@sinclair/typebox";
|
|
21
21
|
|
|
22
22
|
export interface McpProxyConfig {
|
|
23
23
|
/** Whether to enable MCP in the child session. */
|
|
@@ -152,10 +152,7 @@ function tryScanJson(text: string): unknown | undefined {
|
|
|
152
152
|
const candidate = rest.slice(0, end);
|
|
153
153
|
try {
|
|
154
154
|
return JSON.parse(candidate);
|
|
155
|
-
} catch {
|
|
156
|
-
// Not valid JSON at this position; keep scanning for the next '{'/'['.
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
155
|
+
} catch {}
|
|
159
156
|
}
|
|
160
157
|
return undefined;
|
|
161
158
|
}
|
|
@@ -278,7 +278,7 @@ export class RetryRunner {
|
|
|
278
278
|
* Calculate backoff delay with optional capping.
|
|
279
279
|
*/
|
|
280
280
|
private calculateBackoff(baseMs: number, attempt: number, multiplier: number, maxBackoffMs?: number): number {
|
|
281
|
-
let delay = baseMs *
|
|
281
|
+
let delay = baseMs * multiplier ** (attempt - 1);
|
|
282
282
|
if (maxBackoffMs !== undefined) {
|
|
283
283
|
delay = Math.min(delay, maxBackoffMs);
|
|
284
284
|
}
|
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
|
|
12
12
|
import { writeFile } from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
import { runChildPi } from "./child-pi.ts";
|
|
16
|
-
import { splitCoalescedOutput } from "./task-runner/output-splitter.ts";
|
|
17
|
-
import { buildWorkspaceTree } from "./workspace-tree.ts";
|
|
18
|
-
import { saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
14
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
19
15
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
20
16
|
import { appendEventAsync } from "../state/event-log.ts";
|
|
21
|
-
import {
|
|
22
|
-
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
23
|
-
import type { CrewRuntimeMode } from "./runtime-resolver.ts";
|
|
17
|
+
import { saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
24
18
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
25
19
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
20
|
+
import { runChildPi } from "./child-pi.ts";
|
|
21
|
+
import { permissionForRole } from "./role-permission.ts";
|
|
22
|
+
import type { CrewRuntimeMode } from "./runtime-resolver.ts";
|
|
23
|
+
import { splitCoalescedOutput } from "./task-runner/output-splitter.ts";
|
|
26
24
|
import { mergeArtifacts } from "./team-runner-artifacts.ts";
|
|
25
|
+
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
|
|
26
|
+
import { buildWorkspaceTree } from "./workspace-tree.ts";
|
|
27
27
|
|
|
28
28
|
export interface CoalescedTaskGroupInput {
|
|
29
29
|
manifest: TeamRunManifest;
|
package/src/runtime/task-id.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function hashToBase36(content: string, length: number): string {
|
|
|
58
58
|
*/
|
|
59
59
|
export function calculateAdaptiveLength(existingCount: number, config: AdaptiveIDConfig = DEFAULT_CONFIG): number {
|
|
60
60
|
for (let length = config.minLength; length <= config.maxLength; length++) {
|
|
61
|
-
const totalPossibilities =
|
|
61
|
+
const totalPossibilities = 36 ** length;
|
|
62
62
|
const probability = 1 - Math.exp(-(existingCount * existingCount) / (2 * totalPossibilities));
|
|
63
63
|
if (probability <= config.maxCollisionProbability) {
|
|
64
64
|
return length;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { flushPendingAtomicWrites } from "../../state/atomic-write.ts";
|
|
2
3
|
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
-
import { loadRunManifestById,
|
|
4
|
+
import { loadRunManifestById, saveRunTasksCoalesced } from "../../state/state-store.ts";
|
|
4
5
|
import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
5
6
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
6
7
|
import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
@@ -54,7 +55,16 @@ export function persistSingleTaskUpdate(
|
|
|
54
55
|
|
|
55
56
|
try {
|
|
56
57
|
return withRunLockSync(manifest, () => {
|
|
57
|
-
|
|
58
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
59
|
+
// F4: persistSingleTaskUpdate now uses saveRunTasksCoalesced below
|
|
60
|
+
// (50ms debounce window). Read-modify-write loops are unsafe under
|
|
61
|
+
// coalescing — a parallel writer's buffered write is invisible to
|
|
62
|
+
// loadRunManifestById until it actually lands. Force any pending
|
|
63
|
+
// coalesced writes to flush first so this read sees the latest
|
|
64
|
+
// durable state. Without this guard, a parallel writer could
|
|
65
|
+
// overwrite our buffered write between our load and our (async)
|
|
66
|
+
// fsync, silently losing the intermediate update.
|
|
67
|
+
flushPendingAtomicWrites();
|
|
58
68
|
const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
|
|
59
69
|
merged = updateTask(latest, taskWithCheckpoint);
|
|
60
70
|
|
|
@@ -69,7 +79,7 @@ export function persistSingleTaskUpdate(
|
|
|
69
79
|
if (currentMtime !== baseMtime) {
|
|
70
80
|
// Another writer committed — their update is in latest, re-merge on top
|
|
71
81
|
baseMtime = currentMtime;
|
|
72
|
-
continue
|
|
82
|
+
continue;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
// No concurrent writer — check that our merged result is based on the
|
|
@@ -83,7 +93,7 @@ export function persistSingleTaskUpdate(
|
|
|
83
93
|
}
|
|
84
94
|
if (recheckMtime !== baseMtime) {
|
|
85
95
|
baseMtime = recheckMtime;
|
|
86
|
-
continue
|
|
96
|
+
continue;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
// Final pre-write mtime check to catch any concurrent writer that completed
|
|
@@ -97,10 +107,10 @@ export function persistSingleTaskUpdate(
|
|
|
97
107
|
if (preWriteMtime !== baseMtime) {
|
|
98
108
|
// Another writer committed — retry
|
|
99
109
|
baseMtime = preWriteMtime;
|
|
100
|
-
continue
|
|
110
|
+
continue;
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
break
|
|
113
|
+
break;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
if (merged === undefined) {
|
|
@@ -109,7 +119,12 @@ export function persistSingleTaskUpdate(
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
try {
|
|
112
|
-
|
|
122
|
+
// F4: coalesced write inside the withRunLockSync critical section.
|
|
123
|
+
// The mtime CAS retry loop above still guards against concurrent
|
|
124
|
+
// non-coalesced writers; the flushPendingAtomicWrites() guard at
|
|
125
|
+
// the top of the retry loop ensures reads see any other coalesced
|
|
126
|
+
// writer's flushed-before-this-call state.
|
|
127
|
+
saveRunTasksCoalesced(manifest, merged);
|
|
113
128
|
} catch (err) {
|
|
114
129
|
logInternalError("persistSingleTaskUpdate", err);
|
|
115
130
|
throw err;
|
|
@@ -18,10 +18,8 @@ import type { TeamConfig } from "../teams/team-config.ts";
|
|
|
18
18
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
19
19
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
20
20
|
import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
|
|
21
|
-
import { mergeArtifacts } from "./team-runner-artifacts.ts";
|
|
22
21
|
import { buildSyntheticTerminalEvidence, CrewCancellationError, cancellationReasonFromSignal } from "./cancellation.ts";
|
|
23
|
-
import {
|
|
24
|
-
import { runCoalescedTaskGroup } from "./run-coalesced-task-group.ts";
|
|
22
|
+
import { buildDispatchUnits, planCoalescedGroups } from "./coalesce-tasks.ts";
|
|
25
23
|
import { resolveBatchConcurrency } from "./concurrency.ts";
|
|
26
24
|
import { readCrewAgents, saveCrewAgents } from "./crew-agent-records.ts";
|
|
27
25
|
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
@@ -37,6 +35,7 @@ import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts
|
|
|
37
35
|
import { buildRecoveryLedger, shouldRerunFailedTask } from "./recovery-recipes.ts";
|
|
38
36
|
import { DEFAULT_RETRY_POLICY, executeWithRetry, type RetryPolicy } from "./retry-executor.ts";
|
|
39
37
|
import { permissionForRole } from "./role-permission.ts";
|
|
38
|
+
import { runCoalescedTaskGroup } from "./run-coalesced-task-group.ts";
|
|
40
39
|
import { registerRunPromise, rejectRunPromise, resolveRunPromise } from "./run-tracker.ts";
|
|
41
40
|
import { resolveTaskRuntimeKind } from "./runtime-policy.ts";
|
|
42
41
|
import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
|
|
@@ -45,6 +44,7 @@ import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagRea
|
|
|
45
44
|
import { buildTaskGraphIndex, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
|
|
46
45
|
import { aggregateTaskOutputs } from "./task-output-context.ts";
|
|
47
46
|
import { runTeamTask } from "./task-runner.ts";
|
|
47
|
+
import { mergeArtifacts } from "./team-runner-artifacts.ts";
|
|
48
48
|
import { clearTrackedTaskUsage } from "./usage-tracker.ts";
|
|
49
49
|
import {
|
|
50
50
|
createWorkflowStateMachine,
|
|
@@ -71,9 +71,7 @@ export function snapshotManifests(cwd: string): Record<string, string> {
|
|
|
71
71
|
try {
|
|
72
72
|
const content = fs.readFileSync(abs);
|
|
73
73
|
snapshot[rel] = createHash("sha256").update(content).digest("hex");
|
|
74
|
-
} catch {
|
|
75
|
-
continue; // unreadable (permissions/race) — skip gracefully
|
|
76
|
-
}
|
|
74
|
+
} catch {}
|
|
77
75
|
}
|
|
78
76
|
return snapshot;
|
|
79
77
|
}
|
|
@@ -264,6 +264,19 @@ export const TeamToolParams = Type.Object({
|
|
|
264
264
|
// "description-only schema" strict-provider check.
|
|
265
265
|
Type.Any(),
|
|
266
266
|
),
|
|
267
|
+
analysis: Type.Optional(
|
|
268
|
+
Type.String({
|
|
269
|
+
maxLength: 100_000,
|
|
270
|
+
description:
|
|
271
|
+
"Inline analysis/context notes from the calling session. Persisted to artifacts/{runId}/shared/analysis.md (audit trail) and auto-injected into any workflow step declaring reads: analysis.md (e.g. builtin 'plan-execute'). Mutually exclusive with analysisPath. Ignored by goal-wrapped runs and chain dispatch in v1.",
|
|
272
|
+
}),
|
|
273
|
+
),
|
|
274
|
+
analysisPath: Type.Optional(
|
|
275
|
+
Type.String({
|
|
276
|
+
description:
|
|
277
|
+
"Path to an existing markdown analysis file (resolved within cwd). Copied into shared/analysis.md. Mutually exclusive with analysis.",
|
|
278
|
+
}),
|
|
279
|
+
),
|
|
267
280
|
focus: Type.Optional(
|
|
268
281
|
Type.String({
|
|
269
282
|
description:
|
|
@@ -385,4 +398,12 @@ export interface TeamToolParamsValue {
|
|
|
385
398
|
tokenBudget?: number;
|
|
386
399
|
/** Typed workflow arguments for .dwf.ts scripts, accessible via ctx.args<T>() (round-14 P1-5). */
|
|
387
400
|
args?: unknown;
|
|
401
|
+
/** Inline analysis/context notes from the calling session. Persisted to
|
|
402
|
+
* artifacts/{runId}/shared/analysis.md (audit trail) and auto-injected into
|
|
403
|
+
* any workflow step declaring reads: analysis.md (e.g. builtin 'plan-execute').
|
|
404
|
+
* Mutually exclusive with `analysisPath`. */
|
|
405
|
+
analysis?: string;
|
|
406
|
+
/** Path to an existing analysis file resolved within `cwd`. Copied into
|
|
407
|
+
* shared/analysis.md. Mutually exclusive with `analysis`. */
|
|
408
|
+
analysisPath?: string;
|
|
388
409
|
}
|