ultra-builder-pro-cli 0.15.0 → 0.17.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/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +120 -1
- package/README.md +67 -30
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +3 -2
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +38 -18
- package/docs/ARCHITECTURE.md +91 -13
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -12
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +179 -39
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +107 -39
- package/mcp-server/lib/context-spine.cjs +215 -87
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +9 -0
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +23 -14
- package/mcp-server/lib/state-db.cjs +279 -9
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +313 -205
- package/mcp-server/server.cjs +112 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -65
- package/skills/ultra-deliver/SKILL.md +37 -58
- package/skills/ultra-dev/SKILL.md +55 -49
- package/skills/ultra-doctor/SKILL.md +44 -49
- package/skills/ultra-init/SKILL.md +78 -100
- package/skills/ultra-plan/SKILL.md +78 -79
- package/skills/ultra-research/SKILL.md +99 -98
- package/skills/ultra-review/SKILL.md +38 -47
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -40
- package/skills/ultra-test/SKILL.md +61 -48
- package/skills/ultra-think/SKILL.md +47 -47
- package/skills/ultra-think/references/decision-dialogue.md +106 -0
- package/spec/cli-protocol.md +46 -2
- package/spec/mcp-tools.yaml +444 -29
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +66 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +34 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
package/mcp-server/server.cjs
CHANGED
|
@@ -32,11 +32,13 @@ const baselines = require('./lib/baseline-workflow.cjs');
|
|
|
32
32
|
const changes = require('./lib/change-workflow.cjs');
|
|
33
33
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
34
34
|
const workflows = require('./lib/workflow-state.cjs');
|
|
35
|
+
const decisions = require('./lib/decision-dialogue.cjs');
|
|
35
36
|
const doctor = require('./lib/doctor.cjs');
|
|
36
37
|
const {
|
|
37
38
|
readProjectBreadcrumb,
|
|
38
39
|
renderProjectBreadcrumb,
|
|
39
40
|
} = require('./lib/project-breadcrumb.cjs');
|
|
41
|
+
const sessionRunner = require('../orchestrator/session-runner.cjs');
|
|
40
42
|
|
|
41
43
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
42
44
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
@@ -101,12 +103,25 @@ const WORKFLOW_TOOLS = Object.freeze([
|
|
|
101
103
|
'workflow.complete',
|
|
102
104
|
]);
|
|
103
105
|
|
|
106
|
+
const DECISION_TOOLS = Object.freeze([
|
|
107
|
+
'decision.thread_start',
|
|
108
|
+
'decision.get',
|
|
109
|
+
'decision.list',
|
|
110
|
+
'decision.open',
|
|
111
|
+
'decision.resolve',
|
|
112
|
+
'decision.delegate',
|
|
113
|
+
'decision.defer',
|
|
114
|
+
'decision.supersede',
|
|
115
|
+
'decision.checkpoint',
|
|
116
|
+
]);
|
|
117
|
+
|
|
104
118
|
const SYSTEM_TOOLS = Object.freeze([
|
|
105
119
|
'system.doctor',
|
|
106
120
|
]);
|
|
107
121
|
|
|
108
122
|
const REGISTERED_TOOLS = Object.freeze([
|
|
109
123
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
124
|
+
...DECISION_TOOLS,
|
|
110
125
|
...WORKFLOW_TOOLS,
|
|
111
126
|
...SYSTEM_TOOLS,
|
|
112
127
|
]);
|
|
@@ -123,6 +138,8 @@ const MUTATING_TOOLS = new Set([
|
|
|
123
138
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
124
139
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
125
140
|
'change.learning_propose', 'change.learning_resolve',
|
|
141
|
+
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
142
|
+
'decision.defer', 'decision.supersede', 'decision.checkpoint',
|
|
126
143
|
'workflow.start', 'workflow.step', 'workflow.complete',
|
|
127
144
|
]);
|
|
128
145
|
|
|
@@ -137,11 +154,6 @@ function buildAjv() {
|
|
|
137
154
|
return ajv;
|
|
138
155
|
}
|
|
139
156
|
|
|
140
|
-
function mintSessionId() {
|
|
141
|
-
const { randomUUID } = require('node:crypto');
|
|
142
|
-
return `sess-${randomUUID().slice(0, 8)}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
157
|
async function dispatchTool(name, input, db, ctx = {}) {
|
|
146
158
|
switch (name) {
|
|
147
159
|
case 'task.create': {
|
|
@@ -191,40 +203,51 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
191
203
|
return ops.subscribeEventsSince(db, input || {});
|
|
192
204
|
}
|
|
193
205
|
case 'session.spawn': {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
err.code = 'ADMISSION_DENIED';
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
204
|
-
if (!verdict.can_spawn && input.takeover && verdict.conflict) {
|
|
205
|
-
ops.updateSession(db, verdict.conflict.sid, { status: 'crashed' });
|
|
206
|
+
if (ctx.sessionId) {
|
|
207
|
+
const error = new Error(
|
|
208
|
+
`worker session ${ctx.sessionId} already owns an isolated worktree; `
|
|
209
|
+
+ 'resume that session instead of recursively spawning another one',
|
|
210
|
+
);
|
|
211
|
+
error.code = 'NESTED_SESSION_FORBIDDEN';
|
|
212
|
+
throw error;
|
|
206
213
|
}
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const session = ops.createSession(db, {
|
|
212
|
-
sid,
|
|
214
|
+
const rootDir = ctx.rootDir || process.cwd();
|
|
215
|
+
const handle = sessionRunner.spawnSession({
|
|
216
|
+
db,
|
|
217
|
+
repoRoot: rootDir,
|
|
213
218
|
task_id: input.task_id,
|
|
214
219
|
runtime: input.runtime,
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
takeover: Boolean(input.takeover),
|
|
221
|
+
worktree_base: input.worktree_base,
|
|
217
222
|
});
|
|
218
223
|
return {
|
|
219
|
-
sid,
|
|
220
|
-
worktree_path,
|
|
221
|
-
artifact_dir,
|
|
222
|
-
lease_expires_at:
|
|
224
|
+
sid: handle.sid,
|
|
225
|
+
worktree_path: handle.worktree_path,
|
|
226
|
+
artifact_dir: handle.artifact_dir,
|
|
227
|
+
lease_expires_at: handle.lease_expires_at,
|
|
228
|
+
worktree_created: true,
|
|
223
229
|
};
|
|
224
230
|
}
|
|
225
231
|
case 'session.close': {
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
if (ctx.sessionId) {
|
|
233
|
+
const error = new Error(
|
|
234
|
+
ctx.sessionId === input.sid
|
|
235
|
+
? `worker session ${ctx.sessionId} is supervised by its parent; save evidence and exit so the parent can settle the lease`
|
|
236
|
+
: `worker session ${ctx.sessionId} cannot close unrelated session ${input.sid}`,
|
|
237
|
+
);
|
|
238
|
+
error.code = ctx.sessionId === input.sid
|
|
239
|
+
? 'WORKER_SESSION_PARENT_OWNED'
|
|
240
|
+
: 'SESSION_SCOPE_VIOLATION';
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const closed = sessionRunner.closeSession(
|
|
244
|
+
{ db, repoRoot: ctx.rootDir || process.cwd(), sid: input.sid },
|
|
245
|
+
{ status: input.status, remove_worktree: input.remove_worktree === true },
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
worktree_preserved: closed.worktree_preserved,
|
|
250
|
+
};
|
|
228
251
|
}
|
|
229
252
|
case 'session.get': {
|
|
230
253
|
const session = ops.readSession(db, input.sid);
|
|
@@ -236,6 +259,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
236
259
|
return { session };
|
|
237
260
|
}
|
|
238
261
|
case 'session.admission_check': {
|
|
262
|
+
sessionRunner.assertSessionTaskReady(
|
|
263
|
+
db,
|
|
264
|
+
ctx.rootDir || process.cwd(),
|
|
265
|
+
input.task_id,
|
|
266
|
+
);
|
|
239
267
|
return ops.admissionCheck(db, input.task_id);
|
|
240
268
|
}
|
|
241
269
|
case 'session.list': {
|
|
@@ -517,6 +545,44 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
517
545
|
case 'change.archive': {
|
|
518
546
|
return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
519
547
|
}
|
|
548
|
+
case 'decision.thread_start': {
|
|
549
|
+
return { thread: decisions.startDecisionThread(db, input) };
|
|
550
|
+
}
|
|
551
|
+
case 'decision.get': {
|
|
552
|
+
const thread = decisions.readDecisionThread(db, input.id);
|
|
553
|
+
if (!thread) {
|
|
554
|
+
const error = new Error(`decision thread ${input.id} not found`);
|
|
555
|
+
error.code = 'DECISION_THREAD_NOT_FOUND';
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
return { thread };
|
|
559
|
+
}
|
|
560
|
+
case 'decision.list': {
|
|
561
|
+
const threads = decisions.listDecisionThreads(db, input || {});
|
|
562
|
+
return { threads, count: threads.length };
|
|
563
|
+
}
|
|
564
|
+
case 'decision.open': {
|
|
565
|
+
return { thread: decisions.openDecision(db, input) };
|
|
566
|
+
}
|
|
567
|
+
case 'decision.resolve': {
|
|
568
|
+
return { thread: decisions.resolveDecision(db, input) };
|
|
569
|
+
}
|
|
570
|
+
case 'decision.delegate': {
|
|
571
|
+
return { thread: decisions.delegateDecision(db, input) };
|
|
572
|
+
}
|
|
573
|
+
case 'decision.defer': {
|
|
574
|
+
return { thread: decisions.deferDecision(db, input) };
|
|
575
|
+
}
|
|
576
|
+
case 'decision.supersede': {
|
|
577
|
+
return { thread: decisions.supersedeDecision(db, input) };
|
|
578
|
+
}
|
|
579
|
+
case 'decision.checkpoint': {
|
|
580
|
+
return {
|
|
581
|
+
thread: decisions.checkpointDecisionThread(
|
|
582
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
583
|
+
),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
520
586
|
case 'workflow.start': {
|
|
521
587
|
return {
|
|
522
588
|
workflow: workflows.startWorkflow(
|
|
@@ -579,7 +645,13 @@ function errorResponse(code, message, retriable = false, details = undefined) {
|
|
|
579
645
|
};
|
|
580
646
|
}
|
|
581
647
|
|
|
582
|
-
function startServer({
|
|
648
|
+
function startServer({
|
|
649
|
+
dbPath,
|
|
650
|
+
rootDir,
|
|
651
|
+
sessionId = null,
|
|
652
|
+
projectOnWrite = true,
|
|
653
|
+
project = projector.projectAll,
|
|
654
|
+
}) {
|
|
583
655
|
let db = null;
|
|
584
656
|
const getDb = () => {
|
|
585
657
|
if (!db) db = initStateDb(dbPath).db;
|
|
@@ -649,7 +721,12 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
|
|
|
649
721
|
try {
|
|
650
722
|
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
651
723
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
652
|
-
result = await dispatchTool(
|
|
724
|
+
result = await dispatchTool(
|
|
725
|
+
name,
|
|
726
|
+
args,
|
|
727
|
+
toolDb,
|
|
728
|
+
{ rootDir, sessionId, projector: project },
|
|
729
|
+
);
|
|
653
730
|
} catch (err) {
|
|
654
731
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
655
732
|
toolError = code;
|
|
@@ -714,7 +791,8 @@ async function main() {
|
|
|
714
791
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
715
792
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
716
793
|
: path.resolve('.');
|
|
717
|
-
const
|
|
794
|
+
const sessionId = process.env.UBP_SESSION_ID || null;
|
|
795
|
+
const handle = startServer({ dbPath, rootDir, sessionId });
|
|
718
796
|
const transport = new StdioServerTransport();
|
|
719
797
|
await handle.server.connect(transport);
|
|
720
798
|
const cleanup = () => handle.close().finally(() => process.exit(0));
|
|
@@ -40,6 +40,25 @@ function autoMerge({ repoRoot, worktreePath, baseBranch = 'main', sid, task_id =
|
|
|
40
40
|
if (!repoRoot) throw new Error('autoMerge: repoRoot required');
|
|
41
41
|
if (!worktreePath) throw new Error('autoMerge: worktreePath required');
|
|
42
42
|
|
|
43
|
+
try {
|
|
44
|
+
const status = runGit(['status', '--porcelain=v1'], { cwd: worktreePath });
|
|
45
|
+
if (status) {
|
|
46
|
+
return {
|
|
47
|
+
merged: false,
|
|
48
|
+
reason: 'uncommitted_changes',
|
|
49
|
+
session_sha: null,
|
|
50
|
+
base_sha: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
merged: false,
|
|
56
|
+
reason: 'worktree_status_failed',
|
|
57
|
+
session_sha: null,
|
|
58
|
+
base_sha: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
let sessionSha;
|
|
44
63
|
let baseSha;
|
|
45
64
|
try {
|
package/orchestrator/daemon.cjs
CHANGED
|
@@ -32,6 +32,26 @@ function routeTask(task, availableRuntimes) {
|
|
|
32
32
|
return d.action === 'spawn_agent' ? d.runtime : null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function taskDependenciesReady(db, task) {
|
|
36
|
+
return (task.deps || []).every((dependencyId) => {
|
|
37
|
+
const dependency = ops.readTask(db, dependencyId);
|
|
38
|
+
return Boolean(dependency && ['completed', 'expanded'].includes(dependency.status));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasActiveFileConflict(db, task) {
|
|
43
|
+
const declared = new Set(task.files_modified || []);
|
|
44
|
+
if (declared.size === 0) return false;
|
|
45
|
+
return ops.listActiveSessions(db).some((session) => {
|
|
46
|
+
if (!session.task_id || session.task_id === task.id) return false;
|
|
47
|
+
const activeTask = ops.readTask(db, session.task_id);
|
|
48
|
+
return Boolean(
|
|
49
|
+
activeTask
|
|
50
|
+
&& (activeTask.files_modified || []).some((file) => declared.has(file)),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
function maintainState({ db, repoRoot, project = projector.projectAll } = {}) {
|
|
36
56
|
if (!db) throw new Error('maintainState: db required');
|
|
37
57
|
if (!repoRoot) throw new Error('maintainState: repoRoot required');
|
|
@@ -60,6 +80,18 @@ function runDaemon({
|
|
|
60
80
|
if (!Array.isArray(runtimes) || runtimes.length === 0) {
|
|
61
81
|
throw new Error('runDaemon: runtimes array required');
|
|
62
82
|
}
|
|
83
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
84
|
+
const error = new Error(
|
|
85
|
+
'runDaemon: an explicit executable command is required; refusing to reserve empty sessions',
|
|
86
|
+
);
|
|
87
|
+
error.code = 'ORCHESTRATOR_COMMAND_REQUIRED';
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(commandArgs)) {
|
|
91
|
+
const error = new Error('runDaemon: commandArgs must be an array');
|
|
92
|
+
error.code = 'ORCHESTRATOR_COMMAND_INVALID';
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
63
95
|
|
|
64
96
|
let stopped = false;
|
|
65
97
|
const children = [];
|
|
@@ -75,6 +107,54 @@ function runDaemon({
|
|
|
75
107
|
bootRecovery = { recovered: [], count: 0, error: error.message };
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
function settleExecution(handle, task) {
|
|
111
|
+
let settled = false;
|
|
112
|
+
const stopHeartbeat = runner.attachHeartbeat(db, handle.sid);
|
|
113
|
+
const settle = (rawCode, signal, spawnError = null) => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
stopHeartbeat();
|
|
117
|
+
const code = rawCode === null ? -1 : rawCode;
|
|
118
|
+
const success = code === 0;
|
|
119
|
+
// A caller may intentionally close the DB while leaving child processes
|
|
120
|
+
// alive during shutdown. The next boot recovery owns those stale leases.
|
|
121
|
+
if (!db.open) return;
|
|
122
|
+
const liveSession = ops.readSession(db, handle.sid);
|
|
123
|
+
// Takeover or an explicit close already owns the terminal transition.
|
|
124
|
+
// The late child exit must not block the replacement task/session.
|
|
125
|
+
if (!liveSession || liveSession.status !== 'running') return;
|
|
126
|
+
try {
|
|
127
|
+
if (!success) {
|
|
128
|
+
try { ops.patchTask(db, task.id, { status: 'blocked' }); }
|
|
129
|
+
catch (error) {
|
|
130
|
+
runtimeState.recordIncident(db, {
|
|
131
|
+
code: 'ORCHESTRATOR_TASK_BLOCK_FAILED', severity: 'error', retryable: true,
|
|
132
|
+
message: error.message, source_kind: 'task', source_id: task.id,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
ops.recordTaskFailure(db, task.id, {
|
|
136
|
+
reason: spawnError
|
|
137
|
+
? `worker spawn or runtime error: ${spawnError.message}`
|
|
138
|
+
: `runtime process exited with code ${code}${signal ? ` (${signal})` : ''}`,
|
|
139
|
+
session_id: handle.sid,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
runner.closeSession(
|
|
143
|
+
{ db, repoRoot, sid: handle.sid },
|
|
144
|
+
{ status: success ? 'completed' : 'crashed', remove_worktree: false },
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (onError) onError(error);
|
|
148
|
+
else process.stderr.write(`orchestrator settle error: ${error.message}\n`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
handle.process.once('exit', settle);
|
|
152
|
+
handle.process.once('error', (error) => settle(-1, null, error));
|
|
153
|
+
if (handle.process.exitCode !== null && handle.process.exitCode !== undefined) {
|
|
154
|
+
setImmediate(() => settle(handle.process.exitCode, handle.process.signalCode || null));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
78
158
|
function tick() {
|
|
79
159
|
if (stopped) return;
|
|
80
160
|
try {
|
|
@@ -101,6 +181,17 @@ function runDaemon({
|
|
|
101
181
|
}
|
|
102
182
|
for (const task of pending) {
|
|
103
183
|
if (stopped) return;
|
|
184
|
+
if (task.stale) continue;
|
|
185
|
+
if (!taskDependenciesReady(db, task)) continue;
|
|
186
|
+
if (hasActiveFileConflict(db, task)) continue;
|
|
187
|
+
try {
|
|
188
|
+
runner.assertSessionTaskReady(db, repoRoot, task.id);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// A normal workflow gate is a durable pending state, not a daemon
|
|
191
|
+
// failure and not an error to repeat on every poll.
|
|
192
|
+
if (!runner.isExpectedExecutionGate(error) && onError) onError(error);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
104
195
|
// admissionCheck catches both live-session conflicts and tripped breakers.
|
|
105
196
|
let verdict;
|
|
106
197
|
try { verdict = ops.admissionCheck(db, task.id); }
|
|
@@ -118,6 +209,17 @@ function runDaemon({
|
|
|
118
209
|
command,
|
|
119
210
|
args: commandArgs,
|
|
120
211
|
});
|
|
212
|
+
handle.task_id = task.id;
|
|
213
|
+
try {
|
|
214
|
+
ops.patchTask(db, task.id, { status: 'in_progress' });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
runner.closeSession(
|
|
217
|
+
{ db, repoRoot, sid: handle.sid },
|
|
218
|
+
{ status: 'crashed', remove_worktree: false },
|
|
219
|
+
);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
settleExecution(handle, task);
|
|
121
223
|
children.push(handle);
|
|
122
224
|
} catch (err) {
|
|
123
225
|
if (onError) onError(err);
|
|
@@ -147,4 +249,6 @@ module.exports = {
|
|
|
147
249
|
runDaemon,
|
|
148
250
|
routeTask,
|
|
149
251
|
maintainState,
|
|
252
|
+
taskDependenciesReady,
|
|
253
|
+
hasActiveFileConflict,
|
|
150
254
|
};
|