pi-crew 0.1.51 → 0.2.0
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 +56 -1
- package/README.md +176 -781
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +70 -11
- package/agents/writer.md +11 -11
- package/docs/actions-reference.md +595 -0
- package/docs/commands-reference.md +347 -0
- package/docs/runtime-flow.md +148 -148
- package/index.ts +6 -6
- package/package.json +99 -99
- package/skills/async-worker-recovery/SKILL.md +42 -42
- package/skills/context-artifact-hygiene/SKILL.md +52 -52
- package/skills/delegation-patterns/SKILL.md +54 -54
- package/skills/mailbox-interactive/SKILL.md +40 -40
- package/skills/model-routing-context/SKILL.md +39 -39
- package/skills/multi-perspective-review/SKILL.md +58 -58
- package/skills/observability-reliability/SKILL.md +41 -41
- package/skills/orchestration/SKILL.md +157 -157
- package/skills/ownership-session-security/SKILL.md +41 -41
- package/skills/pi-extension-lifecycle/SKILL.md +39 -39
- package/skills/requirements-to-task-packet/SKILL.md +63 -63
- package/skills/resource-discovery-config/SKILL.md +41 -41
- package/skills/runtime-state-reader/SKILL.md +44 -44
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
- package/skills/state-mutation-locking/SKILL.md +42 -42
- package/skills/systematic-debugging/SKILL.md +67 -67
- package/skills/ui-render-performance/SKILL.md +39 -39
- package/skills/verification-before-done/SKILL.md +57 -57
- package/skills/worktree-isolation/SKILL.md +39 -39
- package/src/adapters/claude-adapter.ts +25 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +137 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/agent-search.ts +98 -98
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +13 -1
- package/src/config/drift-detector.ts +211 -0
- package/src/config/markers.ts +327 -0
- package/src/config/resilient-parser.ts +108 -0
- package/src/config/suggestions.ts +74 -0
- package/src/extension/cross-extension-rpc.ts +103 -94
- package/src/extension/project-init.ts +21 -1
- package/src/extension/register.ts +45 -14
- package/src/extension/registration/commands.ts +77 -8
- package/src/extension/registration/subagent-tools.ts +10 -1
- package/src/extension/registration/team-tool.ts +10 -1
- package/src/extension/registration/viewers.ts +48 -34
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-import.ts +25 -1
- package/src/extension/run-index.ts +5 -1
- package/src/extension/run-maintenance.ts +142 -68
- package/src/extension/team-manager-command.ts +10 -1
- package/src/extension/team-tool/doctor.ts +28 -3
- package/src/extension/team-tool/handle-settings.ts +195 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/intent-policy.ts +42 -42
- package/src/extension/team-tool/lifecycle-actions.ts +27 -8
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +12 -1
- package/src/extension/team-tool.ts +11 -1
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +92 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -0
- package/src/runtime/background-runner.ts +11 -1
- package/src/runtime/cancellation-token.ts +89 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi.ts +7 -2
- package/src/runtime/compaction-summary.ts +271 -0
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +33 -0
- package/src/runtime/delta-conflict.ts +360 -0
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/iteration-hooks.ts +262 -0
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-extension-bridge.ts +150 -150
- package/src/runtime/live-irc.ts +92 -92
- package/src/runtime/live-session-health.ts +100 -100
- package/src/runtime/loop-gates.ts +129 -0
- package/src/runtime/metric-parser.ts +40 -0
- package/src/runtime/notebook-helpers.ts +90 -90
- package/src/runtime/orphan-sentinel.ts +7 -7
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/pi-args.ts +38 -11
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +57 -7
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +122 -0
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/prose-compressor.ts +164 -164
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/result-extractor.ts +121 -121
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/sensitive-paths.ts +2 -2
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stream-preview.ts +177 -177
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph.ts +207 -0
- package/src/runtime/task-quality.ts +207 -0
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +7 -1
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +117 -7
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -0
- package/src/runtime/workspace-tree.ts +298 -298
- package/src/schema/config-schema.ts +11 -0
- package/src/schema/validation-types.ts +148 -0
- package/src/skills/skill-templates.ts +374 -0
- package/src/state/active-run-registry.ts +35 -11
- package/src/state/atomic-write.ts +33 -26
- package/src/state/contracts.ts +1 -0
- package/src/state/event-reconstructor.ts +217 -0
- package/src/state/locks.ts +2 -13
- package/src/state/mailbox.ts +4 -3
- package/src/state/state-store.ts +32 -14
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +9 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +5 -2
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +11 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-coalescer.ts +51 -51
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-action-dispatcher.ts +10 -1
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-entries.ts +258 -258
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sleep.ts +40 -26
- package/src/utils/task-name-generator.ts +337 -337
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +30 -29
- package/workflows/fast-fix.workflow.md +23 -22
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/docs/refactor-tasks-phase3.md +0 -394
- package/docs/refactor-tasks-phase4.md +0 -564
- package/docs/refactor-tasks-phase5.md +0 -402
- package/docs/refactor-tasks-phase6.md +0 -662
- package/docs/refactor-tasks.md +0 -1484
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
- package/docs/research/AUDIT_OH_MY_PI.md +0 -261
- package/docs/research/AUDIT_PI_CREW.md +0 -457
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
- package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
- package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
- package/docs/research-awesome-agent-skills-distillation.md +0 -100
- package/docs/research-extension-examples.md +0 -297
- package/docs/research-extension-system.md +0 -324
- package/docs/research-oh-my-pi-distillation.md +0 -369
- package/docs/research-optimization-plan.md +0 -548
- package/docs/research-phase10-distillation.md +0 -199
- package/docs/research-phase11-distillation.md +0 -201
- package/docs/research-phase8-operator-experience-plan.md +0 -819
- package/docs/research-phase9-observability-reliability-plan.md +0 -1190
- package/docs/research-pi-coding-agent.md +0 -357
- package/docs/research-source-pi-crew-reference.md +0 -174
- package/docs/research-ui-optimization-plan.md +0 -480
- package/docs/source-runtime-refactor-map.md +0 -107
- package/src/utils/atomic-write.ts +0 -33
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
-
import type { MailboxMessage } from "../../state/mailbox.ts";
|
|
3
|
-
import type { ArtifactDescriptor } from "../../state/types.ts";
|
|
4
|
-
|
|
5
|
-
export interface RunProjectionSource {
|
|
6
|
-
kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
|
|
7
|
-
bounded: boolean;
|
|
8
|
-
reference?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface RunProjectionResult {
|
|
12
|
-
sources: RunProjectionSource[];
|
|
13
|
-
summary: string;
|
|
14
|
-
injectedAsContext: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Transform run context before a worker starts.
|
|
19
|
-
* Builds a bounded projection of durable history that will be available
|
|
20
|
-
* to the worker as reference context, not as instructions.
|
|
21
|
-
*
|
|
22
|
-
* Rules:
|
|
23
|
-
* - Durable history retains events, mailbox, artifacts, UI/runtime metadata.
|
|
24
|
-
* - Worker prompt gets a bounded projection (truncated/summarized).
|
|
25
|
-
* - UI/runtime events are not prompt text unless explicitly selected.
|
|
26
|
-
*/
|
|
27
|
-
export function transformRunContextBeforeWorkerStart(input: {
|
|
28
|
-
manifest: TeamRunManifest;
|
|
29
|
-
tasks: TeamTaskState[];
|
|
30
|
-
pendingMailbox: MailboxMessage[];
|
|
31
|
-
artifacts: ArtifactDescriptor[];
|
|
32
|
-
maxEvents?: number;
|
|
33
|
-
maxMailboxMessages?: number;
|
|
34
|
-
maxArtifactRefs?: number;
|
|
35
|
-
}): RunProjectionResult {
|
|
36
|
-
const maxEvents = input.maxEvents ?? 20;
|
|
37
|
-
const maxMailbox = input.maxMailboxMessages ?? 10;
|
|
38
|
-
const maxArtifacts = input.maxArtifactRefs ?? 15;
|
|
39
|
-
|
|
40
|
-
const sources: RunProjectionSource[] = [];
|
|
41
|
-
const lines: string[] = [];
|
|
42
|
-
|
|
43
|
-
// Project a bounded slice of task history
|
|
44
|
-
const completedTasks = input.tasks.filter((t) => t.status === "completed" || t.status === "failed");
|
|
45
|
-
if (completedTasks.length > 0) {
|
|
46
|
-
const tasks = completedTasks.slice(0, maxEvents);
|
|
47
|
-
sources.push({ kind: "events", bounded: true, reference: `tasks:${tasks.length}/${completedTasks.length}` });
|
|
48
|
-
lines.push(`Previous tasks (${tasks.length}/${completedTasks.length}):`);
|
|
49
|
-
for (const task of tasks) {
|
|
50
|
-
lines.push(`- ${task.id}: ${task.status}${task.error ? ` (${task.error})` : ""}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Project pending mailbox that is relevant to this worker
|
|
55
|
-
if (input.pendingMailbox.length > 0) {
|
|
56
|
-
const messages = input.pendingMailbox.slice(0, maxMailbox);
|
|
57
|
-
sources.push({ kind: "mailbox", bounded: true, reference: `mailbox:${messages.length}/${input.pendingMailbox.length}` });
|
|
58
|
-
lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
|
|
59
|
-
for (const msg of messages) {
|
|
60
|
-
lines.push(`- ${msg.kind ?? "message"}: ${msg.body.slice(0, 100)}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Project artifact references (not content)
|
|
65
|
-
if (input.artifacts.length > 0) {
|
|
66
|
-
const artifacts = input.artifacts.slice(0, maxArtifacts);
|
|
67
|
-
sources.push({ kind: "artifacts", bounded: true, reference: `artifacts:${artifacts.length}/${input.artifacts.length}` });
|
|
68
|
-
lines.push(`Available artifacts (${artifacts.length}/${input.artifacts.length}):`);
|
|
69
|
-
for (const art of artifacts) {
|
|
70
|
-
lines.push(`- ${art.kind} (${art.producer})`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Metadata markers — not injected as prompt instructions
|
|
75
|
-
sources.push({ kind: "ui_metadata", bounded: false, reference: "excluded_from_prompt" });
|
|
76
|
-
sources.push({ kind: "runtime_metadata", bounded: false, reference: "excluded_from_prompt" });
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
sources,
|
|
80
|
-
summary: lines.join("\n"),
|
|
81
|
-
injectedAsContext: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Convert run history to a bounded worker prompt section.
|
|
87
|
-
* Same logic as transformRunContextBeforeWorkerStart but returns
|
|
88
|
-
* the prompt text directly for embedding in the worker prompt.
|
|
89
|
-
*/
|
|
90
|
-
export function convertRunHistoryToWorkerPrompt(input: {
|
|
91
|
-
manifest: TeamRunManifest;
|
|
92
|
-
tasks: TeamTaskState[];
|
|
93
|
-
pendingMailbox: MailboxMessage[];
|
|
94
|
-
artifacts: ArtifactDescriptor[];
|
|
95
|
-
}): string {
|
|
96
|
-
const projection = transformRunContextBeforeWorkerStart(input);
|
|
97
|
-
if (!projection.summary) return "";
|
|
98
|
-
return [
|
|
99
|
-
"## Run Context (bounded projection)",
|
|
100
|
-
projection.summary,
|
|
101
|
-
"",
|
|
102
|
-
`Projection sources: ${projection.sources.map((s) => s.kind).join(", ")}`,
|
|
103
|
-
].join("\n");
|
|
1
|
+
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
+
import type { MailboxMessage } from "../../state/mailbox.ts";
|
|
3
|
+
import type { ArtifactDescriptor } from "../../state/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface RunProjectionSource {
|
|
6
|
+
kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
|
|
7
|
+
bounded: boolean;
|
|
8
|
+
reference?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RunProjectionResult {
|
|
12
|
+
sources: RunProjectionSource[];
|
|
13
|
+
summary: string;
|
|
14
|
+
injectedAsContext: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transform run context before a worker starts.
|
|
19
|
+
* Builds a bounded projection of durable history that will be available
|
|
20
|
+
* to the worker as reference context, not as instructions.
|
|
21
|
+
*
|
|
22
|
+
* Rules:
|
|
23
|
+
* - Durable history retains events, mailbox, artifacts, UI/runtime metadata.
|
|
24
|
+
* - Worker prompt gets a bounded projection (truncated/summarized).
|
|
25
|
+
* - UI/runtime events are not prompt text unless explicitly selected.
|
|
26
|
+
*/
|
|
27
|
+
export function transformRunContextBeforeWorkerStart(input: {
|
|
28
|
+
manifest: TeamRunManifest;
|
|
29
|
+
tasks: TeamTaskState[];
|
|
30
|
+
pendingMailbox: MailboxMessage[];
|
|
31
|
+
artifacts: ArtifactDescriptor[];
|
|
32
|
+
maxEvents?: number;
|
|
33
|
+
maxMailboxMessages?: number;
|
|
34
|
+
maxArtifactRefs?: number;
|
|
35
|
+
}): RunProjectionResult {
|
|
36
|
+
const maxEvents = input.maxEvents ?? 20;
|
|
37
|
+
const maxMailbox = input.maxMailboxMessages ?? 10;
|
|
38
|
+
const maxArtifacts = input.maxArtifactRefs ?? 15;
|
|
39
|
+
|
|
40
|
+
const sources: RunProjectionSource[] = [];
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Project a bounded slice of task history
|
|
44
|
+
const completedTasks = input.tasks.filter((t) => t.status === "completed" || t.status === "failed");
|
|
45
|
+
if (completedTasks.length > 0) {
|
|
46
|
+
const tasks = completedTasks.slice(0, maxEvents);
|
|
47
|
+
sources.push({ kind: "events", bounded: true, reference: `tasks:${tasks.length}/${completedTasks.length}` });
|
|
48
|
+
lines.push(`Previous tasks (${tasks.length}/${completedTasks.length}):`);
|
|
49
|
+
for (const task of tasks) {
|
|
50
|
+
lines.push(`- ${task.id}: ${task.status}${task.error ? ` (${task.error})` : ""}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Project pending mailbox that is relevant to this worker
|
|
55
|
+
if (input.pendingMailbox.length > 0) {
|
|
56
|
+
const messages = input.pendingMailbox.slice(0, maxMailbox);
|
|
57
|
+
sources.push({ kind: "mailbox", bounded: true, reference: `mailbox:${messages.length}/${input.pendingMailbox.length}` });
|
|
58
|
+
lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
|
|
59
|
+
for (const msg of messages) {
|
|
60
|
+
lines.push(`- ${msg.kind ?? "message"}: ${msg.body.slice(0, 100)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Project artifact references (not content)
|
|
65
|
+
if (input.artifacts.length > 0) {
|
|
66
|
+
const artifacts = input.artifacts.slice(0, maxArtifacts);
|
|
67
|
+
sources.push({ kind: "artifacts", bounded: true, reference: `artifacts:${artifacts.length}/${input.artifacts.length}` });
|
|
68
|
+
lines.push(`Available artifacts (${artifacts.length}/${input.artifacts.length}):`);
|
|
69
|
+
for (const art of artifacts) {
|
|
70
|
+
lines.push(`- ${art.kind} (${art.producer})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Metadata markers — not injected as prompt instructions
|
|
75
|
+
sources.push({ kind: "ui_metadata", bounded: false, reference: "excluded_from_prompt" });
|
|
76
|
+
sources.push({ kind: "runtime_metadata", bounded: false, reference: "excluded_from_prompt" });
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
sources,
|
|
80
|
+
summary: lines.join("\n"),
|
|
81
|
+
injectedAsContext: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convert run history to a bounded worker prompt section.
|
|
87
|
+
* Same logic as transformRunContextBeforeWorkerStart but returns
|
|
88
|
+
* the prompt text directly for embedding in the worker prompt.
|
|
89
|
+
*/
|
|
90
|
+
export function convertRunHistoryToWorkerPrompt(input: {
|
|
91
|
+
manifest: TeamRunManifest;
|
|
92
|
+
tasks: TeamTaskState[];
|
|
93
|
+
pendingMailbox: MailboxMessage[];
|
|
94
|
+
artifacts: ArtifactDescriptor[];
|
|
95
|
+
}): string {
|
|
96
|
+
const projection = transformRunContextBeforeWorkerStart(input);
|
|
97
|
+
if (!projection.summary) return "";
|
|
98
|
+
return [
|
|
99
|
+
"## Run Context (bounded projection)",
|
|
100
|
+
projection.summary,
|
|
101
|
+
"",
|
|
102
|
+
`Projection sources: ${projection.sources.map((s) => s.kind).join(", ")}`,
|
|
103
|
+
].join("\n");
|
|
104
104
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
-
import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
|
|
3
|
-
import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
4
|
-
|
|
5
|
-
export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
6
|
-
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
10
|
-
const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
|
|
11
|
-
const merged = updateTask(latest, updated);
|
|
12
|
-
saveRunTasks(manifest, merged);
|
|
13
|
-
return merged;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
|
|
17
|
-
const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
|
|
18
|
-
const nextTask = { ...task, checkpoint };
|
|
19
|
-
const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
|
|
20
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
|
|
21
|
-
return { task: nextTask, tasks: nextTasks };
|
|
22
|
-
}
|
|
1
|
+
import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
+
import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
|
|
3
|
+
import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
|
|
4
|
+
|
|
5
|
+
export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
6
|
+
return tasks.map((task) => task.id === updated.id ? updated : task);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
|
|
10
|
+
const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
|
|
11
|
+
const merged = updateTask(latest, updated);
|
|
12
|
+
saveRunTasks(manifest, merged);
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
|
|
17
|
+
const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
|
|
18
|
+
const nextTask = { ...task, checkpoint };
|
|
19
|
+
const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
|
|
20
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
|
|
21
|
+
return { task: nextTask, tasks: nextTasks };
|
|
22
|
+
}
|
|
@@ -13,12 +13,14 @@ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.
|
|
|
13
13
|
import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
|
|
14
14
|
import { buildRecoveryLedger } from "./recovery-recipes.ts";
|
|
15
15
|
import { buildTaskGraphIndex, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
|
|
16
|
+
import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagReadyTasks, type TaskNode } from "./task-graph.ts";
|
|
16
17
|
import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
|
|
17
18
|
import { aggregateTaskOutputs } from "./task-output-context.ts";
|
|
18
19
|
import { saveCrewAgents } from "./crew-agent-records.ts";
|
|
19
20
|
import { recordsForMaterializedTasks } from "./task-display.ts";
|
|
20
21
|
import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
|
|
21
22
|
import { runTeamTask } from "./task-runner.ts";
|
|
23
|
+
import { createWorkflowStateMachine, validatePhasePreconditions, transitionPhase, type PhaseState, type PhaseGuardContext } from "./workflow-state.ts";
|
|
22
24
|
import { executeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "./retry-executor.ts";
|
|
23
25
|
import { appendDeadletter } from "./deadletter.ts";
|
|
24
26
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
@@ -164,7 +166,13 @@ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]):
|
|
|
164
166
|
return phases.length ? { phases } : undefined;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
interface CloseUnbalancedJsonResult {
|
|
170
|
+
text: string;
|
|
171
|
+
status: "repaired" | "unstable";
|
|
172
|
+
warning?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function closeUnbalancedJson(raw: string): CloseUnbalancedJsonResult {
|
|
168
176
|
let result = raw.trim();
|
|
169
177
|
const stack: string[] = [];
|
|
170
178
|
let inString = false;
|
|
@@ -188,7 +196,11 @@ function closeUnbalancedJson(raw: string): string {
|
|
|
188
196
|
else if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
|
|
189
197
|
}
|
|
190
198
|
while (stack.length) result += stack.pop();
|
|
191
|
-
|
|
199
|
+
// If still in a string, the JSON string was truncated — values may be semantically different
|
|
200
|
+
if (inString) {
|
|
201
|
+
return { text: result, status: "unstable", warning: "JSON string was truncated — values may be incorrect" };
|
|
202
|
+
}
|
|
203
|
+
return { text: result, status: "repaired" };
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
function salvageCompletePhaseObjects(raw: string): unknown | undefined {
|
|
@@ -255,7 +267,8 @@ function adaptiveRoleAlias(role: string, allowed: Set<string>): string | undefin
|
|
|
255
267
|
export function __test__repairAdaptivePlan(text: string, allowedRoles: string[]): { plan?: AdaptivePlan; repaired: boolean; reason?: string } {
|
|
256
268
|
const raw = extractAdaptivePlanJson(text);
|
|
257
269
|
if (!raw) return { repaired: false, reason: "missing-json" };
|
|
258
|
-
const
|
|
270
|
+
const closeResult = closeUnbalancedJson(raw);
|
|
271
|
+
const candidates = [raw, closeResult.text];
|
|
259
272
|
let parsed: unknown;
|
|
260
273
|
let salvageUsed = false;
|
|
261
274
|
for (const candidate of candidates) {
|
|
@@ -276,7 +289,7 @@ export function __test__repairAdaptivePlan(text: string, allowedRoles: string[])
|
|
|
276
289
|
const allowed = new Set(allowedRoles);
|
|
277
290
|
const phases: AdaptivePlanPhase[] = [];
|
|
278
291
|
let total = 0;
|
|
279
|
-
let repaired = salvageUsed || raw !==
|
|
292
|
+
let repaired = salvageUsed || raw !== closeResult.text;
|
|
280
293
|
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
281
294
|
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
|
|
282
295
|
const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
|
|
@@ -510,6 +523,24 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
|
|
|
510
523
|
return tasks.some((task) => task.status === "queued" && task.adaptive && isMutatingTask(task));
|
|
511
524
|
}
|
|
512
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Check whether any task uses explicit `dependsOn` that would benefit from DAG-based
|
|
528
|
+
* execution planning. If so, build an execution plan and use `getDagReadyTasks`
|
|
529
|
+
* to augment the ready-set selection.
|
|
530
|
+
*/
|
|
531
|
+
function dagReadyTaskIds(tasks: TeamTaskState[], completedIds: Set<string>): string[] | null {
|
|
532
|
+
const hasExplicitDeps = tasks.some((t) => t.dependsOn.length > 0);
|
|
533
|
+
if (!hasExplicitDeps) return null;
|
|
534
|
+
const nodes: TaskNode[] = tasks.map((t) => ({
|
|
535
|
+
id: t.id,
|
|
536
|
+
dependsOn: t.dependsOn,
|
|
537
|
+
phase: t.adaptive?.phase ?? t.stepId,
|
|
538
|
+
}));
|
|
539
|
+
const plan = buildDagExecutionPlan(nodes);
|
|
540
|
+
if (plan.hasCycle) return null; // fall back to existing scheduler
|
|
541
|
+
return getDagReadyTasks(plan, completedIds);
|
|
542
|
+
}
|
|
543
|
+
|
|
513
544
|
export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
514
545
|
let workflow = input.workflow;
|
|
515
546
|
let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
|
|
@@ -584,6 +615,15 @@ async function executeTeamRunCore(
|
|
|
584
615
|
const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
585
616
|
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
586
617
|
|
|
618
|
+
// Build a workflow phase state machine from workflow steps for precondition tracking.
|
|
619
|
+
const workflowPhases: PhaseState[] = workflow.steps.map((step): PhaseState => ({
|
|
620
|
+
name: step.id,
|
|
621
|
+
status: "pending",
|
|
622
|
+
inputs: step.reads === false ? [] : Array.isArray(step.reads) ? step.reads : [],
|
|
623
|
+
outputs: step.output === false ? [] : step.output ? [step.output] : [],
|
|
624
|
+
}));
|
|
625
|
+
let wfMachine = createWorkflowStateMachine(workflowPhases);
|
|
626
|
+
|
|
587
627
|
while (tasks.some((task) => task.status === "queued")) {
|
|
588
628
|
if (input.signal?.aborted) {
|
|
589
629
|
const cancelReason = cancellationReasonFromSignal(input.signal);
|
|
@@ -614,13 +654,41 @@ async function executeTeamRunCore(
|
|
|
614
654
|
}
|
|
615
655
|
|
|
616
656
|
const snapshot = taskGraphSnapshot(tasks, queueIndex);
|
|
617
|
-
|
|
618
|
-
|
|
657
|
+
|
|
658
|
+
// DAG-based execution plan: when tasks have explicit dependsOn, use the
|
|
659
|
+
// topological wave planner to determine ready tasks. Fall back to the
|
|
660
|
+
// existing task-graph-scheduler when no explicit deps exist (backward compat).
|
|
661
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
662
|
+
const dagReady = dagReadyTaskIds(tasks, completedIds);
|
|
663
|
+
const effectiveReady = dagReady ?? snapshot.ready;
|
|
664
|
+
|
|
665
|
+
// Workflow phase precondition check (non-blocking: log warnings only).
|
|
666
|
+
if (wfMachine.currentPhaseIndex < wfMachine.phases.length) {
|
|
667
|
+
const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
|
|
668
|
+
const previousPhaseStatus = wfMachine.currentPhaseIndex > 0 ? (wfMachine.phases[wfMachine.currentPhaseIndex - 1]?.status ?? "pending") : "completed";
|
|
669
|
+
const wfContext: PhaseGuardContext = {
|
|
670
|
+
completedArtifacts,
|
|
671
|
+
previousPhaseStatus,
|
|
672
|
+
taskResults: tasks.filter((t) => t.status === "completed").map((t) => ({ taskId: t.id, status: t.status, outputPath: t.resultArtifact?.path })),
|
|
673
|
+
};
|
|
674
|
+
const preconditions = validatePhasePreconditions(wfMachine, wfContext);
|
|
675
|
+
if (!preconditions.ready) {
|
|
676
|
+
appendEvent(manifest.eventsPath, { type: "workflow.preconditions", runId: manifest.runId, message: `Workflow phase '${wfMachine.phases[wfMachine.currentPhaseIndex]?.name}' is missing inputs: ${preconditions.blocking.join(", ")}`, data: { phaseIndex: wfMachine.currentPhaseIndex, phaseName: wfMachine.phases[wfMachine.currentPhaseIndex]?.name, blocking: preconditions.blocking } });
|
|
677
|
+
} else {
|
|
678
|
+
// Advance the machine past completed phases.
|
|
679
|
+
while (wfMachine.currentPhaseIndex < wfMachine.phases.length && wfMachine.phases[wfMachine.currentPhaseIndex]?.status === "completed") {
|
|
680
|
+
wfMachine = { ...wfMachine, currentPhaseIndex: wfMachine.currentPhaseIndex + 1 };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const readyRoles = effectiveReady.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
|
|
686
|
+
const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency, readyCount: effectiveReady.length, workspaceMode: manifest.workspaceMode, readyRoles });
|
|
619
687
|
if (concurrency.reason.includes(";unbounded:")) {
|
|
620
688
|
appendEvent(manifest.eventsPath, { type: "limits.unbounded", runId: manifest.runId, message: "Unbounded worker concurrency was explicitly enabled for this run.", data: { concurrencyReason: concurrency.reason, maxConcurrent: concurrency.maxConcurrent } });
|
|
621
689
|
}
|
|
622
690
|
const approvalPending = isPlanApprovalPending(manifest);
|
|
623
|
-
const readyIds = approvalPending ?
|
|
691
|
+
const readyIds = approvalPending ? effectiveReady : effectiveReady.slice(0, concurrency.selectedCount);
|
|
624
692
|
const candidateBatch = readyIds.map((id) => tasks.find((task) => task.id === id)).filter((task): task is TeamTaskState => Boolean(task));
|
|
625
693
|
const readyBatch = approvalPending ? candidateBatch.filter((task) => !isMutatingTask(task)).slice(0, concurrency.selectedCount) : candidateBatch;
|
|
626
694
|
if (readyBatch.length === 0) {
|
|
@@ -726,6 +794,48 @@ async function executeTeamRunCore(
|
|
|
726
794
|
if (results.length === 0) break;
|
|
727
795
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
728
796
|
tasks = __test__mergeTaskUpdates(tasks, results);
|
|
797
|
+
|
|
798
|
+
// Advance workflow phases whose tasks are all in terminal state
|
|
799
|
+
const terminalStatuses = new Set(["completed", "failed", "skipped", "cancelled"]);
|
|
800
|
+
const phaseTaskMap = new Map<string, string[]>();
|
|
801
|
+
for (const task of tasks) {
|
|
802
|
+
if (!task.stepId) continue;
|
|
803
|
+
const existing = phaseTaskMap.get(task.stepId) ?? [];
|
|
804
|
+
existing.push(task.id);
|
|
805
|
+
phaseTaskMap.set(task.stepId, existing);
|
|
806
|
+
}
|
|
807
|
+
for (let pi = wfMachine.currentPhaseIndex; pi < wfMachine.phases.length; pi++) {
|
|
808
|
+
const phase = wfMachine.phases[pi]!;
|
|
809
|
+
const phaseTaskIds = phaseTaskMap.get(phase.name) ?? [];
|
|
810
|
+
if (phaseTaskIds.length === 0) continue;
|
|
811
|
+
const allTerminal = phaseTaskIds.every((taskId) => {
|
|
812
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
813
|
+
return task ? terminalStatuses.has(task.status) : false;
|
|
814
|
+
});
|
|
815
|
+
if (!allTerminal) break;
|
|
816
|
+
if (phase.status !== "completed" && phase.status !== "failed" && phase.status !== "skipped") {
|
|
817
|
+
const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
|
|
818
|
+
const previousPhaseStatus = pi > 0 ? (wfMachine.phases[pi - 1]?.status ?? "pending") : "completed";
|
|
819
|
+
const wfContext: PhaseGuardContext = {
|
|
820
|
+
completedArtifacts,
|
|
821
|
+
previousPhaseStatus,
|
|
822
|
+
taskResults: tasks.filter((t) => t.status === "completed").map((t) => ({ taskId: t.id, status: t.status, outputPath: t.resultArtifact?.path })),
|
|
823
|
+
};
|
|
824
|
+
// Determine phase transition status based on individual task outcomes
|
|
825
|
+
const phaseTasks = phaseTaskIds.map((taskId) => tasks.find((t) => t.id === taskId)).filter((t): t is NonNullable<typeof t> => t !== undefined);
|
|
826
|
+
const hasFailedOrCancelled = phaseTasks.some((t) => t.status === "failed" || t.status === "cancelled");
|
|
827
|
+
const phaseStatus = hasFailedOrCancelled ? "failed" : "completed";
|
|
828
|
+
const transition = transitionPhase(wfMachine, pi, phaseStatus, wfContext);
|
|
829
|
+
wfMachine = transition.machine;
|
|
830
|
+
if (transition.guardResult && !transition.guardResult.allowed) {
|
|
831
|
+
appendEvent(manifest.eventsPath, { type: "workflow.phase_guard_blocked", runId: manifest.runId, message: `Workflow phase '${phase.name}' guard blocked: ${transition.guardResult.reason ?? "unknown"}`, data: { phaseIndex: pi, phaseName: phase.name, reason: transition.guardResult.reason } });
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
appendEvent(manifest.eventsPath, { type: phaseStatus === "failed" ? "workflow.phase_failed" : "workflow.phase_completed", runId: manifest.runId, message: `Workflow phase '${phase.name}' ${phaseStatus}.`, data: { phaseIndex: pi, phaseStatus } });
|
|
835
|
+
}
|
|
836
|
+
wfMachine = { ...wfMachine, currentPhaseIndex: pi + 1 };
|
|
837
|
+
}
|
|
838
|
+
|
|
729
839
|
const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
|
|
730
840
|
if (cancelledResult || input.signal?.aborted) {
|
|
731
841
|
const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export interface WorkerHeartbeatState {
|
|
2
|
-
workerId: string;
|
|
3
|
-
pid?: number;
|
|
4
|
-
lastSeenAt: string;
|
|
5
|
-
lastStdoutAt?: string;
|
|
6
|
-
lastEventAt?: string;
|
|
7
|
-
turnCount?: number;
|
|
8
|
-
alive?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
|
|
12
|
-
return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
|
|
16
|
-
return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
|
|
20
|
-
return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
|
|
21
|
-
}
|
|
1
|
+
export interface WorkerHeartbeatState {
|
|
2
|
+
workerId: string;
|
|
3
|
+
pid?: number;
|
|
4
|
+
lastSeenAt: string;
|
|
5
|
+
lastStdoutAt?: string;
|
|
6
|
+
lastEventAt?: string;
|
|
7
|
+
turnCount?: number;
|
|
8
|
+
alive?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
|
|
12
|
+
return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
|
|
16
|
+
return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
|
|
20
|
+
return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
|
|
21
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
|
|
2
|
-
export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
|
|
3
|
-
|
|
4
|
-
export interface WorkerStartupEvidence {
|
|
5
|
-
lastLifecycleState: WorkerLifecycleState;
|
|
6
|
-
command: string;
|
|
7
|
-
promptSentAt?: string;
|
|
8
|
-
promptAccepted: boolean;
|
|
9
|
-
trustPromptDetected: boolean;
|
|
10
|
-
transportHealthy: boolean;
|
|
11
|
-
childProcessAlive: boolean;
|
|
12
|
-
elapsedMs: number;
|
|
13
|
-
classification: StartupFailureClassification;
|
|
14
|
-
stderrPreview?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function detectTrustPrompt(text: string): boolean {
|
|
18
|
-
const lowered = text.toLowerCase();
|
|
19
|
-
return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
|
|
23
|
-
if (!evidence.transportHealthy) return "transport_dead";
|
|
24
|
-
if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
|
|
25
|
-
if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
|
|
26
|
-
if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
|
|
27
|
-
if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
|
|
28
|
-
if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
|
|
29
|
-
return "unknown";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createStartupEvidence(input: {
|
|
33
|
-
command: string;
|
|
34
|
-
startedAt: Date;
|
|
35
|
-
finishedAt?: Date;
|
|
36
|
-
promptSentAt?: Date;
|
|
37
|
-
promptAccepted?: boolean;
|
|
38
|
-
stderr?: string;
|
|
39
|
-
error?: string;
|
|
40
|
-
exitCode?: number | null;
|
|
41
|
-
}): WorkerStartupEvidence {
|
|
42
|
-
const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
|
|
43
|
-
const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
|
|
44
|
-
const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
|
|
45
|
-
const base: Omit<WorkerStartupEvidence, "classification"> = {
|
|
46
|
-
lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
|
|
47
|
-
command: input.command,
|
|
48
|
-
promptSentAt: input.promptSentAt?.toISOString(),
|
|
49
|
-
promptAccepted: input.promptAccepted ?? !input.error,
|
|
50
|
-
trustPromptDetected,
|
|
51
|
-
transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
|
|
52
|
-
childProcessAlive,
|
|
53
|
-
elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
|
|
54
|
-
stderrPreview,
|
|
55
|
-
};
|
|
56
|
-
return { ...base, classification: classifyStartupFailure(base) };
|
|
57
|
-
}
|
|
1
|
+
export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
|
|
2
|
+
export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
|
|
3
|
+
|
|
4
|
+
export interface WorkerStartupEvidence {
|
|
5
|
+
lastLifecycleState: WorkerLifecycleState;
|
|
6
|
+
command: string;
|
|
7
|
+
promptSentAt?: string;
|
|
8
|
+
promptAccepted: boolean;
|
|
9
|
+
trustPromptDetected: boolean;
|
|
10
|
+
transportHealthy: boolean;
|
|
11
|
+
childProcessAlive: boolean;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
classification: StartupFailureClassification;
|
|
14
|
+
stderrPreview?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function detectTrustPrompt(text: string): boolean {
|
|
18
|
+
const lowered = text.toLowerCase();
|
|
19
|
+
return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
|
|
23
|
+
if (!evidence.transportHealthy) return "transport_dead";
|
|
24
|
+
if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
|
|
25
|
+
if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
|
|
26
|
+
if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
|
|
27
|
+
if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
|
|
28
|
+
if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
|
|
29
|
+
return "unknown";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createStartupEvidence(input: {
|
|
33
|
+
command: string;
|
|
34
|
+
startedAt: Date;
|
|
35
|
+
finishedAt?: Date;
|
|
36
|
+
promptSentAt?: Date;
|
|
37
|
+
promptAccepted?: boolean;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
error?: string;
|
|
40
|
+
exitCode?: number | null;
|
|
41
|
+
}): WorkerStartupEvidence {
|
|
42
|
+
const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
|
|
43
|
+
const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
|
|
44
|
+
const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
|
|
45
|
+
const base: Omit<WorkerStartupEvidence, "classification"> = {
|
|
46
|
+
lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
|
|
47
|
+
command: input.command,
|
|
48
|
+
promptSentAt: input.promptSentAt?.toISOString(),
|
|
49
|
+
promptAccepted: input.promptAccepted ?? !input.error,
|
|
50
|
+
trustPromptDetected,
|
|
51
|
+
transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
|
|
52
|
+
childProcessAlive,
|
|
53
|
+
elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
|
|
54
|
+
stderrPreview,
|
|
55
|
+
};
|
|
56
|
+
return { ...base, classification: classifyStartupFailure(base) };
|
|
57
|
+
}
|