pi-crew 0.9.68 → 0.10.2
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 +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
//
|
|
15
15
|
// `__test__parseAdaptivePlan` and `__test__repairAdaptivePlan` are re-exported
|
|
16
16
|
// from team-runner.ts so existing test imports keep working.
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
17
19
|
import * as fs from "node:fs";
|
|
20
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
18
21
|
import { appendEventAsync, appendEventFireAndForget } from "../../state/event-log/event-log.ts";
|
|
19
22
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
23
|
+
import { appendPlanRevision, getCurrentPlanRecord } from "../../state/stores/plan-store.ts";
|
|
24
|
+
import { saveRunManifestAsync, saveRunTasksAsync } from "../../state/stores/state-store.ts";
|
|
25
|
+
import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
22
26
|
import type { TeamConfig } from "../../teams/team-config.ts";
|
|
23
27
|
import type { WorkflowConfig, WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
24
28
|
import { refreshTaskGraphQueues } from "../scheduling/task-graph-scheduler.ts";
|
|
@@ -39,7 +43,15 @@ export interface AdaptivePlan {
|
|
|
39
43
|
phases: AdaptivePlanPhase[];
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
/**
|
|
47
|
+
* T2/R4 (ADR-4 §5): the cap is PER-PHASE, not global. Pre-v2 this was a
|
|
48
|
+
* global flatten cap (MAX_ADAPTIVE_TASKS = 12) applied across the whole plan —
|
|
49
|
+
* a 3-phase plan may now schedule up to 3 x this across its life, bounded and
|
|
50
|
+
* visible per phase in the persisted PlanRecord. Deliberately a module
|
|
51
|
+
* constant, NOT a config key (the `adaptive` config section belongs to
|
|
52
|
+
* T3/WP-5 — review finding F3 of the ADR round).
|
|
53
|
+
*/
|
|
54
|
+
const ADAPTIVE_MAX_TASKS_PER_PHASE = 12;
|
|
43
55
|
|
|
44
56
|
export function slug(value: string): string {
|
|
45
57
|
return (
|
|
@@ -124,7 +136,6 @@ export function parseAdaptivePlan(text: string, allowedRoles: string[]): Adaptiv
|
|
|
124
136
|
if (!phasesRaw) return undefined;
|
|
125
137
|
const allowed = new Set(allowedRoles);
|
|
126
138
|
const phases: AdaptivePlanPhase[] = [];
|
|
127
|
-
let total = 0;
|
|
128
139
|
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
129
140
|
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
|
|
130
141
|
const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
|
|
@@ -139,13 +150,12 @@ export function parseAdaptivePlan(text: string, allowedRoles: string[]): Adaptiv
|
|
|
139
150
|
};
|
|
140
151
|
if (typeof taskObj.role !== "string" || !allowed.has(taskObj.role)) return undefined;
|
|
141
152
|
if (typeof taskObj.task !== "string" || !taskObj.task.trim()) return undefined;
|
|
142
|
-
if (
|
|
153
|
+
if (tasks.length >= ADAPTIVE_MAX_TASKS_PER_PHASE) return undefined; // per-phase (ADR-4 §5)
|
|
143
154
|
tasks.push({
|
|
144
155
|
role: taskObj.role,
|
|
145
156
|
title: typeof taskObj.title === "string" ? taskObj.title : undefined,
|
|
146
157
|
task: taskObj.task.trim(),
|
|
147
158
|
});
|
|
148
|
-
total++;
|
|
149
159
|
}
|
|
150
160
|
phases.push({
|
|
151
161
|
name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`,
|
|
@@ -289,7 +299,6 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
|
|
|
289
299
|
if (!phasesRaw) return { repaired: false, reason: "missing-phases" };
|
|
290
300
|
const allowed = new Set(allowedRoles);
|
|
291
301
|
const phases: AdaptivePlanPhase[] = [];
|
|
292
|
-
let total = 0;
|
|
293
302
|
let repaired = salvageUsed || raw !== closeResult.text;
|
|
294
303
|
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
295
304
|
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
|
|
@@ -297,7 +306,8 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
|
|
|
297
306
|
if (!Array.isArray(phaseObj.tasks)) continue;
|
|
298
307
|
const tasks: AdaptivePlanTask[] = [];
|
|
299
308
|
for (const taskRaw of phaseObj.tasks) {
|
|
300
|
-
if (
|
|
309
|
+
if (tasks.length >= ADAPTIVE_MAX_TASKS_PER_PHASE) {
|
|
310
|
+
// Per-phase cap (ADR-4 §5): truncate THIS phase, keep later phases.
|
|
301
311
|
repaired = true;
|
|
302
312
|
break;
|
|
303
313
|
}
|
|
@@ -321,14 +331,12 @@ export function repairAdaptivePlan(text: string, allowedRoles: string[]): { plan
|
|
|
321
331
|
title: typeof taskObj.title === "string" ? taskObj.title : undefined,
|
|
322
332
|
task: taskText,
|
|
323
333
|
});
|
|
324
|
-
total++;
|
|
325
334
|
}
|
|
326
335
|
if (tasks.length)
|
|
327
336
|
phases.push({
|
|
328
337
|
name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`,
|
|
329
338
|
tasks,
|
|
330
339
|
});
|
|
331
|
-
if (total >= MAX_ADAPTIVE_TASKS) break;
|
|
332
340
|
}
|
|
333
341
|
return phases.length
|
|
334
342
|
? {
|
|
@@ -360,6 +368,11 @@ export interface InjectAdaptivePlanInput {
|
|
|
360
368
|
tasks: TeamTaskState[];
|
|
361
369
|
workflow: WorkflowConfig;
|
|
362
370
|
team: TeamConfig;
|
|
371
|
+
/** False for scaffold (dry-run) runs: the planner never executes, so there
|
|
372
|
+
* is no plan to inject — and no plan to be missing. Reports neutral
|
|
373
|
+
* (neither injected nor missing) so the preview completes instead of
|
|
374
|
+
* blocking on a plan a dry-run can never produce. */
|
|
375
|
+
executeWorkers?: boolean;
|
|
363
376
|
}
|
|
364
377
|
|
|
365
378
|
export interface InjectAdaptivePlanResult {
|
|
@@ -369,8 +382,18 @@ export interface InjectAdaptivePlanResult {
|
|
|
369
382
|
missingPlan: boolean;
|
|
370
383
|
}
|
|
371
384
|
|
|
385
|
+
/**
|
|
386
|
+
* True for workflows whose `assess` step emits a plan the runtime injects as
|
|
387
|
+
* concrete tasks. The `implementation` workflow is adaptive by name (historic
|
|
388
|
+
* hard-code, kept for backward compat); any other workflow opts in via
|
|
389
|
+
* frontmatter `adaptive: true`.
|
|
390
|
+
*/
|
|
391
|
+
export function isAdaptiveWorkflow(workflow: WorkflowConfig): boolean {
|
|
392
|
+
return workflow.adaptive === true || workflow.name === "implementation";
|
|
393
|
+
}
|
|
394
|
+
|
|
372
395
|
export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput): Promise<InjectAdaptivePlanResult> {
|
|
373
|
-
if (input.workflow.
|
|
396
|
+
if (!isAdaptiveWorkflow(input.workflow) || input.executeWorkers === false)
|
|
374
397
|
return {
|
|
375
398
|
tasks: input.tasks,
|
|
376
399
|
workflow: input.workflow,
|
|
@@ -409,6 +432,9 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
409
432
|
};
|
|
410
433
|
}
|
|
411
434
|
const assessTask = completedAssess;
|
|
435
|
+
// Review R5: manifest variant tracked through the repair path — the plan
|
|
436
|
+
// save below must not clobber the repair artifact descriptor.
|
|
437
|
+
let manifestBase = input.manifest;
|
|
412
438
|
const resultPath = completedAssess.resultArtifact.path;
|
|
413
439
|
let text = "";
|
|
414
440
|
try {
|
|
@@ -431,7 +457,7 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
431
457
|
let plan = parseAdaptivePlan(text, allowedRoles);
|
|
432
458
|
if (!plan) {
|
|
433
459
|
const repair =
|
|
434
|
-
|
|
460
|
+
getCrewEnv("PI_CREW_ADAPTIVE_REPAIR") === "0" || getCrewEnv("PI_TEAMS_ADAPTIVE_REPAIR") === "0"
|
|
435
461
|
? { repaired: false, reason: "disabled" }
|
|
436
462
|
: repairAdaptivePlan(text, allowedRoles);
|
|
437
463
|
if (repair.plan) {
|
|
@@ -442,11 +468,12 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
442
468
|
producer: assessTask.id,
|
|
443
469
|
content: `${JSON.stringify({ reason: repair.reason, phases: repair.plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) }, null, 2)}\n`,
|
|
444
470
|
});
|
|
445
|
-
|
|
471
|
+
manifestBase = {
|
|
446
472
|
...input.manifest,
|
|
447
473
|
updatedAt: new Date().toISOString(),
|
|
448
474
|
artifacts: [...input.manifest.artifacts, repairArtifact],
|
|
449
|
-
}
|
|
475
|
+
};
|
|
476
|
+
await saveRunManifestAsync(manifestBase);
|
|
450
477
|
appendEventFireAndForget(input.manifest.eventsPath, {
|
|
451
478
|
type: "adaptive.plan_repaired",
|
|
452
479
|
runId: input.manifest.runId,
|
|
@@ -478,14 +505,41 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
478
505
|
}
|
|
479
506
|
const steps: WorkflowStep[] = [];
|
|
480
507
|
const tasks: TeamTaskState[] = [];
|
|
508
|
+
// T2/R4 (ADR-4 §6 producer 2): item ids are stable across revisions —
|
|
509
|
+
// phase/task POSITION based, independent of role renames.
|
|
510
|
+
const planPhases: PlanRecord["phases"] = [];
|
|
511
|
+
const planItems: PlanRecord["items"] = [];
|
|
481
512
|
let previousStepIds = ["assess"];
|
|
482
513
|
let counter = 0;
|
|
483
514
|
for (const [phaseIndex, phase] of plan.phases.entries()) {
|
|
484
515
|
const currentStepIds: string[] = [];
|
|
516
|
+
const phaseItemIds: string[] = [];
|
|
485
517
|
for (const [taskIndex, planned] of phase.tasks.entries()) {
|
|
486
518
|
counter++;
|
|
519
|
+
const itemId = `adaptive-p${phaseIndex + 1}-t${taskIndex + 1}`;
|
|
487
520
|
const stepId = `adaptive-${phaseIndex + 1}-${taskIndex + 1}-${slug(planned.role)}`;
|
|
488
521
|
const taskId = `adaptive-${String(counter).padStart(2, "0")}-${slug(planned.role)}`;
|
|
522
|
+
// Same display contract as static workflow steps: the planner's
|
|
523
|
+
// title (or the task text's first line) names the task; the full
|
|
524
|
+
// task text is the description for detail surfaces. Never the bare
|
|
525
|
+
// stepId — the task list is the plan, not an id roster.
|
|
526
|
+
const firstLine =
|
|
527
|
+
planned.task
|
|
528
|
+
.split("\n")
|
|
529
|
+
.map((line) => line.trim())
|
|
530
|
+
.filter(Boolean)
|
|
531
|
+
.find((line) => !line.startsWith("#")) ?? "";
|
|
532
|
+
const title = (planned.title ?? firstLine ?? stepId).slice(0, 120);
|
|
533
|
+
phaseItemIds.push(itemId);
|
|
534
|
+
planItems.push({
|
|
535
|
+
id: itemId,
|
|
536
|
+
ref: stepId,
|
|
537
|
+
title,
|
|
538
|
+
taskIds: [],
|
|
539
|
+
specIds: [],
|
|
540
|
+
acceptance: [],
|
|
541
|
+
status: "pending",
|
|
542
|
+
});
|
|
489
543
|
steps.push({
|
|
490
544
|
id: stepId,
|
|
491
545
|
role: planned.role,
|
|
@@ -499,11 +553,13 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
499
553
|
stepId,
|
|
500
554
|
role: planned.role,
|
|
501
555
|
agent: input.team.roles.find((role) => role.name === planned.role)?.agent ?? planned.role,
|
|
502
|
-
title
|
|
556
|
+
title,
|
|
557
|
+
description: planned.task !== title ? planned.task : undefined,
|
|
503
558
|
status: "queued",
|
|
504
559
|
dependsOn: previousStepIds,
|
|
505
560
|
cwd: input.manifest.cwd,
|
|
506
561
|
adaptive: { phase: phase.name, task: planned.task },
|
|
562
|
+
planItem: itemId,
|
|
507
563
|
graph: {
|
|
508
564
|
taskId,
|
|
509
565
|
dependencies: previousStepIds,
|
|
@@ -513,6 +569,12 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
513
569
|
});
|
|
514
570
|
currentStepIds.push(stepId);
|
|
515
571
|
}
|
|
572
|
+
planPhases.push({
|
|
573
|
+
id: `adaptive-phase-${phaseIndex + 1}`,
|
|
574
|
+
title: phase.name,
|
|
575
|
+
itemIds: phaseItemIds,
|
|
576
|
+
status: "pending",
|
|
577
|
+
});
|
|
516
578
|
previousStepIds = currentStepIds;
|
|
517
579
|
}
|
|
518
580
|
const dependencyTaskIdByStep = new Map<string, string>([
|
|
@@ -531,6 +593,45 @@ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput):
|
|
|
531
593
|
: task.graph,
|
|
532
594
|
}));
|
|
533
595
|
const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
|
|
596
|
+
// T2/R4 (ADR-4 §2/§6): persist the PlanRecord + manifest pointer (dual-write;
|
|
597
|
+
// crash between the two is benign — getCurrentPlanRecord falls back to the
|
|
598
|
+
// highest version). Producers never set taskIds (single-writer rule §3).
|
|
599
|
+
const planRecord: PlanRecord = {
|
|
600
|
+
id: randomUUID(),
|
|
601
|
+
runId: input.manifest.runId,
|
|
602
|
+
version: 1,
|
|
603
|
+
title: `Adaptive plan (${plan.phases.length} phase(s))`,
|
|
604
|
+
phases: planPhases,
|
|
605
|
+
items: planItems,
|
|
606
|
+
createdAt: new Date().toISOString(),
|
|
607
|
+
authorTaskId: assessTask.id,
|
|
608
|
+
};
|
|
609
|
+
// Review R1 (P1, crash-window): if a record ALREADY exists (the append
|
|
610
|
+
// survived a kill between the plans.json write and the tasks save below),
|
|
611
|
+
// REUSE it instead of re-appending — the fresh randomUUID would throw
|
|
612
|
+
// lineage-break on resume and permanently brick the run. Task/item ids are
|
|
613
|
+
// position-deterministic from the SAME parsed artifact, so the rebuilt
|
|
614
|
+
// linkage matches the surviving record exactly.
|
|
615
|
+
const existingRecord = getCurrentPlanRecord(input.manifest);
|
|
616
|
+
if (!existingRecord) appendPlanRevision(input.manifest, planRecord);
|
|
617
|
+
// Review round-2 N1: on the crash-window reuse path the pointer MUST name
|
|
618
|
+
// the SURVIVING record — the fresh planRecord was never persisted, so its
|
|
619
|
+
// randomUUID would dangle (masked today by the highest-version fallback,
|
|
620
|
+
// but it violates ADR-4 §2's pointer invariant).
|
|
621
|
+
const planPointer = existingRecord
|
|
622
|
+
? { id: existingRecord.id, version: existingRecord.version }
|
|
623
|
+
: { id: planRecord.id, version: planRecord.version };
|
|
624
|
+
await saveRunManifestAsync({
|
|
625
|
+
...manifestBase,
|
|
626
|
+
updatedAt: new Date().toISOString(),
|
|
627
|
+
plan: planPointer,
|
|
628
|
+
});
|
|
629
|
+
// FIX (T2/B2 case e, pre-existing): the injected tasks MUST be persisted at
|
|
630
|
+
// injection time. mergeUnitResult reloads tasks.json as the merge base and
|
|
631
|
+
// mergeTaskUpdatesPreservingTerminal DROPS updates for ids absent from the
|
|
632
|
+
// base — an injection that only lived in memory had its completed adaptive
|
|
633
|
+
// tasks silently erased from disk at the first merge.
|
|
634
|
+
await saveRunTasksAsync(manifestBase, allTasks);
|
|
534
635
|
await appendEventAsync(input.manifest.eventsPath, {
|
|
535
636
|
type: "adaptive.plan_injected",
|
|
536
637
|
runId: input.manifest.runId,
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { transformSync } from "esbuild";
|
|
23
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
23
24
|
import { appendEvent } from "../../state/event-log/event-log.ts";
|
|
24
25
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
25
26
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
26
27
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
27
28
|
import { packageRoot, projectCrewRoot, userPiRoot } from "../../utils/paths.ts";
|
|
29
|
+
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
28
30
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
29
31
|
import type { DynamicWorkflowConfig } from "../../workflows/workflow-config.ts";
|
|
30
32
|
import { assertDeterministicScript, isDeterminismCheckEnabled } from "../deterministic-ast.ts";
|
|
@@ -151,7 +153,7 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
|
|
|
151
153
|
// require, fs, child_process). Cloning a hostile repo and running its workflow = arbitrary
|
|
152
154
|
// code execution. Default-deny project workflows unless the user explicitly opts in via
|
|
153
155
|
// PI_CREW_TRUST_PROJECT_DWF=1. Builtin and user workflows proceed without restriction.
|
|
154
|
-
if (workflow.source === "project" &&
|
|
156
|
+
if (workflow.source === "project" && getCrewEnv("PI_CREW_TRUST_PROJECT_DWF") !== "1") {
|
|
155
157
|
appendEvent(eventsPath, {
|
|
156
158
|
type: "dwf.trust_denied",
|
|
157
159
|
runId: manifest.runId,
|
|
@@ -223,14 +225,17 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
|
|
|
223
225
|
// it does NOT kill the script. Promise.race with a hard timeout at least returns an
|
|
224
226
|
// error so the runner doesn't hang. The spawned child process is leaked, but the
|
|
225
227
|
// dynamic-workflow returns failure promptly. (v1.5: use Worker threads to actually kill.)
|
|
226
|
-
const SCRIPT_TIMEOUT_MS = Number.parseInt(
|
|
228
|
+
const SCRIPT_TIMEOUT_MS = Number.parseInt(getCrewEnv("PI_CREW_DWF_SCRIPT_TIMEOUT_MS") ?? "", 10) || 1_800_000; // 30 min default (was 10 min; raised for distill-dwf + similar long pipelines)
|
|
227
229
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
228
230
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
229
231
|
timeoutHandle = setTimeout(() => {
|
|
230
232
|
// ERR-1: abort the timeout controller so the abort propagates to
|
|
231
233
|
// runChildPi (via the combined signal passed to makeWorkflowCtx),
|
|
232
234
|
// which kills spawned children instead of letting them run.
|
|
233
|
-
|
|
235
|
+
// safeAbort: the combined signal may feed a signal-spawned child
|
|
236
|
+
// (abortChildProcess AbortError would otherwise reach the
|
|
237
|
+
// uncaughtException handler from this timer callback).
|
|
238
|
+
safeAbort(timeoutController, "dwf-script-timeout");
|
|
234
239
|
reject(
|
|
235
240
|
new Error(
|
|
236
241
|
`Dynamic workflow script timed out after ${SCRIPT_TIMEOUT_MS}ms. The script may have spawned a child process that did not exit. Check for spawn/exec calls without proper stdio handling.`,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* goal loop owns OUTER state) + §0c C10 (hardening: assertSafePathId + UUID goalId).
|
|
7
7
|
*
|
|
8
8
|
* Stores GoalLoopState as atomic JSON at <crewRoot>/state/goals/<goalId>.json.
|
|
9
|
-
* Modeled on
|
|
9
|
+
* Modeled on the (removed in Phase 1.7) ScheduleStore coordination pattern but with atomicWriteJson +
|
|
10
10
|
* path-traversal defense (assertSafePathId on every public method).
|
|
11
11
|
*
|
|
12
12
|
* Per §0c C2: budget lives here (budgetUsed accumulates collectRunMetrics across turns);
|
|
@@ -3,7 +3,7 @@ import { appendMailboxMessage, findMailboxMessageByRequestId, readDeliveryState
|
|
|
3
3
|
import { appendEvent } from "../state/event-log/event-log.ts";
|
|
4
4
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
5
5
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
6
|
-
import { aggregateTaskOutputs } from "./task-output-context.ts";
|
|
6
|
+
import { aggregateTaskOutputs, type ResultArtifactReadCache } from "./task-output-context.ts";
|
|
7
7
|
|
|
8
8
|
export type CrewGroupJoinMode = "off" | "group" | "smart";
|
|
9
9
|
|
|
@@ -51,6 +51,11 @@ export function deliverGroupJoin(input: {
|
|
|
51
51
|
batch: TeamTaskState[];
|
|
52
52
|
allTasks: TeamTaskState[];
|
|
53
53
|
partial?: boolean;
|
|
54
|
+
/** R10-1: optional per-run result-artifact read cache. The closeout
|
|
55
|
+
* aggregates the same settled batch for the batch-summary artifact
|
|
56
|
+
* immediately before this call — passing that cache here turns the
|
|
57
|
+
* group-join aggregation into cache hits (byte-identical output). */
|
|
58
|
+
cache?: ResultArtifactReadCache;
|
|
54
59
|
}): CrewGroupJoinDelivery | undefined {
|
|
55
60
|
if (!shouldGroupJoin(input.mode, input.batch)) return undefined;
|
|
56
61
|
const taskIds = input.batch.map((task) => task.id);
|
|
@@ -63,7 +68,7 @@ export function deliverGroupJoin(input: {
|
|
|
63
68
|
const remaining = latest.filter((task) => task.status === "queued" || task.status === "running").map((task) => task.id);
|
|
64
69
|
const partial = input.partial ?? remaining.length > 0;
|
|
65
70
|
const batchId = batchIdFor(input.manifest.runId, taskIds);
|
|
66
|
-
const summary = aggregateTaskOutputs(latest, input.manifest);
|
|
71
|
+
const summary = aggregateTaskOutputs(latest, input.manifest, input.cache);
|
|
67
72
|
const requestId = requestIdFor(input.manifest.runId, batchId, partial);
|
|
68
73
|
const existingMailbox = findMailboxMessageByRequestId(input.manifest, requestId);
|
|
69
74
|
const existingStatus = existingMailbox
|
|
@@ -141,127 +146,8 @@ export function deliverGroupJoin(input: {
|
|
|
141
146
|
return { ...delivery, artifact, messageId: mailbox.id };
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Persisted settings vocabulary for join mode (imported by settings-store.ts
|
|
151
|
+
* for `defaultJoinMode` validation and config application).
|
|
152
|
+
*/
|
|
146
153
|
export type JoinMode = "async" | "group" | "smart";
|
|
147
|
-
export type DeliveryCallback = (records: CrewAgentRecord[], partial: boolean) => void;
|
|
148
|
-
|
|
149
|
-
interface AgentGroup {
|
|
150
|
-
groupId: string;
|
|
151
|
-
agentIds: Set<string>;
|
|
152
|
-
completedRecords: Map<string, CrewAgentRecord>;
|
|
153
|
-
timeoutHandle?: ReturnType<typeof setTimeout>;
|
|
154
|
-
delivered: boolean;
|
|
155
|
-
isStraggler: boolean;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const DEFAULT_TIMEOUT = 30_000;
|
|
159
|
-
const STRAGGLER_TIMEOUT = 15_000;
|
|
160
|
-
|
|
161
|
-
export class GroupJoinManager {
|
|
162
|
-
private groups = new Map<string, AgentGroup>();
|
|
163
|
-
private agentToGroup = new Map<string, string>();
|
|
164
|
-
|
|
165
|
-
private deliverCb: DeliveryCallback;
|
|
166
|
-
private groupTimeout: number;
|
|
167
|
-
|
|
168
|
-
constructor(deliverCb: DeliveryCallback, groupTimeout = DEFAULT_TIMEOUT) {
|
|
169
|
-
this.deliverCb = deliverCb;
|
|
170
|
-
this.groupTimeout = groupTimeout;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
registerGroup(groupId: string, agentIds: string[]): void {
|
|
174
|
-
const group: AgentGroup = {
|
|
175
|
-
groupId,
|
|
176
|
-
agentIds: new Set(agentIds),
|
|
177
|
-
completedRecords: new Map(),
|
|
178
|
-
delivered: false,
|
|
179
|
-
isStraggler: false,
|
|
180
|
-
};
|
|
181
|
-
this.groups.set(groupId, group);
|
|
182
|
-
for (const id of agentIds) {
|
|
183
|
-
this.agentToGroup.set(id, groupId);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
onAgentComplete(record: CrewAgentRecord): "delivered" | "held" | "pass" {
|
|
188
|
-
const groupId = this.agentToGroup.get(record.taskId);
|
|
189
|
-
if (!groupId) return "pass";
|
|
190
|
-
|
|
191
|
-
const group = this.groups.get(groupId);
|
|
192
|
-
if (!group || group.delivered) return "pass";
|
|
193
|
-
|
|
194
|
-
group.completedRecords.set(record.taskId, record);
|
|
195
|
-
|
|
196
|
-
if (group.completedRecords.size >= group.agentIds.size) {
|
|
197
|
-
this.deliver(group, false);
|
|
198
|
-
return "delivered";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!group.timeoutHandle) {
|
|
202
|
-
const timeout = group.isStraggler ? STRAGGLER_TIMEOUT : this.groupTimeout;
|
|
203
|
-
group.timeoutHandle = setTimeout(() => {
|
|
204
|
-
this.onTimeout(group);
|
|
205
|
-
}, timeout);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return "held";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private onTimeout(group: AgentGroup): void {
|
|
212
|
-
if (group.delivered) return;
|
|
213
|
-
group.timeoutHandle = undefined;
|
|
214
|
-
|
|
215
|
-
const remaining = new Set<string>();
|
|
216
|
-
for (const id of group.agentIds) {
|
|
217
|
-
if (!group.completedRecords.has(id)) remaining.add(id);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for (const id of group.completedRecords.keys()) {
|
|
221
|
-
this.agentToGroup.delete(id);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
this.deliverCb([...group.completedRecords.values()], true);
|
|
225
|
-
|
|
226
|
-
group.completedRecords.clear();
|
|
227
|
-
group.agentIds = remaining;
|
|
228
|
-
group.isStraggler = true;
|
|
229
|
-
|
|
230
|
-
// Re-arm timer for remaining stragglers so they aren't silently abandoned
|
|
231
|
-
if (remaining.size > 0) {
|
|
232
|
-
group.timeoutHandle = setTimeout(() => this.onTimeout(group), STRAGGLER_TIMEOUT);
|
|
233
|
-
group.timeoutHandle.unref();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private deliver(group: AgentGroup, partial: boolean): void {
|
|
238
|
-
if (group.timeoutHandle) {
|
|
239
|
-
clearTimeout(group.timeoutHandle);
|
|
240
|
-
group.timeoutHandle = undefined;
|
|
241
|
-
}
|
|
242
|
-
group.delivered = true;
|
|
243
|
-
this.deliverCb([...group.completedRecords.values()], partial);
|
|
244
|
-
this.cleanupGroup(group.groupId);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private cleanupGroup(groupId: string): void {
|
|
248
|
-
const group = this.groups.get(groupId);
|
|
249
|
-
if (!group) return;
|
|
250
|
-
for (const id of group.agentIds) {
|
|
251
|
-
this.agentToGroup.delete(id);
|
|
252
|
-
}
|
|
253
|
-
this.groups.delete(groupId);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
isGrouped(agentId: string): boolean {
|
|
257
|
-
return this.agentToGroup.has(agentId);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
dispose(): void {
|
|
261
|
-
for (const group of this.groups.values()) {
|
|
262
|
-
if (group.timeoutHandle) clearTimeout(group.timeoutHandle);
|
|
263
|
-
}
|
|
264
|
-
this.groups.clear();
|
|
265
|
-
this.agentToGroup.clear();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
@@ -5,6 +5,7 @@ import { resolveToolPolicy } from "../../agents/agent-config.ts";
|
|
|
5
5
|
import type { CrewRuntimeConfig } from "../../config/config.ts";
|
|
6
6
|
import { loadConfig } from "../../config/config.ts";
|
|
7
7
|
import { DEFAULT_LIVE_SESSION } from "../../config/defaults.ts";
|
|
8
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
8
9
|
import { appendEvent, appendEventFireAndForget } from "../../state/event-log/event-log.ts";
|
|
9
10
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../../state/types.ts";
|
|
10
11
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
@@ -81,6 +82,13 @@ import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-s
|
|
|
81
82
|
* explorer subagents launched together; 3 of 4 crashed.)
|
|
82
83
|
*/
|
|
83
84
|
let liveSessionModulePromise: Promise<LiveSessionModule> | undefined;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Phase 4 (ADR 2026-08-15-runtime-convergence, decision (a) — freeze):
|
|
88
|
+
* warn-once flag so the experimental-path notice fires at most once per
|
|
89
|
+
* process, not per task.
|
|
90
|
+
*/
|
|
91
|
+
let experimentalWarned = false;
|
|
84
92
|
function loadLiveSessionModule(): Promise<LiveSessionModule> {
|
|
85
93
|
if (!liveSessionModulePromise) {
|
|
86
94
|
liveSessionModulePromise = import("@earendil-works/pi-coding-agent") as unknown as Promise<LiveSessionModule>;
|
|
@@ -549,13 +557,30 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
549
557
|
// any module. Under tsx, concurrent first-imports race module-record
|
|
550
558
|
// instantiation; awaiting the registration-time warmup eliminates the window.
|
|
551
559
|
await awaitRuntimeWarmup();
|
|
560
|
+
|
|
561
|
+
// Phase 4 (ADR 2026-08-15-runtime-convergence, decision (a) — freeze):
|
|
562
|
+
// the live-session path is permanently EXPERIMENTAL and intentionally
|
|
563
|
+
// diverges from the supported child-process path (no worker-cap semaphore,
|
|
564
|
+
// no depth guard, permissive SDK tool surface, single-model-fallback
|
|
565
|
+
// report). Warn once per process so operators see the notice without
|
|
566
|
+
// per-task spam.
|
|
567
|
+
if (!experimentalWarned) {
|
|
568
|
+
experimentalWarned = true;
|
|
569
|
+
logInternalError(
|
|
570
|
+
"live-session.experimental",
|
|
571
|
+
new Error("experimental path"),
|
|
572
|
+
"runtime.mode=live-session is EXPERIMENTAL and frozen (decision (a)); it diverges from child-process semantics (no worker-cap/depth guard, permissive tools) — see docs/decisions/2026-08-15-runtime-convergence.md",
|
|
573
|
+
"warn",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
552
577
|
const isCurrent = input.isCurrent ?? (() => true);
|
|
553
578
|
let streamOut: StreamingOutputHandle | undefined;
|
|
554
579
|
|
|
555
580
|
// G1: Capture yield result from custom tool callback
|
|
556
581
|
let customToolYieldResult: YieldResult | undefined;
|
|
557
582
|
let customToolYieldResolved = false;
|
|
558
|
-
if (
|
|
583
|
+
if (getCrewEnv("PI_CREW_MOCK_LIVE_SESSION") === "success") {
|
|
559
584
|
const agentId = `${input.manifest.runId}:${input.task.id}`;
|
|
560
585
|
const inherited =
|
|
561
586
|
input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { TEAM_TASK_STATUSES, TEAM_TERMINAL_TASK_STATUSES, type TeamTaskStatus } from "../state/contracts.ts";
|
|
15
15
|
import type { TeamTaskState } from "../state/types.ts";
|
|
16
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
16
17
|
import { refreshTaskGraphQueues } from "./scheduling/task-graph-scheduler.ts";
|
|
17
18
|
|
|
18
19
|
export function isNonTerminalTaskStatus(status: TeamTaskState["status"]): boolean {
|
|
@@ -123,7 +124,12 @@ export function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskS
|
|
|
123
124
|
// Malformed finishedAt (NaN) is treated as Infinity — invalid state should be
|
|
124
125
|
// replaced rather than persisting corruption. Log warning for visibility.
|
|
125
126
|
if (!Number.isFinite(currentTime)) {
|
|
126
|
-
|
|
127
|
+
logInternalError(
|
|
128
|
+
"merge-gate",
|
|
129
|
+
new Error(`Task ${current.id} has malformed finishedAt: ${current.finishedAt}`),
|
|
130
|
+
undefined,
|
|
131
|
+
"warn",
|
|
132
|
+
);
|
|
127
133
|
}
|
|
128
134
|
if (isMalformedFinishedAtReplacement(currentTime, updatedTime)) {
|
|
129
135
|
return true;
|
|
@@ -175,15 +181,31 @@ export function mergeTaskUpdatesPreservingTerminal(base: TeamTaskState[], result
|
|
|
175
181
|
const current = indexById.get(updated.id);
|
|
176
182
|
if (!current) continue;
|
|
177
183
|
if (!shouldMergeTaskUpdate(current, updated)) {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
console
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
// Routine no-op skips (an unchanged queued/running snapshot
|
|
185
|
+
// arriving from a parallel worker) are NORMAL parallel-merge
|
|
186
|
+
// behaviour and must stay silent — pi renders extension
|
|
187
|
+
// console output inline in the session transcript, so logging
|
|
188
|
+
// every skip floods whatever session is open (main or an
|
|
189
|
+
// agent view). Only a REAL rejected change (status transition,
|
|
190
|
+
// completion time, or result) is surfaced, as a warning: in
|
|
191
|
+
// distributed systems, rejected transitions may indicate bugs
|
|
192
|
+
// (wrong status, timestamp corruption) if they accumulate.
|
|
193
|
+
const meaningfulDiff =
|
|
194
|
+
updated.status !== current.status ||
|
|
195
|
+
updated.finishedAt !== current.finishedAt ||
|
|
196
|
+
updated.startedAt !== current.startedAt ||
|
|
197
|
+
Boolean(updated.resultArtifact) !== Boolean(current.resultArtifact) ||
|
|
198
|
+
Boolean(updated.error) ||
|
|
199
|
+
Boolean(updated.modelAttempts?.length) ||
|
|
200
|
+
Boolean(updated.usage);
|
|
201
|
+
if (meaningfulDiff) {
|
|
202
|
+
console.warn("[merge-gate] Rejected update for task", updated.id, {
|
|
203
|
+
currentStatus: current.status,
|
|
204
|
+
updatedStatus: updated.status,
|
|
205
|
+
currentFinishedAt: current.finishedAt,
|
|
206
|
+
updatedFinishedAt: updated.finishedAt,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
187
209
|
skipped += 1;
|
|
188
210
|
continue;
|
|
189
211
|
}
|