pi-crew 0.1.49 → 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 +74 -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 +14 -1
- package/src/config/defaults.ts +5 -5
- 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 -82
- package/src/extension/project-init.ts +36 -4
- package/src/extension/register.ts +67 -22
- 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-export.ts +26 -12
- 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/context.ts +1 -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 +14 -3
- 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/concurrency.ts +3 -1
- package/src/runtime/crash-recovery.ts +33 -0
- package/src/runtime/delta-conflict.ts +360 -0
- package/src/runtime/diagnostic-export.ts +3 -1
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/event-stream-bridge.ts +3 -1
- 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 -2
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +74 -6
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +122 -0
- package/src/runtime/process-status.ts +14 -1
- 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 +3 -3
- 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-builder.ts +1 -1
- 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 +126 -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 +12 -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 -11
- 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 +9 -4
- 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/powerbar-publisher.ts +6 -0
- 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/src/worktree/worktree-manager.ts +11 -3
- 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 -38
- 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";
|
|
@@ -83,6 +85,12 @@ function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskState):
|
|
|
83
85
|
// contain stale queued/running copies of tasks that another worker already
|
|
84
86
|
// completed. Never let those stale snapshots regress durable task state.
|
|
85
87
|
if (!isNonTerminalTaskStatus(current.status) && isNonTerminalTaskStatus(updated.status)) return false;
|
|
88
|
+
// Prevent a stale completed task from overwriting a fresher one.
|
|
89
|
+
if (current.finishedAt && updated.finishedAt) {
|
|
90
|
+
const currentFinished = new Date(current.finishedAt).getTime();
|
|
91
|
+
const updatedFinished = new Date(updated.finishedAt).getTime();
|
|
92
|
+
if (!Number.isNaN(currentFinished) && !Number.isNaN(updatedFinished) && updatedFinished < currentFinished) return false;
|
|
93
|
+
}
|
|
86
94
|
return updated.status !== current.status || updated.finishedAt !== current.finishedAt || updated.startedAt !== current.startedAt || Boolean(updated.resultArtifact) || Boolean(updated.error) || Boolean(updated.modelAttempts?.length) || Boolean(updated.usage) || Boolean(updated.attempts?.length);
|
|
87
95
|
}
|
|
88
96
|
|
|
@@ -158,7 +166,13 @@ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]):
|
|
|
158
166
|
return phases.length ? { phases } : undefined;
|
|
159
167
|
}
|
|
160
168
|
|
|
161
|
-
|
|
169
|
+
interface CloseUnbalancedJsonResult {
|
|
170
|
+
text: string;
|
|
171
|
+
status: "repaired" | "unstable";
|
|
172
|
+
warning?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function closeUnbalancedJson(raw: string): CloseUnbalancedJsonResult {
|
|
162
176
|
let result = raw.trim();
|
|
163
177
|
const stack: string[] = [];
|
|
164
178
|
let inString = false;
|
|
@@ -182,7 +196,11 @@ function closeUnbalancedJson(raw: string): string {
|
|
|
182
196
|
else if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
|
|
183
197
|
}
|
|
184
198
|
while (stack.length) result += stack.pop();
|
|
185
|
-
|
|
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" };
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
function salvageCompletePhaseObjects(raw: string): unknown | undefined {
|
|
@@ -249,7 +267,8 @@ function adaptiveRoleAlias(role: string, allowed: Set<string>): string | undefin
|
|
|
249
267
|
export function __test__repairAdaptivePlan(text: string, allowedRoles: string[]): { plan?: AdaptivePlan; repaired: boolean; reason?: string } {
|
|
250
268
|
const raw = extractAdaptivePlanJson(text);
|
|
251
269
|
if (!raw) return { repaired: false, reason: "missing-json" };
|
|
252
|
-
const
|
|
270
|
+
const closeResult = closeUnbalancedJson(raw);
|
|
271
|
+
const candidates = [raw, closeResult.text];
|
|
253
272
|
let parsed: unknown;
|
|
254
273
|
let salvageUsed = false;
|
|
255
274
|
for (const candidate of candidates) {
|
|
@@ -270,7 +289,7 @@ export function __test__repairAdaptivePlan(text: string, allowedRoles: string[])
|
|
|
270
289
|
const allowed = new Set(allowedRoles);
|
|
271
290
|
const phases: AdaptivePlanPhase[] = [];
|
|
272
291
|
let total = 0;
|
|
273
|
-
let repaired = salvageUsed || raw !==
|
|
292
|
+
let repaired = salvageUsed || raw !== closeResult.text;
|
|
274
293
|
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
275
294
|
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
|
|
276
295
|
const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
|
|
@@ -504,6 +523,24 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
|
|
|
504
523
|
return tasks.some((task) => task.status === "queued" && task.adaptive && isMutatingTask(task));
|
|
505
524
|
}
|
|
506
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
|
+
|
|
507
544
|
export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
508
545
|
let workflow = input.workflow;
|
|
509
546
|
let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
|
|
@@ -578,6 +615,15 @@ async function executeTeamRunCore(
|
|
|
578
615
|
const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
579
616
|
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
580
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
|
+
|
|
581
627
|
while (tasks.some((task) => task.status === "queued")) {
|
|
582
628
|
if (input.signal?.aborted) {
|
|
583
629
|
const cancelReason = cancellationReasonFromSignal(input.signal);
|
|
@@ -608,13 +654,41 @@ async function executeTeamRunCore(
|
|
|
608
654
|
}
|
|
609
655
|
|
|
610
656
|
const snapshot = taskGraphSnapshot(tasks, queueIndex);
|
|
611
|
-
|
|
612
|
-
|
|
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 });
|
|
613
687
|
if (concurrency.reason.includes(";unbounded:")) {
|
|
614
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 } });
|
|
615
689
|
}
|
|
616
690
|
const approvalPending = isPlanApprovalPending(manifest);
|
|
617
|
-
const readyIds = approvalPending ?
|
|
691
|
+
const readyIds = approvalPending ? effectiveReady : effectiveReady.slice(0, concurrency.selectedCount);
|
|
618
692
|
const candidateBatch = readyIds.map((id) => tasks.find((task) => task.id === id)).filter((task): task is TeamTaskState => Boolean(task));
|
|
619
693
|
const readyBatch = approvalPending ? candidateBatch.filter((task) => !isMutatingTask(task)).slice(0, concurrency.selectedCount) : candidateBatch;
|
|
620
694
|
if (readyBatch.length === 0) {
|
|
@@ -642,6 +716,9 @@ async function executeTeamRunCore(
|
|
|
642
716
|
}
|
|
643
717
|
}
|
|
644
718
|
const batchTasks = readyBatch.filter((task) => tasks.find((t) => t.id === task.id && t.status !== "skipped"));
|
|
719
|
+
if (batchTasks.length > 1) {
|
|
720
|
+
appendEvent(manifest.eventsPath, { type: "task.parallel_start", runId: manifest.runId, message: `Launching ${batchTasks.length} tasks in PARALLEL (concurrency=${concurrency.selectedCount}): ${batchTasks.map((t) => `${t.role}(${t.id})`).join(", ")}`, data: { taskIds: batchTasks.map((t) => t.id), roles: batchTasks.map((t) => t.role), concurrency: concurrency.selectedCount } });
|
|
721
|
+
}
|
|
645
722
|
const results = await mapConcurrent(
|
|
646
723
|
batchTasks,
|
|
647
724
|
concurrency.selectedCount,
|
|
@@ -717,6 +794,48 @@ async function executeTeamRunCore(
|
|
|
717
794
|
if (results.length === 0) break;
|
|
718
795
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
719
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
|
+
|
|
720
839
|
const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
|
|
721
840
|
if (cancelledResult || input.signal?.aborted) {
|
|
722
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
|
+
}
|