@revisium/orchestrator 0.1.0-alpha.8 → 0.1.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/README.md +150 -130
- package/control-plane/default-playbook/catalog/pipelines.json +280 -11
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/prompts/developer.md +1 -0
- package/control-plane/default-playbook/prompts/triager.md +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js +30 -0
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -0
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run.model.js +6 -0
- package/dist/api/graphql-api/runs/model/run.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-ref.model.js +53 -0
- package/dist/api/graphql-api/share/model/issue-ref.model.js.map +1 -0
- package/dist/cli/commands/lifecycle.js +113 -8
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/process-tree.js +42 -0
- package/dist/cli/commands/process-tree.js.map +1 -1
- package/dist/cli/commands/rogue-reaper.js +63 -0
- package/dist/cli/commands/rogue-reaper.js.map +1 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap.js +14 -1
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/client-transport.js +1 -1
- package/dist/control-plane/client-transport.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +593 -0
- package/dist/control-plane/default-playbook-policy.js.map +1 -0
- package/dist/control-plane/seed-default-playbook.js +77 -8
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/e2e/kit/agents.js +15 -21
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +18 -13
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +11 -2
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +2 -1
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/git-target-repo.js +1 -1
- package/dist/engine/dbos.service.js +3 -0
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
- package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
- package/dist/features/runs/queries/handlers/runs-query.handlers.js +1 -0
- package/dist/features/runs/queries/handlers/runs-query.handlers.js.map +1 -1
- package/dist/host/daemon.js +14 -1
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/host-runtime.js +20 -2
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +16 -1
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +325 -12
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +58 -12
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/observability/activity-signal.js +107 -0
- package/dist/observability/activity-signal.js.map +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +549 -44
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +195 -101
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +4 -3
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/interpret.js +2 -0
- package/dist/pipeline-core/interpret.js.map +1 -1
- package/dist/pipeline-core/kit/builders.js +2 -0
- package/dist/pipeline-core/kit/builders.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +72 -21
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/types.js +2 -0
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-dataflow.js +32 -0
- package/dist/pipeline-core/validate-dataflow.js.map +1 -1
- package/dist/playbook/import-mapper.js +8 -1
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +299 -28
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/revisium/playbooks.service.js +17 -2
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/run.service.js +1 -0
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/run/append-event.js +8 -5
- package/dist/run/append-event.js.map +1 -1
- package/dist/run/create-run.js +5 -1
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +30 -4
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +88 -0
- package/dist/run/issue-ref.js.map +1 -0
- package/dist/runners/claude-code.service.js +2 -2
- package/dist/runners/claude-code.service.js.map +1 -1
- package/dist/runners/codex.service.js +2 -2
- package/dist/runners/codex.service.js.map +1 -1
- package/dist/runners/integrator-branch-naming.js +33 -13
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator.js +285 -72
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +2 -2
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/task-control-plane/pr-readiness.service.js +15 -13
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +512 -0
- package/dist/task-control-plane/run-watch.service.js.map +1 -0
- package/dist/task-control-plane/task-control-plane-api.service.js +277 -25
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/artifact-store.js +21 -3
- package/dist/worker/artifact-store.js.map +1 -1
- package/dist/worker/build-context.js +19 -3
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +76 -15
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +40 -13
- package/dist/worker/codex-runner.js.map +1 -1
- package/dist/worker/process-executor.js +149 -20
- package/dist/worker/process-executor.js.map +1 -1
- package/dist/worker/runner-common.js +29 -0
- package/dist/worker/runner-common.js.map +1 -1
- package/dist/worker/runner.js +16 -1
- package/dist/worker/runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|
|
18
18
|
import { baseUrl, getConfig, isAlive, isHealthy, readRuntime } from '../cli/config.js';
|
|
19
19
|
import { AgentObservabilityService } from '../observability/agent-observability.service.js';
|
|
20
20
|
import { ControlPlaneError } from '../control-plane/errors.js';
|
|
21
|
+
import { fnv1a64Hex } from '../control-plane/steps.js';
|
|
21
22
|
import { DbosService } from '../engine/dbos.service.js';
|
|
22
23
|
import { PipelineService } from '../pipeline/pipeline.service.js';
|
|
23
24
|
import { RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
|
|
@@ -27,6 +28,7 @@ import { InboxService } from '../revisium/inbox.service.js';
|
|
|
27
28
|
import { PlaybooksService } from '../revisium/playbooks.service.js';
|
|
28
29
|
import { RolesService } from '../revisium/roles.service.js';
|
|
29
30
|
import { RunService } from '../revisium/run.service.js';
|
|
31
|
+
import { CreateRunWorkflowError, previewCreateRunIds, } from '../run/create-run.js';
|
|
30
32
|
import { PrReadinessService } from './pr-readiness.service.js';
|
|
31
33
|
const execFileAsync = promisify(execFile);
|
|
32
34
|
const GATE_TOPICS = new Set(['plan', 'merge', 'question']);
|
|
@@ -120,6 +122,40 @@ function summarizeAttempts(attempts) {
|
|
|
120
122
|
function normalizedRunStatus(status) {
|
|
121
123
|
return status === 'paused' ? 'blocked' : status;
|
|
122
124
|
}
|
|
125
|
+
function hasWorkflowProgress(events) {
|
|
126
|
+
return events.some((event) => (event.type.startsWith('step_')
|
|
127
|
+
|| event.type.startsWith('attempt_')
|
|
128
|
+
|| event.type.startsWith('gate_')
|
|
129
|
+
|| event.type === 'pipeline_blocked'
|
|
130
|
+
|| event.type === 'pr_polled'
|
|
131
|
+
|| event.type === 'integrate_succeeded'));
|
|
132
|
+
}
|
|
133
|
+
function observedRunStatus(rowStatus, workflowStatus, events) {
|
|
134
|
+
if (rowStatus !== 'ready')
|
|
135
|
+
return rowStatus;
|
|
136
|
+
if (workflowStatus === 'SUCCESS')
|
|
137
|
+
return 'completed';
|
|
138
|
+
if (workflowStatus === 'ERROR')
|
|
139
|
+
return 'failed';
|
|
140
|
+
if (workflowStatus && workflowStatus !== 'NOT_STARTED')
|
|
141
|
+
return 'running';
|
|
142
|
+
if (hasWorkflowProgress(events))
|
|
143
|
+
return 'running';
|
|
144
|
+
return rowStatus;
|
|
145
|
+
}
|
|
146
|
+
function observedTaskStatus(rowStatus, runStatus) {
|
|
147
|
+
if (rowStatus !== 'ready')
|
|
148
|
+
return rowStatus;
|
|
149
|
+
if (runStatus === 'running' || runStatus === 'completed' || runStatus === 'failed' || runStatus === 'cancelled') {
|
|
150
|
+
return runStatus;
|
|
151
|
+
}
|
|
152
|
+
return rowStatus;
|
|
153
|
+
}
|
|
154
|
+
function latestEventPulse(events) {
|
|
155
|
+
const latest = [...events]
|
|
156
|
+
.sort((left, right) => dateOrEpoch(right.createdAt).getTime() - dateOrEpoch(left.createdAt).getTime())[0];
|
|
157
|
+
return latest ? { latestEventAt: latest.createdAt, latestEventType: latest.type } : {};
|
|
158
|
+
}
|
|
123
159
|
function payloadRecord(event) {
|
|
124
160
|
return asRecord(event.payload);
|
|
125
161
|
}
|
|
@@ -138,6 +174,72 @@ function eventSummary(event) {
|
|
|
138
174
|
return output.slice(0, 180);
|
|
139
175
|
return event.type;
|
|
140
176
|
}
|
|
177
|
+
function blockedReasonFromEvent(event) {
|
|
178
|
+
const reason = payloadRecord(event)?.reason;
|
|
179
|
+
return typeof reason === 'string' && reason ? reason : undefined;
|
|
180
|
+
}
|
|
181
|
+
function latestPipelineBlockedEvent(events) {
|
|
182
|
+
return [...events].reverse().find((event) => event.type === 'pipeline_blocked');
|
|
183
|
+
}
|
|
184
|
+
function recoveryCreatedForBlockedEvent(events, blockedEventId) {
|
|
185
|
+
return [...events]
|
|
186
|
+
.reverse()
|
|
187
|
+
.find((event) => {
|
|
188
|
+
if (event.type !== 'run_recovery_created')
|
|
189
|
+
return false;
|
|
190
|
+
return payloadRecord(event)?.blockedEventId === blockedEventId;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function requiredString(value, field, runId) {
|
|
194
|
+
if (typeof value === 'string' && value.trim())
|
|
195
|
+
return value;
|
|
196
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent ${field} is missing`);
|
|
197
|
+
}
|
|
198
|
+
function optionalString(value) {
|
|
199
|
+
return typeof value === 'string' ? value : undefined;
|
|
200
|
+
}
|
|
201
|
+
function optionalInteger(value) {
|
|
202
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
203
|
+
}
|
|
204
|
+
function requiredRecord(value, field, runId) {
|
|
205
|
+
const record = asRecord(value);
|
|
206
|
+
if (record)
|
|
207
|
+
return record;
|
|
208
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent ${field} is not a record`);
|
|
209
|
+
}
|
|
210
|
+
function optionalRecord(value) {
|
|
211
|
+
return asRecord(value) ?? {};
|
|
212
|
+
}
|
|
213
|
+
function firstRepoRef(value, runId) {
|
|
214
|
+
if (Array.isArray(value) && typeof value[0] === 'string' && value[0].trim())
|
|
215
|
+
return value[0];
|
|
216
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent repo is missing`);
|
|
217
|
+
}
|
|
218
|
+
function makeRecoveryLineage(parentRunId, recoveryRunId, blockedEventId) {
|
|
219
|
+
return { parentRunId, recoveryRunId, blockedEventId, reason: 'preflight' };
|
|
220
|
+
}
|
|
221
|
+
function recoveryLineagePayload(lineage) {
|
|
222
|
+
return { parentRunId: lineage.parentRunId, recoveryRunId: lineage.recoveryRunId, blockedEventId: lineage.blockedEventId, reason: lineage.reason };
|
|
223
|
+
}
|
|
224
|
+
function recoveryLineageEvent(lineage, runId, taskId, type) {
|
|
225
|
+
const payload = recoveryLineagePayload(lineage);
|
|
226
|
+
return { runId, taskId, stepId: '', stepKey: 'recovery', type, idempotencyKey: lineage.blockedEventId, payload, actor: 'orchestrator' };
|
|
227
|
+
}
|
|
228
|
+
function recoverableStartRunResponse(runId, route, recoverable) {
|
|
229
|
+
return {
|
|
230
|
+
runId,
|
|
231
|
+
workflowID: recoverable.workflowID,
|
|
232
|
+
alreadyStarted: true,
|
|
233
|
+
recoverable: true,
|
|
234
|
+
retryStarted: false,
|
|
235
|
+
nextAction: 'resume_run',
|
|
236
|
+
blockedEventId: recoverable.blockedEventId,
|
|
237
|
+
blockedReason: recoverable.reason,
|
|
238
|
+
workflowStatus: recoverable.workflowStatus,
|
|
239
|
+
route,
|
|
240
|
+
engine: 'data-driven',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
141
243
|
function stepKeyBase(stepKey) {
|
|
142
244
|
return stepKey.split('#')[0] ?? stepKey;
|
|
143
245
|
}
|
|
@@ -208,6 +310,7 @@ function mapRunForWorkflow(run) {
|
|
|
208
310
|
scope: run.scope,
|
|
209
311
|
repos: run.repos ?? [],
|
|
210
312
|
createdAt: dateOrEpoch(run.createdAt),
|
|
313
|
+
...(run.issueRef ? { issueRef: run.issueRef } : {}),
|
|
211
314
|
};
|
|
212
315
|
}
|
|
213
316
|
function workflowNodeStatus(input) {
|
|
@@ -407,6 +510,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
407
510
|
playbookId: input.playbookId,
|
|
408
511
|
pipelineId: input.pipelineId,
|
|
409
512
|
params: input.params,
|
|
513
|
+
issueRef: input.issueRef,
|
|
410
514
|
executionProfile: input.executionProfile,
|
|
411
515
|
source: input.pipelineId ? 'explicit' : 'deterministic-installed-playbook',
|
|
412
516
|
});
|
|
@@ -420,6 +524,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
420
524
|
playbookId: route.playbookId,
|
|
421
525
|
pipelineId: route.pipelineId,
|
|
422
526
|
params: route.params,
|
|
527
|
+
issueRef: route.params.issueRef,
|
|
423
528
|
routeDecision: route,
|
|
424
529
|
executionProfile: route.executionProfile,
|
|
425
530
|
});
|
|
@@ -436,8 +541,11 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
436
541
|
const run = await this.runs.getRun(input.runId);
|
|
437
542
|
if (!run)
|
|
438
543
|
throw new ControlPlaneError('ROW_NOT_FOUND', `run not found: ${input.runId}`);
|
|
439
|
-
const route = input.route ?? await this.routeForRun(run);
|
|
440
544
|
const existingStatus = await this.dbos.getWorkflowStatus(input.runId);
|
|
545
|
+
const route = input.route ?? await this.routeForRun(run);
|
|
546
|
+
const recoverable = await this.detectRecoverablePreflightBlock(input.runId, run.data.status, existingStatus);
|
|
547
|
+
if (recoverable)
|
|
548
|
+
return recoverableStartRunResponse(input.runId, route, recoverable);
|
|
441
549
|
// CUTOVER (plan 0015 slice 3): the data-driven engine is the SOLE pipeline engine. EVERY pipeline
|
|
442
550
|
// routes to the data-driven workflow, executing the state-machine template carried in its
|
|
443
551
|
// execution_policy (template_json). A pipeline lacking a valid template FAILS LOUD here
|
|
@@ -457,8 +565,115 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
457
565
|
engine: 'data-driven',
|
|
458
566
|
};
|
|
459
567
|
}
|
|
460
|
-
resumeRun(input) {
|
|
461
|
-
|
|
568
|
+
async resumeRun(input) {
|
|
569
|
+
const run = await this.runs.getRun(input.runId);
|
|
570
|
+
if (!run)
|
|
571
|
+
throw new ControlPlaneError('ROW_NOT_FOUND', `run not found: ${input.runId}`);
|
|
572
|
+
const workflow = await this.dbos.getWorkflowStatus(input.runId);
|
|
573
|
+
const recoverable = await this.detectRecoverablePreflightBlock(input.runId, run.data.status, workflow);
|
|
574
|
+
if (!recoverable)
|
|
575
|
+
return this.startRun(input);
|
|
576
|
+
const recovery = await this.createOrReusePreflightRecoveryRun(input.runId, run.data, recoverable.blockedEvent);
|
|
577
|
+
const started = await this.startRun({
|
|
578
|
+
runId: recovery.recoveryRunId,
|
|
579
|
+
runnerMode: input.runnerMode,
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
...started,
|
|
583
|
+
recovered: true,
|
|
584
|
+
recovery,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async detectRecoverablePreflightBlock(runId, runStatus, workflow) {
|
|
588
|
+
if (runStatus !== 'paused' || workflow?.status !== 'SUCCESS')
|
|
589
|
+
return null;
|
|
590
|
+
const events = await this.runs.listRunEvents(runId, { type: 'pipeline_blocked', limit: 500 });
|
|
591
|
+
const blockedEvent = latestPipelineBlockedEvent(events);
|
|
592
|
+
if (!blockedEvent || blockedReasonFromEvent(blockedEvent) !== 'preflight')
|
|
593
|
+
return null;
|
|
594
|
+
return {
|
|
595
|
+
blockedEvent,
|
|
596
|
+
blockedEventId: blockedEvent.eventId,
|
|
597
|
+
reason: 'preflight',
|
|
598
|
+
workflowID: workflow.workflowID,
|
|
599
|
+
workflowStatus: 'SUCCESS',
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async createOrReusePreflightRecoveryRun(parentRunId, parentData, blockedEvent) {
|
|
603
|
+
const existing = await this.existingPreflightRecoveryRun(parentRunId, blockedEvent);
|
|
604
|
+
if (existing)
|
|
605
|
+
return existing;
|
|
606
|
+
const parentDetail = await this.runs.showRun(parentRunId);
|
|
607
|
+
const parentTask = parentDetail?.tasks[0];
|
|
608
|
+
if (!parentTask) {
|
|
609
|
+
throw new ControlPlaneError('ROW_NOT_FOUND', `Cannot recover run ${parentRunId}: parent task is missing`);
|
|
610
|
+
}
|
|
611
|
+
const createInput = this.buildPreflightRecoveryCreateInput(parentRunId, parentData, parentTask.roleHint, blockedEvent);
|
|
612
|
+
const expected = previewCreateRunIds(createInput);
|
|
613
|
+
const created = await this.createRecoveryRunOrReuseExpected(createInput, expected);
|
|
614
|
+
const lineage = makeRecoveryLineage(parentRunId, created.runId, blockedEvent.eventId);
|
|
615
|
+
await this.recordRecoveryLineage(lineage);
|
|
616
|
+
return lineage;
|
|
617
|
+
}
|
|
618
|
+
async existingPreflightRecoveryRun(parentRunId, blockedEvent) {
|
|
619
|
+
const events = await this.runs.listRunEvents(parentRunId, { limit: 500 });
|
|
620
|
+
const event = recoveryCreatedForBlockedEvent(events, blockedEvent.eventId);
|
|
621
|
+
if (!event)
|
|
622
|
+
return null;
|
|
623
|
+
const recoveryRunId = payloadRecord(event)?.recoveryRunId;
|
|
624
|
+
if (typeof recoveryRunId !== 'string' || !recoveryRunId)
|
|
625
|
+
return null;
|
|
626
|
+
const recovery = await this.runs.showRun(recoveryRunId);
|
|
627
|
+
if (!recovery) {
|
|
628
|
+
throw new ControlPlaneError('ROW_NOT_FOUND', `Cannot recover run ${parentRunId}: recovery run ${recoveryRunId} referenced by lineage event is missing`);
|
|
629
|
+
}
|
|
630
|
+
const lineage = makeRecoveryLineage(parentRunId, recoveryRunId, blockedEvent.eventId);
|
|
631
|
+
await this.recordRecoveryLineage(lineage);
|
|
632
|
+
return lineage;
|
|
633
|
+
}
|
|
634
|
+
buildPreflightRecoveryCreateInput(parentRunId, parentData, role, blockedEvent) {
|
|
635
|
+
const now = dateOrEpoch(blockedEvent.createdAt);
|
|
636
|
+
const title = requiredString(parentData.title, 'title', parentRunId);
|
|
637
|
+
const repo = firstRepoRef(parentData.repos, parentRunId);
|
|
638
|
+
const description = optionalString(parentData.description);
|
|
639
|
+
const scope = optionalString(parentData.scope);
|
|
640
|
+
const priority = optionalInteger(parentData.priority);
|
|
641
|
+
const recoveryRole = requiredString(role, 'task role_hint', parentRunId);
|
|
642
|
+
const playbookId = optionalString(parentData.playbook_id);
|
|
643
|
+
const pipelineId = optionalString(parentData.pipeline_id);
|
|
644
|
+
const params = optionalRecord(parentData.params);
|
|
645
|
+
const routeDecision = parentData.route_decision;
|
|
646
|
+
if (!isRouteDecision(routeDecision)) {
|
|
647
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${parentRunId}: parent route_decision is invalid`);
|
|
648
|
+
}
|
|
649
|
+
const executionProfile = requiredRecord(parentData.execution_profile, 'execution_profile', parentRunId);
|
|
650
|
+
const idSuffix = fnv1a64Hex(`${parentRunId}|${blockedEvent.eventId}`).slice(0, 8);
|
|
651
|
+
return { title, repo, description, scope, priority, role: recoveryRole, playbookId, pipelineId, params, routeDecision, executionProfile, now, idSuffix };
|
|
652
|
+
}
|
|
653
|
+
/* node:coverage disable */
|
|
654
|
+
async createRecoveryRunOrReuseExpected(input, expected) {
|
|
655
|
+
try {
|
|
656
|
+
/* node:coverage enable */
|
|
657
|
+
return await this.runs.createRun(input);
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
if (!(error instanceof CreateRunWorkflowError))
|
|
661
|
+
throw error;
|
|
662
|
+
const existing = await this.runs.showRun(expected.runId);
|
|
663
|
+
if (existing?.tasks[0])
|
|
664
|
+
return expected;
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async recordRecoveryLineage(lineage) {
|
|
669
|
+
const [parentDetail, recoveryDetail] = await Promise.all([
|
|
670
|
+
this.runs.showRun(lineage.parentRunId),
|
|
671
|
+
this.runs.showRun(lineage.recoveryRunId),
|
|
672
|
+
]);
|
|
673
|
+
const parentTaskId = parentDetail?.tasks[0]?.taskId ?? '';
|
|
674
|
+
const recoveryTaskId = recoveryDetail?.tasks[0]?.taskId ?? '';
|
|
675
|
+
await this.runs.appendEvent(recoveryLineageEvent(lineage, lineage.parentRunId, parentTaskId, 'run_recovery_created'));
|
|
676
|
+
await this.runs.appendEvent(recoveryLineageEvent(lineage, lineage.recoveryRunId, recoveryTaskId, 'run_recovery_parent'));
|
|
462
677
|
}
|
|
463
678
|
async cancelRun(runId) {
|
|
464
679
|
const result = await this.runs.cancelRun(runId, { actor: 'mcp', source: 'mcp-cancel' });
|
|
@@ -480,7 +695,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
480
695
|
};
|
|
481
696
|
}
|
|
482
697
|
getRunEvents(input) {
|
|
483
|
-
return this.runs.listRunEvents(input.runId, { type: input.type, limit: input.limit });
|
|
698
|
+
return this.runs.listRunEvents(input.runId, { type: input.type, limit: input.limit, expand: input.expand });
|
|
484
699
|
}
|
|
485
700
|
async getRunProgress(runId) {
|
|
486
701
|
const detail = await this.runs.showRun(runId);
|
|
@@ -626,17 +841,22 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
626
841
|
const detail = await this.runs.showRun(runId);
|
|
627
842
|
if (!detail)
|
|
628
843
|
throw new ControlPlaneError('ROW_NOT_FOUND', `run not found: ${runId}`);
|
|
629
|
-
const [events, attempts, inbox] = await Promise.all([
|
|
844
|
+
const [events, attempts, inbox, workflow] = await Promise.all([
|
|
630
845
|
this.runs.listRunEvents(runId, { limit: 20 }),
|
|
631
846
|
this.runs.listRunAttempts(runId, { limit: 100 }),
|
|
632
847
|
this.inbox.listInbox({ runId, status: 'pending', limit: 50 }),
|
|
848
|
+
this.dbos.getWorkflowStatus(runId).catch(() => null),
|
|
633
849
|
]);
|
|
850
|
+
const latestBlockingEvent = latestPipelineBlockedEvent(events);
|
|
851
|
+
const blockedReason = latestBlockingEvent ? blockedReasonFromEvent(latestBlockingEvent) : undefined;
|
|
852
|
+
const runStatus = observedRunStatus(detail.run.status, workflow?.status ?? '', events);
|
|
634
853
|
return {
|
|
635
|
-
run: detail.run,
|
|
636
|
-
tasks: detail.tasks,
|
|
854
|
+
run: { ...detail.run, status: runStatus },
|
|
855
|
+
tasks: detail.tasks.map((task) => ({ ...task, status: observedTaskStatus(task.status, runStatus) })),
|
|
637
856
|
pendingInbox: inbox,
|
|
638
857
|
latestEvents: events.slice(-10),
|
|
639
858
|
usage: summarizeAttempts(attempts),
|
|
859
|
+
...(blockedReason !== undefined ? { blockedReason } : {}),
|
|
640
860
|
};
|
|
641
861
|
}
|
|
642
862
|
async waitForRun(input) {
|
|
@@ -650,24 +870,36 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
650
870
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
651
871
|
}
|
|
652
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Resolve a single run to its actionable state. Public so `RunWatchService` (slice 141 D2) can
|
|
875
|
+
* fan it out across many runs; it is a point-in-time level read (gate = `inbox.find(approval)`),
|
|
876
|
+
* not an event cursor — the watch primitive layers at-least-once + idempotent delivery on top.
|
|
877
|
+
*/
|
|
653
878
|
async resolveRunState(runId) {
|
|
654
879
|
const detail = await this.runs.showRun(runId);
|
|
655
880
|
if (!detail)
|
|
656
881
|
throw new ControlPlaneError('ROW_NOT_FOUND', `run not found: ${runId}`);
|
|
882
|
+
const issueRef = detail.run.issueRef;
|
|
883
|
+
const issueRefPart = issueRef ? { issueRef } : {};
|
|
657
884
|
const [inbox, events, workflow] = await Promise.all([
|
|
658
885
|
this.inbox.listInbox({ runId, status: 'pending', limit: 20 }),
|
|
659
886
|
this.runs.listRunEvents(runId, { limit: 20 }),
|
|
660
887
|
this.dbos.getWorkflowStatus(runId),
|
|
661
888
|
]);
|
|
889
|
+
const workflowStatus = workflow?.status ?? '';
|
|
890
|
+
const runStatus = observedRunStatus(detail.run.status, workflowStatus, events);
|
|
891
|
+
const eventPulsePart = latestEventPulse(events);
|
|
662
892
|
const gate = inbox.find((item) => item.kind === 'approval');
|
|
663
893
|
if (gate) {
|
|
664
894
|
return {
|
|
665
895
|
runId,
|
|
666
896
|
state: 'pending_gate',
|
|
667
897
|
nextAction: 'resolve approval with approve_gate or reject_gate',
|
|
668
|
-
runStatus
|
|
669
|
-
workflowStatus
|
|
898
|
+
runStatus,
|
|
899
|
+
workflowStatus,
|
|
670
900
|
inbox: gate,
|
|
901
|
+
...issueRefPart,
|
|
902
|
+
...eventPulsePart,
|
|
671
903
|
};
|
|
672
904
|
}
|
|
673
905
|
const question = inbox.find((item) => item.kind === 'question');
|
|
@@ -676,46 +908,66 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
676
908
|
runId,
|
|
677
909
|
state: 'question',
|
|
678
910
|
nextAction: 'answer question with answer_question',
|
|
679
|
-
runStatus
|
|
680
|
-
workflowStatus
|
|
911
|
+
runStatus,
|
|
912
|
+
workflowStatus,
|
|
681
913
|
inbox: question,
|
|
914
|
+
...issueRefPart,
|
|
915
|
+
...eventPulsePart,
|
|
682
916
|
};
|
|
683
917
|
}
|
|
684
918
|
if (detail.run.status === 'completed') {
|
|
685
|
-
return { runId, state: 'completed', nextAction: 'none', runStatus
|
|
919
|
+
return { runId, state: 'completed', nextAction: 'none', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
686
920
|
}
|
|
687
921
|
if (detail.run.status === 'failed') {
|
|
688
|
-
return { runId, state: 'failed', nextAction: 'inspect get_run_events/get_run_log', runStatus
|
|
922
|
+
return { runId, state: 'failed', nextAction: 'inspect get_run_events/get_run_log', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
689
923
|
}
|
|
690
924
|
if (detail.run.status === 'cancelled') {
|
|
691
|
-
return { runId, state: 'blocked', nextAction: 'run was cancelled; create or resume a different run', runStatus
|
|
925
|
+
return { runId, state: 'blocked', nextAction: 'run was cancelled; create or resume a different run', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
692
926
|
}
|
|
927
|
+
const blockingEvent = latestPipelineBlockedEvent(events);
|
|
928
|
+
const blockedReason = blockingEvent ? blockedReasonFromEvent(blockingEvent) : undefined;
|
|
693
929
|
if (detail.run.status === 'paused') {
|
|
694
|
-
return {
|
|
930
|
+
return {
|
|
931
|
+
runId,
|
|
932
|
+
state: 'blocked',
|
|
933
|
+
nextAction: 'inspect blocking event and decide whether to create a follow-up run',
|
|
934
|
+
runStatus,
|
|
935
|
+
workflowStatus,
|
|
936
|
+
...(blockedReason !== undefined ? { blockedReason } : {}),
|
|
937
|
+
...issueRefPart,
|
|
938
|
+
...eventPulsePart,
|
|
939
|
+
};
|
|
695
940
|
}
|
|
696
|
-
|
|
697
|
-
if (blocked) {
|
|
941
|
+
if (blockingEvent) {
|
|
698
942
|
return {
|
|
699
943
|
runId,
|
|
700
944
|
state: 'blocked',
|
|
701
945
|
nextAction: 'inspect blocking event and decide whether to create a follow-up run',
|
|
702
|
-
runStatus
|
|
703
|
-
workflowStatus
|
|
704
|
-
latestBlockingEvent:
|
|
946
|
+
runStatus,
|
|
947
|
+
workflowStatus,
|
|
948
|
+
latestBlockingEvent: blockingEvent,
|
|
949
|
+
...(blockedReason !== undefined ? { blockedReason } : {}),
|
|
950
|
+
...issueRefPart,
|
|
951
|
+
...eventPulsePart,
|
|
705
952
|
};
|
|
706
953
|
}
|
|
707
954
|
if (workflow?.status === 'ERROR') {
|
|
708
|
-
return { runId, state: 'failed', nextAction: 'inspect get_run_events/get_run_log', runStatus
|
|
955
|
+
return { runId, state: 'failed', nextAction: 'inspect get_run_events/get_run_log', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
709
956
|
}
|
|
710
957
|
if (workflow?.status === 'SUCCESS') {
|
|
711
|
-
return { runId, state: 'completed', nextAction: 'none', runStatus
|
|
958
|
+
return { runId, state: 'completed', nextAction: 'none', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
959
|
+
}
|
|
960
|
+
if (runStatus === 'ready') {
|
|
961
|
+
return { runId, state: 'ready', nextAction: 'start_run', runStatus, workflowStatus, ...issueRefPart, ...eventPulsePart };
|
|
712
962
|
}
|
|
713
963
|
return {
|
|
714
964
|
runId,
|
|
715
965
|
state: 'running',
|
|
716
966
|
nextAction: 'wait_for_run again or inspect get_run_digest',
|
|
717
|
-
runStatus
|
|
718
|
-
workflowStatus
|
|
967
|
+
runStatus,
|
|
968
|
+
workflowStatus,
|
|
969
|
+
...issueRefPart,
|
|
970
|
+
...eventPulsePart,
|
|
719
971
|
};
|
|
720
972
|
}
|
|
721
973
|
listInbox(filter) {
|
|
@@ -855,7 +1107,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
855
1107
|
});
|
|
856
1108
|
}
|
|
857
1109
|
async resolveRouteDecision(input) {
|
|
858
|
-
const params = normalizeParams(input.params);
|
|
1110
|
+
const params = normalizeParams(input.params, input.issueRef);
|
|
859
1111
|
const executionProfile = normalizeExecutionProfile(input.executionProfile);
|
|
860
1112
|
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
861
1113
|
const pipeline = input.pipelineId
|