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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run finalization for the team-run scheduler loop.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
|
|
5
|
+
* CORE-4 extraction 8 + the writeProgress cluster + applyPolicy it depends
|
|
6
|
+
* on). Pure code motion: formatTaskProgress / runEffectivenessLines /
|
|
7
|
+
* scratchpadSummaryLines / lastProgressContentHash / writeProgress /
|
|
8
|
+
* applyPolicy / finalizeRun moved verbatim, including the R15-1 / S02 / F1
|
|
9
|
+
* / P6 / RT-7 / RT-7a / I5 / BUG A comments.
|
|
10
|
+
*
|
|
11
|
+
* Import direction: this module imports isRunTerminalPreserved from
|
|
12
|
+
* ./merge-loop.ts (never the reverse) and never imports from team-runner.ts —
|
|
13
|
+
* `writeProgress` / `lastProgressContentHash` / `finalizeRun` are exported so
|
|
14
|
+
* team-runner.ts (core loop + finally) can import them here.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
19
|
+
import { flushPendingAtomicWrites } from "../state/atomic-write.ts";
|
|
20
|
+
import { TEAM_TERMINAL_TASK_STATUSES } from "../state/contracts.ts";
|
|
21
|
+
import { withRunLock } from "../state/coordination/locks.ts";
|
|
22
|
+
import { appendEvent, appendEventAsync, appendEventFireAndForget, readEvents } from "../state/event-log/event-log.ts";
|
|
23
|
+
import { hashArtifactContent as hashContent, writeArtifact } from "../state/stores/artifact-store.ts";
|
|
24
|
+
import { HealthStore } from "../state/stores/health-store.ts";
|
|
25
|
+
import { loadRunManifestById, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/stores/state-store.ts";
|
|
26
|
+
import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
27
|
+
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
28
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
29
|
+
import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
|
|
30
|
+
import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
|
|
31
|
+
import { isRunTerminalPreserved } from "./merge-loop.ts";
|
|
32
|
+
import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
|
|
33
|
+
import { buildRecoveryLedger } from "./recovery/recovery-recipes.ts";
|
|
34
|
+
import type { SchedulerContext } from "./scheduler-context.ts";
|
|
35
|
+
import { taskGraphSnapshot } from "./scheduling/task-graph-scheduler.ts";
|
|
36
|
+
|
|
37
|
+
function formatTaskProgress(task: TeamTaskState): string {
|
|
38
|
+
return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runEffectivenessLines(
|
|
42
|
+
manifest: TeamRunManifest,
|
|
43
|
+
tasks: TeamTaskState[],
|
|
44
|
+
executeWorkers: boolean,
|
|
45
|
+
runtimeConfig?: CrewRuntimeConfig,
|
|
46
|
+
): string[] {
|
|
47
|
+
return formatRunEffectivenessLines(
|
|
48
|
+
evaluateRunEffectiveness({
|
|
49
|
+
manifest,
|
|
50
|
+
tasks,
|
|
51
|
+
executeWorkers,
|
|
52
|
+
runtimeConfig,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// I5 (plan): surface scratchpad adoption in the run summary — but ONLY when
|
|
58
|
+
// non-zero, so the 3 armed roles that never call it add no noise. Counts the
|
|
59
|
+
// metric events the workers appended to the run events log (fire-and-forget
|
|
60
|
+
// scratchpad.cell / scratchpad.restored). Silent when the feature is unused or
|
|
61
|
+
// the events path is missing (no throw).
|
|
62
|
+
function scratchpadSummaryLines(manifest: TeamRunManifest): string[] {
|
|
63
|
+
if (!manifest.eventsPath) return [];
|
|
64
|
+
const events = readEvents(manifest.eventsPath);
|
|
65
|
+
const cells = events.filter((e) => e.type === "scratchpad.cell");
|
|
66
|
+
const restores = events.filter((e) => e.type === "scratchpad.restored");
|
|
67
|
+
if (cells.length === 0 && restores.length === 0) return [];
|
|
68
|
+
return [
|
|
69
|
+
`## Scratchpad (RLM adoption) — I5 metric`,
|
|
70
|
+
`- cells executed: ${cells.length}`,
|
|
71
|
+
`- snapshot restores: ${restores.length}`,
|
|
72
|
+
...(cells.length > 0
|
|
73
|
+
? [`- total cell time: ${Math.round(cells.reduce((s, e) => s + ((e.data?.durationMs as number) ?? 0), 0))} ms`]
|
|
74
|
+
: []),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// P6 (perf): Cache the last-rendered progress content so we can skip the
|
|
79
|
+
// artifact write + redaction + atomic write + size/hash read when nothing
|
|
80
|
+
// material changed (rare between batches, but happens between idle heartbeats).
|
|
81
|
+
// The dedup filter also moved from O(N²) findIndex inside .filter(...)
|
|
82
|
+
// (the previous implementation ran 2 redundant passes on every batch) to
|
|
83
|
+
// a single-pass Map-based replacement: remove the existing entry by path, then
|
|
84
|
+
// append the new one. Net complexity: O(N) build + O(1) replace per write.
|
|
85
|
+
// RT-7: key on manifest.runId (stable string) instead of object identity
|
|
86
|
+
// (WeakMap). Every writeProgress mutator returns a NEW manifest object via
|
|
87
|
+
// spread, so object-identity keying meant the cache NEVER hit. Using runId
|
|
88
|
+
// makes back-to-back calls (same millisecond) actually dedup.
|
|
89
|
+
export const lastProgressContentHash = new Map<string, string>();
|
|
90
|
+
|
|
91
|
+
export function writeProgress(
|
|
92
|
+
manifest: TeamRunManifest,
|
|
93
|
+
tasks: TeamTaskState[],
|
|
94
|
+
producer: string,
|
|
95
|
+
executeWorkers = true,
|
|
96
|
+
runtimeConfig?: CrewRuntimeConfig,
|
|
97
|
+
): TeamRunManifest {
|
|
98
|
+
const counts = new Map<string, number>();
|
|
99
|
+
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
100
|
+
const queue = taskGraphSnapshot(tasks);
|
|
101
|
+
const updatedAt = new Date().toISOString();
|
|
102
|
+
const content = [
|
|
103
|
+
`# pi-crew progress ${manifest.runId}`,
|
|
104
|
+
"",
|
|
105
|
+
`Status: ${manifest.status}`,
|
|
106
|
+
`Team: ${manifest.team}`,
|
|
107
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
108
|
+
`Updated: ${updatedAt}`,
|
|
109
|
+
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
110
|
+
`Queue: ready=${queue.ready.length}, blocked=${queue.blocked.length}, running=${queue.running.length}, done=${queue.done.length}, failed=${queue.failed.length}, cancelled=${queue.cancelled.length}`,
|
|
111
|
+
"",
|
|
112
|
+
"## Tasks",
|
|
113
|
+
...tasks.map(formatTaskProgress),
|
|
114
|
+
"",
|
|
115
|
+
"## Effectiveness",
|
|
116
|
+
...runEffectivenessLines(manifest, tasks, executeWorkers, runtimeConfig),
|
|
117
|
+
"",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
// P6 content-cache: even with identical status / counts / queue, the
|
|
121
|
+
// `Updated:` timestamp ticks on every call so the content rarely matches
|
|
122
|
+
// byte-for-byte. We DO compare against the previous rendered byte-stream
|
|
123
|
+
// (which used the previous timestamp) — so this only hits on the
|
|
124
|
+
// back-to-back writeProgress calls during the applyPolicy phase, where
|
|
125
|
+
// both calls happen within the same millisecond. It's a minor win but
|
|
126
|
+
// matches the audit recommendation (skip artifact write when nothing
|
|
127
|
+
// material changed).
|
|
128
|
+
// RT-7: compute the content hash ONCE (was hashed twice per call: once
|
|
129
|
+
// for the canSkip comparison and again for the cache .set). Key the cache
|
|
130
|
+
// on manifest.runId (stable) instead of object identity (never hit).
|
|
131
|
+
const contentHash = hashContent(content);
|
|
132
|
+
const prevHash = lastProgressContentHash.get(manifest.runId);
|
|
133
|
+
// Cheap pre-check: avoid the redaction + atomicWrite + readback roundtrip
|
|
134
|
+
// when both the timestamp and the input args are identical to last time.
|
|
135
|
+
const canSkip = prevHash === contentHash;
|
|
136
|
+
|
|
137
|
+
const progress = canSkip
|
|
138
|
+
? (() => {
|
|
139
|
+
// Reuse the previous artifact rather than rebuilding one via
|
|
140
|
+
// writeArtifact. This skips mkdirSync, resolveRealContainedPath,
|
|
141
|
+
// redactSecrets, atomicWriteFile, and the post-write readFileSync +
|
|
142
|
+
// statSync.
|
|
143
|
+
const existing = manifest.artifacts.find((a) => a.kind === "progress");
|
|
144
|
+
if (existing) {
|
|
145
|
+
// RT-7a: return a FRESH descriptor with a refreshed createdAt
|
|
146
|
+
// instead of reusing the stale existing reference. The existing
|
|
147
|
+
// descriptor's createdAt reflects the FIRST write time, not this
|
|
148
|
+
// skip-write; refreshing it matches the non-skip path (writeArtifact
|
|
149
|
+
// stamps createdAt with the actual write time) so the manifest
|
|
150
|
+
// always carries a descriptor whose createdAt reflects the current
|
|
151
|
+
// write. Content is identical (that's why we skipped), so path /
|
|
152
|
+
// sizeBytes / contentHash / retention are unchanged.
|
|
153
|
+
return { ...existing, createdAt: new Date().toISOString() };
|
|
154
|
+
}
|
|
155
|
+
// No prior progress artifact (rare; first call from a stale manifest
|
|
156
|
+
// view). Fall through to the normal write.
|
|
157
|
+
return writeArtifact(manifest.artifactsRoot, {
|
|
158
|
+
kind: "progress",
|
|
159
|
+
relativePath: "progress.md",
|
|
160
|
+
producer,
|
|
161
|
+
content,
|
|
162
|
+
});
|
|
163
|
+
})()
|
|
164
|
+
: writeArtifact(manifest.artifactsRoot, {
|
|
165
|
+
kind: "progress",
|
|
166
|
+
relativePath: "progress.md",
|
|
167
|
+
producer,
|
|
168
|
+
content,
|
|
169
|
+
});
|
|
170
|
+
lastProgressContentHash.set(manifest.runId, contentHash);
|
|
171
|
+
|
|
172
|
+
// P6 dedup: replace by path in a single Map pass instead of
|
|
173
|
+
// .filter(...) // O(N) to remove the old entry
|
|
174
|
+
// .filter((_, i, self) => self.findIndex(...) === i) // O(N²) for dedup
|
|
175
|
+
// For an artifact list of size 30+ across a long run, this was the
|
|
176
|
+
// dominant cost of writeProgress between batches.
|
|
177
|
+
const byPath = new Map<string, ArtifactDescriptor>();
|
|
178
|
+
for (const artifact of manifest.artifacts) {
|
|
179
|
+
if (artifact.kind === "progress" && artifact.path === progress.path) continue;
|
|
180
|
+
byPath.set(artifact.path, artifact);
|
|
181
|
+
}
|
|
182
|
+
byPath.set(progress.path, progress);
|
|
183
|
+
const deduped = [...byPath.values()];
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...manifest,
|
|
187
|
+
updatedAt,
|
|
188
|
+
artifacts: deduped,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
|
|
193
|
+
const branchFreshness = checkBranchFreshness(manifest.cwd);
|
|
194
|
+
const branchArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
195
|
+
kind: "metadata",
|
|
196
|
+
relativePath: "metadata/branch-freshness.json",
|
|
197
|
+
producer: "branch-freshness",
|
|
198
|
+
content: `${JSON.stringify(branchFreshness, null, 2)}\n`,
|
|
199
|
+
});
|
|
200
|
+
let decisions: PolicyDecision[] = evaluateCrewPolicy({
|
|
201
|
+
manifest,
|
|
202
|
+
tasks,
|
|
203
|
+
limits,
|
|
204
|
+
});
|
|
205
|
+
if (branchFreshness.status === "stale" || branchFreshness.status === "diverged") {
|
|
206
|
+
const branchDecision: PolicyDecision = {
|
|
207
|
+
action: "notify",
|
|
208
|
+
reason: "branch_stale",
|
|
209
|
+
message: branchFreshness.message,
|
|
210
|
+
createdAt: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
decisions = [...decisions, branchDecision];
|
|
213
|
+
appendEvent(manifest.eventsPath, {
|
|
214
|
+
type: "branch.stale",
|
|
215
|
+
runId: manifest.runId,
|
|
216
|
+
message: branchFreshness.message,
|
|
217
|
+
data: { branchFreshness },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const policyArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
221
|
+
kind: "metadata",
|
|
222
|
+
relativePath: "policy-decisions.json",
|
|
223
|
+
producer: "policy-engine",
|
|
224
|
+
content: `${JSON.stringify(decisions, null, 2)}\n`,
|
|
225
|
+
});
|
|
226
|
+
const recoveryLedger = buildRecoveryLedger(decisions);
|
|
227
|
+
const recoveryArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
228
|
+
kind: "metadata",
|
|
229
|
+
relativePath: "recovery-ledger.json",
|
|
230
|
+
producer: "recovery-engine",
|
|
231
|
+
content: `${JSON.stringify(recoveryLedger, null, 2)}\n`,
|
|
232
|
+
});
|
|
233
|
+
for (const item of decisions)
|
|
234
|
+
appendEvent(manifest.eventsPath, {
|
|
235
|
+
type: item.action === "escalate" ? "policy.escalated" : "policy.action",
|
|
236
|
+
runId: manifest.runId,
|
|
237
|
+
taskId: item.taskId,
|
|
238
|
+
message: item.message,
|
|
239
|
+
data: { action: item.action, reason: item.reason },
|
|
240
|
+
});
|
|
241
|
+
for (const item of recoveryLedger.entries)
|
|
242
|
+
appendEvent(manifest.eventsPath, {
|
|
243
|
+
type: item.state === "escalation_required" ? "recovery.escalated" : "recovery.attempted",
|
|
244
|
+
runId: manifest.runId,
|
|
245
|
+
taskId: item.taskId,
|
|
246
|
+
message: item.message,
|
|
247
|
+
data: {
|
|
248
|
+
scenario: item.scenario,
|
|
249
|
+
steps: item.steps,
|
|
250
|
+
attempt: item.attempt,
|
|
251
|
+
state: item.state,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
...manifest,
|
|
256
|
+
updatedAt: new Date().toISOString(),
|
|
257
|
+
policyDecisions: decisions,
|
|
258
|
+
artifacts: [
|
|
259
|
+
...manifest.artifacts.filter(
|
|
260
|
+
(artifact) =>
|
|
261
|
+
!(
|
|
262
|
+
artifact.kind === "metadata" &&
|
|
263
|
+
(artifact.path.endsWith("policy-decisions.json") ||
|
|
264
|
+
artifact.path.endsWith("recovery-ledger.json") ||
|
|
265
|
+
artifact.path.endsWith("branch-freshness.json"))
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
branchArtifact,
|
|
269
|
+
policyArtifact,
|
|
270
|
+
recoveryArtifact,
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* CORE-4 extraction 8: finalize the run after the scheduler loop exits.
|
|
277
|
+
*
|
|
278
|
+
* Computes the final run status (failed/blocked/completed) from task states,
|
|
279
|
+
* policy decisions, and effectiveness evaluation; writes the workflow output
|
|
280
|
+
* deliverable warning, the `summary.md` artifact, the joint atomic manifest+tasks
|
|
281
|
+
* save, and a health snapshot; then returns the terminal `{ manifest, tasks }`.
|
|
282
|
+
*
|
|
283
|
+
* Reads `ctx.input` (limits/workflow/executeWorkers/runtimeConfig). Mutates
|
|
284
|
+
* `ctx.manifest` / `ctx.tasks` and writes them back before returning so the
|
|
285
|
+
* caller stays in sync. This function is the terminal step of
|
|
286
|
+
* `executeTeamRunCore` — its return value is the run result.
|
|
287
|
+
*
|
|
288
|
+
* @param ctx The scheduler context.
|
|
289
|
+
* @returns The final `{ manifest, tasks }` result for the run.
|
|
290
|
+
*/
|
|
291
|
+
export async function finalizeRun(ctx: SchedulerContext): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
292
|
+
const input = ctx.input;
|
|
293
|
+
let tasks = ctx.tasks;
|
|
294
|
+
let manifest = ctx.manifest;
|
|
295
|
+
// Bug-027 (PR #46 ubuntu CI, 3 consecutive runs): under heavy contention a
|
|
296
|
+
// settled unit's merged result can carry a STALE non-terminal snapshot while
|
|
297
|
+
// the worker's durable completion has already landed in tasks.json — the
|
|
298
|
+
// status-derivation chain below would then flip a phantom 'running' into a
|
|
299
|
+
// false 'blocked: still running' (events: reviewer task.completed 18.184 →
|
|
300
|
+
// batch merge 18.190 → run.blocked 18.223, with the "running" task completed
|
|
301
|
+
// on disk the whole time). Disk is the authority at finalize — the same
|
|
302
|
+
// principle as the R15-1 locked save. Reconcile in-memory non-terminal tasks
|
|
303
|
+
// against a fresh tasks.json read, but ONLY when nothing is in-flight
|
|
304
|
+
// (pendingUnits empty) so a genuinely running worker is never clobbered.
|
|
305
|
+
if (ctx.pendingUnits.size === 0 && tasks.some((task) => task.status === "running" || task.status === "waiting")) {
|
|
306
|
+
try {
|
|
307
|
+
flushPendingAtomicWrites();
|
|
308
|
+
const diskTasks = JSON.parse(fs.readFileSync(manifest.tasksPath, "utf8")) as TeamTaskState[];
|
|
309
|
+
const diskById = new Map(diskTasks.map((task) => [task.id, task] as const));
|
|
310
|
+
const healed: string[] = [];
|
|
311
|
+
tasks = tasks.map((task) => {
|
|
312
|
+
if (task.status !== "running" && task.status !== "waiting") return task;
|
|
313
|
+
const disk = diskById.get(task.id);
|
|
314
|
+
if (disk && TEAM_TERMINAL_TASK_STATUSES.has(disk.status)) {
|
|
315
|
+
healed.push(`${task.id}:${task.status}->${disk.status}`);
|
|
316
|
+
return disk;
|
|
317
|
+
}
|
|
318
|
+
return task;
|
|
319
|
+
});
|
|
320
|
+
if (healed.length > 0) {
|
|
321
|
+
ctx.tasks = tasks;
|
|
322
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
323
|
+
type: "task.reconciled_from_disk",
|
|
324
|
+
runId: manifest.runId,
|
|
325
|
+
message: `Finalize healed stale non-terminal snapshot(s) from durable tasks.json: ${healed.join(", ")}`,
|
|
326
|
+
data: { healed },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// Unreadable/missing tasks.json — keep the in-memory view; the chain
|
|
331
|
+
// below decides as before.
|
|
332
|
+
logInternalError("finalize-run.disk-reconcile", error, manifest.runId, "debug");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const failed = tasks.find((task) => task.status === "failed");
|
|
336
|
+
const waiting = tasks.find((task) => task.status === "waiting");
|
|
337
|
+
const running = tasks.find((task) => task.status === "running");
|
|
338
|
+
manifest = applyPolicy(manifest, tasks, input.limits);
|
|
339
|
+
|
|
340
|
+
// S02: Verify workflow-declared output files exist before marking completed
|
|
341
|
+
if (input.workflow?.steps) {
|
|
342
|
+
const missingOutputs: string[] = [];
|
|
343
|
+
for (const step of input.workflow.steps) {
|
|
344
|
+
if (step.output && typeof step.output === "string") {
|
|
345
|
+
const outputPath = path.join(manifest.artifactsRoot, step.output);
|
|
346
|
+
if (!fs.existsSync(outputPath)) {
|
|
347
|
+
missingOutputs.push(step.output);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (missingOutputs.length > 0) {
|
|
352
|
+
// Emit warning event — run still completes normally to avoid hanging
|
|
353
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
354
|
+
type: "run.deliverable_warning",
|
|
355
|
+
runId: manifest.runId,
|
|
356
|
+
message: `Missing workflow output files: ${missingOutputs.join(", ")}`,
|
|
357
|
+
data: { missingFiles: missingOutputs },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const effectiveness = evaluateRunEffectiveness({
|
|
363
|
+
manifest,
|
|
364
|
+
tasks,
|
|
365
|
+
executeWorkers: input.executeWorkers,
|
|
366
|
+
runtimeConfig: input.runtimeConfig,
|
|
367
|
+
});
|
|
368
|
+
const effectivenessDecision = effectivenessPolicyDecision(effectiveness);
|
|
369
|
+
if (effectivenessDecision) {
|
|
370
|
+
manifest = {
|
|
371
|
+
...manifest,
|
|
372
|
+
policyDecisions: [...(manifest.policyDecisions ?? []), effectivenessDecision],
|
|
373
|
+
updatedAt: new Date().toISOString(),
|
|
374
|
+
};
|
|
375
|
+
await appendEventAsync(manifest.eventsPath, {
|
|
376
|
+
type: "run.effectiveness",
|
|
377
|
+
runId: manifest.runId,
|
|
378
|
+
message: effectivenessDecision.message,
|
|
379
|
+
data: { effectiveness, policyDecision: effectivenessDecision },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
|
|
383
|
+
// R15-1: capture the entry status BEFORE the in-memory status-derivation
|
|
384
|
+
// chain. The chain below computes the final status in-memory ONLY — its
|
|
385
|
+
// updateRunStatus persistence calls are replaced with a pure in-memory
|
|
386
|
+
// status application so ALL persistence happens in the single locked save
|
|
387
|
+
// further below, which re-reads disk first and preserves a disk-terminal
|
|
388
|
+
// status (cancel → "cancelled", reconciler → "failed") instead of
|
|
389
|
+
// overwriting it with the derived status (typically "completed"). The
|
|
390
|
+
// DECISION LOGIC of the chain (branches, conditions, statuses, messages)
|
|
391
|
+
// is unchanged; only the persistence mechanism moved.
|
|
392
|
+
const entryStatus = manifest.status;
|
|
393
|
+
// R15-1: true when the chain applied a derived status via a status branch
|
|
394
|
+
// (mirrors the old chain's updateRunStatus call sites, INCLUDING the
|
|
395
|
+
// from===to cases like `if (failed)` on an already-failed manifest that
|
|
396
|
+
// re-emit run.<status>). The preserve branch below leaves it false so the
|
|
397
|
+
// locked save does NOT emit a status event (matching old behavior).
|
|
398
|
+
let chainAppliedStatus = false;
|
|
399
|
+
const applyStatusInMemory = (status: TeamRunManifest["status"], summary: string): TeamRunManifest => {
|
|
400
|
+
chainAppliedStatus = true;
|
|
401
|
+
return { ...manifest, status, summary, updatedAt: new Date().toISOString() };
|
|
402
|
+
};
|
|
403
|
+
if (failed) {
|
|
404
|
+
manifest = applyStatusInMemory("failed", `Failed at task '${failed.id}'.`);
|
|
405
|
+
} else if (waiting) {
|
|
406
|
+
manifest = applyStatusInMemory("blocked", `Waiting for response to task '${waiting.id}'.`);
|
|
407
|
+
} else if (running) {
|
|
408
|
+
manifest = applyStatusInMemory("blocked", `Task '${running.id}' is still running.`);
|
|
409
|
+
} else if (effectiveness.severity === "failed") {
|
|
410
|
+
manifest = applyStatusInMemory("failed", effectivenessDecision?.message ?? "Run effectiveness guard failed.");
|
|
411
|
+
} else if (effectiveness.severity === "blocked") {
|
|
412
|
+
manifest = applyStatusInMemory("blocked", effectivenessDecision?.message ?? "Run effectiveness guard blocked completion.");
|
|
413
|
+
} else if (blockingDecision) {
|
|
414
|
+
manifest = applyStatusInMemory("blocked", blockingDecision.message);
|
|
415
|
+
} else if (tasks.some((task) => task.status === "queued")) {
|
|
416
|
+
// F1 defense-in-depth: the loop exited with queued tasks still pending
|
|
417
|
+
// (e.g. a hook skipped all ready tasks and downstream tasks never became
|
|
418
|
+
// runnable). This is NOT a completed run — mark it blocked rather than
|
|
419
|
+
// false-green "completed".
|
|
420
|
+
manifest = applyStatusInMemory("blocked", "Run exited with queued tasks still pending.");
|
|
421
|
+
} else if (manifest.status === "failed" || manifest.status === "cancelled") {
|
|
422
|
+
// The run was already marked failed/cancelled mid-run (e.g. handleFailedTask
|
|
423
|
+
// on a coalesced-group race where the failing task's status was later
|
|
424
|
+
// mutated by the group-drain, or a cancel). Preserve that terminal status —
|
|
425
|
+
// do NOT force "completed" here: failed -> completed is not in
|
|
426
|
+
// TEAM_RUN_STATUS_TRANSITIONS and would throw an invalid-transition error.
|
|
427
|
+
// (No updateRunStatus call: from===to is a no-op, but the intent here is
|
|
428
|
+
// explicitly "leave the earlier decision intact".)
|
|
429
|
+
} else {
|
|
430
|
+
manifest = applyStatusInMemory(
|
|
431
|
+
"completed",
|
|
432
|
+
input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
|
|
436
|
+
const usage = aggregateUsage(tasks);
|
|
437
|
+
|
|
438
|
+
// R15-1: ALL persistence inside ONE withRunLock that FIRST re-reads disk.
|
|
439
|
+
// The former unlocked `saveRunManifestAsync(manifest)` (pre-lock) is gone —
|
|
440
|
+
// the single locked write below (manifest + tasks via finalManifest) is now
|
|
441
|
+
// the ONLY persistence point, preserving crash-window atomicity. Disk is
|
|
442
|
+
// authoritative for terminal states: if a cancel/reconciler write landed
|
|
443
|
+
// between loop-exit and this save, its terminal status is preserved and the
|
|
444
|
+
// derived in-memory status (typically "completed") is NOT applied.
|
|
445
|
+
const finalManifest = await withRunLock(manifest, async () => {
|
|
446
|
+
flushPendingAtomicWrites();
|
|
447
|
+
const disk = loadRunManifestById(manifest.cwd, manifest.runId);
|
|
448
|
+
const diskManifest = disk?.manifest;
|
|
449
|
+
const diskTerminal = diskManifest && isRunTerminalPreserved(diskManifest.status);
|
|
450
|
+
const signalAborted = ctx.input.signal?.aborted === true;
|
|
451
|
+
|
|
452
|
+
let resolvedManifest: TeamRunManifest;
|
|
453
|
+
let resolvedTasks = tasks;
|
|
454
|
+
if (diskTerminal || signalAborted) {
|
|
455
|
+
// R15-1: preserve the disk terminal status — do NOT overwrite it with
|
|
456
|
+
// the in-memory derived status, do NOT run the completion branch.
|
|
457
|
+
// Signal-aborted also preserves (the run must not complete). Mirror
|
|
458
|
+
// CANCEL-1: disk is authoritative for terminal states — the final
|
|
459
|
+
// tasks write also uses the freshest disk tasks so a cancel that
|
|
460
|
+
// landed between the last merge and this save is not resurrected.
|
|
461
|
+
const preservedStatus = diskTerminal ? diskManifest!.status : "cancelled";
|
|
462
|
+
resolvedManifest = {
|
|
463
|
+
...manifest,
|
|
464
|
+
status: preservedStatus,
|
|
465
|
+
summary: diskTerminal ? (diskManifest!.summary ?? manifest.summary) : "Run cancelled during finalization.",
|
|
466
|
+
updatedAt: new Date().toISOString(),
|
|
467
|
+
};
|
|
468
|
+
resolvedTasks = disk?.tasks ?? tasks;
|
|
469
|
+
await appendEventAsync(manifest.eventsPath, {
|
|
470
|
+
type: "run.terminal_preserved",
|
|
471
|
+
runId: manifest.runId,
|
|
472
|
+
message: `Run terminal status '${preservedStatus}' preserved from disk; finalize did not overwrite it.`,
|
|
473
|
+
data: { preservedStatus, derivedStatus: manifest.status, signalAborted },
|
|
474
|
+
});
|
|
475
|
+
} else if (chainAppliedStatus) {
|
|
476
|
+
// Apply the derived in-memory status via updateRunStatus (validates
|
|
477
|
+
// the entryStatus→derived transition exactly as the pre-R15-1 chain
|
|
478
|
+
// did — from===to included — emits the run.<status> event, unregisters
|
|
479
|
+
// active-run, persists).
|
|
480
|
+
resolvedManifest = updateRunStatus({ ...manifest, status: entryStatus }, manifest.status, manifest.summary);
|
|
481
|
+
} else {
|
|
482
|
+
// preserve branch from the chain — leave the earlier decision intact
|
|
483
|
+
// (no status event, matching pre-R15-1 behavior).
|
|
484
|
+
resolvedManifest = { ...manifest, updatedAt: new Date().toISOString() };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// summaryArtifact + healthSnapshot run in ALL paths (preserved-terminal
|
|
488
|
+
// included) — the summary is written with the FINAL (possibly preserved)
|
|
489
|
+
// status so the run record is complete.
|
|
490
|
+
const summaryArtifact = writeArtifact(resolvedManifest.artifactsRoot, {
|
|
491
|
+
kind: "summary",
|
|
492
|
+
relativePath: "summary.md",
|
|
493
|
+
producer: "team-runner",
|
|
494
|
+
content: [
|
|
495
|
+
`# pi-crew run ${resolvedManifest.runId}`,
|
|
496
|
+
"",
|
|
497
|
+
`Status: ${resolvedManifest.status}`,
|
|
498
|
+
`Team: ${resolvedManifest.team}`,
|
|
499
|
+
`Workflow: ${resolvedManifest.workflow ?? "(none)"}`,
|
|
500
|
+
`Goal: ${resolvedManifest.goal}`,
|
|
501
|
+
`Usage: ${formatUsage(usage)}`,
|
|
502
|
+
"",
|
|
503
|
+
"## Tasks",
|
|
504
|
+
...resolvedTasks.map(formatTaskProgress),
|
|
505
|
+
"",
|
|
506
|
+
"## Effectiveness",
|
|
507
|
+
...runEffectivenessLines(resolvedManifest, tasks, input.executeWorkers, input.runtimeConfig),
|
|
508
|
+
"",
|
|
509
|
+
"## Policy decisions",
|
|
510
|
+
...(resolvedManifest.policyDecisions?.length ? summarizePolicyDecisions(resolvedManifest.policyDecisions) : ["- (none)"]),
|
|
511
|
+
"",
|
|
512
|
+
...scratchpadSummaryLines(resolvedManifest),
|
|
513
|
+
].join("\n"),
|
|
514
|
+
});
|
|
515
|
+
// Joint atomic save: wrap manifest + tasks in a single run lock so they are
|
|
516
|
+
// written together or not at all. Crash between separate saveRunManifestAsync
|
|
517
|
+
// and saveRunTasksAsync calls could leave manifest/tasks.json out of sync.
|
|
518
|
+
// R15-1: this is now the ONLY manifest+tasks write (the unlocked pre-lock
|
|
519
|
+
// saveRunManifestAsync was removed).
|
|
520
|
+
const final = {
|
|
521
|
+
...resolvedManifest,
|
|
522
|
+
updatedAt: new Date().toISOString(),
|
|
523
|
+
artifacts: [...resolvedManifest.artifacts, summaryArtifact],
|
|
524
|
+
};
|
|
525
|
+
await saveRunManifestAsync(final);
|
|
526
|
+
await saveRunTasksAsync(final, resolvedTasks);
|
|
527
|
+
return { manifest: final, tasks: resolvedTasks };
|
|
528
|
+
});
|
|
529
|
+
manifest = finalManifest.manifest;
|
|
530
|
+
const finalTasks = finalManifest.tasks;
|
|
531
|
+
// Save health snapshot on run completion.
|
|
532
|
+
// BUG A (pts/2 hang investigation 2026-06-16): stateRoot = `<crewRoot>/state/runs/<runId>`,
|
|
533
|
+
// so the crew root is THREE dirnames up, not two. Two dirnames gave `<crewRoot>/state`
|
|
534
|
+
// (the state dir), and HealthStore then joined HEALTH_DIR (`.crew/state/health`)
|
|
535
|
+
// onto it → `<crewRoot>/state/.crew/state/health` — a double-joined BOGUS path.
|
|
536
|
+
// That wrote health snapshots to a nonexistent subtree (silently breaking the
|
|
537
|
+
// health feature) AND created junk dirs that the recursive state watcher then
|
|
538
|
+
// attached extra inotify watches to. Fix: compute the real crew root (3 up)
|
|
539
|
+
// and make HEALTH_DIR relative to it.
|
|
540
|
+
const crewRoot = path.dirname(path.dirname(path.dirname(finalManifest.manifest.stateRoot)));
|
|
541
|
+
const healthStore = new HealthStore(crewRoot);
|
|
542
|
+
healthStore.saveSnapshot({
|
|
543
|
+
runId: finalManifest.manifest.runId,
|
|
544
|
+
tasks: finalTasks.map((t) => ({ id: t.id, status: t.status })),
|
|
545
|
+
createdAt: finalManifest.manifest.createdAt,
|
|
546
|
+
});
|
|
547
|
+
ctx.manifest = manifest;
|
|
548
|
+
ctx.tasks = finalTasks;
|
|
549
|
+
return { manifest, tasks: finalTasks };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** @internal RT-7 test export — verify cache is keyed on runId (stable string). */
|
|
553
|
+
export const __test__lastProgressContentHash = lastProgressContentHash;
|
|
554
|
+
/** @internal RT-7 test export — exercise writeProgress directly. */
|
|
555
|
+
export const __test__writeProgress = writeProgress;
|
|
556
|
+
/** @internal R15-1 test export — exercise finalizeRun directly (disk-terminal preservation). */
|
|
557
|
+
export const __test__finalizeRun = finalizeRun;
|