@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
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/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
6
|
+
import { executeDeliveryStep } from "./delivery-execution.js";
|
|
7
|
+
import { readDeliveryIntent } from "./delivery-intents.js";
|
|
8
|
+
import { appendAudit } from "./audit.js";
|
|
9
|
+
import { managedProcessStatus, managedProcessBinding, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
10
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
const text = (value) => typeof value === "string" && Boolean(value.trim()) && value.length <= 4096 && !value.includes("\0");
|
|
12
|
+
const reference = (value) => value === null || record(value) && Object.keys(value).sort().join() === "id,kind" && text(value.id) && text(value.kind);
|
|
13
|
+
function validRollback(value, operation) {
|
|
14
|
+
if (operation !== "deploy")
|
|
15
|
+
return !Object.hasOwn(value, "rollback");
|
|
16
|
+
const rollback = value.rollback;
|
|
17
|
+
return record(rollback) && Object.keys(rollback).sort().join() === "reason,reference,status,strategy"
|
|
18
|
+
&& typeof rollback.strategy === "string" && ["rollback", "roll_forward", "operator_handoff"].includes(rollback.strategy)
|
|
19
|
+
&& typeof rollback.status === "string" && ["not_needed", "required", "completed", "failed", "unknown"].includes(rollback.status)
|
|
20
|
+
&& text(rollback.reason) && reference(rollback.reference)
|
|
21
|
+
&& (rollback.status !== "completed" || rollback.reference !== null)
|
|
22
|
+
&& (value.status === "complete" ? rollback.status === "not_needed" : rollback.status !== "not_needed");
|
|
23
|
+
}
|
|
24
|
+
function refuse(reason) {
|
|
25
|
+
throw new AppError("delivery_result_unproven", "Read back the original delivery; do not repeat its mutation", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
|
|
26
|
+
}
|
|
27
|
+
/** Settlement reads the latest owned observation, never a supplied filename
|
|
28
|
+
* or an older optimistic answer after a newer uncertain readback. A changed
|
|
29
|
+
* operation log needs a new observation; this is not a retry authorization. */
|
|
30
|
+
export function readDeliveryResult(context, scope) {
|
|
31
|
+
const intent = readDeliveryIntent(context, scope);
|
|
32
|
+
if (!intent)
|
|
33
|
+
refuse("original_intent_required");
|
|
34
|
+
const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.operation_readback' AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, scope.runId]);
|
|
35
|
+
if (!row || Buffer.byteLength(row.payload_json) > 131072)
|
|
36
|
+
refuse("retained_readback_required");
|
|
37
|
+
let receipt;
|
|
38
|
+
try {
|
|
39
|
+
receipt = JSON.parse(row.payload_json);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return refuse("retained_readback_invalid");
|
|
43
|
+
}
|
|
44
|
+
const grant = intent.plan.approval.scope, observed = receipt?.observation;
|
|
45
|
+
if (receipt?.schema_id !== "dd-flow/delivery-result@1" || receipt.run_id !== scope.runId || receipt.intent_sha256 !== intent.identity
|
|
46
|
+
|| receipt.operation_head_sha256 !== intent.head || !observed || observed.schema_id !== "dd-flow/delivery-result-observation@1"
|
|
47
|
+
|| observed.operation_id !== grant.operation_id || observed.binding_id !== grant.binding_id
|
|
48
|
+
|| hybridDigest(observed.artifact) !== hybridDigest(grant.artifact) || hybridDigest(observed.selector) !== hybridDigest(grant.selector)
|
|
49
|
+
|| !["complete", "absent", "partial", "unknown"].includes(observed.status)
|
|
50
|
+
|| !reference(observed.reference) || observed.status === "complete" && observed.reference === null
|
|
51
|
+
|| !validRollback(observed, grant.selector.operation)
|
|
52
|
+
|| !Number.isFinite(Date.parse(receipt.observed_at)) || !Array.isArray(receipt.processes) || !receipt.processes.length
|
|
53
|
+
|| receipt.processes.length > 16 || receipt.processes.some(id => typeof id !== "string" || !id.startsWith("PROC-")))
|
|
54
|
+
refuse("retained_readback_changed");
|
|
55
|
+
return { sha256: hybridDigest(receipt), receipt: structuredClone(receipt) };
|
|
56
|
+
}
|
|
57
|
+
/** Private result/reconciliation observation. The caller proves the actual
|
|
58
|
+
* Stage/coordinator authority and project readback safety. No new approval,
|
|
59
|
+
* login, retry, rollback or terminal Stage/RUN decision is inferred here.
|
|
60
|
+
* Reconciliation intentionally does not require unexpired mutation approval
|
|
61
|
+
* or local artifact availability: an earlier effect must remain observable. */
|
|
62
|
+
export async function observeDeliveryResult(context, input) {
|
|
63
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
64
|
+
refuse("observation_outside_writer_required");
|
|
65
|
+
const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [input.binding] }, input.binding.selector);
|
|
66
|
+
const original = readDeliveryIntent(context, input);
|
|
67
|
+
if (!original)
|
|
68
|
+
refuse("original_intent_required");
|
|
69
|
+
const grant = original.plan.approval.scope;
|
|
70
|
+
if (hybridDigest(binding) !== grant.binding_sha256)
|
|
71
|
+
refuse("original_binding_required");
|
|
72
|
+
const assertOwner = () => {
|
|
73
|
+
const checked = input.assertCurrent();
|
|
74
|
+
if (types.isPromise(checked)) {
|
|
75
|
+
void checked.catch(() => { });
|
|
76
|
+
refuse("synchronous_owner_fence_required");
|
|
77
|
+
}
|
|
78
|
+
const current = readDeliveryIntent(context, input);
|
|
79
|
+
if (!current || current.identity !== original.identity || current.head !== original.head)
|
|
80
|
+
refuse("original_operation_changed");
|
|
81
|
+
return current;
|
|
82
|
+
};
|
|
83
|
+
const assertCurrent = () => {
|
|
84
|
+
const current = assertOwner();
|
|
85
|
+
// The old command may lack a result event after interruption. Readback is
|
|
86
|
+
// allowed only after every originally admitted process is physically quiet.
|
|
87
|
+
const processes = managedProcessStatus(context);
|
|
88
|
+
for (const event of current.events) {
|
|
89
|
+
if (event.kind !== "admitted")
|
|
90
|
+
continue;
|
|
91
|
+
const process = processes.find(item => item.id === event.process.process_id);
|
|
92
|
+
if (event.process.resource_home !== resourceHome(context) || !process || process.state !== "stopped"
|
|
93
|
+
|| managedProcessBinding(process, resourceHome(context)).registration_sha256 !== event.process.registration_sha256
|
|
94
|
+
|| processTreeIsAlive(process))
|
|
95
|
+
refuse("original_process_not_settled");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
assertCurrent();
|
|
99
|
+
const procedure = binding.procedures.result_readback, deadline = Date.now() + procedure.timeout_ms, processes = [];
|
|
100
|
+
let observed;
|
|
101
|
+
for (const step of procedure.procedure_steps) {
|
|
102
|
+
let value;
|
|
103
|
+
try {
|
|
104
|
+
const result = await executeDeliveryStep(context, { projectId: input.projectId, runId: input.runId,
|
|
105
|
+
operationId: `result-${randomUUID()}`, stepId: step.id, workspace: original.plan.artifact.workspace,
|
|
106
|
+
procedure, deadlineAt: deadline, assertCurrent, admit() { }, consumeOutput(bytes) {
|
|
107
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
108
|
+
} });
|
|
109
|
+
processes.push(result.process_id);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return refuse("readback_failed");
|
|
113
|
+
}
|
|
114
|
+
const keys = binding.selector.operation === "deploy" ? "artifact,binding_id,operation_id,reference,rollback,schema_id,selector,status"
|
|
115
|
+
: "artifact,binding_id,operation_id,reference,schema_id,selector,status";
|
|
116
|
+
if (!record(value) || Object.keys(value).sort().join() !== keys
|
|
117
|
+
|| value.schema_id !== "dd-flow/delivery-result-observation@1" || value.operation_id !== grant.operation_id || value.binding_id !== binding.id
|
|
118
|
+
|| hybridDigest(value.artifact) !== hybridDigest(grant.artifact) || hybridDigest(value.selector) !== hybridDigest(grant.selector)
|
|
119
|
+
|| !["complete", "absent", "partial", "unknown"].includes(value.status)
|
|
120
|
+
|| !reference(value.reference) || !validRollback(value, binding.selector.operation)
|
|
121
|
+
|| value.status === "complete" && value.reference === null)
|
|
122
|
+
refuse("result_context_mismatch_or_unsafe_output");
|
|
123
|
+
const next = value;
|
|
124
|
+
// Ordered steps may confirm the same result; a conflicting earlier answer
|
|
125
|
+
// is not silently overwritten by a more optimistic later command.
|
|
126
|
+
if (observed && hybridDigest(observed) !== hybridDigest(next))
|
|
127
|
+
refuse("conflicting_readback");
|
|
128
|
+
observed = structuredClone(next);
|
|
129
|
+
}
|
|
130
|
+
assertCurrent();
|
|
131
|
+
if (!observed || Date.now() >= deadline)
|
|
132
|
+
refuse("readback_incomplete");
|
|
133
|
+
const receipt = { schema_id: "dd-flow/delivery-result@1", run_id: input.runId, intent_sha256: original.identity,
|
|
134
|
+
operation_head_sha256: original.head, observed_at: context.now(), observation: observed, processes };
|
|
135
|
+
if (!Number.isFinite(Date.parse(receipt.observed_at)))
|
|
136
|
+
refuse("observation_clock_invalid");
|
|
137
|
+
return context.db.writeTransaction(() => {
|
|
138
|
+
// Physical observation is already settled. Only current SQL/owner fences
|
|
139
|
+
// run in this short writer; receipt persistence does not close the intent.
|
|
140
|
+
assertOwner();
|
|
141
|
+
appendAudit(context, { projectId: input.projectId, eventType: "delivery.operation_readback", payload: receipt });
|
|
142
|
+
return structuredClone(receipt);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { runtimeCommand } from "./runtime-command.js";
|
|
5
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
6
|
+
/** Packet candidates are not invocation admission or mutation approval. */
|
|
7
|
+
export function deliveryStageCommands(context, input) {
|
|
8
|
+
if (!deliveryFlowStages[input.flowId].includes(input.stage))
|
|
9
|
+
throw new AppError("delivery_stage_invalid", "Unknown delivery Stage", 2, { effect: "no_effect" });
|
|
10
|
+
const command = (operation, file) => runtimeCommand(context, ["delivery", operation, input.runId,
|
|
11
|
+
"--project-root", input.projectRoot, "--stage", input.stage, ...(file ? ["--result-file", file] : []), "--json"]);
|
|
12
|
+
return {
|
|
13
|
+
inputs: command("inputs"),
|
|
14
|
+
...(["prepare", "verify"].includes(input.stage) ? { checks: command("checks", path.join(input.stageRoot, "delivery-artifact.json")) } : {}),
|
|
15
|
+
...(["publish", "deploy"].includes(input.stage) ? { execute: command("execute", path.join(input.stageRoot, "delivery-approval.json")) } : {}),
|
|
16
|
+
...(["publish", "deploy", "verify", "finish"].includes(input.stage) ? { reconcile: command("reconcile") } : {})
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Issue only after the native Stage coordinator is bound. Keep assignments
|
|
20
|
+
* out of immutable semantic packets and include literal commands for custody. */
|
|
21
|
+
export function issueDeliveryStageCommands(context, input) {
|
|
22
|
+
const commands = Object.fromEntries(Object.entries(deliveryStageCommands(context, input))
|
|
23
|
+
.map(([operation, candidate]) => [operation, operation === "inputs" ? candidate : managedLifecycleCommand(context, candidate)]));
|
|
24
|
+
const instructions = `\n## Bound delivery command candidates\n\n\`\`\`json\n${JSON.stringify(commands, null, 2)}\n\`\`\`\n\nWrite the exact artifact/approval JSON to the issued result-file before checks/execute. Starting this Stage grants no mutation approval. Reconcile the original intent; never retry an unknown effect.\n\n## Current issued delivery commands\n\n${Object.values(commands).join("\n\n")}\n`;
|
|
25
|
+
return { commands, instructions };
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
3
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
4
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
7
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
8
|
+
import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
9
|
+
import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
|
|
10
|
+
import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
11
|
+
import { currentLifecycleAdmission, lifecycleInvocationScope, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
|
|
12
|
+
function refuse(reason) {
|
|
13
|
+
throw new AppError("delivery_stage_invalid", "Delivery requires its registered flow, original request and current Stage owner", 2, { reason, effect: "no_effect" });
|
|
14
|
+
}
|
|
15
|
+
/** Internal family entry, not a public launcher or permission to publish.
|
|
16
|
+
* The ordinary descriptor runtime owns attempt, coordinator Work and Session.
|
|
17
|
+
* Delivery uses its actual workspace, not a read-only diagnostic clone. */
|
|
18
|
+
export function readDeliveryStageScope(context, input) {
|
|
19
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
20
|
+
const bundle = readFlowRunExecutionBundle(context, input), flow = bundle?.manifest.flow_id;
|
|
21
|
+
if (!bundle || !flow || !Object.hasOwn(deliveryFlowStages, flow)
|
|
22
|
+
|| !deliveryFlowStages[flow].includes(input.stage))
|
|
23
|
+
refuse("registered_delivery_stage_required");
|
|
24
|
+
const flowId = flow, scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
25
|
+
bundle.descriptor.stage(input.stage);
|
|
26
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
27
|
+
const users = readPreparedMemoryFlowUserInputs(context, scope, flowId, bundle), userInputsSha256 = hybridDigest(users);
|
|
28
|
+
return { scope, bundle, run, flowId, users, userInputsSha256,
|
|
29
|
+
assertCustody(writer) {
|
|
30
|
+
assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
|
|
31
|
+
if (hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, flowId, bundle)) !== userInputsSha256)
|
|
32
|
+
refuse("user_inputs_changed_during_entry");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function startDeliveryStage(context, input) {
|
|
37
|
+
const selected = readDeliveryStageScope(context, input);
|
|
38
|
+
const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, selected.assertCustody);
|
|
39
|
+
const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, selected.assertCustody);
|
|
40
|
+
return { ...start, ...coordinator, flow_id: selected.flowId, user_inputs_sha256: selected.userInputsSha256 };
|
|
41
|
+
}
|
|
42
|
+
/** A matching SQL coordinator or ambient scope is not a command capability.
|
|
43
|
+
* The ordinary lifecycle gate must already have admitted this exact CLI process. */
|
|
44
|
+
export function assertDeliveryCommandOwner(context, input) {
|
|
45
|
+
const id = context.env.DD_FLOW_CURRENT_INVOCATION;
|
|
46
|
+
if (!id || !currentLifecycleAdmission(context, id))
|
|
47
|
+
refuse("issued_coordinator_command_required");
|
|
48
|
+
const native = lifecycleInvocationScope(context, id);
|
|
49
|
+
assertLifecycleInvocationCurrent(context, native, input.command);
|
|
50
|
+
const selected = readDeliveryStageScope(context, input), actor = readActiveDescriptorStageCoordinator(context, selected.scope, selected.bundle);
|
|
51
|
+
const hook = context.db.get("SELECT daemon_id FROM hook_events WHERE project_id=? AND event_key=?", [selected.scope.projectId, actor.start_hook]);
|
|
52
|
+
if (native.projectRoot !== input.projectRoot || native.runId !== input.runId || native.rootSessionId !== actor.provider_session_id
|
|
53
|
+
|| !hook?.daemon_id || hook.daemon_id !== native.daemonId)
|
|
54
|
+
refuse("current_physical_coordinator_required");
|
|
55
|
+
return selected;
|
|
56
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
4
|
+
import { readDeliveryStageScope, startDeliveryStage } from "./delivery-stage-entry.js";
|
|
5
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
6
|
+
import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
7
|
+
import { readAcceptedStageData, prepareStageReport, acceptStageReport, publishStageReport } from "./stage-report-renderer.js";
|
|
8
|
+
import { prepareDeliveryStageResult } from "./delivery-stage-results.js";
|
|
9
|
+
import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
|
|
10
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
11
|
+
import { prepareDeliveryChecks, prepareDeliveryHistoricalChecks } from "./delivery-checks.js";
|
|
12
|
+
import { deliveryApprovalScope } from "./delivery-approval.js";
|
|
13
|
+
import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
|
|
14
|
+
import { readDeliveryIntent } from "./delivery-intents.js";
|
|
15
|
+
import { readDeliveryResult } from "./delivery-result.js";
|
|
16
|
+
import { appendAudit } from "./audit.js";
|
|
17
|
+
export const deliveryStageHandlers = Object.freeze(Object.entries(deliveryFlowStages).flatMap(([flowId, stages]) => stages.map(stage => Object.freeze({ flowId, stage, entryId: "delivery-stage-entry", finishId: "delivery-stage-finish",
|
|
18
|
+
entry: startDeliveryStage, finish: finishDeliveryStage }))));
|
|
19
|
+
function refuse(reason) {
|
|
20
|
+
throw new AppError("delivery_stage_invalid", "Delivery Stage requires observed project policy and owned evidence", 2, { reason, effect: "no_effect", phase: "prepare", recoverable: true });
|
|
21
|
+
}
|
|
22
|
+
/** Family settlement never runs the protected command. Only the operation
|
|
23
|
+
* owner can retain its original effect/readback; Stage reports cannot invent it.
|
|
24
|
+
* Public family admission/launch still requires its separate installed gates. */
|
|
25
|
+
export function finishDeliveryStage(context, input) {
|
|
26
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle, flowId } = selected;
|
|
27
|
+
const stages = deliveryFlowStages[flowId], index = stages.indexOf(input.stage);
|
|
28
|
+
const packet = readDescriptorStageInput(context, scope, bundle, true, true);
|
|
29
|
+
const previous = index ? readAcceptedStageData(context, { ...scope, stage: stages[index - 1] }) : null;
|
|
30
|
+
let decision = null;
|
|
31
|
+
let policy;
|
|
32
|
+
if (index === 1) {
|
|
33
|
+
if (!input.bytes.length || input.bytes.length > 1024 * 1024)
|
|
34
|
+
refuse("result_exceeds_bound");
|
|
35
|
+
try {
|
|
36
|
+
decision = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)).details?.decision;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return refuse("invalid_result_json");
|
|
40
|
+
}
|
|
41
|
+
if (!decision?.policy || typeof decision.policy.path !== "string" || typeof decision.policy.sha256 !== "string"
|
|
42
|
+
|| !decision.binding?.selector || typeof decision.version !== "string" || !decision.version.trim())
|
|
43
|
+
refuse("project_policy_required");
|
|
44
|
+
const workspace = bundle.execution.snapshot().scope.workspace;
|
|
45
|
+
policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
|
|
46
|
+
if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
|
|
47
|
+
refuse("project_policy_binding_mismatch");
|
|
48
|
+
}
|
|
49
|
+
const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
|
|
50
|
+
let checks;
|
|
51
|
+
let verification;
|
|
52
|
+
let artifact = null;
|
|
53
|
+
let historical;
|
|
54
|
+
if (index > 1) {
|
|
55
|
+
decision = prior?.details?.decision;
|
|
56
|
+
artifact = prior?.details?.artifact;
|
|
57
|
+
}
|
|
58
|
+
if (["prepare", "verify"].includes(input.stage)) {
|
|
59
|
+
decision = prior?.details?.decision;
|
|
60
|
+
if (input.stage === "prepare") {
|
|
61
|
+
if (!input.bytes.length || input.bytes.length > 1024 * 1024)
|
|
62
|
+
refuse("result_exceeds_bound");
|
|
63
|
+
try {
|
|
64
|
+
artifact = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)).details?.artifact;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return refuse("invalid_result_json");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
artifact = prior?.details?.artifact;
|
|
72
|
+
if (!artifact || !decision)
|
|
73
|
+
refuse("accepted_artifact_required");
|
|
74
|
+
checks = prepareDeliveryChecks(context, { ...input, artifact });
|
|
75
|
+
verification = checks.evidence();
|
|
76
|
+
}
|
|
77
|
+
const afterEffect = index >= stages.indexOf(flowId === "deploy" ? "deploy" : "publish");
|
|
78
|
+
const intent = afterEffect ? readDeliveryIntent(context, scope) : null;
|
|
79
|
+
const readback = afterEffect ? readDeliveryResult(context, scope) : null;
|
|
80
|
+
const operation = decision && artifact ? deliveryApprovalScope(scope, bundle, { operationId: decision.operation_id, binding: decision.binding,
|
|
81
|
+
artifact: { subject_ref: artifact.subject_ref, version: artifact.version, sha256: artifact.sha256 } }) : null;
|
|
82
|
+
if (afterEffect && (!intent || hybridDigest(intent.plan.approval.scope) !== hybridDigest(operation)))
|
|
83
|
+
refuse("original_delivery_scope_required");
|
|
84
|
+
if (afterEffect && !checks)
|
|
85
|
+
historical = prepareDeliveryHistoricalChecks(context, { ...input,
|
|
86
|
+
sourceStage: input.stage === "deploy" ? "prepare" : "verify", operationScopeSha256: hybridDigest(operation) });
|
|
87
|
+
const binding = { flowId, runId: input.runId, stage: input.stage,
|
|
88
|
+
inputSha256: packet.inputSha256, userInputsSha256: selected.userInputsSha256,
|
|
89
|
+
predecessors: packet.input.predecessor ? [{ stage: packet.input.predecessor.stage, ...packet.input.predecessor.result }] : [],
|
|
90
|
+
decision, artifact, deliveryScopeSha256: operation ? hybridDigest(operation) : null,
|
|
91
|
+
verificationSha256: verification ? hybridDigest(verification.receipt) : historical?.sha256 ?? null, intentSha256: intent?.identity ?? null,
|
|
92
|
+
readbackSha256: readback?.sha256 ?? null, readbackStatus: readback?.receipt.observation.status ?? null,
|
|
93
|
+
rollback: readback?.receipt.observation.rollback ?? null, retainedLimitations: prior?.details?.limitations ?? [] };
|
|
94
|
+
const bytes = prepareDeliveryStageResult(input.bytes, bundle.descriptor.stageBlob(input.stage, "result_schema"), binding);
|
|
95
|
+
const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes });
|
|
96
|
+
policy?.assertCurrent();
|
|
97
|
+
checks?.revalidate();
|
|
98
|
+
verification?.proof.revalidate(context);
|
|
99
|
+
historical?.revalidate();
|
|
100
|
+
const reportRoot = input.stage === "finish" ? path.join(packet.runRoot, bundle.descriptor.stage(input.stage).directory) : "";
|
|
101
|
+
const report = input.stage === "finish" ? prepareStageReport({ run_id: input.runId, stage: "finish", verdict: "completed",
|
|
102
|
+
summary: JSON.parse(bytes.toString("utf8")).summary, delivery: { result: JSON.parse(bytes.toString("utf8")), readback: readback?.receipt,
|
|
103
|
+
verification: historical?.receipt }, semantic: { next_action: "The exact operation was observed; this report grants no new effect or retry." } }) : null;
|
|
104
|
+
const fence = (writer) => {
|
|
105
|
+
selected.assertCustody(writer);
|
|
106
|
+
if (previous && hybridDigest(readAcceptedStageData(writer, { ...scope, stage: stages[index - 1] })) !== hybridDigest(previous))
|
|
107
|
+
refuse("accepted_predecessor_changed");
|
|
108
|
+
checks?.assertCustody(writer);
|
|
109
|
+
verification?.proof.assertCustody(writer);
|
|
110
|
+
historical?.assertCustody(writer);
|
|
111
|
+
if (intent && readDeliveryIntent(writer, scope)?.identity !== intent.identity || readback && readDeliveryResult(writer, scope).sha256 !== readback.sha256)
|
|
112
|
+
refuse("delivery_readback_changed");
|
|
113
|
+
if (verification) {
|
|
114
|
+
const payload = { run_id: input.runId, stage: input.stage, sha256: hybridDigest(verification.receipt), receipt: verification.receipt };
|
|
115
|
+
const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.stage_checks' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, input.stage]);
|
|
116
|
+
if (rows.length > 1 || rows.length && hybridDigest(JSON.parse(rows[0].payload_json)) !== hybridDigest(payload))
|
|
117
|
+
refuse("accepted_verification_changed");
|
|
118
|
+
if (!rows.length)
|
|
119
|
+
appendAudit(writer, { projectId: scope.projectId, eventType: "delivery.stage_checks", payload });
|
|
120
|
+
}
|
|
121
|
+
if (report) {
|
|
122
|
+
const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='finish' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, packet.attempt]);
|
|
123
|
+
if (rows.length > 1 || rows.length && JSON.parse(rows[0].payload_json).sha256 !== report.sha256)
|
|
124
|
+
refuse("terminal_report_changed");
|
|
125
|
+
if (!rows.length)
|
|
126
|
+
acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const accepted = settlement.apply(context, fence);
|
|
130
|
+
if (!report)
|
|
131
|
+
return accepted;
|
|
132
|
+
const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
|
|
133
|
+
publishStageReport(context, scope.projectId, input.runId, "finish", reportRoot);
|
|
134
|
+
return { ...accepted, ...complete, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
|
|
135
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
6
|
+
function refuse(reason, parameter, message = "Delivery Stage requires its accepted selection and observed operation evidence") {
|
|
7
|
+
throw new AppError("delivery_stage_result_invalid", message, 2, { reason, ...(parameter ? { parameter } : {}), effect: "no_effect", phase: "prepare", recoverable: true });
|
|
8
|
+
}
|
|
9
|
+
/** Projection only. Bindings must be independently read from accepted Stage,
|
|
10
|
+
* check, approval, intent and readback owners, not copied from a model result.
|
|
11
|
+
* This validator does not authorize execution or prove semantic sufficiency. */
|
|
12
|
+
export function prepareDeliveryStageResult(bytes, schemaBytes, binding) {
|
|
13
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
|
|
14
|
+
refuse("result_exceeds_bound");
|
|
15
|
+
let proposal, validate;
|
|
16
|
+
try {
|
|
17
|
+
proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
18
|
+
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
|
|
19
|
+
if (schema.$id !== "dd-flow/delivery-stage-result@1")
|
|
20
|
+
refuse("frozen_schema_invalid");
|
|
21
|
+
validate = new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof AppError)
|
|
25
|
+
throw error;
|
|
26
|
+
return refuse("invalid_schema");
|
|
27
|
+
}
|
|
28
|
+
if (!proposal || typeof proposal !== "object" || Array.isArray(proposal) || !proposal.details
|
|
29
|
+
|| typeof proposal.details !== "object" || Array.isArray(proposal.details))
|
|
30
|
+
refuse("invalid_schema");
|
|
31
|
+
const stages = deliveryFlowStages[binding.flowId], index = stages.indexOf(binding.stage);
|
|
32
|
+
if (index < 0 || binding.predecessors.length !== (index ? 1 : 0)
|
|
33
|
+
|| index && binding.predecessors[0]?.stage !== stages[index - 1])
|
|
34
|
+
refuse("foreign_stage_predecessor");
|
|
35
|
+
const owned = { schema_id: "dd-flow/delivery-stage-result@1", flow_id: binding.flowId, run_id: binding.runId,
|
|
36
|
+
stage: binding.stage, input_sha256: binding.inputSha256, user_inputs_sha256: binding.userInputsSha256, predecessors: binding.predecessors };
|
|
37
|
+
const details = proposal.details;
|
|
38
|
+
const proof = { decision: binding.decision, artifact: binding.artifact, delivery_scope_sha256: binding.deliveryScopeSha256,
|
|
39
|
+
intent_sha256: binding.intentSha256, readback_sha256: binding.readbackSha256, verification_sha256: binding.verificationSha256,
|
|
40
|
+
rollback: binding.rollback };
|
|
41
|
+
for (const [target, values] of [[proposal, owned], [details, proof]]) {
|
|
42
|
+
for (const [key, value] of Object.entries(values)) {
|
|
43
|
+
if (Object.hasOwn(target, key) && hybridDigest(target[key]) !== hybridDigest(value)) {
|
|
44
|
+
const parameter = target === proposal ? key : `details.${key}`;
|
|
45
|
+
refuse("unowned_stage_claim", parameter, value === null
|
|
46
|
+
? `${binding.stage} has no accepted ${parameter}. Omit this CLI-owned field or set it to null; do not advance selection or operation evidence into this Stage.`
|
|
47
|
+
: `${parameter} differs from the CLI-owned Stage evidence. Use the current Stage and project-policy values, or omit a projected field; do not invent or copy a different binding.`);
|
|
48
|
+
}
|
|
49
|
+
target[key] = structuredClone(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(details.limitations) || details.limitations.some(value => typeof value !== "string" || !value.trim()))
|
|
53
|
+
refuse("invalid_schema");
|
|
54
|
+
details.limitations = [...new Set([...binding.retainedLimitations, ...details.limitations])];
|
|
55
|
+
if (!validate(proposal) || typeof proposal.summary !== "string" || !proposal.summary.trim())
|
|
56
|
+
refuse("invalid_schema");
|
|
57
|
+
if (binding.stage === "preflight") {
|
|
58
|
+
if (Object.values(proof).some(value => value !== null) || binding.readbackStatus !== null)
|
|
59
|
+
refuse("preflight_cannot_grant_delivery");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const decision = binding.decision;
|
|
63
|
+
if (!decision)
|
|
64
|
+
refuse("accepted_decision_required");
|
|
65
|
+
const normalized = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [decision.binding] }, decision.binding.selector);
|
|
66
|
+
if (normalized.selector.operation !== binding.flowId)
|
|
67
|
+
refuse("foreign_delivery_decision");
|
|
68
|
+
if (binding.flowId === "deploy" ? !decision.rollback : decision.rollback !== undefined)
|
|
69
|
+
refuse("deployment_rollback_plan_required");
|
|
70
|
+
if (decision.rollback && (!decision.rollback.reason.trim()
|
|
71
|
+
|| decision.rollback.strategy !== "operator_handoff" && !decision.rollback.procedure_ref?.trim()))
|
|
72
|
+
refuse("deployment_rollback_plan_required");
|
|
73
|
+
if (index >= 2) {
|
|
74
|
+
const artifact = binding.artifact;
|
|
75
|
+
if (!artifact || artifact.subject_ref !== normalized.selector.subject_ref || artifact.version !== decision.version
|
|
76
|
+
|| !binding.deliveryScopeSha256 || !binding.verificationSha256)
|
|
77
|
+
refuse("verified_artifact_required");
|
|
78
|
+
}
|
|
79
|
+
else if (binding.artifact !== null || binding.deliveryScopeSha256 !== null || binding.verificationSha256 !== null)
|
|
80
|
+
refuse("premature_artifact_claim");
|
|
81
|
+
const mutationIndex = stages.indexOf(binding.flowId === "deploy" ? "deploy" : "publish");
|
|
82
|
+
if (index >= mutationIndex) {
|
|
83
|
+
if (!binding.intentSha256 || !binding.readbackSha256 || binding.readbackStatus !== "complete")
|
|
84
|
+
refuse("completed_delivery_readback_required");
|
|
85
|
+
if (binding.flowId === "deploy" && binding.rollback?.status !== "not_needed")
|
|
86
|
+
refuse("deployment_recovery_unresolved");
|
|
87
|
+
}
|
|
88
|
+
else if (binding.intentSha256 !== null || binding.readbackSha256 !== null || binding.readbackStatus !== null)
|
|
89
|
+
refuse("premature_delivery_claim");
|
|
90
|
+
if ((binding.flowId !== "deploy" || index < mutationIndex) && binding.rollback !== null)
|
|
91
|
+
refuse("premature_rollback_claim");
|
|
92
|
+
}
|
|
93
|
+
return Buffer.from(JSON.stringify(proposal));
|
|
94
|
+
}
|
|
@@ -4,6 +4,7 @@ import { readDescriptorStageInput, readDescriptorStageInputForRecovery } from ".
|
|
|
4
4
|
import { runtimeHarness } from "./execution-policy.js";
|
|
5
5
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
import { hasAssignedCoordinatorLaunch } from "./assigned-lifecycle.js";
|
|
7
8
|
/** SQL-only physical coordinator proof, independent of allocation/prompt
|
|
8
9
|
* custody. It creates no Session, hook claim or provider operation.
|
|
9
10
|
*/
|
|
@@ -20,7 +21,8 @@ export function readDescriptorCoordinatorSession(context, input, refuse = reason
|
|
|
20
21
|
|| session.result_path !== path.join(selected.runRoot, directory, "result-input.json"))
|
|
21
22
|
refuse("coordinator_packet_link_changed");
|
|
22
23
|
if (session.harness !== runtimeHarness(route.profile)
|
|
23
|
-
|| !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id])
|
|
24
|
+
|| !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id])
|
|
25
|
+
&& !hasAssignedCoordinatorLaunch(context, { projectId: scope.projectId, runId: scope.runId, stage: scope.stage, workSessionId: session.id, sessionId: session.session_id }))
|
|
24
26
|
refuse("coordinator_start_custody_missing");
|
|
25
27
|
const bindings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='stage.coordinator_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, session.id]);
|
|
26
28
|
if (!bindings.length || bindings.length > 32 || bindings.some(row => Buffer.byteLength(row.payload_json) > 32768))
|
|
@@ -20,8 +20,12 @@ import { allocateDescriptorRootWork, readDescriptorRootWork } from "./descriptor
|
|
|
20
20
|
import { nextRunId } from "./ids.js";
|
|
21
21
|
import { readMemoryInitRunAllocation } from "./memory-init-run-allocation.js";
|
|
22
22
|
import { readMemoryUpgradeEvidence, readMemoryUpgradeTargetEvidence, upgradeEvidenceFilePath, upgradeEvidenceIndexPath, upgradeTargetIndexPath } from "./memory-upgrade-evidence.js";
|
|
23
|
-
import {
|
|
23
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
24
24
|
import { readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
|
|
25
|
+
import { assertMemoryFixRequestBundle } from "./memory-fix-request.js";
|
|
26
|
+
import { assertInteractiveRequestBundle, revalidateInteractiveEntryWorkspace } from "./interactive-request.js";
|
|
27
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
28
|
+
import { assertDeliveryRequestBundle } from "./delivery-request.js";
|
|
25
29
|
const encode = (value) => Buffer.from(JSON.stringify(value));
|
|
26
30
|
const owner = (directory) => {
|
|
27
31
|
const stat = fs.lstatSync(directory);
|
|
@@ -76,6 +80,8 @@ function assertRunIdentity(context, projectId, bundle, fresh) {
|
|
|
76
80
|
}
|
|
77
81
|
else if (fresh && nextRunId(context, projectId, parseFullEntityId(bundle.manifest.run_id).slug) !== bundle.manifest.run_id)
|
|
78
82
|
refuseRunStart("run_id_changed");
|
|
83
|
+
if (fresh && (["mb-fix", "interactive"].includes(bundle.manifest.flow_id) || isDeliveryFlow(bundle.manifest.flow_id)))
|
|
84
|
+
memoryBootstrap(context, projectId, bundle);
|
|
79
85
|
}
|
|
80
86
|
function compatibleSdlcProfile(bundle, profile, home) {
|
|
81
87
|
if (!profile?.settings.execution || !profile.agent_profiles || profile.settings.stage_session_mode !== bundle.execution.settings.stage_session_mode)
|
|
@@ -92,8 +98,20 @@ function compatibleSdlcProfile(bundle, profile, home) {
|
|
|
92
98
|
function memoryBootstrap(context, projectId, bundle) {
|
|
93
99
|
if (bundle.descriptor.descriptor.id === "sdlc")
|
|
94
100
|
return;
|
|
101
|
+
if (bundle.manifest.flow_id === "mb-fix") {
|
|
102
|
+
assertMemoryFixRequestBundle(context, projectId, bundle);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (bundle.manifest.flow_id === "interactive") {
|
|
106
|
+
assertInteractiveRequestBundle(context, projectId, bundle);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (isDeliveryFlow(bundle.manifest.flow_id)) {
|
|
110
|
+
assertDeliveryRequestBundle(context, projectId, bundle);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
95
113
|
const scope = bundle.execution.snapshot().scope;
|
|
96
|
-
if (
|
|
114
|
+
if (isSourceSnapshotFlow(bundle.manifest.flow_id)) {
|
|
97
115
|
const snapshot = readDiagnosticSnapshotCustody(context, { projectId, projectRoot: scope.projectRoot,
|
|
98
116
|
sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
|
|
99
117
|
if (!snapshot || snapshot.root !== scope.workspace || bundle.manifest.bootstrap_receipts.length !== 0)
|
|
@@ -124,7 +142,7 @@ function memoryBootstrap(context, projectId, bundle) {
|
|
|
124
142
|
}
|
|
125
143
|
function observeRunWorkspace(context, bundle, projectId) {
|
|
126
144
|
const scope = bundle.execution.snapshot().scope;
|
|
127
|
-
if (!
|
|
145
|
+
if (!isSourceSnapshotFlow(bundle.manifest.flow_id))
|
|
128
146
|
return observeDescriptorWorkspace(context, scope.projectRoot, scope.workspace);
|
|
129
147
|
const snapshot = readDiagnosticSnapshot(context, { projectId, projectRoot: scope.projectRoot,
|
|
130
148
|
sourceRoot: scope.projectRoot, requestId: scope.requestId, flowId: bundle.manifest.flow_id });
|
|
@@ -187,7 +205,7 @@ export class PreparedDescriptorRunStart {
|
|
|
187
205
|
{ path: "engine-binding.json", base64: bundle.blob(bundle.manifest.engine_binding.path).toString("base64") }
|
|
188
206
|
];
|
|
189
207
|
const isSdlc = bundle.descriptor.descriptor.id === "sdlc", flowKind = isSdlc ? "vnext_specify" : bundle.descriptor.descriptor.id;
|
|
190
|
-
if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind) && !
|
|
208
|
+
if (!["vnext_specify", "mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(flowKind) && !isSourceSnapshotFlow(flowKind) && !isDeliveryFlow(flowKind))
|
|
191
209
|
refuseRunStart("unsupported_flow_kind");
|
|
192
210
|
if (flowKind === "mb-upgrade") {
|
|
193
211
|
const upgradeScope = { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace };
|
|
@@ -268,6 +286,8 @@ export class PreparedDescriptorRunStart {
|
|
|
268
286
|
}
|
|
269
287
|
revalidatePhysical(context) {
|
|
270
288
|
this.revalidateDirectoryOwners(context);
|
|
289
|
+
if (this.#bundle.manifest.flow_id === "interactive")
|
|
290
|
+
revalidateInteractiveEntryWorkspace(context, this.#bundle);
|
|
271
291
|
const p = this.#payload;
|
|
272
292
|
if (hybridDigest(observeRunWorkspace(context, this.#bundle, p.project_id)) !== hybridDigest(p.git))
|
|
273
293
|
refuseRunStart("physical_scope_changed");
|