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
|
@@ -10,7 +10,6 @@ import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
|
10
10
|
import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
11
11
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
12
12
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
13
|
-
import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
|
|
14
13
|
import { assertCleanLeaderAsync, findGitRootAsync } from "../../worktree/worktree-manager.ts";
|
|
15
14
|
|
|
16
15
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
|
|
@@ -29,10 +28,10 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
|
|
|
29
28
|
return _cachedExecuteTeamRun(...args);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
|
|
33
31
|
import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
|
|
34
|
-
import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
|
|
35
32
|
import { appendEventAsync, readEvents } from "../../state/event-log.ts";
|
|
33
|
+
import type { RunMetrics } from "../../state/run-metrics.ts";
|
|
34
|
+
import type { RuntimeResolutionState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
36
35
|
import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
|
|
37
36
|
|
|
38
37
|
/**
|
|
@@ -197,6 +196,182 @@ function resolveAnalysisText(
|
|
|
197
196
|
return { text: sanitized, source: "inline" };
|
|
198
197
|
}
|
|
199
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Options for {@link formatRunResult} (EXT-2 refactor).
|
|
201
|
+
*/
|
|
202
|
+
export interface FormatRunResultOptions {
|
|
203
|
+
/** Task states for the completed run (used to render the per-task summary in "waited" mode). */
|
|
204
|
+
tasks: TeamTaskState[];
|
|
205
|
+
/** Pre-computed run metrics snapshot; omitted when the run finished before metrics were recorded. */
|
|
206
|
+
metrics: RunMetrics | undefined;
|
|
207
|
+
/** Goal text — truncated to 100 chars in the "waited" banner. */
|
|
208
|
+
goal: string;
|
|
209
|
+
/** Team name (for the banner). */
|
|
210
|
+
team: string;
|
|
211
|
+
/** Workflow name (for the scaffold banner). */
|
|
212
|
+
workflow: string;
|
|
213
|
+
/** Optional workspace identifier (session id / cwd) — surfaced in `details.data` for traceability. */
|
|
214
|
+
workspaceId?: string;
|
|
215
|
+
/** Runtime resolution — required for `mode: "scaffold"`, optional otherwise. */
|
|
216
|
+
runtime?: RuntimeResolutionState;
|
|
217
|
+
/** Output mode:
|
|
218
|
+
* - `"waited"` (default) — detailed per-task summary after `waitForRun` resolved
|
|
219
|
+
* (replaces the former async + foreground completion blocks).
|
|
220
|
+
* - `"scaffold"` — concise "Created pi-crew run" banner for runs that completed
|
|
221
|
+
* without worker execution (scaffold mode / `executeWorkers=false`).
|
|
222
|
+
*/
|
|
223
|
+
mode?: "waited" | "scaffold";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Format the text + {@link PiTeamsToolResult} for a completed team run.
|
|
228
|
+
*
|
|
229
|
+
* Replaces three near-identical inline blocks in `handleRun` (EXT-2 finding):
|
|
230
|
+
* 1. Async-spawned run completion (after `waitForRun`).
|
|
231
|
+
* 2. Foreground-run completion (after `waitForRun`).
|
|
232
|
+
* 3. Scaffold / no-executeWorkers completion (after `executeTeamRun` returns
|
|
233
|
+
* synchronously without launching workers).
|
|
234
|
+
*
|
|
235
|
+
* The output is byte-identical to the prior inline implementations when
|
|
236
|
+
* `workspaceId` is unset. When `workspaceId` is provided it is included in
|
|
237
|
+
* `details.data.workspaceId` so downstream consumers (TUI widgets, audit logs)
|
|
238
|
+
* can attribute the result to the originating session/cwd.
|
|
239
|
+
*/
|
|
240
|
+
function formatRunResult(manifest: TeamRunManifest, options: FormatRunResultOptions): PiTeamsToolResult {
|
|
241
|
+
const { tasks, metrics, goal, team, workflow, workspaceId, runtime, mode = "waited" } = options;
|
|
242
|
+
|
|
243
|
+
if (mode === "scaffold") {
|
|
244
|
+
// Scaffold / no-worker banner. `runtime` is required in this mode — callers
|
|
245
|
+
// must provide it so we can render the runtime kind + explanation.
|
|
246
|
+
const runtimeLine = runtime
|
|
247
|
+
? `Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`
|
|
248
|
+
: "Runtime: unknown";
|
|
249
|
+
const runtimeExplanation = !runtime
|
|
250
|
+
? ""
|
|
251
|
+
: runtime.kind === "child-process"
|
|
252
|
+
? "Child Pi worker execution is enabled by default; each task is launched as a separate Pi process. Set runtime.mode=scaffold or executeWorkers=false only for dry runs."
|
|
253
|
+
: runtime.kind === "live-session"
|
|
254
|
+
? "Experimental live-session worker execution was enabled."
|
|
255
|
+
: "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.";
|
|
256
|
+
const text = [
|
|
257
|
+
`Created pi-crew run ${manifest.runId}.`,
|
|
258
|
+
`Team: ${team}`,
|
|
259
|
+
`Workflow: ${workflow}`,
|
|
260
|
+
`Status: ${manifest.status}`,
|
|
261
|
+
`Tasks: ${tasks.length}`,
|
|
262
|
+
`State: ${manifest.stateRoot}`,
|
|
263
|
+
`Artifacts: ${manifest.artifactsRoot}`,
|
|
264
|
+
"",
|
|
265
|
+
runtimeLine,
|
|
266
|
+
runtimeExplanation,
|
|
267
|
+
]
|
|
268
|
+
.filter((line) => line !== "")
|
|
269
|
+
.join("\n");
|
|
270
|
+
const isError = manifest.status === "failed";
|
|
271
|
+
return result(
|
|
272
|
+
text,
|
|
273
|
+
{
|
|
274
|
+
action: "run",
|
|
275
|
+
status: isError ? "error" : "ok",
|
|
276
|
+
runId: manifest.runId,
|
|
277
|
+
artifactsRoot: manifest.artifactsRoot,
|
|
278
|
+
metrics,
|
|
279
|
+
...(workspaceId ? { data: { workspaceId } } : {}),
|
|
280
|
+
},
|
|
281
|
+
isError,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// mode === "waited" — detailed per-task summary.
|
|
286
|
+
const lines: string[] = [`pi-crew run ${manifest.status}: ${manifest.runId} (${team})`, `Goal: ${goal.slice(0, 100)}`];
|
|
287
|
+
if (metrics) {
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push(
|
|
290
|
+
`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (tasks.length > 0) {
|
|
295
|
+
// Read run-level summary artifact if present
|
|
296
|
+
let summaryContent: string | undefined;
|
|
297
|
+
const summaryArtifact = manifest.artifacts?.find((a: { kind?: string }) => a.kind === "summary");
|
|
298
|
+
if (summaryArtifact) {
|
|
299
|
+
try {
|
|
300
|
+
const sumPath = path.join(manifest.artifactsRoot, summaryArtifact.path);
|
|
301
|
+
summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
|
|
302
|
+
} catch {
|
|
303
|
+
/* summary unavailable */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const taskLines: string[] = [];
|
|
308
|
+
let failedCount = 0;
|
|
309
|
+
const failedIds: string[] = [];
|
|
310
|
+
for (const task of tasks) {
|
|
311
|
+
let resultExcerpt = "";
|
|
312
|
+
if (task.resultArtifact?.path) {
|
|
313
|
+
try {
|
|
314
|
+
// H-3 fix (code-review 2026-06-23): resolve the result artifact path
|
|
315
|
+
// inside artifactsRoot via the project's safe-path primitive. Rejects
|
|
316
|
+
// absolute paths (/etc/passwd) and ../ traversal that the old
|
|
317
|
+
// path.isAbsolute shortcut + bare path.join allowed.
|
|
318
|
+
const resPath = resolveRealContainedPath(manifest.artifactsRoot, task.resultArtifact.path);
|
|
319
|
+
resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
|
|
320
|
+
} catch {
|
|
321
|
+
resultExcerpt = "(result unavailable)";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const shortResult = resultExcerpt.slice(0, 500);
|
|
325
|
+
const statusTag = task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
|
|
326
|
+
taskLines.push(
|
|
327
|
+
`- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
328
|
+
);
|
|
329
|
+
if (task.status === "failed" || task.status === "needs_attention") {
|
|
330
|
+
failedCount++;
|
|
331
|
+
failedIds.push(task.id);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push(`Tasks (${tasks.length}):`);
|
|
337
|
+
lines.push(...taskLines);
|
|
338
|
+
|
|
339
|
+
if (summaryContent) {
|
|
340
|
+
lines.push("");
|
|
341
|
+
lines.push("Summary:");
|
|
342
|
+
lines.push(summaryContent.slice(0, 2000));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (failedCount === 0) {
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push("All tasks completed successfully.");
|
|
348
|
+
} else {
|
|
349
|
+
lines.push("");
|
|
350
|
+
lines.push(`${failedCount} task(s) failed: ${failedIds.join(", ")}. Consider retrying.`);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
lines.push(
|
|
354
|
+
manifest.status === "completed"
|
|
355
|
+
? "Run completed with no task results."
|
|
356
|
+
: `The run ended with status: ${manifest.status}. Check the run artifacts for details.`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const runFailed = manifest.status === "failed" || manifest.status === "blocked";
|
|
361
|
+
return result(
|
|
362
|
+
lines.join("\n"),
|
|
363
|
+
{
|
|
364
|
+
action: "run",
|
|
365
|
+
status: runFailed ? "error" : "ok",
|
|
366
|
+
runId: manifest.runId,
|
|
367
|
+
artifactsRoot: manifest.artifactsRoot,
|
|
368
|
+
metrics,
|
|
369
|
+
...(workspaceId ? { data: { workspaceId } } : {}),
|
|
370
|
+
},
|
|
371
|
+
runFailed,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
200
375
|
export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
201
376
|
// CHAIN DISPATCH: runs before goal validation since a chain has no top-level
|
|
202
377
|
// goal. The injected handleRun reference breaks the run.ts ↔ chain-dispatch.ts
|
|
@@ -319,6 +494,17 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
319
494
|
// 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.
|
|
320
495
|
const { expandParallelResearchWorkflow: expandParallelResearch } = await import("../../runtime/parallel-research.ts");
|
|
321
496
|
const workflow = directAgent ? baseWorkflow : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
|
|
497
|
+
const isDynamicWorkflow =
|
|
498
|
+
!directAgent && (workflow as import("../../workflows/workflow-config.ts").WorkflowConfig).runtime === "dynamic";
|
|
499
|
+
const effectiveRunKind = isDynamicWorkflow ? params.runKind : undefined;
|
|
500
|
+
if (params.runKind !== undefined && !isDynamicWorkflow) {
|
|
501
|
+
logInternalError(
|
|
502
|
+
"team-tool.run.runKindIgnored",
|
|
503
|
+
new Error(`Ignoring runKind='${params.runKind}' because workflow '${workflow.name}' is not dynamic.`),
|
|
504
|
+
undefined,
|
|
505
|
+
"warn",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
322
508
|
|
|
323
509
|
// PREFLIGHT (advisory only, since v0.9.15): classify workflow topology and emit
|
|
324
510
|
// informational notes per the rule in .crew/knowledge.md "pi-crew USAGE THRESHOLD
|
|
@@ -396,7 +582,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
396
582
|
goal,
|
|
397
583
|
workspaceMode: params.workspaceMode,
|
|
398
584
|
ownerSessionId: ctx.sessionId,
|
|
399
|
-
runKind:
|
|
585
|
+
runKind: effectiveRunKind,
|
|
400
586
|
args: params.args,
|
|
401
587
|
});
|
|
402
588
|
const goalArtifact = writeArtifact(paths.artifactsRoot, {
|
|
@@ -431,7 +617,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
431
617
|
// run it via runDynamicWorkflow instead of the static executeTeamRun path. The script
|
|
432
618
|
// orchestrates subagents via ctx.agent(); only ctx.setResult() reaches the main context.
|
|
433
619
|
// Placed AFTER manifest creation so runId/paths/artifactsRoot are available.
|
|
434
|
-
if (
|
|
620
|
+
if (isDynamicWorkflow) {
|
|
621
|
+
console.warn(
|
|
622
|
+
`[pi-crew SECURITY] Dynamic workflow '${workflow.name}' executes as trusted Node.js code with full process/require/import access; run only reviewed .dwf.ts files.`,
|
|
623
|
+
);
|
|
435
624
|
// LAZY: defer dynamic import of ../../runtime/dynamic-workflow-runner.ts to its call site.
|
|
436
625
|
const { runDynamicWorkflow } = await import("../../runtime/dynamic-workflow-runner.ts");
|
|
437
626
|
// Re-synthesize a dynamic-team (§0c C9) for role resolution.
|
|
@@ -658,97 +847,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
658
847
|
// Wait for the async run to complete and return actual results.
|
|
659
848
|
try {
|
|
660
849
|
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`,
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (completed.tasks.length > 0) {
|
|
674
|
-
// Read run-level summary artifact if present
|
|
675
|
-
let summaryContent: string | undefined;
|
|
676
|
-
const summaryArtifact = completed.manifest.artifacts?.find((a: { kind?: string }) => a.kind === "summary");
|
|
677
|
-
if (summaryArtifact) {
|
|
678
|
-
try {
|
|
679
|
-
const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
|
|
680
|
-
summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
|
|
681
|
-
} catch {
|
|
682
|
-
/* summary unavailable */
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const taskLines: string[] = [];
|
|
687
|
-
let failedCount = 0;
|
|
688
|
-
const failedIds: string[] = [];
|
|
689
|
-
for (const task of completed.tasks) {
|
|
690
|
-
let resultExcerpt = "";
|
|
691
|
-
if (task.resultArtifact?.path) {
|
|
692
|
-
try {
|
|
693
|
-
// H-3 fix (code-review 2026-06-23): resolve the result artifact path
|
|
694
|
-
// inside artifactsRoot via the project's safe-path primitive. Rejects
|
|
695
|
-
// absolute paths (/etc/passwd) and ../ traversal that the old
|
|
696
|
-
// path.isAbsolute shortcut + bare path.join allowed.
|
|
697
|
-
const resPath = resolveRealContainedPath(completed.manifest.artifactsRoot, task.resultArtifact.path);
|
|
698
|
-
resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
|
|
699
|
-
} catch {
|
|
700
|
-
resultExcerpt = "(result unavailable)";
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
const shortResult = resultExcerpt.slice(0, 500);
|
|
704
|
-
const statusTag =
|
|
705
|
-
task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
|
|
706
|
-
taskLines.push(
|
|
707
|
-
`- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
708
|
-
);
|
|
709
|
-
if (task.status === "failed" || task.status === "needs_attention") {
|
|
710
|
-
failedCount++;
|
|
711
|
-
failedIds.push(task.id);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
lines.push("");
|
|
716
|
-
lines.push(`Tasks (${completed.tasks.length}):`);
|
|
717
|
-
lines.push(...taskLines);
|
|
718
|
-
|
|
719
|
-
if (summaryContent) {
|
|
720
|
-
lines.push("");
|
|
721
|
-
lines.push("Summary:");
|
|
722
|
-
lines.push(summaryContent.slice(0, 2000));
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (failedCount === 0) {
|
|
726
|
-
lines.push("");
|
|
727
|
-
lines.push("All tasks completed successfully.");
|
|
728
|
-
} else {
|
|
729
|
-
lines.push("");
|
|
730
|
-
lines.push(`${failedCount} task(s) failed: ${failedIds.join(", ")}. Consider retrying.`);
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
lines.push(
|
|
734
|
-
completed.manifest.status === "completed"
|
|
735
|
-
? "Run completed with no task results."
|
|
736
|
-
: `The run ended with status: ${completed.manifest.status}. Check the run artifacts for details.`,
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
741
|
-
return result(
|
|
742
|
-
lines.join("\n"),
|
|
743
|
-
{
|
|
744
|
-
action: "run",
|
|
745
|
-
status: runFailed ? "error" : "ok",
|
|
746
|
-
runId: completed.manifest.runId,
|
|
747
|
-
artifactsRoot: completed.manifest.artifactsRoot,
|
|
748
|
-
metrics,
|
|
749
|
-
},
|
|
750
|
-
runFailed,
|
|
751
|
-
);
|
|
850
|
+
return formatRunResult(completed.manifest, {
|
|
851
|
+
tasks: completed.tasks,
|
|
852
|
+
metrics: collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId),
|
|
853
|
+
goal,
|
|
854
|
+
team: team.name,
|
|
855
|
+
workflow: workflow.name,
|
|
856
|
+
workspaceId: ctx.sessionId ?? ctx.cwd,
|
|
857
|
+
});
|
|
752
858
|
} catch (waitError: unknown) {
|
|
753
859
|
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
754
860
|
return result(
|
|
@@ -855,97 +961,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
855
961
|
// Wait for the foreground run to complete and return actual results.
|
|
856
962
|
try {
|
|
857
963
|
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`,
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (completed.tasks.length > 0) {
|
|
871
|
-
// Read run-level summary artifact if present
|
|
872
|
-
let summaryContent: string | undefined;
|
|
873
|
-
const summaryArtifact = completed.manifest.artifacts?.find((a: { kind?: string }) => a.kind === "summary");
|
|
874
|
-
if (summaryArtifact) {
|
|
875
|
-
try {
|
|
876
|
-
const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
|
|
877
|
-
summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
|
|
878
|
-
} catch {
|
|
879
|
-
/* summary unavailable */
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const taskLines: string[] = [];
|
|
884
|
-
let failedCount = 0;
|
|
885
|
-
const failedIds: string[] = [];
|
|
886
|
-
for (const task of completed.tasks) {
|
|
887
|
-
let resultExcerpt = "";
|
|
888
|
-
if (task.resultArtifact?.path) {
|
|
889
|
-
try {
|
|
890
|
-
// H-3 fix (code-review 2026-06-23): resolve the result artifact path
|
|
891
|
-
// inside artifactsRoot via the project's safe-path primitive. Rejects
|
|
892
|
-
// absolute paths (/etc/passwd) and ../ traversal that the old
|
|
893
|
-
// path.isAbsolute shortcut + bare path.join allowed.
|
|
894
|
-
const resPath = resolveRealContainedPath(completed.manifest.artifactsRoot, task.resultArtifact.path);
|
|
895
|
-
resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
|
|
896
|
-
} catch {
|
|
897
|
-
resultExcerpt = "(result unavailable)";
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
const shortResult = resultExcerpt.slice(0, 500);
|
|
901
|
-
const statusTag =
|
|
902
|
-
task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
|
|
903
|
-
taskLines.push(
|
|
904
|
-
`- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
905
|
-
);
|
|
906
|
-
if (task.status === "failed" || task.status === "needs_attention") {
|
|
907
|
-
failedCount++;
|
|
908
|
-
failedIds.push(task.id);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
lines.push("");
|
|
913
|
-
lines.push(`Tasks (${completed.tasks.length}):`);
|
|
914
|
-
lines.push(...taskLines);
|
|
915
|
-
|
|
916
|
-
if (summaryContent) {
|
|
917
|
-
lines.push("");
|
|
918
|
-
lines.push("Summary:");
|
|
919
|
-
lines.push(summaryContent.slice(0, 2000));
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (failedCount === 0) {
|
|
923
|
-
lines.push("");
|
|
924
|
-
lines.push("All tasks completed successfully.");
|
|
925
|
-
} else {
|
|
926
|
-
lines.push("");
|
|
927
|
-
lines.push(`${failedCount} task(s) failed: ${failedIds.join(", ")}. Consider retrying.`);
|
|
928
|
-
}
|
|
929
|
-
} else {
|
|
930
|
-
lines.push(
|
|
931
|
-
completed.manifest.status === "completed"
|
|
932
|
-
? "Run completed with no task results."
|
|
933
|
-
: `The run ended with status: ${completed.manifest.status}. Check the run artifacts for details.`,
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
938
|
-
return result(
|
|
939
|
-
lines.join("\n"),
|
|
940
|
-
{
|
|
941
|
-
action: "run",
|
|
942
|
-
status: runFailed ? "error" : "ok",
|
|
943
|
-
runId: completed.manifest.runId,
|
|
944
|
-
artifactsRoot: completed.manifest.artifactsRoot,
|
|
945
|
-
metrics,
|
|
946
|
-
},
|
|
947
|
-
runFailed,
|
|
948
|
-
);
|
|
964
|
+
return formatRunResult(completed.manifest, {
|
|
965
|
+
tasks: completed.tasks,
|
|
966
|
+
metrics: collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId),
|
|
967
|
+
goal,
|
|
968
|
+
team: team.name,
|
|
969
|
+
workflow: workflow.name,
|
|
970
|
+
workspaceId: ctx.sessionId ?? ctx.cwd,
|
|
971
|
+
});
|
|
949
972
|
} catch (waitError: unknown) {
|
|
950
973
|
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
951
974
|
return result(
|
|
@@ -998,31 +1021,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
998
1021
|
} finally {
|
|
999
1022
|
unregisterActiveRun(updatedManifest.runId);
|
|
1000
1023
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
"",
|
|
1010
|
-
|
|
1011
|
-
runtime.kind === "child-process"
|
|
1012
|
-
? "Child Pi worker execution is enabled by default; each task is launched as a separate Pi process. Set runtime.mode=scaffold or executeWorkers=false only for dry runs."
|
|
1013
|
-
: runtime.kind === "live-session"
|
|
1014
|
-
? "Experimental live-session worker execution was enabled."
|
|
1015
|
-
: "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
|
|
1016
|
-
].join("\n");
|
|
1017
|
-
return result(
|
|
1018
|
-
text,
|
|
1019
|
-
{
|
|
1020
|
-
action: "run",
|
|
1021
|
-
status: executed.manifest.status === "failed" ? "error" : "ok",
|
|
1022
|
-
runId: executed.manifest.runId,
|
|
1023
|
-
artifactsRoot: executed.manifest.artifactsRoot,
|
|
1024
|
-
metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId),
|
|
1025
|
-
},
|
|
1026
|
-
executed.manifest.status === "failed",
|
|
1027
|
-
);
|
|
1024
|
+
return formatRunResult(executed.manifest, {
|
|
1025
|
+
tasks: executed.tasks,
|
|
1026
|
+
metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId),
|
|
1027
|
+
goal,
|
|
1028
|
+
team: team.name,
|
|
1029
|
+
workflow: workflow.name,
|
|
1030
|
+
workspaceId: ctx.cwd,
|
|
1031
|
+
runtime: runtimeResolutionState(runtime),
|
|
1032
|
+
mode: "scaffold",
|
|
1033
|
+
});
|
|
1028
1034
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
-
import {
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
19
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
20
20
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
21
21
|
import { packageRoot, projectCrewRoot, userPiRoot } from "../../utils/paths.ts";
|
|
@@ -8,13 +8,14 @@ import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/conf
|
|
|
8
8
|
import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
|
|
9
9
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
10
10
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
11
|
+
import { TEAM_TERMINAL_TASK_STATUSES } from "../state/contracts.ts";
|
|
11
12
|
import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
|
|
12
13
|
import { withRunLock } from "../state/locks.ts";
|
|
13
14
|
import { replayPendingMailboxMessages } from "../state/mailbox.ts";
|
|
14
15
|
import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
15
16
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
16
17
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
17
|
-
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
18
|
+
import { assertSafePathId, resolveContainedPath } from "../utils/safe-paths.ts";
|
|
18
19
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
19
20
|
import { piTeamsHelp } from "./help.ts";
|
|
20
21
|
import { handleCreate, handleDelete, handleUpdate } from "./management.ts";
|
|
@@ -250,8 +251,8 @@ async function recoverCheckpointedTasks(
|
|
|
250
251
|
if (files.length > 0) {
|
|
251
252
|
// Sort by attempt index descending to get the most recent
|
|
252
253
|
files.sort((a, b) => {
|
|
253
|
-
const idxA = parseInt(a.match(/\.attempt-(\d+)\./)?.[1] ?? "0");
|
|
254
|
-
const idxB = parseInt(b.match(/\.attempt-(\d+)\./)?.[1] ?? "0");
|
|
254
|
+
const idxA = parseInt(a.match(/\.attempt-(\d+)\./)?.[1] ?? "0", 10);
|
|
255
|
+
const idxB = parseInt(b.match(/\.attempt-(\d+)\./)?.[1] ?? "0", 10);
|
|
255
256
|
return idxB - idxA;
|
|
256
257
|
});
|
|
257
258
|
transcriptPath = path.join(transcriptsDir, files[0]);
|
|
@@ -501,6 +502,10 @@ export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTe
|
|
|
501
502
|
const task = loaded.tasks.find((t) => t.id === taskId);
|
|
502
503
|
if (!task) return result(`Task '${taskId}' not found`, { action: "steer", status: "error" }, true);
|
|
503
504
|
if (!task.pendingSteers) task.pendingSteers = [];
|
|
505
|
+
// T-S1: do not allow steering a task that has already reached a terminal status.
|
|
506
|
+
if (TEAM_TERMINAL_TASK_STATUSES.has(task.status)) {
|
|
507
|
+
return result(`Task '${taskId}' is ${task.status}; cannot steer.`, { action: "steer", status: "error" }, true);
|
|
508
|
+
}
|
|
504
509
|
// HIGH-04: Cap pendingSteers array to prevent unbounded memory growth
|
|
505
510
|
const MAX_PENDING_STEERS = 100;
|
|
506
511
|
if (task.pendingSteers.length >= MAX_PENDING_STEERS) {
|
|
@@ -523,10 +528,12 @@ export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTe
|
|
|
523
528
|
try {
|
|
524
529
|
const steeringDir = `${loaded.manifest.artifactsRoot}/steering`;
|
|
525
530
|
fs.mkdirSync(steeringDir, { recursive: true });
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)
|
|
531
|
+
// AUDIT-08 defense-in-depth: validate the steering-file path is contained
|
|
532
|
+
// within steeringDir. taskId is currently sanitized via createTaskId, but this
|
|
533
|
+
// guards against future changes to task-id generation (e.g. if it ever
|
|
534
|
+
// accepted user input).
|
|
535
|
+
const safeSteeringPath = resolveContainedPath(steeringDir, `${taskId}.jsonl`);
|
|
536
|
+
fs.appendFileSync(safeSteeringPath, JSON.stringify({ type: "steer", message, ts: new Date().toISOString() }) + "\n");
|
|
530
537
|
} catch {
|
|
531
538
|
// Best-effort: file write failure doesn't block the steer from pending array
|
|
532
539
|
}
|
|
@@ -1042,19 +1049,65 @@ function getCrewGlobalRegistry(): CrewRegistry | undefined {
|
|
|
1042
1049
|
return (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as CrewRegistry | undefined;
|
|
1043
1050
|
}
|
|
1044
1051
|
|
|
1045
|
-
/**
|
|
1046
|
-
|
|
1047
|
-
|
|
1052
|
+
/** Manifest cache shape needed to construct the global registry's read-side. */
|
|
1053
|
+
interface ManifestCacheForRegistry {
|
|
1054
|
+
get: (runId: string) => TeamRunManifest | undefined;
|
|
1055
|
+
list: (limit: number) => TeamRunManifest[];
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Build and install the global CrewRegistry singleton in a single atomic step.
|
|
1060
|
+
*
|
|
1061
|
+
* EXT-7 (Round 3): The previous design called `installCrewGlobalRegistry()` to
|
|
1062
|
+
* install stubs, then patched the manifest-backed methods asynchronously inside
|
|
1063
|
+
* `register.ts`. That left a window where cross-extension consumers could observe
|
|
1064
|
+
* the stub object on `globalThis[Symbol.for("pi-crew:registry")]`. By taking the
|
|
1065
|
+
* real dependencies up-front, we install the registry once with no stub phase —
|
|
1066
|
+
* callers see either no registry (pre-init) or the fully-real registry.
|
|
1067
|
+
*/
|
|
1068
|
+
export function installCrewGlobalRegistry(deps?: { manifestCache: ManifestCacheForRegistry; cwdProvider: () => string }): void {
|
|
1069
|
+
const manifestCache = deps?.manifestCache;
|
|
1070
|
+
const cwdProvider = deps?.cwdProvider ?? ((): string => process.cwd());
|
|
1071
|
+
const registry: CrewRegistry = {
|
|
1048
1072
|
version: 2,
|
|
1049
|
-
getRecord: (runId: string) =>
|
|
1050
|
-
listRuns: () =>
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1073
|
+
getRecord: (runId: string) => manifestCache?.get(runId),
|
|
1074
|
+
listRuns: () =>
|
|
1075
|
+
manifestCache
|
|
1076
|
+
? manifestCache.list(100).map((m) => ({ runId: m.runId, status: m.status, goal: m.goal }))
|
|
1077
|
+
: ([] as Array<{ runId: string; status: string; goal: string }>),
|
|
1078
|
+
appendEvent: (runId: string, event: Record<string, unknown>) => {
|
|
1079
|
+
if (!manifestCache) return;
|
|
1080
|
+
const manifest = manifestCache.get(runId);
|
|
1081
|
+
if (manifest) {
|
|
1082
|
+
// LAZY: event-log is already loaded at module top, so use the
|
|
1083
|
+
// pre-resolved appendEventFireAndForget instead of re-importing.
|
|
1084
|
+
appendEventFireAndForget(manifest.eventsPath, event as Parameters<typeof appendEventFireAndForget>[1]);
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
waitForAll: async (runId: string) => {
|
|
1088
|
+
if (!manifestCache) return;
|
|
1089
|
+
// LAZY: state-store is already loaded at module top; use the pre-resolved loadRunManifestById.
|
|
1090
|
+
const check = (): boolean => {
|
|
1091
|
+
const loaded = loadRunManifestById(cwdProvider(), runId);
|
|
1092
|
+
if (!loaded) return true;
|
|
1093
|
+
return !loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
1094
|
+
};
|
|
1095
|
+
while (!check()) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1096
|
+
},
|
|
1097
|
+
hasRunning: (runId: string) => {
|
|
1098
|
+
if (!manifestCache) return false;
|
|
1099
|
+
const manifest = manifestCache.get(runId);
|
|
1100
|
+
if (!manifest) return false;
|
|
1101
|
+
// LAZY: state-store is already loaded at module top; use the pre-resolved loadRunManifestById.
|
|
1102
|
+
const loaded = loadRunManifestById(cwdProvider(), runId);
|
|
1103
|
+
if (!loaded) return false;
|
|
1104
|
+
return loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
1105
|
+
},
|
|
1054
1106
|
registerAgent: registerDynamicAgent,
|
|
1055
1107
|
unregisterAgent: unregisterDynamicAgent,
|
|
1056
1108
|
listDynamicAgents,
|
|
1057
|
-
}
|
|
1109
|
+
};
|
|
1110
|
+
registerCrewGlobalRegistry(registry);
|
|
1058
1111
|
}
|
|
1059
1112
|
|
|
1060
1113
|
/** Remove the global CrewRegistry singleton. Call during session cleanup. */
|
package/src/hooks/registry.ts
CHANGED
|
@@ -141,8 +141,13 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
if (result.outcome === "modify" && result.data) {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// EXT-13 (Round 3): Deep-clone hook result.data so subsequent hooks
|
|
145
|
+
// (or the same hook if reused) cannot mutate shared ctx via shared
|
|
146
|
+
// nested-object references. sanitizeMergeData already produces a
|
|
147
|
+
// fresh top-level object, but nested values would still alias.
|
|
148
|
+
const clonedData = sanitizeMergeData(result.data);
|
|
149
|
+
Object.assign(ctx, clonedData);
|
|
150
|
+
capturedModifications = clonedData;
|
|
146
151
|
}
|
|
147
152
|
} catch (error) {
|
|
148
153
|
const message = sanitizeErrorMessage(error instanceof Error ? error.message : String(error));
|