@devflow-tools/delivery-line 0.18.34 → 0.18.36
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/plugin-capability.d.ts +1 -1
- package/dist/adapters/plugin-capability.d.ts.map +1 -1
- package/dist/adapters/plugin-capability.js +6 -6
- package/dist/adapters/plugin-capability.js.map +1 -1
- package/dist/artifacts/artifact-contract.d.ts +14 -2
- package/dist/artifacts/artifact-contract.d.ts.map +1 -1
- package/dist/artifacts/artifact-contract.js +31 -10
- package/dist/artifacts/artifact-contract.js.map +1 -1
- package/dist/domain/clarification-policy.d.ts +1 -0
- package/dist/domain/clarification-policy.d.ts.map +1 -1
- package/dist/domain/clarification-policy.js +5 -2
- package/dist/domain/clarification-policy.js.map +1 -1
- package/dist/domain/types.d.ts +6 -6
- 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 +11 -11
- package/dist/facade/delivery-line-facade.d.ts.map +1 -1
- package/dist/facade/delivery-line-facade.js +169 -41
- package/dist/facade/delivery-line-facade.js.map +1 -1
- package/dist/ports/index.d.ts +4 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/runtime/plan-compiler.d.ts.map +1 -1
- package/dist/runtime/plan-compiler.js +11 -0
- package/dist/runtime/plan-compiler.js.map +1 -1
- package/package.json +3 -3
|
@@ -43,15 +43,23 @@ function normalizeDecisionOptions(options) {
|
|
|
43
43
|
return false;
|
|
44
44
|
seen.add(id);
|
|
45
45
|
return true;
|
|
46
|
-
}).map(option => ({
|
|
46
|
+
}).map(option => ({
|
|
47
|
+
id: option.id.trim(),
|
|
48
|
+
label: option.label.trim(),
|
|
49
|
+
...(option.description?.trim() ? { description: option.description.trim() } : {}),
|
|
50
|
+
...(option.recommended ? { recommended: true } : {}),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
function normalizeDecisionText(value) {
|
|
54
|
+
return value.replace(/[\u0000-\u001f\u007f\ufffd]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
47
55
|
}
|
|
48
56
|
function optionsForExistingImplementations(paths) {
|
|
49
57
|
return [
|
|
50
|
-
{ id: 'create-new', label: 'Create a new implementation' },
|
|
58
|
+
{ id: 'create-new', label: 'Create a new implementation', description: 'Add a separate implementation and leave every discovered candidate unchanged.' },
|
|
51
59
|
...paths.flatMap((path, index) => [
|
|
52
|
-
{ id: `extend-${index + 1}`, label: `Extend existing implementation: ${path}` },
|
|
53
|
-
{ id: `replace-${index + 1}`, label: `Replace existing implementation: ${path}` },
|
|
54
|
-
{ id: `retain-${index + 1}`, label: `Retain existing implementation unchanged: ${path}` },
|
|
60
|
+
{ id: `extend-${index + 1}`, label: `Extend existing implementation: ${path}`, description: `Reuse ${path} and preserve its existing public behavior while adding the requested capability.` },
|
|
61
|
+
{ id: `replace-${index + 1}`, label: `Replace existing implementation: ${path}`, description: `Replace ${path}; the plan must preserve or explicitly revise every current behavior and caller.` },
|
|
62
|
+
{ id: `retain-${index + 1}`, label: `Retain existing implementation unchanged: ${path}`, description: `Keep ${path} unchanged and constrain the new work to a separate integration point.` },
|
|
55
63
|
]),
|
|
56
64
|
];
|
|
57
65
|
}
|
|
@@ -68,8 +76,10 @@ function nextActionsFor(record) {
|
|
|
68
76
|
&& record.interaction.blockingUnknowns.length === 0
|
|
69
77
|
&& record.artifacts.some(artifact => artifact.type === 'intent'))
|
|
70
78
|
|| (record.stage === 'spec_ready' && record.artifacts.some(artifact => artifact.type === 'spec'))
|
|
71
|
-
|| record.stage === 'plan_ready'
|
|
79
|
+
|| (record.stage === 'plan_ready' && record.artifacts.some(artifact => artifact.type === 'plan'))
|
|
72
80
|
|| record.stage === 'reviewed';
|
|
81
|
+
if (record.stage === 'plan_ready' && !record.artifacts.some(artifact => artifact.type === 'plan'))
|
|
82
|
+
nextActions.push('prepare_plan');
|
|
73
83
|
if (approvalReady)
|
|
74
84
|
nextActions.push('approve');
|
|
75
85
|
if (record.blockers?.some(blocker => blocker.code === 'WORKTREE_ADMISSION_REQUIRED'))
|
|
@@ -157,7 +167,15 @@ function artifactApprovalDecision(record) {
|
|
|
157
167
|
caseVersion: record.version,
|
|
158
168
|
turn: record.interaction.turn,
|
|
159
169
|
targetDecisionHash: hash({ caseId: record.id, caseVersion: record.version, turn: record.interaction.turn, approval }),
|
|
160
|
-
options:
|
|
170
|
+
options: stage === 'release'
|
|
171
|
+
? [
|
|
172
|
+
{ id: 'approve', label: 'Approve release', description: 'Accept the reviewed integration revision and continue to package export.' },
|
|
173
|
+
{ id: 'reject', label: 'Reject release', description: 'Stop this delivery because the reviewed revision is not acceptable.' },
|
|
174
|
+
]
|
|
175
|
+
: [
|
|
176
|
+
{ id: 'approve', label: `Approve ${stage} and continue`, description: `Accept the current ${stage} artifact and continue to the next delivery stage.` },
|
|
177
|
+
{ id: 'reject', label: `Reject ${stage} and stop`, description: `Reject the current ${stage} artifact and stop this delivery case.` },
|
|
178
|
+
],
|
|
161
179
|
approval,
|
|
162
180
|
status: 'pending',
|
|
163
181
|
createdAt: Date.now(),
|
|
@@ -348,12 +366,21 @@ export class DeliveryLineFacade {
|
|
|
348
366
|
requireIdentity(input);
|
|
349
367
|
if (input.actor.kind !== 'agent')
|
|
350
368
|
throw new DeliveryLineError('UNAUTHORIZED_ROLE', 'only an agent may propose clarification; a trusted user must resolve it');
|
|
369
|
+
const current = await this.ports.store.get(caseId);
|
|
370
|
+
const pending = await this.ports.store.getPendingDecision?.(caseId);
|
|
371
|
+
const options = normalizeDecisionOptions(input.options);
|
|
372
|
+
if (pending?.status === 'pending') {
|
|
373
|
+
const sameQuestion = normalizeDecisionText(current.interaction.openQuestions[0] ?? '') === normalizeDecisionText(input.question);
|
|
374
|
+
const sameOptions = JSON.stringify(pending.options) === JSON.stringify(options);
|
|
375
|
+
if (sameQuestion && sameOptions)
|
|
376
|
+
return this.view(current);
|
|
377
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'a user decision is already pending', { pendingDecisionId: pending.id, currentTurn: pending.turn, recoveryAction: 'Render and resolve the current pending decision before proposing another question.' });
|
|
378
|
+
}
|
|
351
379
|
return this.mutate(caseId, input, 'proposal.clarification', async (record) => {
|
|
352
380
|
if (!input.question.trim())
|
|
353
381
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'clarification question is required');
|
|
354
382
|
const nextTurn = record.interaction.turn + 1;
|
|
355
383
|
record.interaction = { ...record.interaction, status: 'awaiting_user', turn: nextTurn, openQuestions: [input.question], blockingUnknowns: [input.question] };
|
|
356
|
-
const options = normalizeDecisionOptions(input.options);
|
|
357
384
|
await this.ports.store.createPendingDecision?.({ id: `${record.id}:decision:${nextTurn}`, caseId: record.id, kind: 'clarification', caseVersion: record.version + 1, turn: nextTurn, targetDecisionHash: hash({ caseId: record.id, question: input.question, options }), options, status: 'pending', createdAt: Date.now() });
|
|
358
385
|
return record;
|
|
359
386
|
});
|
|
@@ -368,6 +395,13 @@ export class DeliveryLineFacade {
|
|
|
368
395
|
const options = normalizeDecisionOptions(input.options);
|
|
369
396
|
if (!input.question.trim() || options.length < 2)
|
|
370
397
|
throw new DeliveryLineError('MISSING_PREREQUISITE', 'at least two materially different options are required');
|
|
398
|
+
if (pending?.status === 'pending' && !enrichesInitialDecision) {
|
|
399
|
+
const sameQuestion = normalizeDecisionText(current.interaction.openQuestions[0] ?? '') === normalizeDecisionText(input.question);
|
|
400
|
+
const sameOptions = JSON.stringify(pending.options) === JSON.stringify(options);
|
|
401
|
+
if (sameQuestion && sameOptions)
|
|
402
|
+
return this.view(current);
|
|
403
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'a user decision is already pending', { pendingDecisionId: pending.id, currentTurn: pending.turn, recoveryAction: 'Render and resolve the current pending decision before proposing another option set.' });
|
|
404
|
+
}
|
|
371
405
|
return this.mutate(caseId, input, 'proposal.options', async (record) => {
|
|
372
406
|
const nextTurn = enrichesInitialDecision ? record.interaction.turn : record.interaction.turn + 1;
|
|
373
407
|
record.interaction = { ...record.interaction, status: 'awaiting_user', turn: nextTurn, openQuestions: [input.question], blockingUnknowns: [input.question] };
|
|
@@ -424,8 +458,10 @@ export class DeliveryLineFacade {
|
|
|
424
458
|
if (acceptanceCriteria.length === 0)
|
|
425
459
|
acceptanceCriteria.push(rawAnswer);
|
|
426
460
|
record.interaction = answerInteraction(record.interaction, input.turn);
|
|
427
|
-
if (record.unknownRegistry)
|
|
428
|
-
|
|
461
|
+
if (record.unknownRegistry) {
|
|
462
|
+
const answeredQuestion = normalizeDecisionText(question);
|
|
463
|
+
record.unknownRegistry = record.unknownRegistry.map(item => normalizeDecisionText(item.question) === answeredQuestion ? { ...item, blocking: false } : item);
|
|
464
|
+
}
|
|
429
465
|
if (input.trustedDecision) {
|
|
430
466
|
const authority = { id: input.actor.id, kind: 'human' };
|
|
431
467
|
record.authorities = { ...record.authorities, decisionAuthority: record.authorities?.decisionAuthority ?? authority, approvalAuthority: record.authorities?.approvalAuthority ?? authority };
|
|
@@ -434,7 +470,7 @@ export class DeliveryLineFacade {
|
|
|
434
470
|
const decisionHash = hash({ questionId, optionId: input.optionId, selectedLabel, rawAnswer, acceptanceCriteria, optionsSnapshot });
|
|
435
471
|
record.decisions.push({ id: randomUUID(), kind: 'product', questionId, question, options: optionsSnapshot.map(option => option.label), optionsSnapshot, optionId: input.optionId, selectedLabel, rawAnswer, acceptanceCriteria, decisionHash, sessionId: input.sessionId, toolUseId: input.toolUseId, decision: selectedLabel, decidedBy: input.actor, artifactHash: decisionHash });
|
|
436
472
|
if (record.interaction.phase === 'clarification')
|
|
437
|
-
record.artifacts.push(await this.createArtifact(record, 'intent', renderIntentMarkdown({ title: record.title, originalRequest: record.originalRequest, problem:
|
|
473
|
+
record.artifacts.push(await this.createArtifact(record, 'intent', renderIntentMarkdown({ title: record.title, originalRequest: record.originalRequest, problem: record.originalRequest ?? record.title, outcome: selectedLabel, answer: rawAnswer, contextQuality: record.contextQuality, affectedSystems: [record.projectRoot], constraints: ['Preserve existing behavior outside the confirmed scope.'], successCriteria: acceptanceCriteria, openQuestions: record.taskSemantics && record.taskSemantics.confidence > 0 ? (record.unknownRegistry?.filter(item => item.blocking).map(item => item.question) ?? []) : record.interaction.blockingUnknowns, unknownRegistry: record.taskSemantics && record.taskSemantics.confidence > 0 ? record.unknownRegistry : undefined, actor: input.actor }), input.actor));
|
|
438
474
|
await this.createProjectionArtifact(record, 'stack-facts.md', 'Stack facts', 'Project stack evidence bound at case creation.', { baseRef: record.baseRef, targetRef: record.targetRef, projectRoot: record.projectRoot }, input.actor);
|
|
439
475
|
await this.createProjectionArtifact(record, 'decisions.md', 'Decisions', 'Current-turn decision ledger projection.', { decisionHash, questionId, optionId: input.optionId, selectedLabel }, input.actor);
|
|
440
476
|
record.interaction = appendInteractionSummary(record.interaction, { id: randomUUID(), phase: record.interaction.phase, turn: input.turn, question, answer: rawAnswer, rawAnswer, acceptanceCriteria, decision: selectedLabel, options: optionsSnapshot.map(option => option.label), contextReceipt: input.contextReceipt, artifactHashes: [decisionHash], actor: input.actor });
|
|
@@ -511,10 +547,118 @@ export class DeliveryLineFacade {
|
|
|
511
547
|
const requiredContextChannels = [...new Set(Object.values(selectedCapabilities?.selected ?? {}).flatMap(manifest => manifest.requiredContextChannels))]
|
|
512
548
|
.filter((channel) => ['code', 'memory', 'knowledge'].includes(channel));
|
|
513
549
|
record.pluginSelection = { selectedManifestHash: pluginSelectionHash, 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 } : {}), ...(selectedCapabilities ? { selectedByRole: Object.fromEntries(Object.entries(selectedCapabilities.selected).map(([role, manifest]) => [role, { pluginName: manifest.name, version: manifest.version, manifestHash: hash(manifest) }])), ...(selectedCapabilities.hostWorkerFallback ? { hostWorkerFallback: true } : {}) } : {}), selectedAt: Date.now() };
|
|
550
|
+
if (selectedCapabilities && this.ports.capabilities) {
|
|
551
|
+
const stackFactsFingerprint = record.stackFacts?.fingerprint ?? record.stackFactsHash ?? hash({ projectRoot: record.projectRoot, baseRef: record.baseRef, targetRef: record.targetRef });
|
|
552
|
+
for (const role of ['analyzer', 'designer']) {
|
|
553
|
+
const selected = selectedCapabilities.selected[role];
|
|
554
|
+
if (!selected || selected.kind !== 'runner')
|
|
555
|
+
continue;
|
|
556
|
+
const runtimeManifest = await this.ports.capabilities.manifest(selected.name, role);
|
|
557
|
+
const receipt = await this.ports.capabilities.run({
|
|
558
|
+
caseId: record.id,
|
|
559
|
+
taskId: `${record.id}:${role}:${record.interaction.turn}`,
|
|
560
|
+
role,
|
|
561
|
+
pluginName: runtimeManifest.pluginName,
|
|
562
|
+
capability: runtimeManifest.capability,
|
|
563
|
+
manifestHash: runtimeManifest.manifestHash,
|
|
564
|
+
stackFactsFingerprint,
|
|
565
|
+
allowedFiles: [],
|
|
566
|
+
payload: {
|
|
567
|
+
changedFiles: [],
|
|
568
|
+
role,
|
|
569
|
+
request: record.originalRequest ?? record.title,
|
|
570
|
+
componentName: record.taskSemantics?.entities[0] ?? selectedLabel,
|
|
571
|
+
contextReceiptHash: record.contextReceipt ?? input.contextReceipt,
|
|
572
|
+
decisionHash,
|
|
573
|
+
optionsSnapshot,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
if (receipt.manifestHash !== runtimeManifest.manifestHash || receipt.verified !== true)
|
|
577
|
+
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', `plugin ${role} receipt is not verified`);
|
|
578
|
+
record.evidence.push({
|
|
579
|
+
id: randomUUID(),
|
|
580
|
+
kind: 'tool',
|
|
581
|
+
hash: hash({ role, pluginName: runtimeManifest.pluginName, pluginVersion: receipt.pluginVersion, manifestHash: receipt.manifestHash, outputHash: receipt.outputHash, contextReceiptHash: record.contextReceipt ?? input.contextReceipt, decisionHash }),
|
|
582
|
+
createdAt: Date.now(),
|
|
583
|
+
metadata: { role, pluginName: runtimeManifest.pluginName, pluginVersion: receipt.pluginVersion, manifestHash: receipt.manifestHash, outputHash: receipt.outputHash, stackFactsFingerprint, contextReceiptHash: record.contextReceipt ?? input.contextReceipt, decisionHash, verified: receipt.verified },
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
514
587
|
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);
|
|
515
588
|
record.stage = 'spec_ready';
|
|
516
589
|
record.interaction = { ...record.interaction, phase: 'design', status: 'awaiting_approval', openQuestions: [], blockingUnknowns: [] };
|
|
517
|
-
|
|
590
|
+
const selectedOption = optionsSnapshot.find(option => option.id === optionId);
|
|
591
|
+
const acceptanceCriteria = record.decisions
|
|
592
|
+
.filter(decision => decision.kind === 'product')
|
|
593
|
+
.flatMap(decision => decision.acceptanceCriteria ?? [decision.selectedLabel ?? decision.decision]);
|
|
594
|
+
record.artifacts.push(await this.createArtifact(record, 'spec', renderSpecMarkdown({
|
|
595
|
+
title: record.title,
|
|
596
|
+
request: record.originalRequest ?? record.title,
|
|
597
|
+
selectedOption,
|
|
598
|
+
alternatives: optionsSnapshot.filter(option => option.id !== optionId),
|
|
599
|
+
acceptanceCriteria,
|
|
600
|
+
constraints: ['Preserve existing behavior outside the approved scope.', 'Use only dependencies and project conventions verified from the current repository.'],
|
|
601
|
+
existingImplementations: record.existingImplementations,
|
|
602
|
+
detectedStacks: record.stackFacts?.detectedStacks,
|
|
603
|
+
contextReceipt: record.contextReceipt ?? input.contextReceipt,
|
|
604
|
+
contextGaps: record.contextQuality?.gaps,
|
|
605
|
+
actor: input.actor,
|
|
606
|
+
}), input.actor));
|
|
607
|
+
return record;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Compile and persist a plan proposal before asking the user to approve it.
|
|
612
|
+
* Approval must consume this immutable draft by hash; it must never depend on
|
|
613
|
+
* the model being able to submit a plan object through a trusted command.
|
|
614
|
+
*/
|
|
615
|
+
async preparePlan(caseId, input) {
|
|
616
|
+
requireIdentity(input);
|
|
617
|
+
if (input.actor.kind !== 'agent')
|
|
618
|
+
throw new DeliveryLineError('UNAUTHORIZED_ROLE', 'only an agent may propose an executable plan; a trusted user must approve it');
|
|
619
|
+
return this.mutate(caseId, input, 'plan.prepared', async (record) => {
|
|
620
|
+
if (input.turn !== record.interaction.turn)
|
|
621
|
+
throw staleTurnError(record, input.turn);
|
|
622
|
+
if (record.stage !== 'plan_ready')
|
|
623
|
+
throw new DeliveryLineError('INVALID_TRANSITION', 'plan preparation requires a ready plan stage', { currentStage: record.stage, recoveryAction: 'Refresh delivery_status and follow its nextActions.' });
|
|
624
|
+
if (record.artifacts.some(artifact => artifact.type === 'plan'))
|
|
625
|
+
throw new DeliveryLineError('IDEMPOTENCY_CONFLICT', 'a plan draft is already prepared; refresh delivery_status and approve the pending plan decision');
|
|
626
|
+
if (this.ports.context) {
|
|
627
|
+
const plannerReceipt = await this.ports.context.build({
|
|
628
|
+
role: 'planner',
|
|
629
|
+
...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
|
|
630
|
+
retrieval: {
|
|
631
|
+
taskRevision: record.version + 1,
|
|
632
|
+
projectRoot: record.projectRoot,
|
|
633
|
+
query: `${record.originalRequest ?? record.title} implementation plan`,
|
|
634
|
+
sessionId: input.sessionId,
|
|
635
|
+
executionId: input.executionId,
|
|
636
|
+
requestId: `delivery:plan:${record.id}:${record.version}`,
|
|
637
|
+
turnId: `delivery:plan:${record.id}:${record.interaction.turn}`,
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
record.contextReceipt = plannerReceipt.receiptHash;
|
|
641
|
+
if (plannerReceipt.channelStatus && plannerReceipt.gaps && plannerReceipt.attribution)
|
|
642
|
+
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 } : {}) };
|
|
643
|
+
}
|
|
644
|
+
const planContent = renderPlanMarkdown(input.plan);
|
|
645
|
+
const planHash = contentHash(planContent);
|
|
646
|
+
new PlanCompiler().compile({
|
|
647
|
+
caseId: record.id,
|
|
648
|
+
projectRoot: record.projectRoot,
|
|
649
|
+
baseRevision: record.baseRef,
|
|
650
|
+
planHash,
|
|
651
|
+
contextReceiptHash: record.contextReceipt ?? input.contextReceipt,
|
|
652
|
+
hostSelectionSnapshotHash: hash({ mode: 'manual-plan', actor: input.actor, sessionId: input.sessionId, executionId: input.executionId }),
|
|
653
|
+
executionAuthority: hash({ caseId: record.id, stage: 'plan', planHash }),
|
|
654
|
+
plan: input.plan,
|
|
655
|
+
});
|
|
656
|
+
const artifact = await this.createArtifact(record, 'plan', planContent, input.actor, { path: 'plan.json', content: renderPlanJson(input.plan) });
|
|
657
|
+
if (artifact.contentHash !== planHash)
|
|
658
|
+
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', 'plan artifact hash changed while persisting the proposal');
|
|
659
|
+
record.planDraft = structuredClone(input.plan);
|
|
660
|
+
record.artifacts.push(artifact);
|
|
661
|
+
record.interaction = { ...record.interaction, status: 'awaiting_approval' };
|
|
518
662
|
return record;
|
|
519
663
|
});
|
|
520
664
|
}
|
|
@@ -530,31 +674,17 @@ export class DeliveryLineFacade {
|
|
|
530
674
|
let planContent;
|
|
531
675
|
let proposedArtifactHash;
|
|
532
676
|
if (input.stage === 'plan') {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
role: 'planner',
|
|
538
|
-
...(record.pluginSelection?.requiredContextChannels?.length ? { requiredChannels: record.pluginSelection.requiredContextChannels } : {}),
|
|
539
|
-
retrieval: {
|
|
540
|
-
taskRevision: record.version + 1,
|
|
541
|
-
projectRoot: record.projectRoot,
|
|
542
|
-
query: `${record.title} implementation plan`,
|
|
543
|
-
sessionId: input.sessionId,
|
|
544
|
-
executionId: input.executionId,
|
|
545
|
-
requestId: `delivery:plan:${record.id}:${record.version}`,
|
|
546
|
-
turnId: `delivery:plan:${record.id}:${record.interaction.turn}`,
|
|
547
|
-
},
|
|
548
|
-
});
|
|
549
|
-
record.contextReceipt = plannerReceipt.receiptHash;
|
|
550
|
-
if (plannerReceipt.channelStatus && plannerReceipt.gaps && plannerReceipt.attribution)
|
|
551
|
-
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 } : {}) };
|
|
552
|
-
}
|
|
553
|
-
planContent = renderPlanMarkdown(input.plan);
|
|
677
|
+
const proposedPlan = record.planDraft;
|
|
678
|
+
if (!proposedPlan)
|
|
679
|
+
throw new DeliveryLineError('PLAN_NOT_EXECUTABLE', 'plan approval requires a prepared executable plan');
|
|
680
|
+
planContent = renderPlanMarkdown(proposedPlan);
|
|
554
681
|
proposedArtifactHash = contentHash(planContent);
|
|
682
|
+
const preparedArtifact = [...record.artifacts].reverse().find(item => item.type === 'plan');
|
|
683
|
+
if (preparedArtifact && preparedArtifact.contentHash !== proposedArtifactHash)
|
|
684
|
+
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', 'prepared plan does not match the approval artifact');
|
|
555
685
|
}
|
|
556
686
|
let artifact = [...record.artifacts].reverse().find(item => item.type === artifactType);
|
|
557
|
-
if (input.stage !== '
|
|
687
|
+
if (input.stage !== 'release') {
|
|
558
688
|
if (input.artifactId && artifact?.id !== input.artifactId)
|
|
559
689
|
throw new DeliveryLineError('ARTIFACT_VERSION_CONFLICT', 'approval artifact id is stale');
|
|
560
690
|
if (input.expectedVersion !== undefined && artifact?.version !== input.expectedVersion)
|
|
@@ -596,6 +726,9 @@ export class DeliveryLineFacade {
|
|
|
596
726
|
if (input.stage === 'spec')
|
|
597
727
|
record.stage = 'plan_ready';
|
|
598
728
|
if (input.stage === 'plan') {
|
|
729
|
+
const proposedPlan = record.planDraft;
|
|
730
|
+
if (!proposedPlan)
|
|
731
|
+
throw new DeliveryLineError('PLAN_NOT_EXECUTABLE', 'plan approval requires a prepared executable plan');
|
|
599
732
|
const envelopes = new PlanCompiler().compile({
|
|
600
733
|
caseId: record.id,
|
|
601
734
|
projectRoot: record.projectRoot,
|
|
@@ -604,13 +737,8 @@ export class DeliveryLineFacade {
|
|
|
604
737
|
contextReceiptHash: record.contextReceipt ?? input.contextReceipt,
|
|
605
738
|
hostSelectionSnapshotHash: hash({ mode: 'manual-plan', actor: input.actor, sessionId: input.sessionId, executionId: input.executionId }),
|
|
606
739
|
executionAuthority: hash({ actor: input.actor, stage: input.stage, planHash: proposedArtifactHash }),
|
|
607
|
-
plan:
|
|
740
|
+
plan: proposedPlan,
|
|
608
741
|
});
|
|
609
|
-
record.artifacts = record.artifacts.filter(item => item.type !== 'plan');
|
|
610
|
-
artifact = await this.createArtifact(record, 'plan', planContent, input.actor, { path: 'plan.json', content: renderPlanJson(input.plan) });
|
|
611
|
-
if (artifact.contentHash !== proposedArtifactHash)
|
|
612
|
-
throw new DeliveryLineError('ARTIFACT_HASH_MISMATCH', 'plan artifact hash changed while persisting the approval artifact');
|
|
613
|
-
record.artifacts.push(artifact);
|
|
614
742
|
record.stage = 'plan_approved';
|
|
615
743
|
const decision = [...record.decisions].reverse().find(item => item.decisionHash);
|
|
616
744
|
if (decision && record.pluginSelection && record.stackFactsHash) {
|
|
@@ -1036,7 +1164,7 @@ export class DeliveryLineFacade {
|
|
|
1036
1164
|
id: pending.id,
|
|
1037
1165
|
kind: pending.kind,
|
|
1038
1166
|
question: `Approve the current ${pending.approval.stage} artifact?`,
|
|
1039
|
-
options:
|
|
1167
|
+
options: pending.options,
|
|
1040
1168
|
blocking: true,
|
|
1041
1169
|
turn: pending.turn,
|
|
1042
1170
|
command: `/devflow:approve ${pending.id}`,
|