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
|
@@ -17,13 +17,39 @@ import { canTransitionTaskStatus, isTeamTaskStatus } from "../../../state/contra
|
|
|
17
17
|
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
18
18
|
import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../../state/coordination/task-claims.ts";
|
|
19
19
|
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
20
|
-
import { saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
20
|
+
import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
21
|
+
import type { TeamTaskState } from "../../../state/types.ts";
|
|
22
|
+
import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
|
|
21
23
|
import type { ApiHandlerContext, ApiOperationHandler } from "./handler-context.ts";
|
|
22
24
|
|
|
23
25
|
/** Find a task by id OR stepId — the convention used by every claim op. */
|
|
24
|
-
function findTaskByIdOrStepId(
|
|
26
|
+
function findTaskByIdOrStepId(tasks: TeamTaskState[], taskId: string | undefined) {
|
|
25
27
|
if (!taskId) return undefined;
|
|
26
|
-
return
|
|
28
|
+
return tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* R13-S1: fresh re-read INSIDE the lock (respond.ts:43 canonical pattern).
|
|
33
|
+
* `loaded.manifest` passed to withRunLockSync is used ONLY for lock-path
|
|
34
|
+
* derivation (locks.ts:596-598 — the lock does NOT re-read); the DATA
|
|
35
|
+
* (tasks, status) must always come from this fresh read.
|
|
36
|
+
*/
|
|
37
|
+
function freshRunOrMissing(
|
|
38
|
+
loaded: ApiHandlerContext["loaded"],
|
|
39
|
+
result: ApiHandlerContext["result"],
|
|
40
|
+
): { manifest: import("../../../state/types.ts").TeamRunManifest; tasks: TeamTaskState[] } | ReturnType<ApiHandlerContext["result"]> {
|
|
41
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, loaded.manifest.runId);
|
|
42
|
+
if (!fresh)
|
|
43
|
+
return result(
|
|
44
|
+
`Run '${loaded.manifest.runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
45
|
+
{
|
|
46
|
+
action: "api",
|
|
47
|
+
status: "error",
|
|
48
|
+
runId: loaded.manifest.runId,
|
|
49
|
+
},
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
return fresh;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
55
|
/** handleApi operation: `claim-task`. */
|
|
@@ -31,7 +57,7 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
|
|
|
31
57
|
const { cfg, loaded, result, paramRequired } = ctx;
|
|
32
58
|
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
33
59
|
const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
|
|
34
|
-
const task = findTaskByIdOrStepId(
|
|
60
|
+
const task = findTaskByIdOrStepId(loaded.tasks, taskId);
|
|
35
61
|
if (!task)
|
|
36
62
|
return result(
|
|
37
63
|
paramRequired(
|
|
@@ -48,13 +74,28 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
|
|
|
48
74
|
);
|
|
49
75
|
try {
|
|
50
76
|
return withRunLockSync(loaded.manifest, () => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
const fresh = freshRunOrMissing(loaded, result);
|
|
78
|
+
if (!("tasks" in fresh)) return fresh;
|
|
79
|
+
const freshTask = findTaskByIdOrStepId(fresh.tasks, taskId);
|
|
80
|
+
if (!freshTask)
|
|
81
|
+
return result(
|
|
82
|
+
`Task '${taskId}' not found in run '${fresh.manifest.runId}'.`,
|
|
83
|
+
{
|
|
84
|
+
action: "api",
|
|
85
|
+
status: "error",
|
|
86
|
+
runId: fresh.manifest.runId,
|
|
87
|
+
},
|
|
88
|
+
true,
|
|
89
|
+
);
|
|
90
|
+
// R13-S1: claim operates on the FRESH task — a task that became terminal on
|
|
91
|
+
// disk keeps its terminal status (no resurrection via the stale array).
|
|
92
|
+
const updatedTask = claimTask(freshTask, owner);
|
|
93
|
+
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
94
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
95
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
55
96
|
type: "task.claimed",
|
|
56
|
-
runId:
|
|
57
|
-
taskId:
|
|
97
|
+
runId: fresh.manifest.runId,
|
|
98
|
+
taskId: freshTask.id,
|
|
58
99
|
data: {
|
|
59
100
|
owner,
|
|
60
101
|
token: "[REDACTED]",
|
|
@@ -64,8 +105,8 @@ export const handleClaimTask: ApiOperationHandler = (ctx) => {
|
|
|
64
105
|
return result(JSON.stringify(updatedTask.claim, null, 2), {
|
|
65
106
|
action: "api",
|
|
66
107
|
status: "ok",
|
|
67
|
-
runId:
|
|
68
|
-
artifactsRoot:
|
|
108
|
+
runId: fresh.manifest.runId,
|
|
109
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
69
110
|
});
|
|
70
111
|
});
|
|
71
112
|
} catch (error) {
|
|
@@ -88,7 +129,7 @@ export const handleReleaseTaskClaim: ApiOperationHandler = (ctx) => {
|
|
|
88
129
|
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
89
130
|
const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
|
|
90
131
|
const token = typeof cfg.token === "string" ? cfg.token : undefined;
|
|
91
|
-
const task = findTaskByIdOrStepId(
|
|
132
|
+
const task = findTaskByIdOrStepId(loaded.tasks, taskId);
|
|
92
133
|
if (!task || !owner || !token)
|
|
93
134
|
return result(
|
|
94
135
|
paramRequired(
|
|
@@ -105,20 +146,33 @@ export const handleReleaseTaskClaim: ApiOperationHandler = (ctx) => {
|
|
|
105
146
|
);
|
|
106
147
|
try {
|
|
107
148
|
return withRunLockSync(loaded.manifest, () => {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
149
|
+
const fresh = freshRunOrMissing(loaded, result);
|
|
150
|
+
if (!("tasks" in fresh)) return fresh;
|
|
151
|
+
const freshTask = findTaskByIdOrStepId(fresh.tasks, taskId);
|
|
152
|
+
if (!freshTask)
|
|
153
|
+
return result(
|
|
154
|
+
`Task '${taskId}' not found in run '${fresh.manifest.runId}'.`,
|
|
155
|
+
{
|
|
156
|
+
action: "api",
|
|
157
|
+
status: "error",
|
|
158
|
+
runId: fresh.manifest.runId,
|
|
159
|
+
},
|
|
160
|
+
true,
|
|
161
|
+
);
|
|
162
|
+
const updatedTask = releaseTaskClaim(freshTask, owner, token);
|
|
163
|
+
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
164
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
165
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
112
166
|
type: "task.claim_released",
|
|
113
|
-
runId:
|
|
114
|
-
taskId:
|
|
167
|
+
runId: fresh.manifest.runId,
|
|
168
|
+
taskId: freshTask.id,
|
|
115
169
|
data: { owner },
|
|
116
170
|
});
|
|
117
171
|
return result(JSON.stringify(updatedTask, null, 2), {
|
|
118
172
|
action: "api",
|
|
119
173
|
status: "ok",
|
|
120
|
-
runId:
|
|
121
|
-
artifactsRoot:
|
|
174
|
+
runId: fresh.manifest.runId,
|
|
175
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
122
176
|
});
|
|
123
177
|
});
|
|
124
178
|
} catch (error) {
|
|
@@ -142,7 +196,7 @@ export const handleTransitionTaskStatus: ApiOperationHandler = (ctx) => {
|
|
|
142
196
|
const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
|
|
143
197
|
const token = typeof cfg.token === "string" ? cfg.token : undefined;
|
|
144
198
|
const to = cfg.status;
|
|
145
|
-
const task = findTaskByIdOrStepId(
|
|
199
|
+
const task = findTaskByIdOrStepId(loaded.tasks, taskId);
|
|
146
200
|
if (!task || !owner || !token || !isTeamTaskStatus(to))
|
|
147
201
|
return result(
|
|
148
202
|
paramRequired(
|
|
@@ -157,32 +211,47 @@ export const handleTransitionTaskStatus: ApiOperationHandler = (ctx) => {
|
|
|
157
211
|
},
|
|
158
212
|
true,
|
|
159
213
|
);
|
|
160
|
-
if (!canTransitionTaskStatus(task.status, to))
|
|
161
|
-
return result(
|
|
162
|
-
`Invalid task status transition: ${task.status} -> ${to}`,
|
|
163
|
-
{
|
|
164
|
-
action: "api",
|
|
165
|
-
status: "error",
|
|
166
|
-
runId: loaded.manifest.runId,
|
|
167
|
-
},
|
|
168
|
-
true,
|
|
169
|
-
);
|
|
170
214
|
try {
|
|
171
215
|
return withRunLockSync(loaded.manifest, () => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
216
|
+
const fresh = freshRunOrMissing(loaded, result);
|
|
217
|
+
if (!("tasks" in fresh)) return fresh;
|
|
218
|
+
const freshTask = findTaskByIdOrStepId(fresh.tasks, taskId);
|
|
219
|
+
if (!freshTask)
|
|
220
|
+
return result(
|
|
221
|
+
`Task '${taskId}' not found in run '${fresh.manifest.runId}'.`,
|
|
222
|
+
{
|
|
223
|
+
action: "api",
|
|
224
|
+
status: "error",
|
|
225
|
+
runId: fresh.manifest.runId,
|
|
226
|
+
},
|
|
227
|
+
true,
|
|
228
|
+
);
|
|
229
|
+
// R13-S1: validate the transition against FRESH task status — a task that
|
|
230
|
+
// became terminal on disk must be rejected (no terminal flip / no resurrection).
|
|
231
|
+
if (!canTransitionTaskStatus(freshTask.status, to))
|
|
232
|
+
return result(
|
|
233
|
+
`Invalid task status transition: ${freshTask.status} -> ${to}`,
|
|
234
|
+
{
|
|
235
|
+
action: "api",
|
|
236
|
+
status: "error",
|
|
237
|
+
runId: fresh.manifest.runId,
|
|
238
|
+
},
|
|
239
|
+
true,
|
|
240
|
+
);
|
|
241
|
+
const updatedTask = transitionClaimedTaskStatus(freshTask, owner, token, to);
|
|
242
|
+
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? updatedTask : item));
|
|
243
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
244
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
176
245
|
type: "task.status_transitioned",
|
|
177
|
-
runId:
|
|
178
|
-
taskId:
|
|
246
|
+
runId: fresh.manifest.runId,
|
|
247
|
+
taskId: freshTask.id,
|
|
179
248
|
data: { owner, status: to },
|
|
180
249
|
});
|
|
181
250
|
return result(JSON.stringify(updatedTask, null, 2), {
|
|
182
251
|
action: "api",
|
|
183
252
|
status: "ok",
|
|
184
|
-
runId:
|
|
185
|
-
artifactsRoot:
|
|
253
|
+
runId: fresh.manifest.runId,
|
|
254
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
186
255
|
});
|
|
187
256
|
});
|
|
188
257
|
} catch (error) {
|
|
@@ -25,12 +25,16 @@ import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
|
25
25
|
const RETRYABLE_STATUSES: ReadonlySet<string> = new Set(["failed", "cancelled"]);
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* Decision for `action='retry'`: a run whose manifest status is "completed"
|
|
29
|
+
* (terminal success) and has no retryable tasks has nothing to retry. Returns
|
|
30
|
+
* true so the caller short-circuits with a clear message instead of retrying.
|
|
31
|
+
*
|
|
32
|
+
* R13-3 fold: the caller MUST evaluate this on FRESH state read INSIDE the
|
|
33
|
+
* run lock — a pre-lock decision on a best-effort snapshot could wrongly
|
|
34
|
+
* refuse a run that was re-queued/failed on disk (finding #4,
|
|
35
|
+
* real-test-2026-08-10-full-9-tier). Kept as a short-circuit so a completed
|
|
36
|
+
* async run does not surface a misleading "run.lock is locked by another
|
|
37
|
+
* operation" error from a stale lock file left behind by that run.
|
|
34
38
|
*
|
|
35
39
|
* Exported for unit testing (handleRetry itself needs filesystem state).
|
|
36
40
|
*/
|
|
@@ -148,22 +152,43 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
|
|
|
148
152
|
|
|
149
153
|
const targetTaskId = typeof params.taskId === "string" ? params.taskId : undefined;
|
|
150
154
|
|
|
151
|
-
// Pre-lock terminal-status check: a completed run has nothing to retry.
|
|
152
|
-
// Short-circuit BEFORE acquiring the run lock so a stale lock file left by a
|
|
153
|
-
// completed async run does not surface a misleading "run.lock is locked by
|
|
154
|
-
// another operation" error (finding #4 in real-test-2026-08-10-full-9-tier).
|
|
155
|
-
if (retryShortCircuitsCompleted(loaded.manifest.status, loaded.tasks, targetTaskId)) {
|
|
156
|
-
return result(
|
|
157
|
-
`Run ${loaded.manifest.runId} is already completed; retry only applies to failed/cancelled runs.`,
|
|
158
|
-
{ action: "retry", status: "error", runId: loaded.manifest.runId },
|
|
159
|
-
true,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
155
|
return withRunLockSync(loaded.manifest, () => {
|
|
156
|
+
// R13-1 (2026-08-14): fresh re-read INSIDE the lock. `loaded` was captured
|
|
157
|
+
// lock-free BEFORE the unbounded before_retry hook gap; a concurrent writer
|
|
158
|
+
// may have completed the run or re-queued tasks on disk. All decisions and
|
|
159
|
+
// writes below derive from `fresh` — never from the stale `loaded` snapshot.
|
|
160
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!); // NOTE: inside withRunLockSync - consistent read
|
|
161
|
+
if (!fresh) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "retry", status: "error" }, true);
|
|
162
|
+
|
|
163
|
+
// R15-S1 (2026-08-14): in-lock ownership re-check on the FRESH manifest.
|
|
164
|
+
// The pre-lock check above ran on the `loaded` snapshot captured BEFORE
|
|
165
|
+
// the unbounded before_retry hook gap; ownership may have changed on disk
|
|
166
|
+
// in that window. This in-lock check is authoritative — the pre-lock check
|
|
167
|
+
// remains only as the fast-path UX message.
|
|
168
|
+
if (typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId && params.force !== true) {
|
|
169
|
+
return result(
|
|
170
|
+
`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`,
|
|
171
|
+
{ action: "retry", status: "error", runId: fresh.manifest.runId },
|
|
172
|
+
true,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Terminal-status check on FRESH in-lock state (R13-3 fold): a completed
|
|
177
|
+
// run has nothing to retry. Short-circuit with a clear message instead of
|
|
178
|
+
// surfacing a misleading "run.lock is locked by another operation" error
|
|
179
|
+
// from a stale lock file left by a completed async run (finding #4 in
|
|
180
|
+
// real-test-2026-08-10-full-9-tier).
|
|
181
|
+
if (retryShortCircuitsCompleted(fresh.manifest.status, fresh.tasks, targetTaskId)) {
|
|
182
|
+
return result(
|
|
183
|
+
`Run ${fresh.manifest.runId} is already completed; retry only applies to failed/cancelled runs.`,
|
|
184
|
+
{ action: "retry", status: "error", runId: fresh.manifest.runId },
|
|
185
|
+
true,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
const retryableStatuses: ReadonlySet<string> = new Set(["failed", "cancelled"]);
|
|
165
190
|
|
|
166
|
-
const matchingTasks =
|
|
191
|
+
const matchingTasks = fresh.tasks.filter((task) => {
|
|
167
192
|
if (targetTaskId && task.id !== targetTaskId) return false;
|
|
168
193
|
return retryableStatuses.has(task.status);
|
|
169
194
|
});
|
|
@@ -174,26 +199,26 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
|
|
|
174
199
|
{
|
|
175
200
|
action: "retry",
|
|
176
201
|
status: "error",
|
|
177
|
-
runId:
|
|
202
|
+
runId: fresh.manifest.runId,
|
|
178
203
|
},
|
|
179
204
|
true,
|
|
180
205
|
);
|
|
181
206
|
}
|
|
182
207
|
|
|
183
208
|
const retriedIds = new Set(matchingTasks.map((t) => t.id));
|
|
184
|
-
const tasks =
|
|
209
|
+
const tasks = fresh.tasks.map((task) => {
|
|
185
210
|
if (!retriedIds.has(task.id)) return task;
|
|
186
211
|
const { error: _error, finishedAt: _finishedAt, terminalEvidence: _terminalEvidence, ...rest } = task;
|
|
187
212
|
return { ...rest, status: "queued" as const };
|
|
188
213
|
});
|
|
189
|
-
saveRunTasks(
|
|
214
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
190
215
|
try {
|
|
191
216
|
saveCrewAgents(
|
|
192
|
-
|
|
193
|
-
tasks.map((task) => recordFromTask(
|
|
217
|
+
fresh.manifest,
|
|
218
|
+
tasks.map((task) => recordFromTask(fresh.manifest, task, "child-process")),
|
|
194
219
|
);
|
|
195
220
|
} catch (error) {
|
|
196
|
-
logInternalError("team-tool.handleRetry.crewAgents", error, `runId=${
|
|
221
|
+
logInternalError("team-tool.handleRetry.crewAgents", error, `runId=${fresh.manifest.runId}`);
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
const retriedTaskIds = [...retriedIds];
|
|
@@ -201,27 +226,27 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
|
|
|
201
226
|
// H1 (2026-08-10): inside a sync run-lock callback — cannot await;
|
|
202
227
|
// fire-and-forget async. task.retried is informational; the queued
|
|
203
228
|
// status is the authoritative record in tasks.json.
|
|
204
|
-
void appendEventAsync(
|
|
229
|
+
void appendEventAsync(fresh.manifest.eventsPath, {
|
|
205
230
|
type: "task.retried",
|
|
206
|
-
runId:
|
|
231
|
+
runId: fresh.manifest.runId,
|
|
207
232
|
taskId,
|
|
208
233
|
message: `Task ${taskId} queued for retry.`,
|
|
209
234
|
}).catch((error) =>
|
|
210
235
|
logInternalError(
|
|
211
236
|
"cancel.retry-event",
|
|
212
237
|
error instanceof Error ? error : new Error(String(error)),
|
|
213
|
-
`runId=${
|
|
238
|
+
`runId=${fresh.manifest.runId}`,
|
|
214
239
|
),
|
|
215
240
|
);
|
|
216
241
|
}
|
|
217
242
|
|
|
218
|
-
if (deps) invalidateSnapshot(
|
|
219
|
-
return result(`Retried ${retriedTaskIds.length} task(s) in run ${
|
|
243
|
+
if (deps) invalidateSnapshot(fresh.manifest.runId, runCwd, deps);
|
|
244
|
+
return result(`Retried ${retriedTaskIds.length} task(s) in run ${fresh.manifest.runId}.`, {
|
|
220
245
|
action: "retry",
|
|
221
246
|
status: "ok",
|
|
222
|
-
runId:
|
|
247
|
+
runId: fresh.manifest.runId,
|
|
223
248
|
retriedTaskIds: retriedTaskIds,
|
|
224
|
-
intent: `retrying ${retriedTaskIds.length} task(s) in ${
|
|
249
|
+
intent: `retrying ${retriedTaskIds.length} task(s) in ${fresh.manifest.runId}`,
|
|
225
250
|
});
|
|
226
251
|
});
|
|
227
252
|
}
|
|
@@ -290,26 +315,35 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
return withRunLockSync(loaded.manifest, () => {
|
|
293
|
-
|
|
318
|
+
// R13-2 (2026-08-14): fresh re-read INSIDE the lock. `loaded` was captured
|
|
319
|
+
// lock-free BEFORE the before_cancel hook + live-agent termination gap; a
|
|
320
|
+
// concurrent writer may have completed the run on disk. The terminal-status
|
|
321
|
+
// short-circuit and ALL writes below derive from `fresh` — never flip a
|
|
322
|
+
// terminal status (completed → cancelled) from the stale `loaded` snapshot.
|
|
323
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, loaded.manifest.runId); // NOTE: inside withRunLockSync - consistent read
|
|
324
|
+
if (!fresh)
|
|
325
|
+
return result(`Run '${loaded.manifest.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "cancel", status: "error" }, true);
|
|
326
|
+
|
|
327
|
+
if ((fresh.manifest.status === "completed" || fresh.manifest.status === "cancelled") && params.force !== true)
|
|
294
328
|
return result(
|
|
295
|
-
`Run ${
|
|
329
|
+
`Run ${fresh.manifest.runId} is already ${fresh.manifest.status}; nothing to cancel. Use force: true to mark it cancelled anyway.`,
|
|
296
330
|
{
|
|
297
331
|
action: "cancel",
|
|
298
332
|
status: "ok",
|
|
299
|
-
runId:
|
|
300
|
-
artifactsRoot:
|
|
333
|
+
runId: fresh.manifest.runId,
|
|
334
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
301
335
|
},
|
|
302
336
|
);
|
|
303
337
|
|
|
304
338
|
// Classify tasks for foreign-aware cancellation
|
|
305
|
-
const abortResult = abortOwned(
|
|
339
|
+
const abortResult = abortOwned(fresh.manifest.runId, undefined, ctx, params.force);
|
|
306
340
|
if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0 && params.force !== true) {
|
|
307
341
|
return result(
|
|
308
|
-
`Run ${
|
|
342
|
+
`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`,
|
|
309
343
|
{
|
|
310
344
|
action: "cancel",
|
|
311
345
|
status: "error",
|
|
312
|
-
runId:
|
|
346
|
+
runId: fresh.manifest.runId,
|
|
313
347
|
foreignIds: abortResult.foreignIds,
|
|
314
348
|
},
|
|
315
349
|
true,
|
|
@@ -321,7 +355,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
321
355
|
const cancelData = cancelIntent ? { reason: cancelReason.code, intent: cancelIntent } : { reason: cancelReason.code };
|
|
322
356
|
const cancelMessage = `${cancelReason.message} (${cancelReason.code})`;
|
|
323
357
|
|
|
324
|
-
const tasks =
|
|
358
|
+
const tasks = fresh.tasks.map((task) => {
|
|
325
359
|
if (cancellableIds.has(task.id) && (task.status === "queued" || task.status === "running" || task.status === "waiting")) {
|
|
326
360
|
const base = {
|
|
327
361
|
...task,
|
|
@@ -342,32 +376,32 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
342
376
|
}
|
|
343
377
|
return task;
|
|
344
378
|
});
|
|
345
|
-
saveRunTasks(
|
|
379
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
346
380
|
try {
|
|
347
381
|
saveCrewAgents(
|
|
348
|
-
|
|
349
|
-
tasks.map((task) => recordFromTask(
|
|
382
|
+
fresh.manifest,
|
|
383
|
+
tasks.map((task) => recordFromTask(fresh.manifest, task, fresh.manifest.runtimeResolution?.kind ?? "child-process")),
|
|
350
384
|
);
|
|
351
385
|
} catch (error) {
|
|
352
|
-
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${
|
|
386
|
+
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${fresh.manifest.runId}`);
|
|
353
387
|
}
|
|
354
388
|
try {
|
|
355
|
-
writeForegroundInterruptRequest(
|
|
389
|
+
writeForegroundInterruptRequest(fresh.manifest, cancelMessage);
|
|
356
390
|
} catch (error) {
|
|
357
|
-
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${
|
|
391
|
+
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${fresh.manifest.runId}`);
|
|
358
392
|
}
|
|
359
|
-
ctx.abortForegroundRun?.(
|
|
393
|
+
ctx.abortForegroundRun?.(fresh.manifest.runId);
|
|
360
394
|
for (const taskId of abortResult.abortedIds) {
|
|
361
|
-
appendEvent(
|
|
395
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
362
396
|
type: "task.cancelled",
|
|
363
|
-
runId:
|
|
397
|
+
runId: fresh.manifest.runId,
|
|
364
398
|
taskId,
|
|
365
399
|
message: cancelMessage,
|
|
366
400
|
data: cancelData,
|
|
367
401
|
});
|
|
368
402
|
}
|
|
369
403
|
const updated = updateRunStatus(
|
|
370
|
-
|
|
404
|
+
fresh.manifest,
|
|
371
405
|
"cancelled",
|
|
372
406
|
`${cancelMessage} Already-finished worker processes are not retroactively changed.`,
|
|
373
407
|
{ data: cancelData },
|
|
@@ -19,6 +19,7 @@ import { handleGoal } from "../goal.ts";
|
|
|
19
19
|
import { handleOrchestrate } from "../orchestrate.ts";
|
|
20
20
|
import { handleParallel } from "../parallel-dispatch.ts";
|
|
21
21
|
import { handlePlan } from "../plan.ts";
|
|
22
|
+
import { handlePlans } from "../plans.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Actions owned by the Run domain. Single source of truth for the switch
|
|
@@ -27,7 +28,7 @@ import { handlePlan } from "../plan.ts";
|
|
|
27
28
|
* the `default` branch errors if a RunDomainAction is added here without a
|
|
28
29
|
* matching `case`.
|
|
29
30
|
*/
|
|
30
|
-
export const RUN_DOMAIN_ACTIONS = ["run", "parallel", "plan", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
|
|
31
|
+
export const RUN_DOMAIN_ACTIONS = ["run", "parallel", "plan", "plans", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
|
|
31
32
|
type RunDomainAction = (typeof RUN_DOMAIN_ACTIONS)[number];
|
|
32
33
|
|
|
33
34
|
export async function handleRunDomain(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
@@ -40,6 +41,8 @@ export async function handleRunDomain(params: TeamToolParamsValue, ctx: TeamCont
|
|
|
40
41
|
return handleParallel(params, ctx);
|
|
41
42
|
case "plan":
|
|
42
43
|
return handlePlan(params, ctx);
|
|
44
|
+
case "plans":
|
|
45
|
+
return handlePlans(params, ctx);
|
|
43
46
|
case "orchestrate":
|
|
44
47
|
return handleOrchestrate(params, ctx);
|
|
45
48
|
case "resume":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
4
|
+
import { allAgents, discoverAgents, getSecurityEventLog } from "../../agents/discover-agents.ts";
|
|
5
5
|
import { loadConfig } from "../../config/config.ts";
|
|
6
6
|
import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
7
7
|
import { type DriftReport, detectDrift, formatDriftReport } from "../../config/drift-detector.ts";
|
|
@@ -14,6 +14,7 @@ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
|
14
14
|
import { TeamToolParams } from "../../schema/team-tool-schema.ts";
|
|
15
15
|
import { atomicWriteFile } from "../../state/atomic-write.ts";
|
|
16
16
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
17
|
+
import { type FatalFsCause, fsFailureLabel } from "../../utils/fs-errno.ts";
|
|
17
18
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
18
19
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
19
20
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
@@ -26,6 +27,71 @@ interface DoctorCheck {
|
|
|
26
27
|
detail: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** bug-026 sub-issue B: how many of the most recent runs to scan for tasks
|
|
31
|
+
* carrying a fatal-fs failureCause. Bounded so doctor stays fast on projects
|
|
32
|
+
* with long run histories. */
|
|
33
|
+
const FS_FAILURE_SCAN_RUN_LIMIT = 10;
|
|
34
|
+
|
|
35
|
+
function relativeTimeAgo(iso: string): string {
|
|
36
|
+
const ms = Date.now() - Date.parse(iso);
|
|
37
|
+
if (!Number.isFinite(ms)) return iso;
|
|
38
|
+
const minutes = Math.floor(ms / 60_000);
|
|
39
|
+
if (minutes < 1) return "just now";
|
|
40
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
41
|
+
const hours = Math.floor(minutes / 60);
|
|
42
|
+
if (hours < 24) return `${hours}h ago`;
|
|
43
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** bug-026 sub-issue B: scan the most recent runs (by mtime, capped) for
|
|
47
|
+
* tasks with a fatal-fs failureCause (enospc/edquot/emfile/enfile). Reads
|
|
48
|
+
* tasks.json directly (no state-store import) and never throws — doctor is
|
|
49
|
+
* a diagnostic tool; unreadable runs are simply skipped. */
|
|
50
|
+
function scanRecentFsFailureCauses(cwd: string): string {
|
|
51
|
+
const runsRoot = path.join(projectCrewRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
|
|
52
|
+
let recentRunIds: string[];
|
|
53
|
+
try {
|
|
54
|
+
recentRunIds = fs
|
|
55
|
+
.readdirSync(runsRoot, { withFileTypes: true })
|
|
56
|
+
.filter((entry) => entry.isDirectory())
|
|
57
|
+
.map((entry) => {
|
|
58
|
+
let mtimeMs = 0;
|
|
59
|
+
try {
|
|
60
|
+
mtimeMs = fs.statSync(path.join(runsRoot, entry.name)).mtimeMs;
|
|
61
|
+
} catch {
|
|
62
|
+
/* unreadable run dir — mtime 0 sorts last */
|
|
63
|
+
}
|
|
64
|
+
return { runId: entry.name, mtimeMs };
|
|
65
|
+
})
|
|
66
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
67
|
+
.slice(0, FS_FAILURE_SCAN_RUN_LIMIT)
|
|
68
|
+
.map((entry) => entry.runId);
|
|
69
|
+
} catch {
|
|
70
|
+
return "no run history";
|
|
71
|
+
}
|
|
72
|
+
let count = 0;
|
|
73
|
+
let last: { runId: string; cause: FatalFsCause; finishedAt?: string } | undefined;
|
|
74
|
+
for (const runId of recentRunIds) {
|
|
75
|
+
let tasks: unknown;
|
|
76
|
+
try {
|
|
77
|
+
tasks = JSON.parse(fs.readFileSync(path.join(runsRoot, runId, "tasks.json"), "utf-8"));
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(tasks)) continue;
|
|
82
|
+
for (const task of tasks) {
|
|
83
|
+
const failureCause = (task as { failureCause?: FatalFsCause }).failureCause;
|
|
84
|
+
if (!failureCause) continue;
|
|
85
|
+
count += 1;
|
|
86
|
+
const finishedAt = (task as { finishedAt?: string }).finishedAt;
|
|
87
|
+
if (!last || (finishedAt ?? "") > (last.finishedAt ?? "")) last = { runId, cause: failureCause, finishedAt };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (count === 0) return `none in last ${FS_FAILURE_SCAN_RUN_LIMIT} runs`;
|
|
91
|
+
const when = last?.finishedAt ? `, ${relativeTimeAgo(last.finishedAt)}` : "";
|
|
92
|
+
return `${count} task(s) in last ${FS_FAILURE_SCAN_RUN_LIMIT} runs (last: ${last?.runId}${when}, ${fsFailureLabel(last!.cause)})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
29
95
|
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
30
96
|
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
31
97
|
return (
|
|
@@ -43,6 +109,10 @@ function firstOutputLine(stdout: string | null | undefined, stderr: string | nul
|
|
|
43
109
|
// file with 12 tests would take 20s+ even with empty cwd. The cache is
|
|
44
110
|
// safe: a doctor check is informational, and a stale ok=true would
|
|
45
111
|
// self-correct on the next process restart.
|
|
112
|
+
// MISSED-2 (R5): FIFO cap so the memo cannot grow unbounded. Naturally
|
|
113
|
+
// bounded by ~10-20 probed commands; the cap is a safety net (Map preserves
|
|
114
|
+
// insertion order).
|
|
115
|
+
const MAX_COMMAND_EXISTS_CACHE = 128;
|
|
46
116
|
const commandExistsCache = new Map<string, { ok: boolean; detail: string }>();
|
|
47
117
|
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
48
118
|
const cacheKey = `${command} ${args.join(" ")}`;
|
|
@@ -74,6 +144,11 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
74
144
|
};
|
|
75
145
|
}
|
|
76
146
|
commandExistsCache.set(cacheKey, result);
|
|
147
|
+
// MISSED-2 (R5): FIFO eviction — drop the oldest entry past the cap.
|
|
148
|
+
if (commandExistsCache.size > MAX_COMMAND_EXISTS_CACHE) {
|
|
149
|
+
const oldest = commandExistsCache.keys().next().value;
|
|
150
|
+
if (oldest !== undefined) commandExistsCache.delete(oldest);
|
|
151
|
+
}
|
|
77
152
|
return result;
|
|
78
153
|
}
|
|
79
154
|
|
|
@@ -235,6 +310,14 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
|
|
|
235
310
|
ok: true,
|
|
236
311
|
detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir),
|
|
237
312
|
},
|
|
313
|
+
{
|
|
314
|
+
// bug-026 sub-issue B: INFORMATIONAL (ok always true) — a historical
|
|
315
|
+
// disk-full incident must not permanently fail doctor. The line makes
|
|
316
|
+
// fs failureCauses discoverable without digging through run logs.
|
|
317
|
+
label: "fs failure causes",
|
|
318
|
+
ok: true,
|
|
319
|
+
detail: scanRecentFsFailureCauses(input.cwd),
|
|
320
|
+
},
|
|
238
321
|
];
|
|
239
322
|
}),
|
|
240
323
|
section("Model Routing", () => {
|
|
@@ -393,6 +476,25 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
|
|
|
393
476
|
return checks;
|
|
394
477
|
}),
|
|
395
478
|
];
|
|
479
|
+
// R7-13: surface security telemetry. The event log was previously
|
|
480
|
+
// write-only (0 production readers); doctor now reports a compact summary
|
|
481
|
+
// — total events plus the latest event's time and scope. Emitted only
|
|
482
|
+
// when events exist (cheap single line, no per-event dump). A non-empty
|
|
483
|
+
// log is informational, not a failure: blocked registrations mean the
|
|
484
|
+
// SEC-001 protection fired.
|
|
485
|
+
const securityEvents = getSecurityEventLog();
|
|
486
|
+
const lastSecurityEvent = securityEvents.at(-1);
|
|
487
|
+
if (lastSecurityEvent) {
|
|
488
|
+
sections.push(
|
|
489
|
+
section("Security", () => [
|
|
490
|
+
{
|
|
491
|
+
label: "security events",
|
|
492
|
+
ok: true,
|
|
493
|
+
detail: `${securityEvents.length} logged; last ${lastSecurityEvent.type} agent="${lastSecurityEvent.name}" at ${new Date(lastSecurityEvent.timestamp).toISOString()}`,
|
|
494
|
+
},
|
|
495
|
+
]),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
396
498
|
if (input.smokeChildPi) {
|
|
397
499
|
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
398
500
|
}
|