ultra-builder-pro-cli 0.25.1 → 0.26.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/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +40 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -1,903 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 8B.2 — Parallel orchestrator.
|
|
4
|
-
//
|
|
5
|
-
// Consumes the current Change-scoped plan.json (published by a Plan checkpoint) and
|
|
6
|
-
// drives resumable per-wave session spawning. Each wave is either parallel
|
|
7
|
-
// (batch Promise.all on proc exit) or serial (await per task) based on the
|
|
8
|
-
// file-overlap detection already done by plan-builder.
|
|
9
|
-
//
|
|
10
|
-
// Responsibilities:
|
|
11
|
-
// • resolve the canonical accepted Change plan
|
|
12
|
-
// • for each ready wave: evaluate dispatch-rules per task → spawn sessions
|
|
13
|
-
// via session-runner → wait for transport exit → preserve task-gate state
|
|
14
|
-
// • stop at the first non-converged wave and resume safely on the next call
|
|
15
|
-
// • emit wave_started / wave_paused|wave_completed and
|
|
16
|
-
// plan_paused|plan_completed events
|
|
17
|
-
//
|
|
18
|
-
// Not responsible for:
|
|
19
|
-
// • circuit breaker (Phase 5.2 recordTaskFailure owns that path)
|
|
20
|
-
// • worktree lifecycle beyond what session-runner already does
|
|
21
|
-
// • merge back (Phase 8B.4 layers on top of closeSession)
|
|
22
|
-
|
|
23
|
-
const fs = require('node:fs');
|
|
24
|
-
const path = require('node:path');
|
|
25
|
-
const { isDeepStrictEqual } = require('node:util');
|
|
26
|
-
|
|
27
|
-
const planStore = require('../mcp-server/lib/plan-store.cjs');
|
|
28
|
-
const ops = require('../mcp-server/lib/state-ops.cjs');
|
|
29
|
-
const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
|
|
30
|
-
const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
|
|
31
|
-
const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
|
|
32
|
-
const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
|
|
33
|
-
const runner = require('./session-runner.cjs');
|
|
34
|
-
const dispatchRules = require('./dispatch-rules.cjs');
|
|
35
|
-
|
|
36
|
-
const TERMINAL_TASK_STATUSES = new Set(['completed', 'expanded']);
|
|
37
|
-
|
|
38
|
-
function resolvePlan({ db, plan, planPath, repoRoot, changeId = null }) {
|
|
39
|
-
if (plan) {
|
|
40
|
-
if (!plan.change_id) {
|
|
41
|
-
throw planError(
|
|
42
|
-
'PLAN_CHANGE_SCOPED_REQUIRED',
|
|
43
|
-
'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return plan;
|
|
47
|
-
}
|
|
48
|
-
if (planPath) {
|
|
49
|
-
throw planError(
|
|
50
|
-
'PLAN_LEGACY_RETIRED',
|
|
51
|
-
'explicit planPath execution is retired; migrate the project and use its accepted Change Plan',
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
let candidates;
|
|
55
|
-
if (changeId) {
|
|
56
|
-
candidates = db.prepare(
|
|
57
|
-
`SELECT id, artifact_root, status
|
|
58
|
-
FROM changes
|
|
59
|
-
WHERE id = ? AND status = 'active'`,
|
|
60
|
-
).all(changeId);
|
|
61
|
-
} else {
|
|
62
|
-
candidates = db.prepare(
|
|
63
|
-
`SELECT id, artifact_root, status
|
|
64
|
-
FROM changes WHERE status = 'active' ORDER BY updated_at DESC, id ASC`,
|
|
65
|
-
).all().filter((candidate) => checkpoints.currentCheckpoint(
|
|
66
|
-
db,
|
|
67
|
-
'plan',
|
|
68
|
-
{ change_id: candidate.id },
|
|
69
|
-
{ includeDraft: false },
|
|
70
|
-
));
|
|
71
|
-
}
|
|
72
|
-
if (candidates.length > 1) {
|
|
73
|
-
throw planError(
|
|
74
|
-
'PLAN_CHANGE_REQUIRED',
|
|
75
|
-
'more than one current Change has an executable plan; provide changeId',
|
|
76
|
-
{ change_ids: candidates.map((candidate) => candidate.id).sort() },
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
const change = candidates[0] || null;
|
|
80
|
-
const loaded = change
|
|
81
|
-
? planStore.loadChangePlanArtifact(repoRoot, change, { db, strict: true })
|
|
82
|
-
: null;
|
|
83
|
-
if (!loaded) {
|
|
84
|
-
const err = new Error(
|
|
85
|
-
changeId
|
|
86
|
-
? `runPlan: no current scoped plan found for change ${changeId}`
|
|
87
|
-
: 'runPlan: no current accepted Change-scoped Plan found',
|
|
88
|
-
);
|
|
89
|
-
err.code = 'PLAN_NOT_FOUND';
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
|
-
return loaded;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function planError(code, message, details = null) {
|
|
96
|
-
const error = new Error(message);
|
|
97
|
-
error.code = code;
|
|
98
|
-
if (details) error.details = details;
|
|
99
|
-
return error;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function validatePlanShape(plan) {
|
|
103
|
-
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
104
|
-
throw planError('PLAN_INVALID', 'execution plan must be an object');
|
|
105
|
-
}
|
|
106
|
-
if (plan.schema_version !== undefined && plan.schema_version !== '1.0') {
|
|
107
|
-
throw planError(
|
|
108
|
-
'PLAN_INVALID',
|
|
109
|
-
`unsupported execution plan schema_version: ${String(plan.schema_version)}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
if (plan.change_id !== undefined && plan.change_id !== null
|
|
113
|
-
&& (typeof plan.change_id !== 'string' || plan.change_id.trim() === '')) {
|
|
114
|
-
throw planError('PLAN_INVALID', 'execution plan change_id must be null or a non-empty string');
|
|
115
|
-
}
|
|
116
|
-
if (!Array.isArray(plan.waves) || plan.waves.length === 0) {
|
|
117
|
-
throw planError('PLAN_INVALID', 'execution plan must contain at least one dependency wave');
|
|
118
|
-
}
|
|
119
|
-
if (plan.cycles !== undefined && !Array.isArray(plan.cycles)) {
|
|
120
|
-
throw planError('PLAN_INVALID', 'execution plan cycles must be an array');
|
|
121
|
-
}
|
|
122
|
-
if (Array.isArray(plan.cycles) && plan.cycles.length > 0) {
|
|
123
|
-
throw planError('PLAN_INVALID', 'execution plan contains dependency cycles', {
|
|
124
|
-
cycles: plan.cycles,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const waveIds = new Set();
|
|
129
|
-
const taskIds = new Set();
|
|
130
|
-
for (const [index, wave] of plan.waves.entries()) {
|
|
131
|
-
if (!wave || typeof wave !== 'object' || Array.isArray(wave)) {
|
|
132
|
-
throw planError('PLAN_INVALID', `execution plan wave ${index + 1} must be an object`);
|
|
133
|
-
}
|
|
134
|
-
if ((typeof wave.id !== 'string' && typeof wave.id !== 'number')
|
|
135
|
-
|| String(wave.id).trim() === '') {
|
|
136
|
-
throw planError('PLAN_INVALID', `execution plan wave ${index + 1} requires an id`);
|
|
137
|
-
}
|
|
138
|
-
const waveKey = String(wave.id);
|
|
139
|
-
if (waveIds.has(waveKey)) {
|
|
140
|
-
throw planError('PLAN_INVALID', `execution plan contains duplicate wave id ${waveKey}`);
|
|
141
|
-
}
|
|
142
|
-
waveIds.add(waveKey);
|
|
143
|
-
if (!Array.isArray(wave.tasks) || wave.tasks.length === 0) {
|
|
144
|
-
throw planError('PLAN_INVALID', `execution plan wave ${waveKey} must contain tasks`);
|
|
145
|
-
}
|
|
146
|
-
if (typeof wave.parallel !== 'boolean') {
|
|
147
|
-
throw planError('PLAN_INVALID', `execution plan wave ${waveKey} requires parallel boolean`);
|
|
148
|
-
}
|
|
149
|
-
for (const taskId of wave.tasks) {
|
|
150
|
-
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
151
|
-
throw planError('PLAN_INVALID', `execution plan wave ${waveKey} has an invalid task id`);
|
|
152
|
-
}
|
|
153
|
-
if (taskIds.has(taskId)) {
|
|
154
|
-
throw planError('PLAN_INVALID', `execution plan contains duplicate task ${taskId}`);
|
|
155
|
-
}
|
|
156
|
-
taskIds.add(taskId);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return plan;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function assertPlanAuthority(db, repoRoot, plan) {
|
|
163
|
-
const taskIds = plan.waves.flatMap((wave) => wave.tasks);
|
|
164
|
-
const taskRows = taskIds.map((id) => ops.readTask(db, id)).filter(Boolean);
|
|
165
|
-
const taskChangeIds = new Set(taskRows.map((task) => task.change_id).filter(Boolean));
|
|
166
|
-
|
|
167
|
-
if (!plan.change_id) {
|
|
168
|
-
throw planError(
|
|
169
|
-
'PLAN_CHANGE_SCOPED_REQUIRED',
|
|
170
|
-
'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
if (taskChangeIds.size > 1 || (taskChangeIds.size === 1 && !taskChangeIds.has(plan.change_id))) {
|
|
174
|
-
throw planError(
|
|
175
|
-
'PLAN_CHANGE_MISMATCH',
|
|
176
|
-
`execution plan ${plan.change_id} contains tasks owned by another change`,
|
|
177
|
-
{ task_change_ids: [...taskChangeIds].sort() },
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const currentPlan = checkpoints.currentCheckpoint(
|
|
182
|
-
db,
|
|
183
|
-
'plan',
|
|
184
|
-
{ change_id: plan.change_id },
|
|
185
|
-
{ includeDraft: false },
|
|
186
|
-
);
|
|
187
|
-
if (!currentPlan || checkpoints.checkpointDigest(currentPlan) !== currentPlan.digest) {
|
|
188
|
-
throw planError(
|
|
189
|
-
'PLAN_NOT_COMPLETED',
|
|
190
|
-
`change ${plan.change_id} has no current accepted Plan checkpoint`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
const context = currentPlan.context_envelope_id
|
|
194
|
-
? db.prepare('SELECT * FROM context_envelopes WHERE id = ?')
|
|
195
|
-
.get(currentPlan.context_envelope_id)
|
|
196
|
-
: null;
|
|
197
|
-
if (!context || context.stage !== 'plan' || context.scope_type !== 'change'
|
|
198
|
-
|| context.scope_id !== plan.change_id) {
|
|
199
|
-
throw planError(
|
|
200
|
-
'PLAN_STALE',
|
|
201
|
-
`execution plan for ${plan.change_id} is not bound to its Context Envelope`,
|
|
202
|
-
{ authority_code: 'PLAN_CONTEXT_MISMATCH' },
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
let contextPayload;
|
|
206
|
-
try {
|
|
207
|
-
contextPayload = JSON.parse(context.payload_json);
|
|
208
|
-
} catch {
|
|
209
|
-
throw planError(
|
|
210
|
-
'PLAN_STALE',
|
|
211
|
-
`execution plan for ${plan.change_id} has a corrupt Context Envelope`,
|
|
212
|
-
{ authority_code: 'PLAN_CONTEXT_INVALID' },
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
const baseline = db.prepare(
|
|
216
|
-
`SELECT scope_json FROM baselines
|
|
217
|
-
WHERE status <> 'superseded' ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
218
|
-
).get();
|
|
219
|
-
const scope = baseline
|
|
220
|
-
? JSON.parse(baseline.scope_json || '["."]')
|
|
221
|
-
: ['.'];
|
|
222
|
-
const currentCheckout = require('../mcp-server/lib/baseline-workflow.cjs')
|
|
223
|
-
.gitWorktreeSnapshot(repoRoot, scope);
|
|
224
|
-
const plannedWorktreeDigest = contextPayload.git?.scoped_worktree_digest
|
|
225
|
-
|| contextPayload.git?.worktree_digest
|
|
226
|
-
|| null;
|
|
227
|
-
if (plannedWorktreeDigest && currentCheckout.digest !== plannedWorktreeDigest) {
|
|
228
|
-
throw planError(
|
|
229
|
-
'PLAN_STALE',
|
|
230
|
-
`execution plan for ${plan.change_id} no longer matches the scoped checkout`,
|
|
231
|
-
{
|
|
232
|
-
authority_code: 'PLAN_CONTEXT_STALE',
|
|
233
|
-
blockers: ['CONTEXT_WORKTREE_STALE'],
|
|
234
|
-
},
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
const staleTasks = taskRows.filter((task) => task.stale);
|
|
238
|
-
if (staleTasks.length > 0) {
|
|
239
|
-
throw planError(
|
|
240
|
-
'PLAN_STALE',
|
|
241
|
-
`execution plan for ${plan.change_id} contains stale task contracts`,
|
|
242
|
-
{
|
|
243
|
-
authority_code: 'PLAN_TASK_CONTRACT_STALE',
|
|
244
|
-
task_ids: staleTasks.map((task) => task.id),
|
|
245
|
-
},
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
const expected = planStore.buildPlan(taskRows, { changeId: plan.change_id });
|
|
249
|
-
const { context: planContext, ...planCore } = plan;
|
|
250
|
-
if (!isDeepStrictEqual(planCore, expected)) {
|
|
251
|
-
throw planError(
|
|
252
|
-
'PLAN_STALE',
|
|
253
|
-
`execution plan for ${plan.change_id} does not match the current DB task graph`,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (!planContext
|
|
257
|
-
|| planContext.snapshot_id !== context.id
|
|
258
|
-
|| planContext.manifest_path !== context.artifact_path
|
|
259
|
-
|| planContext.manifest_digest !== context.digest) {
|
|
260
|
-
throw planError(
|
|
261
|
-
'PLAN_STALE',
|
|
262
|
-
`execution plan for ${plan.change_id} does not match its planning context`,
|
|
263
|
-
{ authority_code: 'PLAN_CONTEXT_MISMATCH' },
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
const change = db.prepare(
|
|
267
|
-
'SELECT id, artifact_root FROM changes WHERE id = ?',
|
|
268
|
-
).get(plan.change_id);
|
|
269
|
-
const canonicalPaths = planStore.changePlanPaths(repoRoot, change);
|
|
270
|
-
const expectedPlanPath = path.relative(repoRoot, canonicalPaths.json);
|
|
271
|
-
const expectedPlanMdPath = path.relative(repoRoot, canonicalPaths.md);
|
|
272
|
-
const canonicalPlan = planStore.loadChangePlanArtifact(
|
|
273
|
-
repoRoot,
|
|
274
|
-
change,
|
|
275
|
-
{ db, strict: true },
|
|
276
|
-
);
|
|
277
|
-
if (!canonicalPlan || !isDeepStrictEqual(canonicalPlan, plan)) {
|
|
278
|
-
throw planError(
|
|
279
|
-
'PLAN_STALE',
|
|
280
|
-
`execution plan for ${plan.change_id} differs from its canonical Change artifact`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
return { mode: 'current-change', checkpoint_id: currentPlan.id };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function awaitExit(proc) {
|
|
287
|
-
return new Promise((resolve) => {
|
|
288
|
-
if (!proc) {
|
|
289
|
-
resolve({ code: -1, signal: null, error: new Error('worker process was not created') });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (proc.exitCode !== null && proc.exitCode !== undefined) {
|
|
293
|
-
resolve({ code: proc.exitCode, signal: proc.signalCode || null, error: null });
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
let settled = false;
|
|
297
|
-
const settle = (result) => {
|
|
298
|
-
if (settled) return;
|
|
299
|
-
settled = true;
|
|
300
|
-
proc.removeListener('exit', onExit);
|
|
301
|
-
proc.removeListener('error', onError);
|
|
302
|
-
resolve(result);
|
|
303
|
-
};
|
|
304
|
-
const onExit = (code, signal) => settle({
|
|
305
|
-
code: code === null ? -1 : code,
|
|
306
|
-
signal,
|
|
307
|
-
error: null,
|
|
308
|
-
});
|
|
309
|
-
const onError = (error) => settle({ code: -1, signal: null, error });
|
|
310
|
-
proc.once('exit', onExit);
|
|
311
|
-
proc.once('error', onError);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function failureReason(execution) {
|
|
316
|
-
if (execution.error) return `worker spawn or runtime error: ${execution.error.message}`;
|
|
317
|
-
return `runtime process exited with code ${execution.code}`
|
|
318
|
-
+ `${execution.signal ? ` (${execution.signal})` : ''}`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function authorityBlockedResult(error, taskId, {
|
|
322
|
-
sid = null,
|
|
323
|
-
reservationFailed = false,
|
|
324
|
-
} = {}) {
|
|
325
|
-
const gate = runner.findExpectedExecutionGate(error);
|
|
326
|
-
if (!gate) return null;
|
|
327
|
-
return {
|
|
328
|
-
sid,
|
|
329
|
-
task_id: taskId,
|
|
330
|
-
code: -4,
|
|
331
|
-
status: 'authority_blocked',
|
|
332
|
-
retryable: true,
|
|
333
|
-
authority_code: gate.code,
|
|
334
|
-
error: error.message,
|
|
335
|
-
...(Array.isArray(gate.details?.blockers)
|
|
336
|
-
? { blockers: gate.details.blockers }
|
|
337
|
-
: {}),
|
|
338
|
-
...(reservationFailed ? { _reservationFailed: true } : {}),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function recordExecutionFailure(db, taskId, {
|
|
343
|
-
sessionId = null,
|
|
344
|
-
reason,
|
|
345
|
-
} = {}) {
|
|
346
|
-
const current = ops.readTask(db, taskId);
|
|
347
|
-
if (current && !TERMINAL_TASK_STATUSES.has(current.status) && current.status !== 'blocked') {
|
|
348
|
-
try { ops.patchTask(db, taskId, { status: 'blocked' }); }
|
|
349
|
-
catch (error) {
|
|
350
|
-
process.stderr.write(
|
|
351
|
-
`parallel-orchestrator: patchTask(${taskId}→blocked) failed: ${error.message}\n`,
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
try {
|
|
356
|
-
ops.recordTaskFailure(db, taskId, { reason, session_id: sessionId });
|
|
357
|
-
} catch (error) {
|
|
358
|
-
process.stderr.write(
|
|
359
|
-
`parallel-orchestrator: recordTaskFailure(${taskId}) failed: ${error.message}\n`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function finalizeSession(db, handle, execution, ctx = {}) {
|
|
365
|
-
const { code } = execution;
|
|
366
|
-
const sessionStatus = code === 0 ? 'completed' : 'crashed';
|
|
367
|
-
const liveSession = ops.readSession(db, handle.sid);
|
|
368
|
-
if (!liveSession || liveSession.status !== 'running') {
|
|
369
|
-
return {
|
|
370
|
-
sid: handle.sid,
|
|
371
|
-
task_id: handle.task_id,
|
|
372
|
-
code,
|
|
373
|
-
signal: execution.signal || null,
|
|
374
|
-
error: execution.error?.message || null,
|
|
375
|
-
status: liveSession?.status || 'missing',
|
|
376
|
-
task_status: ops.readTask(db, handle.task_id)?.status || null,
|
|
377
|
-
worktree_preserved: Boolean(
|
|
378
|
-
liveSession?.worktree_path && fs.existsSync(liveSession.worktree_path),
|
|
379
|
-
),
|
|
380
|
-
merge: undefined,
|
|
381
|
-
close_error: null,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
if (code !== 0) {
|
|
385
|
-
recordExecutionFailure(db, handle.task_id, {
|
|
386
|
-
sessionId: handle.sid,
|
|
387
|
-
reason: failureReason(execution),
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Process exit is transport evidence, not Ultra task completion. The worker
|
|
392
|
-
// must converge the task through the normal dev/test/review authority before
|
|
393
|
-
// an explicit auto-merge may integrate it.
|
|
394
|
-
let task = ops.readTask(db, handle.task_id);
|
|
395
|
-
const taskConverged = Boolean(task && TERMINAL_TASK_STATUSES.has(task.status));
|
|
396
|
-
let merge;
|
|
397
|
-
let worktreePreserved = true;
|
|
398
|
-
let closeError = null;
|
|
399
|
-
try {
|
|
400
|
-
const closeResult = runner.closeSession(
|
|
401
|
-
{ db, repoRoot: ctx.repoRoot, sid: handle.sid },
|
|
402
|
-
{
|
|
403
|
-
status: sessionStatus,
|
|
404
|
-
autoMerge: !!ctx.autoMerge && sessionStatus === 'completed' && taskConverged,
|
|
405
|
-
mergeBaseBranch: ctx.mergeBaseBranch || 'main',
|
|
406
|
-
},
|
|
407
|
-
);
|
|
408
|
-
merge = closeResult.merge;
|
|
409
|
-
worktreePreserved = closeResult.worktree_preserved;
|
|
410
|
-
} catch (err) {
|
|
411
|
-
closeError = err.message;
|
|
412
|
-
process.stderr.write(`parallel-orchestrator: closeSession(${handle.sid}) failed: ${err.message}\n`);
|
|
413
|
-
if (ctx.onError) ctx.onError(err);
|
|
414
|
-
}
|
|
415
|
-
task = ops.readTask(db, handle.task_id);
|
|
416
|
-
return {
|
|
417
|
-
sid: handle.sid,
|
|
418
|
-
task_id: handle.task_id,
|
|
419
|
-
code,
|
|
420
|
-
signal: execution.signal || null,
|
|
421
|
-
error: execution.error?.message || null,
|
|
422
|
-
status: sessionStatus,
|
|
423
|
-
task_status: task?.status || null,
|
|
424
|
-
worktree_preserved: worktreePreserved,
|
|
425
|
-
merge,
|
|
426
|
-
close_error: closeError,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function prepareTaskDispatch(task, wave, ctx) {
|
|
431
|
-
const { runtimes, command, commandArgs, commandArgsFor, onError } = ctx;
|
|
432
|
-
const decision = dispatchRules.evaluate({
|
|
433
|
-
task,
|
|
434
|
-
deps_ready: true, // topo order guarantees all deps are in prior waves
|
|
435
|
-
available_runtimes: runtimes,
|
|
436
|
-
breaker_state: 'ok', // admissionCheck below enforces the real gate
|
|
437
|
-
wave,
|
|
438
|
-
});
|
|
439
|
-
if (decision.action !== 'spawn_agent') {
|
|
440
|
-
if (onError) onError(new Error(`task ${task.id}: deferred/blocked by rule ${decision.rule_id}`));
|
|
441
|
-
return {
|
|
442
|
-
task,
|
|
443
|
-
result: { sid: null, task_id: task.id, code: -2, status: decision.action },
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const args = commandArgsFor ? commandArgsFor(task) : commandArgs;
|
|
448
|
-
if (!Array.isArray(args)) {
|
|
449
|
-
throw planError(
|
|
450
|
-
'ORCHESTRATOR_COMMAND_INVALID',
|
|
451
|
-
`task ${task.id} command arguments must be an array`,
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
return { task, decision, command, args };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function reservePreparedTask(db, repoRoot, prepared, ctx) {
|
|
458
|
-
if (prepared.result) return prepared.result;
|
|
459
|
-
const {
|
|
460
|
-
task, decision, command, args,
|
|
461
|
-
} = prepared;
|
|
462
|
-
const { onError } = ctx;
|
|
463
|
-
let handle;
|
|
464
|
-
let packet = null;
|
|
465
|
-
let authority = null;
|
|
466
|
-
const priorContextIds = new Set(
|
|
467
|
-
db.prepare('SELECT id FROM context_envelopes').all().map((row) => row.id),
|
|
468
|
-
);
|
|
469
|
-
try {
|
|
470
|
-
if (!task.change_id) {
|
|
471
|
-
throw planError(
|
|
472
|
-
'TASK_CHANGE_REQUIRED',
|
|
473
|
-
`orchestrator task ${task.id} must belong to the accepted Change`,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
authority = runtimePaths.ensureRuntimeState(repoRoot, {
|
|
477
|
-
admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
|
|
478
|
-
});
|
|
479
|
-
ops.tx(db, () => {
|
|
480
|
-
packet = workerPackets.createWorkerPacket(db, {
|
|
481
|
-
role: 'implement',
|
|
482
|
-
task_id: task.id,
|
|
483
|
-
runtime: decision.runtime,
|
|
484
|
-
output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
|
|
485
|
-
output_schema: {
|
|
486
|
-
type: 'object',
|
|
487
|
-
required: ['packet_digest'],
|
|
488
|
-
properties: {
|
|
489
|
-
packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
}, { rootDir: repoRoot });
|
|
493
|
-
workerPackets.markWorkerPacketAssigned(db, packet.id);
|
|
494
|
-
handle = runner.spawnSession({
|
|
495
|
-
db, repoRoot,
|
|
496
|
-
task_id: task.id,
|
|
497
|
-
runtime: decision.runtime,
|
|
498
|
-
authority,
|
|
499
|
-
command: null,
|
|
500
|
-
args: [],
|
|
501
|
-
mark_task_started: true,
|
|
502
|
-
kernel_mode: true,
|
|
503
|
-
packet_digest: packet.packet_digest,
|
|
504
|
-
});
|
|
505
|
-
handle.task_id = task.id;
|
|
506
|
-
handle.packet = packet;
|
|
507
|
-
});
|
|
508
|
-
} catch (err) {
|
|
509
|
-
rollbackFailedReservation(db, repoRoot, {
|
|
510
|
-
handle,
|
|
511
|
-
packet,
|
|
512
|
-
priorContextIds,
|
|
513
|
-
});
|
|
514
|
-
const authorityBlocked = authorityBlockedResult(err, task.id, {
|
|
515
|
-
reservationFailed: true,
|
|
516
|
-
});
|
|
517
|
-
if (authorityBlocked) return authorityBlocked;
|
|
518
|
-
if (onError) onError(err);
|
|
519
|
-
recordExecutionFailure(db, task.id, {
|
|
520
|
-
reason: `worker session could not be created: ${err.message}`,
|
|
521
|
-
});
|
|
522
|
-
return {
|
|
523
|
-
sid: null,
|
|
524
|
-
task_id: task.id,
|
|
525
|
-
code: -3,
|
|
526
|
-
status: 'spawn_failed',
|
|
527
|
-
error: err.message,
|
|
528
|
-
_reservationFailed: true,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return {
|
|
533
|
-
_pendingStart: true,
|
|
534
|
-
handle,
|
|
535
|
-
command,
|
|
536
|
-
args,
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function rollbackFailedReservation(db, repoRoot, {
|
|
541
|
-
handle,
|
|
542
|
-
packet,
|
|
543
|
-
priorContextIds,
|
|
544
|
-
}) {
|
|
545
|
-
if (handle?.worktree_path) {
|
|
546
|
-
try {
|
|
547
|
-
runner._internal.reconcileRemovedWorktree(
|
|
548
|
-
repoRoot,
|
|
549
|
-
handle.worktree_path,
|
|
550
|
-
{ sid: handle.sid },
|
|
551
|
-
);
|
|
552
|
-
} catch { /* the caller preserves the original reservation error */ }
|
|
553
|
-
}
|
|
554
|
-
if (handle?.artifact_dir) {
|
|
555
|
-
const expected = path.resolve(
|
|
556
|
-
repoRoot,
|
|
557
|
-
'.ultra',
|
|
558
|
-
'.runtime',
|
|
559
|
-
'sessions',
|
|
560
|
-
handle.sid,
|
|
561
|
-
);
|
|
562
|
-
const actual = path.resolve(handle.artifact_dir);
|
|
563
|
-
if (actual === expected) {
|
|
564
|
-
try { fs.rmSync(actual, { recursive: true, force: true }); }
|
|
565
|
-
catch { /* the caller preserves the original reservation error */ }
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (packet?.created && !db.prepare(
|
|
569
|
-
'SELECT 1 FROM worker_packets WHERE id = ?',
|
|
570
|
-
).get(packet.id)) {
|
|
571
|
-
const expected = path.resolve(
|
|
572
|
-
repoRoot,
|
|
573
|
-
'.ultra',
|
|
574
|
-
'.runtime',
|
|
575
|
-
'worker-packets',
|
|
576
|
-
`${packet.id}.json`,
|
|
577
|
-
);
|
|
578
|
-
const actual = path.resolve(repoRoot, packet.packet_path);
|
|
579
|
-
if (actual === expected) {
|
|
580
|
-
try { fs.rmSync(actual, { force: true }); }
|
|
581
|
-
catch { /* the caller preserves the original reservation error */ }
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
const context = packet?.context_envelope;
|
|
585
|
-
if (context?.id
|
|
586
|
-
&& !priorContextIds.has(context.id)
|
|
587
|
-
&& !db.prepare('SELECT 1 FROM context_envelopes WHERE id = ?').get(context.id)) {
|
|
588
|
-
const actual = path.resolve(repoRoot, context.path);
|
|
589
|
-
const runtimeContextRoot = path.resolve(
|
|
590
|
-
repoRoot,
|
|
591
|
-
'.ultra',
|
|
592
|
-
'.runtime',
|
|
593
|
-
'contexts',
|
|
594
|
-
);
|
|
595
|
-
if (actual.startsWith(`${runtimeContextRoot}${path.sep}`)
|
|
596
|
-
&& path.basename(actual) === `${context.id}.json`) {
|
|
597
|
-
try { fs.rmSync(actual, { force: true }); }
|
|
598
|
-
catch { /* the caller preserves the original reservation error */ }
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function runTask(db, repoRoot, task, wave, ctx) {
|
|
604
|
-
const prepared = ctx.prepared || prepareTaskDispatch(task, wave, ctx);
|
|
605
|
-
const reservation = reservePreparedTask(db, repoRoot, prepared, ctx);
|
|
606
|
-
if (!reservation?._pendingStart) return reservation;
|
|
607
|
-
const { handle } = reservation;
|
|
608
|
-
if (!handle.process) {
|
|
609
|
-
handle.process = runner.startSessionProcess({
|
|
610
|
-
db,
|
|
611
|
-
repoRoot,
|
|
612
|
-
sid: handle.sid,
|
|
613
|
-
command: reservation.command,
|
|
614
|
-
args: reservation.args,
|
|
615
|
-
});
|
|
616
|
-
handle.pid = handle.process.pid;
|
|
617
|
-
}
|
|
618
|
-
const execution = await awaitExit(handle.process);
|
|
619
|
-
if (ctx.deferFinalize) {
|
|
620
|
-
return { _pendingFinalize: true, handle, execution };
|
|
621
|
-
}
|
|
622
|
-
return finalizeSession(db, handle, execution, { ...ctx, repoRoot });
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function unwindReservations(db, repoRoot, reservations, ctx) {
|
|
626
|
-
const errors = [];
|
|
627
|
-
for (const reservation of [...reservations].reverse()) {
|
|
628
|
-
if (!reservation?._pendingStart) continue;
|
|
629
|
-
const { handle } = reservation;
|
|
630
|
-
try {
|
|
631
|
-
runner.closeSession(
|
|
632
|
-
{ db, repoRoot, sid: handle.sid },
|
|
633
|
-
{ status: 'crashed', remove_worktree: true },
|
|
634
|
-
);
|
|
635
|
-
} catch (error) {
|
|
636
|
-
errors.push(error);
|
|
637
|
-
if (ctx.onError) ctx.onError(error);
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
try {
|
|
641
|
-
workerPackets.abandonWorkerPacket(
|
|
642
|
-
db,
|
|
643
|
-
handle.packet?.id,
|
|
644
|
-
'parallel reservation cancelled before execution',
|
|
645
|
-
);
|
|
646
|
-
} catch (error) {
|
|
647
|
-
errors.push(error);
|
|
648
|
-
if (ctx.onError) ctx.onError(error);
|
|
649
|
-
}
|
|
650
|
-
const task = ops.readTask(db, handle.task_id);
|
|
651
|
-
if (task?.status === 'in_progress') {
|
|
652
|
-
try {
|
|
653
|
-
ops.updateTaskStatus(db, handle.task_id, 'pending');
|
|
654
|
-
} catch (error) {
|
|
655
|
-
errors.push(error);
|
|
656
|
-
if (ctx.onError) ctx.onError(error);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
if (errors.length > 0) {
|
|
661
|
-
throw planError(
|
|
662
|
-
'WAVE_RESERVATION_ROLLBACK_FAILED',
|
|
663
|
-
`parallel wave reservation rollback failed: ${errors.map((error) => error.message).join('; ')}`,
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function taskDependenciesReady(db, task) {
|
|
669
|
-
return (task.deps || []).every((dependencyId) => {
|
|
670
|
-
const dependency = ops.readTask(db, dependencyId);
|
|
671
|
-
return Boolean(dependency && TERMINAL_TASK_STATUSES.has(dependency.status));
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function runWave(db, repoRoot, wave, ctx) {
|
|
676
|
-
const taskRows = wave.tasks.map((id) => ({ id, task: ops.readTask(db, id) }));
|
|
677
|
-
const missingTaskIds = taskRows
|
|
678
|
-
.filter((entry) => !entry.task)
|
|
679
|
-
.map((entry) => entry.id);
|
|
680
|
-
if (ctx.onError) {
|
|
681
|
-
for (const id of missingTaskIds) {
|
|
682
|
-
ctx.onError(new Error(`task ${id} not found in DB; wave cannot converge`));
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
const tasks = taskRows.map((entry) => entry.task).filter(Boolean);
|
|
686
|
-
const runnable = tasks.filter((task) => (
|
|
687
|
-
task.status === 'pending'
|
|
688
|
-
&& !task.stale
|
|
689
|
-
&& taskDependenciesReady(db, task)
|
|
690
|
-
));
|
|
691
|
-
// Pure preparation must finish for the whole wave before any reservation
|
|
692
|
-
// mutates tasks, sessions, or worktrees.
|
|
693
|
-
const prepared = runnable.map((task) => prepareTaskDispatch(task, wave, ctx));
|
|
694
|
-
|
|
695
|
-
if (prepared.length > 0) {
|
|
696
|
-
ops.appendEvent(db, {
|
|
697
|
-
type: 'wave_started',
|
|
698
|
-
payload: {
|
|
699
|
-
wave_id: wave.id,
|
|
700
|
-
tasks: runnable.map((task) => task.id),
|
|
701
|
-
parallel: wave.parallel,
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
let results;
|
|
707
|
-
if (wave.parallel && runnable.length > 1) {
|
|
708
|
-
const reservations = [];
|
|
709
|
-
let reservationFailure = null;
|
|
710
|
-
for (const item of prepared) {
|
|
711
|
-
const reservation = reservePreparedTask(db, repoRoot, item, {
|
|
712
|
-
...ctx,
|
|
713
|
-
deferStart: true,
|
|
714
|
-
deferFinalize: true,
|
|
715
|
-
});
|
|
716
|
-
reservations.push(reservation);
|
|
717
|
-
if (reservation?._reservationFailed) {
|
|
718
|
-
reservationFailure = reservation;
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (reservationFailure) {
|
|
723
|
-
unwindReservations(db, repoRoot, reservations, ctx);
|
|
724
|
-
results = reservations.map((reservation) => (
|
|
725
|
-
reservation?._pendingStart
|
|
726
|
-
? {
|
|
727
|
-
sid: reservation.handle.sid,
|
|
728
|
-
task_id: reservation.handle.task_id,
|
|
729
|
-
code: -5,
|
|
730
|
-
status: 'reservation_cancelled',
|
|
731
|
-
}
|
|
732
|
-
: reservation
|
|
733
|
-
));
|
|
734
|
-
} else {
|
|
735
|
-
const executions = await Promise.all(reservations.map(async (reservation) => {
|
|
736
|
-
if (!reservation?._pendingStart) return reservation;
|
|
737
|
-
const { handle } = reservation;
|
|
738
|
-
try {
|
|
739
|
-
handle.process = runner.startSessionProcess({
|
|
740
|
-
db,
|
|
741
|
-
repoRoot,
|
|
742
|
-
sid: handle.sid,
|
|
743
|
-
command: reservation.command,
|
|
744
|
-
args: reservation.args,
|
|
745
|
-
});
|
|
746
|
-
handle.pid = handle.process.pid;
|
|
747
|
-
const execution = await awaitExit(handle.process);
|
|
748
|
-
return { _pendingFinalize: true, handle, execution };
|
|
749
|
-
} catch (error) {
|
|
750
|
-
const authorityBlocked = authorityBlockedResult(
|
|
751
|
-
error,
|
|
752
|
-
handle.task_id,
|
|
753
|
-
{ sid: handle.sid },
|
|
754
|
-
);
|
|
755
|
-
if (authorityBlocked) {
|
|
756
|
-
unwindReservations(db, repoRoot, [reservation], ctx);
|
|
757
|
-
return authorityBlocked;
|
|
758
|
-
}
|
|
759
|
-
if (ctx.onError) ctx.onError(error);
|
|
760
|
-
recordExecutionFailure(db, handle.task_id, {
|
|
761
|
-
sessionId: handle.sid,
|
|
762
|
-
reason: `worker process could not be started: ${error.message}`,
|
|
763
|
-
});
|
|
764
|
-
return finalizeSession(
|
|
765
|
-
db,
|
|
766
|
-
handle,
|
|
767
|
-
{ code: -1, signal: null, error },
|
|
768
|
-
{ ...ctx, repoRoot },
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}));
|
|
772
|
-
// Parallel execution completes in parallel; settlement is serialized
|
|
773
|
-
// only after every worker exit has been observed.
|
|
774
|
-
results = executions.map((result) => (
|
|
775
|
-
result?._pendingFinalize
|
|
776
|
-
? finalizeSession(db, result.handle, result.execution, { ...ctx, repoRoot })
|
|
777
|
-
: result
|
|
778
|
-
));
|
|
779
|
-
}
|
|
780
|
-
} else {
|
|
781
|
-
results = [];
|
|
782
|
-
for (const item of prepared) {
|
|
783
|
-
results.push(await runTask(db, repoRoot, item.task, wave, {
|
|
784
|
-
...ctx,
|
|
785
|
-
prepared: item,
|
|
786
|
-
}));
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const finalTasks = tasks.map((task) => ops.readTask(db, task.id)).filter(Boolean);
|
|
791
|
-
const complete = missingTaskIds.length === 0
|
|
792
|
-
&& finalTasks.length === tasks.length
|
|
793
|
-
&& finalTasks.every((task) => TERMINAL_TASK_STATUSES.has(task.status));
|
|
794
|
-
const awaiting = finalTasks
|
|
795
|
-
.filter((task) => !TERMINAL_TASK_STATUSES.has(task.status))
|
|
796
|
-
.map((task) => ({ id: task.id, status: task.status, stale: Boolean(task.stale) }))
|
|
797
|
-
.concat(missingTaskIds.map((id) => ({ id, status: 'missing', stale: false })));
|
|
798
|
-
ops.appendEvent(db, {
|
|
799
|
-
type: complete ? 'wave_completed' : 'wave_paused',
|
|
800
|
-
payload: {
|
|
801
|
-
wave_id: wave.id,
|
|
802
|
-
executions_succeeded: results.filter((r) => r.status === 'completed').length,
|
|
803
|
-
executions_crashed: results.filter((r) => r.status === 'crashed').length,
|
|
804
|
-
deferred: results.filter((r) => r.status === 'defer' || r.status === 'block').length,
|
|
805
|
-
awaiting_workflow_gates: awaiting,
|
|
806
|
-
},
|
|
807
|
-
});
|
|
808
|
-
return { results, complete, awaiting };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
async function runPlan({
|
|
812
|
-
db, repoRoot, plan, planPath, changeId = null,
|
|
813
|
-
runtimes,
|
|
814
|
-
command = null, commandArgs = [], commandArgsFor = null,
|
|
815
|
-
autoMerge = false, mergeBaseBranch = 'main',
|
|
816
|
-
onError = null,
|
|
817
|
-
}) {
|
|
818
|
-
if (!db) throw new Error('runPlan: db required');
|
|
819
|
-
if (!repoRoot) throw new Error('runPlan: repoRoot required');
|
|
820
|
-
if (!Array.isArray(runtimes) || runtimes.length === 0) {
|
|
821
|
-
throw new Error('runPlan: runtimes array required');
|
|
822
|
-
}
|
|
823
|
-
if (typeof command !== 'string' || command.trim() === '') {
|
|
824
|
-
const error = new Error(
|
|
825
|
-
'runPlan: an explicit executable command is required; refusing to reserve empty sessions',
|
|
826
|
-
);
|
|
827
|
-
error.code = 'ORCHESTRATOR_COMMAND_REQUIRED';
|
|
828
|
-
throw error;
|
|
829
|
-
}
|
|
830
|
-
if (!Array.isArray(commandArgs)) {
|
|
831
|
-
const error = new Error('runPlan: commandArgs must be an array');
|
|
832
|
-
error.code = 'ORCHESTRATOR_COMMAND_INVALID';
|
|
833
|
-
throw error;
|
|
834
|
-
}
|
|
835
|
-
const resolved = validatePlanShape(resolvePlan({
|
|
836
|
-
db, plan, planPath, repoRoot, changeId,
|
|
837
|
-
}));
|
|
838
|
-
const planAuthority = assertPlanAuthority(db, repoRoot, resolved);
|
|
839
|
-
|
|
840
|
-
const ctx = {
|
|
841
|
-
runtimes, command, commandArgs, commandArgsFor, onError,
|
|
842
|
-
autoMerge, mergeBaseBranch,
|
|
843
|
-
};
|
|
844
|
-
const allResults = [];
|
|
845
|
-
let completedWaves = 0;
|
|
846
|
-
let pausedWave = null;
|
|
847
|
-
for (const wave of resolved.waves) {
|
|
848
|
-
const outcome = await runWave(db, repoRoot, wave, ctx);
|
|
849
|
-
allResults.push(...outcome.results);
|
|
850
|
-
if (!outcome.complete) {
|
|
851
|
-
pausedWave = { id: wave.id, awaiting: outcome.awaiting };
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
completedWaves += 1;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const planTaskIds = [...new Set(resolved.waves.flatMap((wave) => wave.tasks))];
|
|
858
|
-
const awaitingWorkflowGates = planTaskIds
|
|
859
|
-
.map((id) => ops.readTask(db, id) || { id, status: 'missing', stale: false })
|
|
860
|
-
.filter((task) => !TERMINAL_TASK_STATUSES.has(task.status))
|
|
861
|
-
.map((task) => ({ id: task.id, status: task.status, stale: Boolean(task.stale) }));
|
|
862
|
-
const completed = awaitingWorkflowGates.length === 0;
|
|
863
|
-
ops.appendEvent(db, {
|
|
864
|
-
type: completed ? 'plan_completed' : 'plan_paused',
|
|
865
|
-
payload: {
|
|
866
|
-
waves: resolved.waves.length,
|
|
867
|
-
waves_completed: completedWaves,
|
|
868
|
-
paused_wave_id: pausedWave?.id || null,
|
|
869
|
-
executions_succeeded: allResults.filter((r) => r.status === 'completed').length,
|
|
870
|
-
crashed: allResults.filter((r) => r.status === 'crashed').length,
|
|
871
|
-
awaiting_workflow_gates: awaitingWorkflowGates,
|
|
872
|
-
},
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
return {
|
|
876
|
-
status: completed ? 'completed' : 'paused',
|
|
877
|
-
waves: resolved.waves.length,
|
|
878
|
-
waves_completed: completedWaves,
|
|
879
|
-
paused_wave_id: pausedWave?.id || null,
|
|
880
|
-
plan_checkpoint_id: planAuthority.checkpoint_id || null,
|
|
881
|
-
awaiting_workflow_gates: awaitingWorkflowGates,
|
|
882
|
-
results: allResults,
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
module.exports = {
|
|
887
|
-
runPlan,
|
|
888
|
-
// exposed for targeted tests
|
|
889
|
-
_internal: {
|
|
890
|
-
runWave,
|
|
891
|
-
runTask,
|
|
892
|
-
resolvePlan,
|
|
893
|
-
validatePlanShape,
|
|
894
|
-
assertPlanAuthority,
|
|
895
|
-
awaitExit,
|
|
896
|
-
finalizeSession,
|
|
897
|
-
taskDependenciesReady,
|
|
898
|
-
recordExecutionFailure,
|
|
899
|
-
prepareTaskDispatch,
|
|
900
|
-
reservePreparedTask,
|
|
901
|
-
unwindReservations,
|
|
902
|
-
},
|
|
903
|
-
};
|