@devflow-tools/delivery-line 0.18.19 → 0.18.21
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/dist/adapters/delivery-package.d.ts.map +1 -1
- package/dist/adapters/delivery-package.js +2 -1
- package/dist/adapters/delivery-package.js.map +1 -1
- package/dist/adapters/integration.d.ts +7 -0
- package/dist/adapters/integration.d.ts.map +1 -1
- package/dist/adapters/integration.js +11 -0
- package/dist/adapters/integration.js.map +1 -1
- package/dist/adapters/plugin-capability.d.ts +14 -0
- package/dist/adapters/plugin-capability.d.ts.map +1 -0
- package/dist/adapters/plugin-capability.js +61 -0
- package/dist/adapters/plugin-capability.js.map +1 -0
- package/dist/adapters/verification.d.ts +1 -0
- package/dist/adapters/verification.d.ts.map +1 -1
- package/dist/adapters/verification.js +60 -2
- package/dist/adapters/verification.js.map +1 -1
- package/dist/adapters/workflow-execution.d.ts +6 -0
- package/dist/adapters/workflow-execution.d.ts.map +1 -1
- package/dist/adapters/workflow-execution.js +61 -6
- package/dist/adapters/workflow-execution.js.map +1 -1
- package/dist/artifacts/artifact-contract.d.ts.map +1 -1
- package/dist/artifacts/artifact-contract.js +7 -6
- package/dist/artifacts/artifact-contract.js.map +1 -1
- package/dist/composition.d.ts +1 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +18 -1
- package/dist/composition.js.map +1 -1
- package/dist/domain/plugin-capability.d.ts +33 -0
- package/dist/domain/plugin-capability.d.ts.map +1 -1
- package/dist/domain/plugin-capability.js +16 -0
- package/dist/domain/plugin-capability.js.map +1 -1
- package/dist/domain/types.d.ts +96 -2
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/domain/types.js.map +1 -1
- package/dist/facade/delivery-line-facade.d.ts +8 -0
- package/dist/facade/delivery-line-facade.d.ts.map +1 -1
- package/dist/facade/delivery-line-facade.js +157 -27
- package/dist/facade/delivery-line-facade.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/persistence/memory-store.js +1 -1
- package/dist/persistence/memory-store.js.map +1 -1
- package/dist/persistence/sqlite-store.js +1 -1
- package/dist/persistence/sqlite-store.js.map +1 -1
- package/dist/ports/index.d.ts +116 -3
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/runtime/context-binder.d.ts +15 -0
- package/dist/runtime/context-binder.d.ts.map +1 -1
- package/dist/runtime/context-binder.js +26 -7
- package/dist/runtime/context-binder.js.map +1 -1
- package/dist/runtime/execution-coordinator.d.ts +12 -1
- package/dist/runtime/execution-coordinator.d.ts.map +1 -1
- package/dist/runtime/execution-coordinator.js +204 -46
- package/dist/runtime/execution-coordinator.js.map +1 -1
- package/dist/runtime/memory-writeback.d.ts +8 -0
- package/dist/runtime/memory-writeback.d.ts.map +1 -1
- package/dist/runtime/memory-writeback.js +12 -2
- package/dist/runtime/memory-writeback.js.map +1 -1
- package/dist/runtime/operation-journal.d.ts +1 -1
- package/dist/runtime/operation-journal.d.ts.map +1 -1
- package/dist/runtime/operation-journal.js +5 -1
- package/dist/runtime/operation-journal.js.map +1 -1
- package/dist/runtime/plan-compiler.d.ts +4 -0
- package/dist/runtime/plan-compiler.d.ts.map +1 -1
- package/dist/runtime/plan-compiler.js +15 -0
- package/dist/runtime/plan-compiler.js.map +1 -1
- package/package.json +3 -3
|
@@ -32,9 +32,17 @@ function nextActionsFor(record) {
|
|
|
32
32
|
nextActions.push('compare_options');
|
|
33
33
|
if (['clarifying', 'spec_ready', 'plan_ready', 'reviewed'].includes(record.stage))
|
|
34
34
|
nextActions.push('approve');
|
|
35
|
-
if (
|
|
35
|
+
if (record.blockers?.some(blocker => blocker.code === 'WORKTREE_ADMISSION_REQUIRED'))
|
|
36
|
+
nextActions.push('admit_worktree');
|
|
37
|
+
else if (record.blockers?.some(blocker => blocker.code === 'STALE_STACK_FACTS'))
|
|
38
|
+
nextActions.push('refresh_activation');
|
|
39
|
+
else if (record.blockers?.some(blocker => blocker.code === 'EXTERNAL_CANDIDATE_REQUIRED'))
|
|
40
|
+
nextActions.push('import_external_change');
|
|
41
|
+
else if (['plan_approved', 'executing', 'needs_changes', 'held', 'blocked'].includes(record.stage))
|
|
36
42
|
nextActions.push('resume');
|
|
37
|
-
if (record.
|
|
43
|
+
if (record.tasks.some(task => task.status === 'running' || task.status === 'merge_pending') || record.blockers?.some(blocker => blocker.code === 'WORKER_RECONCILIATION_INCONCLUSIVE'))
|
|
44
|
+
nextActions.push('recover_task');
|
|
45
|
+
if (record.stage === 'failed' && (record.blockers?.some(blocker => blocker.retryable) ?? true))
|
|
38
46
|
nextActions.push('retry_task');
|
|
39
47
|
if (record.stage === 'release_pending')
|
|
40
48
|
nextActions.push('export');
|
|
@@ -95,17 +103,6 @@ export class DeliveryLineFacade {
|
|
|
95
103
|
if (input.caseId)
|
|
96
104
|
return this.status(id);
|
|
97
105
|
}
|
|
98
|
-
if (this.ports.revision?.inspectWorktree) {
|
|
99
|
-
const worktree = await this.ports.revision.inspectWorktree({ repositoryPath: projectRoot, baseRevision: baseRef });
|
|
100
|
-
if (worktree.dirtyFiles.length > 0) {
|
|
101
|
-
throw new DeliveryLineError('MISSING_PREREQUISITE', 'delivery requires a clean worktree before case creation', {
|
|
102
|
-
dirtyFiles: worktree.dirtyFiles,
|
|
103
|
-
baseRevision: baseRef,
|
|
104
|
-
currentRevision: worktree.currentRevision,
|
|
105
|
-
recoveryAction: 'stash or commit existing changes, then create the Delivery case again',
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
106
|
let contextReceipt = input.contextReceipt;
|
|
110
107
|
let contextQuality;
|
|
111
108
|
if (this.ports.context) {
|
|
@@ -125,22 +122,45 @@ export class DeliveryLineFacade {
|
|
|
125
122
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'context receipt is not authoritative');
|
|
126
123
|
contextReceipt = bound.receiptHash;
|
|
127
124
|
if (bound.channelStatus && bound.gaps && bound.attribution)
|
|
128
|
-
contextQuality = { blocking: bound.blocking ?? false, ...(bound.channelAvailability ? { channelAvailability: bound.channelAvailability } : {}), channelStatus: bound.channelStatus, gaps: bound.gaps, attribution: bound.attribution };
|
|
125
|
+
contextQuality = { blocking: bound.blocking ?? false, ...(bound.channelAvailability ? { channelAvailability: bound.channelAvailability } : {}), channelStatus: bound.channelStatus, gaps: bound.gaps, attribution: bound.attribution, ...('quality' in bound && bound.quality ? { quality: bound.quality } : {}) };
|
|
129
126
|
}
|
|
130
127
|
if (!contextReceipt)
|
|
131
128
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'context receipt is required when authoritative retrieval is not configured');
|
|
132
129
|
const now = Date.now();
|
|
133
130
|
const riskTier = input.riskSignals?.some(signal => /auth|scope|security|database|gateway|release/i.test(signal)) ? 'high' : input.riskTier ?? 'low';
|
|
131
|
+
const worktreeMode = input.worktreeMode ?? 'isolated';
|
|
132
|
+
const admission = await this.ports.worktreeAdmission?.inspect({ projectRoot });
|
|
133
|
+
const stackFacts = await this.ports.stackFacts?.inspect({ projectRoot });
|
|
134
|
+
const runtimeProjection = await this.ports.runtimeActivation?.capture({ projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: baseRef }) ?? {
|
|
135
|
+
host: 'external', provider: 'none', model: 'none', permissions: { mode: 'external-candidate-only' }, probeReceiptHash: 'not-applicable',
|
|
136
|
+
};
|
|
137
|
+
const activationBody = {
|
|
138
|
+
schemaVersion: 'delivery-runtime-activation.v1',
|
|
139
|
+
projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: baseRef,
|
|
140
|
+
...runtimeProjection,
|
|
141
|
+
contextReceiptHash: contextReceipt,
|
|
142
|
+
stackFactsHash: stackFacts?.fingerprint ?? hash({ projectRoot, baseRef }),
|
|
143
|
+
pluginSelectionHash: hash({ selectedPlugins: stackFacts?.selectedPlugins ?? [], selectedRunners: stackFacts?.selectedRunners ?? [] }),
|
|
144
|
+
createdAt: now,
|
|
145
|
+
};
|
|
146
|
+
const admissionBlocked = Boolean(admission && !admission.clean && (worktreeMode === 'auto' || worktreeMode === 'clean'));
|
|
134
147
|
const record = {
|
|
135
148
|
id, projectRoot, baseRef, integrationRef: `refs/devflow/delivery/${id}`, targetRef: input.targetRef,
|
|
136
149
|
title: input.title, source: input.source ?? 'human', riskTier, autonomy: input.autonomy ?? 'manual', stage: 'clarifying',
|
|
137
150
|
budgets: { ...defaults, ...input.budgets }, artifacts: [], tasks: [], approvals: [], decisions: [], evidence: [],
|
|
138
151
|
approvalStates: {},
|
|
139
152
|
interaction: { phase: 'clarification', status: 'awaiting_user', turn: 1, openQuestions: [input.existingImplementations?.length ? `Existing implementations were detected (${input.existingImplementations.join(', ')}). Which implementation should be extended, replaced, or retained?` : 'What outcome and acceptance criteria should this change satisfy?'], blockingUnknowns: [input.existingImplementations?.length ? `Existing implementations were detected (${input.existingImplementations.join(', ')}). Which implementation should be extended, replaced, or retained?` : 'What outcome and acceptance criteria should this change satisfy?'], summaryRefs: [] },
|
|
140
|
-
createdBy: input.actor, sessionId: input.sessionId, executionId: input.executionId, createdAt: now, updatedAt: now, version: 0, contextReceipt,
|
|
153
|
+
createdBy: input.actor, sessionId: input.sessionId, executionId: input.executionId, createdAt: now, updatedAt: now, version: 0, contextReceipt, worktreeMode,
|
|
154
|
+
runtimeActivation: { ...activationBody, snapshotHash: hash(activationBody) },
|
|
141
155
|
...(input.existingImplementations?.length ? { existingImplementations: [...input.existingImplementations] } : {}),
|
|
142
156
|
...(contextQuality ? { contextQuality } : {}),
|
|
157
|
+
...(stackFacts ? { stackFacts: { ...stackFacts, capturedAt: now } } : {}),
|
|
143
158
|
};
|
|
159
|
+
if (admissionBlocked) {
|
|
160
|
+
record.stage = 'held';
|
|
161
|
+
record.interaction.status = 'paused';
|
|
162
|
+
record.blockers = [{ code: 'WORKTREE_ADMISSION_REQUIRED', message: 'The project worktree contains uncommitted changes.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', artifactRefs: [], evidenceRefs: [], recoveryAction: 'Choose isolated, external, or clean admission before continuing.', retryable: true, diagnostics: { dirtyFiles: admission?.dirtyFiles ?? [], actions: ['isolated', 'external', 'clean'], currentRevision: admission?.revision, baseRevision: baseRef, admissionToken: hash({ projectRoot, baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [] }) } }];
|
|
163
|
+
}
|
|
144
164
|
await this.ports.store.create(record);
|
|
145
165
|
await appendLedgerEvent(this.ports.store, { caseId: id, type: 'case.created', actor: input.actor, idempotencyKey: input.idempotencyKey, payload: { stage: record.stage } });
|
|
146
166
|
return this.view(record);
|
|
@@ -170,6 +190,8 @@ export class DeliveryLineFacade {
|
|
|
170
190
|
async answer(caseId, input) {
|
|
171
191
|
requireIdentity(input);
|
|
172
192
|
return this.mutate(caseId, input, 'interaction.answered', async (record) => {
|
|
193
|
+
if (!['clarifying', 'designing'].includes(record.stage))
|
|
194
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'interaction answers are only accepted during clarification or design', { currentStage: record.stage, allowedStages: ['clarifying', 'designing'] });
|
|
173
195
|
if (input.turn !== record.interaction.turn)
|
|
174
196
|
throw staleTurnError(record, input.turn);
|
|
175
197
|
if (record.interaction.turn >= record.budgets.maxInteractionTurns) {
|
|
@@ -200,6 +222,9 @@ export class DeliveryLineFacade {
|
|
|
200
222
|
async revise(caseId, input) {
|
|
201
223
|
requireIdentity(input);
|
|
202
224
|
return this.mutate(caseId, input, 'interaction.revised', record => {
|
|
225
|
+
const allowedStages = input.phase === 'clarification' ? ['draft', 'clarifying'] : ['designing'];
|
|
226
|
+
if (!allowedStages.includes(record.stage))
|
|
227
|
+
throw new DeliveryLineError('INVALID_TRANSITION', `${input.phase} revisions are not allowed at ${record.stage}`, { currentStage: record.stage, requestedPhase: input.phase, allowedStages });
|
|
203
228
|
const stage = input.phase === 'clarification' ? 'clarifying' : 'designing';
|
|
204
229
|
const next = invalidateDownstream(stage, record);
|
|
205
230
|
next.interaction = { ...next.interaction, phase: input.phase, status: 'awaiting_user', turn: next.interaction.turn + 1, openQuestions: [input.content], blockingUnknowns: [input.content] };
|
|
@@ -244,11 +269,11 @@ export class DeliveryLineFacade {
|
|
|
244
269
|
});
|
|
245
270
|
record.contextReceipt = designerReceipt.receiptHash;
|
|
246
271
|
if (designerReceipt.channelStatus && designerReceipt.gaps && designerReceipt.attribution)
|
|
247
|
-
record.contextQuality = { blocking: designerReceipt.blocking ?? false, ...(designerReceipt.channelAvailability ? { channelAvailability: designerReceipt.channelAvailability } : {}), channelStatus: designerReceipt.channelStatus, gaps: designerReceipt.gaps, attribution: designerReceipt.attribution };
|
|
272
|
+
record.contextQuality = { blocking: designerReceipt.blocking ?? false, ...(designerReceipt.channelAvailability ? { channelAvailability: designerReceipt.channelAvailability } : {}), channelStatus: designerReceipt.channelStatus, gaps: designerReceipt.gaps, attribution: designerReceipt.attribution, ...('quality' in designerReceipt && designerReceipt.quality ? { quality: designerReceipt.quality } : {}) };
|
|
248
273
|
}
|
|
249
274
|
const decisionHash = hash({ questionId, optionId, selectedLabel, optionsSnapshot });
|
|
250
275
|
record.decisions.push({ id: randomUUID(), kind: 'technical', questionId, question: 'Which implementation option should Delivery execute?', options: optionsSnapshot.map(option => option.label), optionsSnapshot, optionId, selectedLabel, decisionHash, sessionId: input.sessionId, toolUseId: input.toolUseId, decision: selectedLabel, decidedBy: input.actor, artifactHash: decisionHash });
|
|
251
|
-
const stackFactsHash = hash({ projectRoot: record.projectRoot, baseRef: record.baseRef, targetRef: record.targetRef });
|
|
276
|
+
const stackFactsHash = record.stackFacts?.fingerprint ?? record.runtimeActivation?.stackFactsHash ?? record.stackFactsHash ?? hash({ projectRoot: record.projectRoot, baseRef: record.baseRef, targetRef: record.targetRef });
|
|
252
277
|
const selectedCapabilities = this.capabilities?.manifests.length
|
|
253
278
|
? selectPluginCapabilities({
|
|
254
279
|
detectedStacks: this.capabilities.detectedStacks,
|
|
@@ -258,7 +283,9 @@ export class DeliveryLineFacade {
|
|
|
258
283
|
: undefined;
|
|
259
284
|
const pluginManifestHash = selectedCapabilities?.selectedManifestHash ?? hash({ capability: selectedLabel, protocolVersion: 'delivery-capability.v1' });
|
|
260
285
|
record.stackFactsHash = stackFactsHash;
|
|
261
|
-
|
|
286
|
+
const requiredContextChannels = [...new Set(Object.values(selectedCapabilities?.selected ?? {}).flatMap(manifest => manifest.requiredContextChannels))]
|
|
287
|
+
.filter((channel) => ['code', 'memory', 'knowledge'].includes(channel));
|
|
288
|
+
record.pluginSelection = { selectedManifestHash: pluginManifestHash, stackFactsHash, decisionRefs: [decisionHash], selectionReason: selectedCapabilities?.selectionReason ?? 'selected by current user decision', rejectedAlternatives: [...new Set([...(selectedCapabilities?.rejectedAlternatives ?? []), ...optionsSnapshot.filter(option => option.id !== optionId).map(option => option.id)])], ...(requiredContextChannels.length ? { requiredContextChannels } : {}), selectedAt: Date.now() };
|
|
262
289
|
await this.createProjectionArtifact(record, 'plugin-selection.md', 'Plugin selection', 'Capability providers selected by Delivery from stack facts and the current decision.', record.pluginSelection, input.actor);
|
|
263
290
|
record.stage = 'spec_ready';
|
|
264
291
|
record.interaction = { ...record.interaction, phase: 'design', status: 'awaiting_approval', openQuestions: [], blockingUnknowns: [] };
|
|
@@ -282,6 +309,7 @@ export class DeliveryLineFacade {
|
|
|
282
309
|
if (this.ports.context) {
|
|
283
310
|
const plannerReceipt = await this.ports.context.build({
|
|
284
311
|
role: 'planner',
|
|
312
|
+
...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
|
|
285
313
|
retrieval: {
|
|
286
314
|
taskRevision: record.version + 1,
|
|
287
315
|
projectRoot: record.projectRoot,
|
|
@@ -294,7 +322,7 @@ export class DeliveryLineFacade {
|
|
|
294
322
|
});
|
|
295
323
|
record.contextReceipt = plannerReceipt.receiptHash;
|
|
296
324
|
if (plannerReceipt.channelStatus && plannerReceipt.gaps && plannerReceipt.attribution)
|
|
297
|
-
record.contextQuality = { blocking: plannerReceipt.blocking ?? false, ...(plannerReceipt.channelAvailability ? { channelAvailability: plannerReceipt.channelAvailability } : {}), channelStatus: plannerReceipt.channelStatus, gaps: plannerReceipt.gaps, attribution: plannerReceipt.attribution };
|
|
325
|
+
record.contextQuality = { blocking: plannerReceipt.blocking ?? false, ...(plannerReceipt.channelAvailability ? { channelAvailability: plannerReceipt.channelAvailability } : {}), channelStatus: plannerReceipt.channelStatus, gaps: plannerReceipt.gaps, attribution: plannerReceipt.attribution, ...('quality' in plannerReceipt && plannerReceipt.quality ? { quality: plannerReceipt.quality } : {}) };
|
|
298
326
|
}
|
|
299
327
|
planContent = renderPlanMarkdown(input.plan);
|
|
300
328
|
proposedArtifactHash = contentHash(planContent);
|
|
@@ -370,6 +398,9 @@ export class DeliveryLineFacade {
|
|
|
370
398
|
taskId: envelopes[0]?.taskId ?? '',
|
|
371
399
|
baseRevision: record.baseRef,
|
|
372
400
|
verificationEvidenceHashes: [],
|
|
401
|
+
reviewEvidenceHashes: [],
|
|
402
|
+
intentArtifactHash: record.artifacts.find(item => item.type === 'intent')?.contentHash,
|
|
403
|
+
specArtifactHash: record.artifacts.find(item => item.type === 'spec')?.contentHash,
|
|
373
404
|
};
|
|
374
405
|
}
|
|
375
406
|
record.tasks = envelopes.map(envelope => ({
|
|
@@ -382,6 +413,7 @@ export class DeliveryLineFacade {
|
|
|
382
413
|
dependsOn: envelope.dependencies,
|
|
383
414
|
allowedFiles: envelope.allowedFiles,
|
|
384
415
|
expectedChanges: envelope.implementationSteps,
|
|
416
|
+
acceptanceCriteria: envelope.verification.acceptanceCriteria,
|
|
385
417
|
verificationCommands: envelope.verification.commands,
|
|
386
418
|
inputArtifacts: envelope.inputArtifacts,
|
|
387
419
|
planHash: envelope.planHash,
|
|
@@ -389,6 +421,7 @@ export class DeliveryLineFacade {
|
|
|
389
421
|
hostSelectionSnapshotHash: envelope.hostSelectionSnapshotHash,
|
|
390
422
|
executionAuthority: envelope.executionAuthority,
|
|
391
423
|
constraints: envelope.constraints,
|
|
424
|
+
planNodes: envelope.nodes,
|
|
392
425
|
status: 'pending',
|
|
393
426
|
evidence: [],
|
|
394
427
|
}));
|
|
@@ -413,7 +446,15 @@ export class DeliveryLineFacade {
|
|
|
413
446
|
}
|
|
414
447
|
if (record.stage === 'plan_approved')
|
|
415
448
|
record.stage = 'executing';
|
|
416
|
-
if (record.stage === 'executing') {
|
|
449
|
+
if (record.stage === 'executing' || (record.stage === 'held' && record.tasks.some(task => task.status === 'running' || task.status === 'merge_pending') && this.ports.operationJournal)) {
|
|
450
|
+
if (record.stackFacts && this.ports.stackFacts) {
|
|
451
|
+
const currentStackFacts = await this.ports.stackFacts.inspect({ projectRoot: record.projectRoot });
|
|
452
|
+
if (currentStackFacts.fingerprint !== record.stackFacts.fingerprint)
|
|
453
|
+
return hold(record, { code: 'STALE_STACK_FACTS', message: 'Project stack facts changed after case creation.', responsibleRole: 'planner', requiredRole: 'planner', recoveryAction: 'Refresh stack facts and re-approve the executable plan.', retryable: false, diagnostics: { previousFingerprint: record.stackFacts.fingerprint, currentFingerprint: currentStackFacts.fingerprint } });
|
|
454
|
+
}
|
|
455
|
+
if (record.worktreeMode === 'external' && !record.evidence.some(item => item.kind === 'tool' && item.metadata.trust === 'external-untrusted' && item.metadata.imported === true)) {
|
|
456
|
+
return hold(record, { code: 'EXTERNAL_CANDIDATE_REQUIRED', message: 'The admitted external workspace must be imported as an untrusted candidate before execution.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', recoveryAction: 'Commit the admitted workspace changes, then call import-change with that candidate revision.', retryable: false, diagnostics: { baseRevision: record.worktreeAdmission?.baseRevision ?? record.baseRef, currentRevision: record.worktreeAdmission?.currentRevision, dirtyFiles: record.worktreeAdmission?.dirtyFiles ?? [] } });
|
|
457
|
+
}
|
|
417
458
|
const verifier = this.ports.verification;
|
|
418
459
|
if (!this.ports.execution)
|
|
419
460
|
return hold(record, { code: 'WORKER_UNAVAILABLE', message: 'No Delivery execution adapter is configured for this case.', responsibleRole: 'execution-owner', recoveryAction: 'Configure a Delivery execution adapter or import an external change.', retryable: false });
|
|
@@ -426,6 +467,18 @@ export class DeliveryLineFacade {
|
|
|
426
467
|
verify: input => this.ports.verification.verify(input),
|
|
427
468
|
integrate: input => this.ports.integration.integrate(input),
|
|
428
469
|
inspectIntegration: async (input) => await this.ports.integration.inspect?.(input) ?? null,
|
|
470
|
+
inspectWorker: this.ports.execution.inspectWorker ? (input => this.ports.execution.inspectWorker(input)) : undefined,
|
|
471
|
+
runCapabilities: record.stackFacts?.selectedRunners?.length && this.ports.capabilities ? async ({ task, execution }) => {
|
|
472
|
+
const receipts = [];
|
|
473
|
+
for (const pluginName of record.stackFacts.selectedRunners) {
|
|
474
|
+
const manifest = await this.ports.capabilities.manifest(pluginName);
|
|
475
|
+
const receipt = await this.ports.capabilities.run({ caseId: record.id, taskId: task.id, pluginName, capability: manifest.capability, manifestHash: manifest.manifestHash, stackFactsFingerprint: record.stackFacts.fingerprint, allowedFiles: task.allowedFiles, payload: { changedFiles: execution.handoff?.changedFiles ?? [], planHash: task.planHash, contextReceiptHash: task.contextReceiptHash, baseRevision: task.baseRevision, fencingToken: task.fencingToken } });
|
|
476
|
+
if (receipt.manifestHash !== manifest.manifestHash)
|
|
477
|
+
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', `plugin receipt hash mismatch: ${pluginName}`);
|
|
478
|
+
receipts.push({ ...receipt, pluginName, capability: manifest.capability, stackFactsFingerprint: record.stackFacts.fingerprint });
|
|
479
|
+
}
|
|
480
|
+
return receipts;
|
|
481
|
+
} : undefined,
|
|
429
482
|
}, this.ports.operationJournal).run(record);
|
|
430
483
|
if (record.tasks.length > 0 && record.tasks.every(task => task.status === 'integrated')) {
|
|
431
484
|
await this.createProjectionArtifact(record, 'code-evidence.md', 'Code evidence', 'Worker handoffs and changed files bound to the integrated candidate revision.', {
|
|
@@ -434,7 +487,7 @@ export class DeliveryLineFacade {
|
|
|
434
487
|
}, input.actor);
|
|
435
488
|
const integrationRevision = record.integrationRevision ?? record.baseRef;
|
|
436
489
|
try {
|
|
437
|
-
const planReceipt = await verifier.verify({ caseId: record.id, integrationRef: record.integrationRef, commit: integrationRevision, planLevel: true, commands: record.tasks.flatMap(task => task.verificationCommands) });
|
|
490
|
+
const planReceipt = await verifier.verify({ caseId: record.id, integrationRef: record.integrationRef, commit: integrationRevision, planLevel: true, commands: record.tasks.flatMap(task => task.verificationCommands), acceptanceCriteria: record.tasks.flatMap(task => task.acceptanceCriteria) });
|
|
438
491
|
if (!planReceipt.passed) {
|
|
439
492
|
record.stage = 'failed';
|
|
440
493
|
return record;
|
|
@@ -442,7 +495,7 @@ export class DeliveryLineFacade {
|
|
|
442
495
|
record.evidence.push(this.evidence('verification', planReceipt));
|
|
443
496
|
await this.createProjectionArtifact(record, 'verification.md', 'Verification', 'Verification evidence for the integrated revision.', { commit: planReceipt.commit, passed: planReceipt.passed, evidenceHash: hash(planReceipt) }, input.actor);
|
|
444
497
|
if (record.lineage) {
|
|
445
|
-
record.lineage = { ...record.lineage, candidateRevision: integrationRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
|
|
498
|
+
record.lineage = { ...record.lineage, candidateRevision: integrationRevision, integrationRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
|
|
446
499
|
}
|
|
447
500
|
record.stage = 'verified';
|
|
448
501
|
}
|
|
@@ -461,7 +514,10 @@ export class DeliveryLineFacade {
|
|
|
461
514
|
if (result.receipts.some(receipt => receipt.artifactHash !== reviewArtifactHash))
|
|
462
515
|
return hold(record, { code: 'STALE_REVIEW_EVIDENCE', message: 'Review evidence does not match the current integration revision.', responsibleRole: 'review-owner', recoveryAction: 'Run review again against the current integration revision.', retryable: true });
|
|
463
516
|
else {
|
|
464
|
-
|
|
517
|
+
const reviewEvidence = result.receipts.map(receipt => this.evidence('review', receipt));
|
|
518
|
+
record.evidence.push(...reviewEvidence);
|
|
519
|
+
if (record.lineage)
|
|
520
|
+
record.lineage = { ...record.lineage, reviewEvidenceHashes: reviewEvidence.map(item => item.hash) };
|
|
465
521
|
await this.createProjectionArtifact(record, 'review.md', 'Review', 'Independent review evidence for the integrated revision.', { artifactHash: reviewArtifactHash, passed: result.passed, receipts: result.receipts.map(receipt => receipt.id) }, input.actor);
|
|
466
522
|
if (!result.passed) {
|
|
467
523
|
record.stage = 'needs_changes';
|
|
@@ -480,9 +536,62 @@ export class DeliveryLineFacade {
|
|
|
480
536
|
return record;
|
|
481
537
|
});
|
|
482
538
|
}
|
|
539
|
+
/** Explicit recovery action used after a process restart or an unknown operation receipt. */
|
|
540
|
+
async recoverTask(caseId, input) {
|
|
541
|
+
return this.resume(caseId, input);
|
|
542
|
+
}
|
|
543
|
+
async admitWorktree(caseId, input) {
|
|
544
|
+
requireIdentity(input);
|
|
545
|
+
return this.mutate(caseId, input, 'worktree.admitted', async (record) => {
|
|
546
|
+
if (record.blockers?.[0]?.code !== 'WORKTREE_ADMISSION_REQUIRED')
|
|
547
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'case is not awaiting worktree admission');
|
|
548
|
+
const expectedToken = record.blockers[0].diagnostics?.admissionToken;
|
|
549
|
+
if (typeof expectedToken !== 'string' || input.admissionToken !== expectedToken)
|
|
550
|
+
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', 'worktree admission token is stale');
|
|
551
|
+
const admission = await this.ports.worktreeAdmission?.inspect({ projectRoot: record.projectRoot });
|
|
552
|
+
if (input.mode === 'clean' && admission && !admission.clean)
|
|
553
|
+
return hold(record, { code: 'WORKTREE_ADMISSION_REQUIRED', message: 'The project worktree is still dirty.', responsibleRole: 'delivery-owner', requiredRole: 'delivery-owner', recoveryAction: 'Clean the listed files or choose isolated/external admission.', retryable: true, diagnostics: { dirtyFiles: admission.dirtyFiles, choices: ['isolated', 'external', 'clean'] } });
|
|
554
|
+
record.worktreeMode = input.mode;
|
|
555
|
+
record.worktreeAdmission = { mode: input.mode, baseRevision: record.baseRef, currentRevision: admission?.revision, dirtyFiles: admission?.dirtyFiles ?? [], admissionToken: input.admissionToken, admittedAt: Date.now() };
|
|
556
|
+
record.stage = 'clarifying';
|
|
557
|
+
record.blockers = [];
|
|
558
|
+
record.interaction.status = 'awaiting_user';
|
|
559
|
+
return record;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
async refreshActivation(caseId, input) {
|
|
563
|
+
requireIdentity(input);
|
|
564
|
+
return this.mutate(caseId, input, 'activation.refreshed', async (record) => {
|
|
565
|
+
if (!this.ports.stackFacts)
|
|
566
|
+
throw new DeliveryLineError('MISSING_PREREQUISITE', 'stack facts adapter is required to refresh activation');
|
|
567
|
+
const current = await this.ports.stackFacts.inspect({ projectRoot: record.projectRoot });
|
|
568
|
+
let runtimeActivation = record.runtimeActivation;
|
|
569
|
+
if (this.ports.runtimeActivation) {
|
|
570
|
+
const projection = await this.ports.runtimeActivation.capture({ projectRoot: record.projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: record.baseRef });
|
|
571
|
+
const body = {
|
|
572
|
+
schemaVersion: 'delivery-runtime-activation.v1',
|
|
573
|
+
projectRoot: record.projectRoot, sessionId: input.sessionId, executionId: input.executionId, revision: record.baseRef,
|
|
574
|
+
...projection,
|
|
575
|
+
contextReceiptHash: record.contextReceipt ?? input.contextReceipt,
|
|
576
|
+
stackFactsHash: current.fingerprint,
|
|
577
|
+
pluginSelectionHash: record.pluginSelection ? hash(record.pluginSelection) : hash({ selectedPlugins: current.selectedPlugins, selectedRunners: current.selectedRunners }),
|
|
578
|
+
createdAt: Date.now(),
|
|
579
|
+
};
|
|
580
|
+
runtimeActivation = { ...body, snapshotHash: hash(body) };
|
|
581
|
+
}
|
|
582
|
+
const changed = record.stackFacts?.fingerprint !== current.fingerprint || runtimeActivation?.snapshotHash !== record.runtimeActivation?.snapshotHash;
|
|
583
|
+
let next = changed && ['plan_approved', 'executing', 'held', 'blocked', 'failed', 'needs_changes', 'verified', 'reviewed', 'release_pending'].includes(record.stage)
|
|
584
|
+
? invalidateDownstream('plan_ready', record) : record;
|
|
585
|
+
next.stackFacts = { ...current, capturedAt: Date.now() };
|
|
586
|
+
next.runtimeActivation = runtimeActivation;
|
|
587
|
+
next.blockers = next.blockers?.filter(blocker => blocker.code !== 'STALE_STACK_FACTS');
|
|
588
|
+
return next;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
483
591
|
async retryTask(caseId, input) { return this.mutate(caseId, input, 'task.retried', record => { const task = record.tasks.find(item => item.id === input.taskId); if (!task)
|
|
484
592
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'task not found'); if (!['failed', 'blocked', 'needs_changes', 'verified'].includes(task.status))
|
|
485
|
-
throw new DeliveryLineError('INVALID_TRANSITION', 'task is not retryable');
|
|
593
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'task is not retryable'); const blocker = record.blockers?.find(item => item.taskId === task.id); if (blocker && !blocker.retryable)
|
|
594
|
+
throw new DeliveryLineError('INVALID_TRANSITION', `task failure is not retryable: ${blocker.code}`); task.status = 'pending'; task.integratedRevision = undefined; record.blockers = record.blockers?.filter(item => item.taskId !== task.id); if (['failed', 'blocked', 'needs_changes', 'held'].includes(record.stage))
|
|
486
595
|
record.stage = 'executing'; return record; }); }
|
|
487
596
|
async importExternalChange(caseId, input) {
|
|
488
597
|
requireIdentity(input);
|
|
@@ -512,7 +621,7 @@ export class DeliveryLineFacade {
|
|
|
512
621
|
task.evidence = [];
|
|
513
622
|
if (!this.ports.verification || !this.ports.integration)
|
|
514
623
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'verification and integration adapters are required for external import');
|
|
515
|
-
const verification = await this.ports.verification.verify({ caseId: record.id, taskId: task.id, projectRoot: record.projectRoot, commit: authority.candidateRevision, commands: task.verificationCommands });
|
|
624
|
+
const verification = await this.ports.verification.verify({ caseId: record.id, taskId: task.id, projectRoot: record.projectRoot, commit: authority.candidateRevision, commands: task.verificationCommands, acceptanceCriteria: task.acceptanceCriteria });
|
|
516
625
|
if (!verification.passed || verification.commit !== authority.candidateRevision)
|
|
517
626
|
throw new DeliveryLineError('VERIFICATION_FAILED', 'external candidate verification failed');
|
|
518
627
|
record.evidence.push(this.evidence('verification', verification));
|
|
@@ -525,7 +634,7 @@ export class DeliveryLineFacade {
|
|
|
525
634
|
record.evidence.push({ id: randomUUID(), kind: 'merge', hash: hash(integrated), createdAt: Date.now(), metadata: integrated });
|
|
526
635
|
record.stage = 'verified';
|
|
527
636
|
if (record.lineage)
|
|
528
|
-
record.lineage = { ...record.lineage, candidateRevision: integrated.candidateRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
|
|
637
|
+
record.lineage = { ...record.lineage, candidateRevision: integrated.candidateRevision, integrationRevision: integrated.candidateRevision, verificationEvidenceHashes: record.evidence.filter(item => item.kind === 'verification').map(item => item.hash) };
|
|
529
638
|
return record;
|
|
530
639
|
});
|
|
531
640
|
}
|
|
@@ -538,10 +647,29 @@ export class DeliveryLineFacade {
|
|
|
538
647
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'delivery package adapter is required');
|
|
539
648
|
if (record.tasks.length === 0 || record.tasks.some(task => task.status !== 'integrated' || !task.integratedRevision))
|
|
540
649
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'all tasks must be integrated before export');
|
|
650
|
+
if (!['intent', 'spec', 'plan'].every(type => record.artifacts.some(artifact => artifact.type === type)))
|
|
651
|
+
throw new DeliveryLineError('MISSING_PREREQUISITE', 'intent, spec, and plan lineage is required before export');
|
|
541
652
|
if (!record.lineage || (record.tasks[0]?.planHash && record.lineage.planHash !== record.tasks[0].planHash) || record.lineage.baseRevision !== record.baseRef)
|
|
542
653
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'execution lineage is incomplete or stale');
|
|
543
654
|
if (!record.evidence.some(item => item.kind === 'verification') || !record.evidence.some(item => item.kind === 'review'))
|
|
544
655
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'verification and review evidence are required before export');
|
|
656
|
+
if (!record.evidence.some(item => item.kind === 'merge' && item.metadata.candidateRevision === record.integrationRevision))
|
|
657
|
+
throw new DeliveryLineError('MISSING_PREREQUISITE', 'integration lineage is required before export');
|
|
658
|
+
const intentHash = record.artifacts.find(item => item.type === 'intent')?.contentHash;
|
|
659
|
+
const specHash = record.artifacts.find(item => item.type === 'spec')?.contentHash;
|
|
660
|
+
const currentVerificationHashes = record.evidence.filter(item => item.kind === 'verification').map(item => item.hash);
|
|
661
|
+
const currentReviewHashes = record.evidence.filter(item => item.kind === 'review').map(item => item.hash);
|
|
662
|
+
if (record.lineage.intentArtifactHash !== intentHash || record.lineage.specArtifactHash !== specHash
|
|
663
|
+
|| record.lineage.integrationRevision !== record.integrationRevision
|
|
664
|
+
|| record.lineage.verificationEvidenceHashes.length === 0
|
|
665
|
+
|| record.lineage.verificationEvidenceHashes.some(item => !currentVerificationHashes.includes(item))
|
|
666
|
+
|| record.lineage.reviewEvidenceHashes.length === 0
|
|
667
|
+
|| record.lineage.reviewEvidenceHashes.some(item => !currentReviewHashes.includes(item))) {
|
|
668
|
+
throw new DeliveryLineError('MISSING_PREREQUISITE', 'execution lineage does not contain the current intent, spec, verification, review, and integration evidence');
|
|
669
|
+
}
|
|
670
|
+
for (const pluginName of record.stackFacts?.selectedRunners ?? [])
|
|
671
|
+
if (!record.evidence.some(item => item.kind === 'tool' && item.metadata.pluginName === pluginName && item.metadata.stackFactsFingerprint === record.stackFacts?.fingerprint && item.metadata.verified === true))
|
|
672
|
+
throw new DeliveryLineError('MISSING_PREREQUISITE', `verified plugin runner lineage is missing: ${pluginName}`);
|
|
545
673
|
const packageInput = { record, path: input.path };
|
|
546
674
|
const operationId = `package:${record.id}:${input.idempotencyKey}`;
|
|
547
675
|
const requestHash = hash({ caseId: record.id, path: input.path, integrationRevision: record.integrationRevision, artifacts: record.artifacts.map(item => item.contentHash), evidence: record.evidence.map(item => item.hash) });
|
|
@@ -593,6 +721,8 @@ export class DeliveryLineFacade {
|
|
|
593
721
|
const packageArtifact = { id: randomUUID(), caseId: record.id, type: 'delivery-package', repoId: record.projectRoot, ref: record.integrationRef, commit: record.integrationRevision ?? record.baseRef, path: exported.path, blobHash: exported.packageHash, parentBlobHashes: record.artifacts.length ? [record.artifacts.at(-1).blobHash] : [], contentHash: exported.packageHash, createdBy: input.actor, createdAt: Date.now() };
|
|
594
722
|
record.artifacts.push(packageArtifact);
|
|
595
723
|
record.evidence.push({ id: randomUUID(), kind: 'export', hash: exported.packageHash, createdAt: Date.now(), metadata: { path: exported.path, packageHash: exported.packageHash, integrationRevision: record.integrationRevision, exportBindingHash: hash({ packageHash: exported.packageHash, path: exported.path, integrationRevision: record.integrationRevision }), candidateRevisions: record.tasks.map(task => task.integratedRevision) } });
|
|
724
|
+
if (record.lineage)
|
|
725
|
+
record.lineage = { ...record.lineage, exportEvidenceHash: exported.packageHash };
|
|
596
726
|
record.stage = 'delivered';
|
|
597
727
|
return record;
|
|
598
728
|
});
|
|
@@ -651,7 +781,7 @@ export class DeliveryLineFacade {
|
|
|
651
781
|
return this.createArtifact(record, 'interaction', content, actor, { artifactPath: path, path: path.replace(/\.md$/u, '.json'), content: `${JSON.stringify(data, null, 2)}\n` });
|
|
652
782
|
}
|
|
653
783
|
evidence(kind, value) { return { id: randomUUID(), kind, hash: hash(value), createdAt: Date.now(), metadata: value }; }
|
|
654
|
-
view(record) { const pendingDecisions = record.interaction.openQuestions.map((question, index) => ({ id: `${record.id}:question:${record.interaction.turn}:${index}`, kind: 'product', question, options: [], blocking: record.interaction.blockingUnknowns.includes(question), turn: record.interaction.turn })); const nextActions = nextActionsFor(record); const blockers = record.blockers ?? (['held', 'failed', 'needs_changes', 'blocked'].includes(record.stage) ? [this.defaultBlocker(record)] : []); return { caseId: record.id, currentTurn: record.interaction.turn, contextReceipt: record.contextReceipt, ...(record.contextQuality ? { contextQuality: record.contextQuality } : {}), ...(record.sessionId && record.executionId ? { canonicalIdentity: { sessionId: record.sessionId, executionId: record.executionId, actorId: record.createdBy.id } } : {}), stage: record.stage, pendingDecisions, nextActions, blockers, artifactRefs: record.artifacts, evidenceRefs: record.evidence, taskSummaries: record.tasks.map(task => ({ taskId: task.id, status: task.status, attempt: task.attempt })), budget: { interactionsUsed: record.interaction.turn, taskAttemptsUsed: record.tasks.reduce((sum, task) => sum + task.attempt, 0), wallTimeMs: Math.max(0, Date.now() - (record.createdAt ?? record.updatedAt)), toolCalls: 0 } }; }
|
|
784
|
+
view(record) { const pendingDecisions = record.interaction.openQuestions.map((question, index) => ({ id: `${record.id}:question:${record.interaction.turn}:${index}`, kind: 'product', question, options: [], blocking: record.interaction.blockingUnknowns.includes(question), turn: record.interaction.turn })); const nextActions = nextActionsFor(record); const blockers = record.blockers ?? (['held', 'failed', 'needs_changes', 'blocked'].includes(record.stage) ? [this.defaultBlocker(record)] : []); return { caseId: record.id, currentTurn: record.interaction.turn, contextReceipt: record.contextReceipt, ...(record.contextQuality ? { contextQuality: record.contextQuality } : {}), ...(record.sessionId && record.executionId ? { canonicalIdentity: { sessionId: record.sessionId, executionId: record.executionId, actorId: record.createdBy.id } } : {}), ...(record.runtimeActivation ? { runtimeActivationHash: record.runtimeActivation.snapshotHash } : {}), stage: record.stage, pendingDecisions, nextActions, blockers, artifactRefs: record.artifacts, evidenceRefs: record.evidence, taskSummaries: record.tasks.map(task => ({ taskId: task.id, status: task.status, attempt: task.attempt })), budget: { interactionsUsed: record.interaction.turn, taskAttemptsUsed: record.tasks.reduce((sum, task) => sum + task.attempt, 0), wallTimeMs: Math.max(0, Date.now() - (record.createdAt ?? record.updatedAt)), toolCalls: 0 } }; }
|
|
655
785
|
defaultBlocker(record) {
|
|
656
786
|
const task = record.tasks.find(item => ['blocked', 'failed', 'needs_changes'].includes(item.status));
|
|
657
787
|
const code = record.stage === 'held' ? 'HELD' : record.stage.toUpperCase();
|