@deksden-com/dd-flow-cli 0.9.0-beta.151 → 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 +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- 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 +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- 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 -4
- 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 +9 -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 +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { interactiveStages } from "../domain/interactive-flow.js";
|
|
3
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { readInteractiveMutationScope } from "./interactive-mutation-scope.js";
|
|
7
|
+
import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
8
|
+
import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
|
|
9
|
+
function refuse(reason) {
|
|
10
|
+
throw new AppError("interactive_stage_invalid", "Interactive Stage requires its own RUN, delivered inputs and retained scope", 2, { reason, effect: "no_effect" });
|
|
11
|
+
}
|
|
12
|
+
/** Internal entry only, not public availability or Work/Git authorization.
|
|
13
|
+
* Generic runtime remains the sole owner of attempts, root Work and Session. */
|
|
14
|
+
export function readInteractiveStageScope(context, input, historical = false) {
|
|
15
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
|
|
16
|
+
if (!bundle || bundle.manifest.flow_id !== "interactive" || !interactiveStages.includes(input.stage))
|
|
17
|
+
refuse("interactive_stage_required");
|
|
18
|
+
bundle.descriptor.stage(input.stage);
|
|
19
|
+
const scope = { projectId: project.id, runId: input.runId, stage: input.stage }, run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
20
|
+
const index = JSON.parse(run.index_json);
|
|
21
|
+
const replay = Boolean(historical && index.stage_runs?.some((item) => item.stage === input.stage && item.status === "done"));
|
|
22
|
+
const selected = readInteractiveMutationScope(context, scope, bundle);
|
|
23
|
+
if (!replay && !["preflight", "interactive"].includes(input.stage) && !selected.inputs.finish_requested)
|
|
24
|
+
refuse("explicit_finish_required");
|
|
25
|
+
return { bundle, scope, run, replay, selected,
|
|
26
|
+
/** SQL/data only. A turn, HITL answer or scope decision racing entry cannot
|
|
27
|
+
* silently become a different input to an already prepared coordinator. */
|
|
28
|
+
assertCustody(writer) {
|
|
29
|
+
const current = readInteractiveMutationScope(writer, scope, bundle);
|
|
30
|
+
if (current.revision !== selected.revision || current.inputs.sha256 !== selected.inputs.sha256
|
|
31
|
+
|| current.baseline.sha256 !== selected.baseline.sha256)
|
|
32
|
+
refuse("interactive_stage_inputs_changed");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function startInteractiveStage(context, input) {
|
|
37
|
+
const selected = readInteractiveStageScope(context, input, true);
|
|
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 };
|
|
41
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { appendAudit } from "./audit.js";
|
|
5
|
+
import { readInteractiveStageScope, startInteractiveStage } from "./interactive-stage-entry.js";
|
|
6
|
+
import { interactiveStages } from "../domain/interactive-flow.js";
|
|
7
|
+
import { PreparedInteractiveStageObservation } from "./interactive-stage-observation.js";
|
|
8
|
+
import { PreparedInteractiveChecks } from "./interactive-checks.js";
|
|
9
|
+
import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
10
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
11
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
12
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
13
|
+
import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
|
|
14
|
+
import { PreparedRetainedCheckEvidence } from "./code-checks.js";
|
|
15
|
+
import { prepareInteractiveStageResult, assertInteractiveStageContinuation } from "./interactive-stage-results.js";
|
|
16
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
17
|
+
import { readInteractiveWorkHistory } from "./interactive-work-history.js";
|
|
18
|
+
import { registerInteractiveStageGraph, readRegisteredInteractiveWork } from "./interactive-work-registration.js";
|
|
19
|
+
import { readInteractiveWorkGit } from "./interactive-work-start.js";
|
|
20
|
+
import { PreparedInteractiveWorkCollection } from "./interactive-work-collection.js";
|
|
21
|
+
import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
|
|
22
|
+
import { prepareInteractiveFinishIntegration, prepareInteractiveIntegrationCustody } from "./interactive-git-dispatch.js";
|
|
23
|
+
const event = "interactive.stage_acceptance";
|
|
24
|
+
export const interactiveStageHandlers = Object.freeze(interactiveStages.map(stage => Object.freeze({ flowId: "interactive", stage,
|
|
25
|
+
entryId: "interactive-stage-entry", finishId: "interactive-stage-finish", entry: startInteractiveStage, finish: finishInteractiveStage })));
|
|
26
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
function refuse(reason) {
|
|
28
|
+
throw new AppError("interactive_stage_invalid", "Interactive completion requires actual reviewed work and owned checks", 2, { reason, effect: "no_effect" });
|
|
29
|
+
}
|
|
30
|
+
function receipt(context, scope) {
|
|
31
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage]);
|
|
32
|
+
if (rows.length > 1 || rows.some(row => Buffer.byteLength(row.payload_json) > 2 * 1024 * 1024))
|
|
33
|
+
refuse("ambiguous_stage_acceptance");
|
|
34
|
+
return rows[0] ? JSON.parse(rows[0].payload_json) : null;
|
|
35
|
+
}
|
|
36
|
+
function assertRetainedPlan(context, scope, proof) {
|
|
37
|
+
if (!proof.check_plan_sha256)
|
|
38
|
+
return;
|
|
39
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='interactive.check_plan' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.sha256')=? LIMIT 2", [scope.projectId, scope.runId, proof.check_plan_sha256]);
|
|
40
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
41
|
+
refuse("accepted_check_plan_required");
|
|
42
|
+
const value = JSON.parse(rows[0].payload_json), plan = value?.plan, binding = proof.binding;
|
|
43
|
+
if (value.sha256 !== proof.check_plan_sha256 || hybridDigest(plan) !== value.sha256 || plan.run_id !== scope.runId || plan.stage !== scope.stage
|
|
44
|
+
|| plan.bundle_sha256 !== proof.bundle_sha256 || plan.input_sha256 !== binding.inputSha256
|
|
45
|
+
|| plan.user_inputs_sha256 !== binding.userInputsSha256 || plan.scope_revision !== binding.scopeRevision
|
|
46
|
+
|| plan.file_changes_sha256 !== binding.fileChangesSha256 || plan.no_checks_reason !== proof.no_checks_reason
|
|
47
|
+
|| !Array.isArray(plan.checks) || Boolean(plan.checks.length) !== Boolean(proof.checks.length))
|
|
48
|
+
refuse("accepted_check_plan_changed");
|
|
49
|
+
}
|
|
50
|
+
/** Historical replay uses accepted SQL/result/check custody, never today's
|
|
51
|
+
* workspace or today's scope to manufacture a replacement successful result. */
|
|
52
|
+
function acceptedStage(context, input, selected) {
|
|
53
|
+
const { scope, bundle } = selected, accepted = readAcceptedStageData(context, scope), proof = receipt(context, scope);
|
|
54
|
+
const value = JSON.parse(accepted.bytes.toString("utf8"));
|
|
55
|
+
if (!proof || proof.schema_id !== "dd-flow/interactive-stage-acceptance@1" || proof.run_id !== input.runId || proof.stage !== input.stage
|
|
56
|
+
|| proof.bundle_sha256 !== bundle.sha256 || proof.result_sha256 !== accepted.result.sha256 || accepted.data_schema_id !== "dd-flow/interactive-stage-result@1"
|
|
57
|
+
|| proof.binding.runId !== input.runId || proof.binding.stage !== input.stage || proof.binding.inputSha256 !== accepted.input_sha256
|
|
58
|
+
|| value.details.review?.verdict === "revise" || !Array.isArray(proof.checks)
|
|
59
|
+
|| (["hardening", "review"].includes(input.stage)
|
|
60
|
+
? !/^[a-f0-9]{64}$/.test(proof.check_plan_sha256 ?? "") || !proof.checks.length && !proof.no_checks_reason?.trim()
|
|
61
|
+
: proof.check_plan_sha256 !== null || proof.checks.length !== 0 || proof.no_checks_reason !== null))
|
|
62
|
+
refuse("accepted_stage_proof_required");
|
|
63
|
+
const prepared = prepareInteractiveStageResult(accepted.bytes, bundle.descriptor.stageBlob(input.stage, "result_schema"), proof.binding);
|
|
64
|
+
if (!prepared.bytes.equals(accepted.bytes))
|
|
65
|
+
refuse("accepted_stage_result_changed");
|
|
66
|
+
const checks = proof.checks.length ? PreparedRetainedCheckEvidence.prepare(context, { projectId: scope.projectId, runId: input.runId, references: proof.checks }) : null;
|
|
67
|
+
const integration = input.stage === "integration" ? prepareInteractiveIntegrationCustody(context, input) : null;
|
|
68
|
+
if (integration ? !proof.integration?.binding || hybridDigest(proof.integration.binding) !== hybridDigest(integration.binding)
|
|
69
|
+
|| proof.integration.observation?.schema_id !== "dd-flow/interactive-finish-integration@1"
|
|
70
|
+
|| proof.integration.observation.operation_sha256 !== integration.binding.operation_sha256
|
|
71
|
+
|| proof.integration.observation.no_op !== integration.reviewed.empty
|
|
72
|
+
|| Boolean(proof.integration.observation.git) === integration.reviewed.empty
|
|
73
|
+
: proof.integration !== undefined)
|
|
74
|
+
refuse("accepted_integration_proof_required");
|
|
75
|
+
const assertProof = (writer) => {
|
|
76
|
+
if (hybridDigest(receipt(writer, scope)) !== hybridDigest(proof)
|
|
77
|
+
|| hybridDigest(readAcceptedStageData(writer, scope)) !== hybridDigest(accepted))
|
|
78
|
+
refuse("accepted_stage_proof_changed");
|
|
79
|
+
assertRetainedPlan(writer, scope, proof);
|
|
80
|
+
checks?.assertCustody(writer);
|
|
81
|
+
integration?.assertCustody(writer);
|
|
82
|
+
const collection = proof.collection;
|
|
83
|
+
if (collection) {
|
|
84
|
+
if (hybridDigest(collection) !== proof.binding.workCollectionSha256 || collection.run_id !== input.runId || collection.stage !== input.stage
|
|
85
|
+
|| collection.bundle_sha256 !== bundle.sha256 || collection.input_sha256 !== accepted.input_sha256)
|
|
86
|
+
refuse("accepted_work_collection_changed");
|
|
87
|
+
const work = readCompletedInteractiveWork(writer, { ...scope, workId: collection.work_id }, bundle);
|
|
88
|
+
if (hybridDigest(work.receipt) !== collection.settlement_sha256 || work.receipt.result_sha256 !== collection.result_sha256
|
|
89
|
+
|| hybridDigest(work.start.assignment.receipt) !== collection.registration_sha256
|
|
90
|
+
|| hybridDigest(work.start.receipt.actor) !== hybridDigest(collection.actor))
|
|
91
|
+
refuse("accepted_work_collection_changed");
|
|
92
|
+
}
|
|
93
|
+
else if (proof.binding.workCollectionSha256 !== null)
|
|
94
|
+
refuse("accepted_work_collection_changed");
|
|
95
|
+
};
|
|
96
|
+
context.db.readSnapshot(() => assertProof(context));
|
|
97
|
+
return { accepted, proof, result: prepared.result, assertCustody: assertProof };
|
|
98
|
+
}
|
|
99
|
+
/** Historical semantic and physical Stage custody for terminal integration.
|
|
100
|
+
* Reading an accepted proposal alone is not proof of coordinator settlement.
|
|
101
|
+
* Lifecycle keys are prepared outside the SQL writer; no current source capture
|
|
102
|
+
* or new Stage/Work allocation occurs here. */
|
|
103
|
+
export function readAcceptedInteractiveStage(context, input) {
|
|
104
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
105
|
+
refuse("physical_preparation_outside_writer_required");
|
|
106
|
+
const selected = readInteractiveStageScope(context, input, true);
|
|
107
|
+
if (!selected.replay)
|
|
108
|
+
refuse("accepted_stage_required");
|
|
109
|
+
const retained = acceptedStage(context, input, selected);
|
|
110
|
+
const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
|
|
111
|
+
const settlement = readAcceptedDescriptorStageSettlement(context, selected.scope, selected.bundle, matchKeys);
|
|
112
|
+
const assertCustody = (writer) => {
|
|
113
|
+
retained.assertCustody(writer);
|
|
114
|
+
if (hybridDigest(readAcceptedDescriptorStageSettlement(writer, selected.scope, selected.bundle, matchKeys)) !== hybridDigest(settlement))
|
|
115
|
+
refuse("accepted_stage_settlement_changed");
|
|
116
|
+
};
|
|
117
|
+
context.db.readSnapshot(() => assertCustody(context));
|
|
118
|
+
return { result: structuredClone(retained.result), proof: structuredClone(retained.proof),
|
|
119
|
+
bytes: Buffer.from(retained.accepted.bytes), reference: structuredClone(retained.accepted.result), assertCustody };
|
|
120
|
+
}
|
|
121
|
+
function prepareReplay(context, input, selected) {
|
|
122
|
+
const retained = acceptedStage(context, input, selected);
|
|
123
|
+
const prepared = prepareInteractiveStageResult(input.bytes, selected.bundle.descriptor.stageBlob(input.stage, "result_schema"), retained.proof.binding);
|
|
124
|
+
if (!prepared.bytes.equals(retained.accepted.bytes))
|
|
125
|
+
refuse("accepted_stage_result_changed");
|
|
126
|
+
return { settlement: PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes }),
|
|
127
|
+
resultSha256: retained.proof.result_sha256, assertCustody: retained.assertCustody };
|
|
128
|
+
}
|
|
129
|
+
function retainAcceptance(context, scope, proof) {
|
|
130
|
+
if (Buffer.byteLength(JSON.stringify(proof)) > 2 * 1024 * 1024)
|
|
131
|
+
refuse("unbounded_stage_acceptance");
|
|
132
|
+
const prior = receipt(context, scope);
|
|
133
|
+
if (prior) {
|
|
134
|
+
if (hybridDigest(prior) !== hybridDigest(proof))
|
|
135
|
+
refuse("accepted_stage_proof_changed");
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
appendAudit(context, { projectId: scope.projectId, eventType: event, payload: proof });
|
|
139
|
+
}
|
|
140
|
+
function pendingAcceptedIntegration(context, input, resultSha256, cause) {
|
|
141
|
+
let committed = false;
|
|
142
|
+
try {
|
|
143
|
+
const selected = readInteractiveStageScope(context, input, true);
|
|
144
|
+
committed = selected.replay && receipt(context, selected.scope)?.result_sha256 === resultSha256
|
|
145
|
+
&& readAcceptedStageData(context, selected.scope).result.sha256 === resultSha256;
|
|
146
|
+
}
|
|
147
|
+
catch { /* No independently retained terminal acceptance. */ }
|
|
148
|
+
if (!committed)
|
|
149
|
+
throw cause;
|
|
150
|
+
throw new AppError("interactive_finish_pending", "Accepted integration requires projection or RUN settlement reconciliation", 1, { effect: "committed", recoverable: true, run_id: input.runId, next_action: "reconcile_accepted_integration",
|
|
151
|
+
cause: cause instanceof AppError ? cause.code : "finish_settlement_failed" });
|
|
152
|
+
}
|
|
153
|
+
/** Git effects are issued separately by the scoped operation owner. This
|
|
154
|
+
* handler consumes observed integration, never an agent's commit claim. A
|
|
155
|
+
* committed Stage can be followed by an interrupted RUN closure; replay seals
|
|
156
|
+
* that original result without revisiting today's mutable workspace. */
|
|
157
|
+
function finishIntegration(context, input) {
|
|
158
|
+
const selected = readInteractiveStageScope(context, input, true), { scope, bundle } = selected;
|
|
159
|
+
if (selected.replay) {
|
|
160
|
+
const replay = prepareReplay(context, input, selected);
|
|
161
|
+
try {
|
|
162
|
+
const stage = replay.settlement.apply(context, replay.assertCustody), accepted = readAcceptedInteractiveStage(context, input);
|
|
163
|
+
return { ...stage, ...PreparedDescriptorRunSettlement.prepare(context, input).apply(context, accepted.assertCustody) };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return pendingAcceptedIntegration(context, input, replay.resultSha256, error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (receipt(context, scope))
|
|
170
|
+
refuse("orphan_stage_acceptance");
|
|
171
|
+
const review = readAcceptedInteractiveStage(context, { ...input, stage: "review" });
|
|
172
|
+
const git = prepareInteractiveFinishIntegration(context, input);
|
|
173
|
+
const packet = readDescriptorStageInput(context, scope, bundle);
|
|
174
|
+
if (packet.input.predecessor?.stage !== "review" || hybridDigest(packet.input.predecessor.result) !== hybridDigest(review.reference))
|
|
175
|
+
refuse("accepted_review_predecessor_required");
|
|
176
|
+
const original = git.reviewed;
|
|
177
|
+
const binding = { runId: input.runId, stage: "integration", inputSha256: packet.inputSha256,
|
|
178
|
+
baselineSha256: original.baseline_sha256, userInputsSha256: original.user_inputs_sha256, scopeRevision: original.scope_revision,
|
|
179
|
+
fileChangesSha256: original.file_changes_sha256, finishRequested: true, workCollectionSha256: null, review: review.result.details.review,
|
|
180
|
+
retainedLimitations: review.result.details.limitations,
|
|
181
|
+
predecessors: [{ stage: "review", ...review.reference }] };
|
|
182
|
+
const prepared = prepareInteractiveStageResult(input.bytes, bundle.descriptor.stageBlob("integration", "result_schema"), binding);
|
|
183
|
+
assertInteractiveStageContinuation(prepared.result, review.result);
|
|
184
|
+
const proof = { schema_id: "dd-flow/interactive-stage-acceptance@1", run_id: input.runId, stage: "integration", bundle_sha256: bundle.sha256,
|
|
185
|
+
result_sha256: hash(prepared.bytes), binding, collection: null, check_plan_sha256: null, checks: [], no_checks_reason: null,
|
|
186
|
+
integration: { binding: git.binding, observation: git.value() } };
|
|
187
|
+
const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes });
|
|
188
|
+
review.assertCustody(context);
|
|
189
|
+
git.revalidate();
|
|
190
|
+
try {
|
|
191
|
+
const stage = settlement.apply(context, writer => {
|
|
192
|
+
review.assertCustody(writer);
|
|
193
|
+
git.assertCustody(writer);
|
|
194
|
+
retainAcceptance(writer, scope, proof);
|
|
195
|
+
});
|
|
196
|
+
const accepted = readAcceptedInteractiveStage(context, input);
|
|
197
|
+
return { ...stage, ...PreparedDescriptorRunSettlement.prepare(context, input).apply(context, accepted.assertCustody) };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return pendingAcceptedIntegration(context, input, proof.result_sha256, error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Internal handlers. Public admission still requires registered descriptors,
|
|
204
|
+
* controller/command wiring and installed family qualification. */
|
|
205
|
+
export function finishInteractiveStage(context, input) {
|
|
206
|
+
if (input.stage === "integration")
|
|
207
|
+
return finishIntegration(context, input);
|
|
208
|
+
const selected = readInteractiveStageScope(context, input, true), { scope, bundle } = selected;
|
|
209
|
+
if (selected.replay) {
|
|
210
|
+
const replay = prepareReplay(context, input, selected);
|
|
211
|
+
return replay.settlement.apply(context, replay.assertCustody);
|
|
212
|
+
}
|
|
213
|
+
if (receipt(context, scope))
|
|
214
|
+
refuse("orphan_stage_acceptance");
|
|
215
|
+
const observation = PreparedInteractiveStageObservation.prepare(context, input), proposal = observation.prepareResult(input.bytes);
|
|
216
|
+
if (proposal.result.details.review?.verdict === "revise")
|
|
217
|
+
throw new AppError("interactive_review_corrections_required", "Independent review has unresolved work; the Stage remains open", 2, { phase: "prepare", effect: "no_effect", recoverable: true, projection: { outcome: "needs_repair", review: proposal.result.details.review,
|
|
218
|
+
review_work_id: observation.collectionProof()?.work_id, work_collection_sha256: proposal.result.work_collection_sha256 }, continuation: { kind: "correction_required" } });
|
|
219
|
+
const checked = ["hardening", "review"].includes(input.stage) ? observation.prepareCheckedResult(context, input.bytes, PreparedInteractiveChecks.restore(context, { projectRoot: input.projectRoot, runId: input.runId, stage: input.stage })) : null;
|
|
220
|
+
const prepared = checked ?? proposal;
|
|
221
|
+
const proof = { schema_id: "dd-flow/interactive-stage-acceptance@1", run_id: input.runId, stage: input.stage,
|
|
222
|
+
bundle_sha256: bundle.sha256, result_sha256: hash(prepared.bytes), binding: observation.binding(), collection: observation.collectionProof(),
|
|
223
|
+
check_plan_sha256: checked?.check_plan_sha256 ?? null, checks: checked?.checks ?? [], no_checks_reason: checked?.no_checks_reason ?? null };
|
|
224
|
+
const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes: prepared.bytes });
|
|
225
|
+
observation.revalidate(context);
|
|
226
|
+
checked?.revalidate(context);
|
|
227
|
+
return settlement.apply(context, writer => {
|
|
228
|
+
observation.assertCustody(writer);
|
|
229
|
+
checked?.assertCustody(writer);
|
|
230
|
+
retainAcceptance(writer, scope, proof);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** A negative review permits a separate scoped author, not reviewer writes or
|
|
234
|
+
* Stage rewind. Replaying allocation returns its original Work even if active. */
|
|
235
|
+
export function registerInteractiveReviewCorrection(context, input) {
|
|
236
|
+
if (typeof input.reviewWorkId !== "string" || !input.reviewWorkId || input.reviewWorkId.length > 256 || /[\0\r\n]/.test(input.reviewWorkId))
|
|
237
|
+
refuse("review_work_id_required");
|
|
238
|
+
const stage = "review", selected = readInteractiveStageScope(context, { ...input, stage }), { scope, bundle } = selected;
|
|
239
|
+
const packet = readDescriptorStageInput(context, scope, bundle), review = readCompletedInteractiveWork(context, { ...scope, workId: input.reviewWorkId }, bundle);
|
|
240
|
+
if (review.start.assignment.packet.stage !== "review" || review.start.assignment.packet.owner_stage !== "review"
|
|
241
|
+
|| review.start.assignment.receipt.attempt !== packet.attempt || review.result.verdict === "ready")
|
|
242
|
+
refuse("negative_review_required_for_correction");
|
|
243
|
+
const requestId = `correction-${hybridDigest({ work_id: input.reviewWorkId, result_sha256: review.receipt.result_sha256, input_sha256: packet.inputSha256 }).slice(0, 40)}`;
|
|
244
|
+
const args = { projectRoot: input.projectRoot, runId: input.runId, stage, role: "hardening",
|
|
245
|
+
requestId, gitExecutable: readInteractiveWorkGit(bundle).git_executable };
|
|
246
|
+
const prior = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='interactive.work_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='review' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 1", [scope.projectId, input.runId, packet.attempt, requestId]);
|
|
247
|
+
if (prior)
|
|
248
|
+
return registerInteractiveStageGraph(context, args);
|
|
249
|
+
const history = readInteractiveWorkHistory(context, scope, bundle);
|
|
250
|
+
if (history.works.at(-1)?.work_id !== input.reviewWorkId)
|
|
251
|
+
refuse("latest_negative_review_required");
|
|
252
|
+
return registerInteractiveStageGraph(context, args, writer => {
|
|
253
|
+
if (hybridDigest(readInteractiveWorkHistory(writer, scope, bundle)) !== hybridDigest(history))
|
|
254
|
+
refuse("correction_history_changed");
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** Hand off a settled correction and its current check evidence to a fresh
|
|
258
|
+
* read-only critic. An author result never becomes accepted review itself. */
|
|
259
|
+
export function registerInteractiveCorrectionReview(context, input) {
|
|
260
|
+
if (typeof input.correctionWorkId !== "string" || !input.correctionWorkId || input.correctionWorkId.length > 256 || /[\0\r\n]/.test(input.correctionWorkId))
|
|
261
|
+
refuse("correction_work_id_required");
|
|
262
|
+
const stage = "review", { scope, bundle } = readInteractiveStageScope(context, { ...input, stage });
|
|
263
|
+
const packet = readDescriptorStageInput(context, scope, bundle), author = readCompletedInteractiveWork(context, { ...scope, workId: input.correctionWorkId }, bundle);
|
|
264
|
+
if (author.start.assignment.packet.owner_stage !== "review" || author.start.assignment.packet.stage !== "hardening"
|
|
265
|
+
|| author.start.assignment.receipt.attempt !== packet.attempt || author.result.verdict !== "ready")
|
|
266
|
+
refuse("ready_correction_author_required");
|
|
267
|
+
const requestId = `recheck-${hybridDigest({ work_id: input.correctionWorkId, result_sha256: author.receipt.result_sha256, input_sha256: packet.inputSha256 }).slice(0, 40)}`;
|
|
268
|
+
const args = { projectRoot: input.projectRoot, runId: input.runId, stage, requestId, gitExecutable: readInteractiveWorkGit(bundle).git_executable };
|
|
269
|
+
const prior = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='interactive.work_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='review' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.request_id')=? LIMIT 1", [scope.projectId, input.runId, packet.attempt, requestId]);
|
|
270
|
+
if (prior) {
|
|
271
|
+
const registered = registerInteractiveStageGraph(context, args);
|
|
272
|
+
const retained = readRegisteredInteractiveWork(context, scope, bundle, registered.work_ids.review).retained.body;
|
|
273
|
+
const originalAuthor = retained.work_history.works.at(-1);
|
|
274
|
+
if (!retained.stage_checks || originalAuthor?.work_id !== input.correctionWorkId
|
|
275
|
+
|| originalAuthor.result_sha256 !== author.receipt.result_sha256)
|
|
276
|
+
refuse("recheck_registration_changed");
|
|
277
|
+
return registered;
|
|
278
|
+
}
|
|
279
|
+
const collection = PreparedInteractiveWorkCollection.prepareCorrectionAuthor(context, input);
|
|
280
|
+
if (collection.snapshot().work_id !== input.correctionWorkId)
|
|
281
|
+
refuse("latest_correction_author_required");
|
|
282
|
+
const checks = PreparedInteractiveChecks.restore(context, { ...input, stage }), evidence = checks.evidence(context);
|
|
283
|
+
collection.revalidate(context);
|
|
284
|
+
return registerInteractiveStageGraph(context, { ...args, checks }, writer => {
|
|
285
|
+
collection.assertCustody(writer);
|
|
286
|
+
checks.assertCustody(writer);
|
|
287
|
+
evidence?.assertCustody(writer);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { interactiveStages } from "../domain/interactive-flow.js";
|
|
3
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { readInteractiveStageScope } from "./interactive-stage-entry.js";
|
|
6
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
7
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
8
|
+
import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
|
|
9
|
+
import { readInteractiveWorkGit } from "./interactive-work-start.js";
|
|
10
|
+
import { PreparedInteractiveWorkCollection } from "./interactive-work-collection.js";
|
|
11
|
+
import { readInteractiveWorkHistory } from "./interactive-work-history.js";
|
|
12
|
+
import { pendingInteractiveIssues } from "./interactive-work-results.js";
|
|
13
|
+
import { prepareInteractiveStageResult, assertInteractiveStageContinuation } from "./interactive-stage-results.js";
|
|
14
|
+
import { PreparedInteractiveChecks } from "./interactive-checks.js";
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("interactive_stage_observation_invalid", "Interactive result requires the current task, actual patch and independently settled Work", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Actual task/patch/Work observation for Stage handlers. This grants neither
|
|
19
|
+
* successful command checks nor Git integration or Stage/RUN acceptance. */
|
|
20
|
+
export class PreparedInteractiveStageObservation {
|
|
21
|
+
#selected;
|
|
22
|
+
#packet;
|
|
23
|
+
#previous;
|
|
24
|
+
#prior;
|
|
25
|
+
#changes;
|
|
26
|
+
#history;
|
|
27
|
+
#collection;
|
|
28
|
+
#binding;
|
|
29
|
+
constructor(selected, packet, previous, changes, history, collection, binding) {
|
|
30
|
+
this.#selected = selected;
|
|
31
|
+
this.#packet = packet;
|
|
32
|
+
this.#previous = previous;
|
|
33
|
+
this.#prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
|
|
34
|
+
this.#changes = changes;
|
|
35
|
+
this.#history = history;
|
|
36
|
+
this.#collection = collection;
|
|
37
|
+
this.#binding = binding;
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
static prepare(context, input) {
|
|
41
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
42
|
+
refuse("preparation_outside_writer_required");
|
|
43
|
+
// Integration must observe accepted reviewed bytes through the Git owner,
|
|
44
|
+
// not reuse a pre-commit workspace observation as integration evidence.
|
|
45
|
+
if (input.stage === "integration")
|
|
46
|
+
refuse("integration_observation_required");
|
|
47
|
+
const selected = readInteractiveStageScope(context, input), { scope, bundle, run } = selected, stage = input.stage;
|
|
48
|
+
const packet = readDescriptorStageInput(context, scope, bundle), index = JSON.parse(run.index_json);
|
|
49
|
+
if (run.status !== "running" || index.current_stage !== stage
|
|
50
|
+
|| !index.stage_runs?.some((item) => item.stage === stage && item.attempt === packet.attempt && item.status === "running")
|
|
51
|
+
|| selected.selected.needs_scope_refresh)
|
|
52
|
+
refuse("active_current_scope_required");
|
|
53
|
+
const previousStage = interactiveStages[interactiveStages.indexOf(stage) - 1];
|
|
54
|
+
const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
|
|
55
|
+
if (previous && previous.data_schema_id !== "dd-flow/interactive-stage-result@1")
|
|
56
|
+
refuse("accepted_predecessor_required");
|
|
57
|
+
if (previous ? packet.input.predecessor?.stage !== previousStage || hybridDigest(packet.input.predecessor.result) !== hybridDigest(previous.result)
|
|
58
|
+
: packet.input.predecessor != null)
|
|
59
|
+
refuse("accepted_predecessor_required");
|
|
60
|
+
const frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
|
|
61
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot,
|
|
62
|
+
workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
|
|
63
|
+
const patch = changes.snapshot(), workStage = stage === "hardening" || stage === "review";
|
|
64
|
+
const history = workStage ? readInteractiveWorkHistory(context, scope, bundle) : null;
|
|
65
|
+
const registered = history?.works.some(work => work.owner_stage === stage && work.attempt === packet.attempt);
|
|
66
|
+
const collection = workStage && (stage === "review" || patch.delta.changes.length || registered)
|
|
67
|
+
? PreparedInteractiveWorkCollection.prepare(context, { ...input, stage }) : null;
|
|
68
|
+
const work = collection?.results()[0]?.result;
|
|
69
|
+
const pending = history ? pendingInteractiveIssues(history, { file_changes_sha256: hybridDigest(patch.delta.changes),
|
|
70
|
+
user_inputs_sha256: selected.selected.inputs.sha256, scope_revision: selected.selected.revision }) : [];
|
|
71
|
+
const review = stage === "review" && work ? { verdict: work.verdict === "ready" && !pending.some(issue => issue.severity !== "minor")
|
|
72
|
+
? "accept" : "revise", summary: work.summary } : null;
|
|
73
|
+
const binding = { runId: input.runId, stage, inputSha256: packet.inputSha256,
|
|
74
|
+
baselineSha256: selected.selected.baseline.sha256, userInputsSha256: selected.selected.inputs.sha256,
|
|
75
|
+
scopeRevision: selected.selected.revision, fileChangesSha256: hybridDigest(patch.delta.changes),
|
|
76
|
+
finishRequested: selected.selected.inputs.finish_requested, workCollectionSha256: collection?.sha256 ?? null, review,
|
|
77
|
+
retainedLimitations: work?.limitations ?? [],
|
|
78
|
+
predecessors: previous && previousStage ? [{ stage: previousStage, ...previous.result }] : [] };
|
|
79
|
+
const prepared = new PreparedInteractiveStageObservation(selected, packet, previous, changes, history, collection, binding);
|
|
80
|
+
prepared.revalidate(context);
|
|
81
|
+
return prepared;
|
|
82
|
+
}
|
|
83
|
+
binding() { return structuredClone(this.#binding); }
|
|
84
|
+
collectionProof() { return this.#collection?.snapshot() ?? null; }
|
|
85
|
+
prepareResult(bytes) {
|
|
86
|
+
const verdict = this.#collection?.results()[0]?.result.verdict;
|
|
87
|
+
if (this.#binding.stage === "hardening" && verdict !== undefined && verdict !== "ready")
|
|
88
|
+
refuse("hardening_work_not_ready");
|
|
89
|
+
const prepared = prepareInteractiveStageResult(bytes, this.#selected.bundle.descriptor.stageBlob(this.#binding.stage, "result_schema"), this.#binding);
|
|
90
|
+
// Projection retains the latest Work's limitations; original history stays
|
|
91
|
+
// separate so a later independent Work can reassess earlier limitations.
|
|
92
|
+
if (this.#prior)
|
|
93
|
+
assertInteractiveStageContinuation(prepared.result, this.#prior);
|
|
94
|
+
return prepared;
|
|
95
|
+
}
|
|
96
|
+
/** A handler must consume this joint fence with ordinary Stage settlement.
|
|
97
|
+
* No-check decisions remain explicit, never a fabricated passed receipt. */
|
|
98
|
+
prepareCheckedResult(context, bytes, checks) {
|
|
99
|
+
if (!(checks instanceof PreparedInteractiveChecks) || !["hardening", "review"].includes(this.#binding.stage))
|
|
100
|
+
refuse("owned_stage_checks_required");
|
|
101
|
+
const plan = checks.value(), binding = this.#binding;
|
|
102
|
+
if (plan.run_id !== binding.runId || plan.stage !== binding.stage || plan.bundle_sha256 !== this.#selected.bundle.sha256
|
|
103
|
+
|| plan.input_sha256 !== binding.inputSha256 || plan.user_inputs_sha256 !== binding.userInputsSha256
|
|
104
|
+
|| plan.scope_revision !== binding.scopeRevision || plan.file_changes_sha256 !== binding.fileChangesSha256)
|
|
105
|
+
refuse("foreign_stage_checks");
|
|
106
|
+
this.revalidate(context);
|
|
107
|
+
const evidence = checks.evidence(context), prepared = this.prepareResult(bytes);
|
|
108
|
+
return { ...prepared, check_plan_sha256: checks.sha256, checks: evidence?.retainedReferences(context) ?? [], no_checks_reason: plan.no_checks_reason,
|
|
109
|
+
assertCustody: (writer) => { this.assertCustody(writer); checks.assertCustody(writer); evidence?.assertCustody(writer); },
|
|
110
|
+
revalidate: (reader) => { this.revalidate(reader); checks.revalidate(reader); evidence?.revalidate(reader); } };
|
|
111
|
+
}
|
|
112
|
+
revalidate(context) {
|
|
113
|
+
readInteractiveWorkGit(this.#selected.bundle);
|
|
114
|
+
this.#changes.revalidate();
|
|
115
|
+
this.#collection?.revalidate(context);
|
|
116
|
+
context.db.readSnapshot(() => this.assertSelection(context));
|
|
117
|
+
}
|
|
118
|
+
assertCustody(context) {
|
|
119
|
+
this.#changes.assertCustody(context);
|
|
120
|
+
this.assertSelection(context);
|
|
121
|
+
}
|
|
122
|
+
assertSelection(context) {
|
|
123
|
+
this.#selected.assertCustody(context);
|
|
124
|
+
this.#collection?.assertCustody(context);
|
|
125
|
+
const { scope, bundle } = this.#selected, packet = readDescriptorStageInput(context, scope, bundle);
|
|
126
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
|
|
127
|
+
if (run.status !== "running" || index.current_stage !== scope.stage || !index.stage_runs?.some((item) => item.stage === scope.stage && item.attempt === packet.attempt && item.status === "running"))
|
|
128
|
+
refuse("active_current_scope_required");
|
|
129
|
+
if (hybridDigest(packet) !== hybridDigest(this.#packet))
|
|
130
|
+
refuse("stage_input_changed");
|
|
131
|
+
if (this.#previous && hybridDigest(readAcceptedStageData(context, { ...scope, stage: this.#binding.predecessors[0].stage })) !== hybridDigest(this.#previous))
|
|
132
|
+
refuse("predecessor_changed");
|
|
133
|
+
if (this.#history && hybridDigest(readInteractiveWorkHistory(context, scope, bundle)) !== hybridDigest(this.#history))
|
|
134
|
+
refuse("work_history_changed");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { interactiveStages } from "../domain/interactive-flow.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
function refuse(reason) {
|
|
6
|
+
throw new AppError("interactive_stage_result_invalid", "Interactive Stage requires its actual inputs, scoped changes and independent review", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
|
|
7
|
+
}
|
|
8
|
+
/** Pure proposal validation. A valid JSON result does not prove semantic
|
|
9
|
+
* correctness, physical reviewer independence, successful checks or Git effects.
|
|
10
|
+
* Those bindings come from ordinary Stage/Work/operation owners at settlement. */
|
|
11
|
+
export function validateInteractiveStageResult(bytes, schemaBytes, binding) {
|
|
12
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
|
|
13
|
+
refuse("result_exceeds_bound");
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return refuse("invalid_schema");
|
|
20
|
+
}
|
|
21
|
+
const validate = (() => {
|
|
22
|
+
try {
|
|
23
|
+
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
|
|
24
|
+
if (schema.$id !== "dd-flow/interactive-stage-result@1")
|
|
25
|
+
refuse("frozen_schema_invalid");
|
|
26
|
+
return new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return refuse("frozen_schema_invalid");
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
if (!validate(result) || result.schema_id !== "dd-flow/interactive-stage-result@1" || result.flow_id !== "interactive"
|
|
33
|
+
|| !result.summary.trim() || !result.details.change_summary.trim() || result.details.limitations.some(item => !item.trim())
|
|
34
|
+
|| result.details.review && !result.details.review.summary.trim())
|
|
35
|
+
refuse("invalid_schema");
|
|
36
|
+
if (result.run_id !== binding.runId || result.stage !== binding.stage || result.input_sha256 !== binding.inputSha256
|
|
37
|
+
|| result.baseline_sha256 !== binding.baselineSha256 || result.user_inputs_sha256 !== binding.userInputsSha256
|
|
38
|
+
|| result.scope_revision !== binding.scopeRevision || result.file_changes_sha256 !== binding.fileChangesSha256
|
|
39
|
+
|| hybridDigest(result.predecessors) !== hybridDigest(binding.predecessors))
|
|
40
|
+
refuse("foreign_stage_binding");
|
|
41
|
+
if (result.work_collection_sha256 !== binding.workCollectionSha256 || hybridDigest(result.details.review) !== hybridDigest(binding.review))
|
|
42
|
+
refuse("unowned_work_or_review");
|
|
43
|
+
const preflight = binding.stage === "preflight", empty = binding.fileChangesSha256 === hybridDigest([]);
|
|
44
|
+
if (preflight ? result.predecessors.length !== 0 : result.predecessors.length !== 1
|
|
45
|
+
|| result.predecessors[0]?.stage !== interactiveStages[interactiveStages.indexOf(binding.stage) - 1])
|
|
46
|
+
refuse("foreign_stage_predecessor");
|
|
47
|
+
if (preflight && (!empty || binding.finishRequested))
|
|
48
|
+
refuse("preflight_changed_or_already_finished");
|
|
49
|
+
if (!preflight && !binding.finishRequested)
|
|
50
|
+
refuse("explicit_finish_required");
|
|
51
|
+
if (binding.stage === "review" || binding.stage === "hardening" && !empty) {
|
|
52
|
+
if (!binding.workCollectionSha256)
|
|
53
|
+
refuse("completed_work_collection_required");
|
|
54
|
+
}
|
|
55
|
+
if (["review", "integration"].includes(binding.stage) ? binding.review === null : binding.review !== null)
|
|
56
|
+
refuse("review_stage_required");
|
|
57
|
+
if (binding.stage === "integration" && binding.review?.verdict !== "accept")
|
|
58
|
+
refuse("accepted_review_required");
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/** Agents supply semantic summary/details, not manually calculated custody
|
|
62
|
+
* hashes. Binding must come from CLI-issued context/Work selection and be
|
|
63
|
+
* fenced against current SQL at settlement, never inferred from this proposal
|
|
64
|
+
* or silently refreshed to newer ambient inputs. Conflicting supplied claims
|
|
65
|
+
* refuse rather than being overwritten with valid-looking current metadata. */
|
|
66
|
+
export function prepareInteractiveStageResult(bytes, schemaBytes, binding) {
|
|
67
|
+
if (!bytes.length || bytes.length > 1024 * 1024)
|
|
68
|
+
refuse("result_exceeds_bound");
|
|
69
|
+
let proposal;
|
|
70
|
+
try {
|
|
71
|
+
proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return refuse("invalid_schema");
|
|
75
|
+
}
|
|
76
|
+
if (!proposal || typeof proposal !== "object" || Array.isArray(proposal)
|
|
77
|
+
|| !proposal.details || typeof proposal.details !== "object" || Array.isArray(proposal.details))
|
|
78
|
+
refuse("invalid_schema");
|
|
79
|
+
const owned = { schema_id: "dd-flow/interactive-stage-result@1", flow_id: "interactive", run_id: binding.runId, stage: binding.stage,
|
|
80
|
+
input_sha256: binding.inputSha256, baseline_sha256: binding.baselineSha256, user_inputs_sha256: binding.userInputsSha256,
|
|
81
|
+
scope_revision: binding.scopeRevision, file_changes_sha256: binding.fileChangesSha256, work_collection_sha256: binding.workCollectionSha256,
|
|
82
|
+
predecessors: binding.predecessors };
|
|
83
|
+
for (const [key, value] of Object.entries(owned))
|
|
84
|
+
if (Object.hasOwn(proposal, key) && hybridDigest(proposal[key]) !== hybridDigest(value))
|
|
85
|
+
refuse("foreign_stage_binding");
|
|
86
|
+
const details = proposal.details;
|
|
87
|
+
if (Object.hasOwn(details, "review") && hybridDigest(details.review) !== hybridDigest(binding.review))
|
|
88
|
+
refuse("unowned_work_or_review");
|
|
89
|
+
const supplied = details.limitations;
|
|
90
|
+
const limitations = Array.isArray(supplied)
|
|
91
|
+
? [...supplied, ...(binding.retainedLimitations ?? []).filter(item => !supplied.includes(item))] : supplied;
|
|
92
|
+
const encoded = Buffer.from(JSON.stringify({ ...proposal, ...owned, details: { ...details, review: binding.review, limitations } }));
|
|
93
|
+
return { result: validateInteractiveStageResult(encoded, schemaBytes, binding), bytes: encoded };
|
|
94
|
+
}
|
|
95
|
+
/** Later HITL answers/hardening may legitimately revise an earlier scope. Once
|
|
96
|
+
* review is accepted, integration cannot change that reviewed task or patch. */
|
|
97
|
+
export function assertInteractiveStageContinuation(result, prior) {
|
|
98
|
+
if (result.run_id !== prior.run_id || result.baseline_sha256 !== prior.baseline_sha256
|
|
99
|
+
|| interactiveStages.indexOf(result.stage) !== interactiveStages.indexOf(prior.stage) + 1)
|
|
100
|
+
refuse("foreign_stage_predecessor");
|
|
101
|
+
if (result.stage === "integration" && (prior.details.review?.verdict !== "accept"
|
|
102
|
+
|| result.user_inputs_sha256 !== prior.user_inputs_sha256 || result.scope_revision !== prior.scope_revision
|
|
103
|
+
|| result.file_changes_sha256 !== prior.file_changes_sha256
|
|
104
|
+
|| result.details.change_summary !== prior.details.change_summary || hybridDigest(result.details.review) !== hybridDigest(prior.details.review)
|
|
105
|
+
|| prior.details.limitations.some(item => !result.details.limitations.includes(item))))
|
|
106
|
+
refuse("accepted_review_changed");
|
|
107
|
+
}
|
|
108
|
+
/** A real revise verdict must never be projected as successful completion. */
|
|
109
|
+
export function interactiveStageDisposition(result) {
|
|
110
|
+
return result.details.review?.verdict === "revise" ? "needs_work" : "ready";
|
|
111
|
+
}
|