@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
|
@@ -17,6 +17,7 @@ import { appendAudit } from "./audit.js";
|
|
|
17
17
|
import { prepareStagePacket, acceptStagePacket, publishStagePacket, readAcceptedStageData } from "./stage-report-renderer.js";
|
|
18
18
|
import { assertStageSettlementAllowed } from "./run-recovery.js";
|
|
19
19
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
20
|
+
import { currentAssignedLifecycle, assertAssignedSettlementCurrent, hasAssignedCoordinatorLaunch } from "./assigned-lifecycle.js";
|
|
20
21
|
const event = "flow_run.descriptor_stage_settlement";
|
|
21
22
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
22
23
|
function refuse(reason) { throw new AppError("descriptor_stage_settlement_invalid", "Stage settlement lacks exact coordinator and result custody", 1, { reason, effect: "no_effect" }); }
|
|
@@ -26,10 +27,10 @@ function receipts(context, scope, kind) {
|
|
|
26
27
|
refuse("ambiguous_receipt");
|
|
27
28
|
return rows[0] ? JSON.parse(rows[0].payload_json) : null;
|
|
28
29
|
}
|
|
29
|
-
function
|
|
30
|
+
function coordinatorOwner(context, scope, bundle, historicalRead = false) {
|
|
30
31
|
const prior = receipts(context, scope, event);
|
|
31
32
|
const input = readDescriptorStageInput(context, scope, bundle, true, prior !== null), root = readDescriptorRootWork(context, scope, bundle);
|
|
32
|
-
const closedTerminal = prior && root.status === "completed" && bundle.descriptor.descriptor.terminal_stages.includes(scope.stage)
|
|
33
|
+
const closedTerminal = prior && root.status === "completed" && (historicalRead || bundle.descriptor.descriptor.terminal_stages.includes(scope.stage))
|
|
33
34
|
&& context.db.get("SELECT 1 FROM runs WHERE project_id=? AND id=? AND status='done'", [scope.projectId, scope.runId]);
|
|
34
35
|
if (root.status !== "running" && !closedTerminal || !stageWorkIds(context, scope.projectId, scope.runId, scope.stage).has(root.workId))
|
|
35
36
|
refuse("root_not_active");
|
|
@@ -67,13 +68,50 @@ function owner(context, scope, bundle, finishKey, matchKeys) {
|
|
|
67
68
|
const promptPath = path.join(input.runRoot, bundle.descriptor.stage(scope.stage).directory, "coordinator-prompt.md");
|
|
68
69
|
if (link.prompt_path !== promptPath)
|
|
69
70
|
refuse("prompt_link_changed");
|
|
70
|
-
const prompts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.file')=?
|
|
71
|
-
if (prompts.length
|
|
71
|
+
const prompts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.file')=? AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=?", [scope.projectId, scope.runId, scope.stage, promptPath, input.attempt, association.binding.prompt_sha256]);
|
|
72
|
+
if (!prompts.length)
|
|
72
73
|
refuse("prompt_custody_missing");
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
let promptBytes;
|
|
75
|
+
for (const row of prompts) {
|
|
76
|
+
const prompt = JSON.parse(row.payload_json), bytes = Buffer.from(prompt.base64, "base64");
|
|
77
|
+
if (hash(bytes) !== prompt.sha256 || bytes.toString("base64") !== prompt.base64 || promptBytes && !promptBytes.equals(bytes))
|
|
78
|
+
refuse("prompt_custody_changed");
|
|
79
|
+
promptBytes = bytes;
|
|
80
|
+
}
|
|
76
81
|
const start = context.db.get("SELECT daemon_id,session_id,harness,provider_session_id FROM hook_events WHERE project_id=? AND event_key=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, link.hook_event_id]);
|
|
82
|
+
if (start ? start.session_id !== link.session_id || start.harness !== session.harness || start.provider_session_id !== session.provider_session_id
|
|
83
|
+
: !hasAssignedCoordinatorLaunch(context, { ...scope, workSessionId: link.id, sessionId: link.session_id }))
|
|
84
|
+
refuse("start_owner_mismatch");
|
|
85
|
+
return { input, root, link, session, binding, association, promptPath, prior, start };
|
|
86
|
+
}
|
|
87
|
+
/** SQL-only current author identity. This proves the registered coordinator,
|
|
88
|
+
* exact prompt, physical binding and claimed or assigned start, not output approval,
|
|
89
|
+
* permission to publish, or Stage completion. No Session is allocated here. */
|
|
90
|
+
export function readActiveDescriptorStageCoordinator(context, input, bundle) {
|
|
91
|
+
const scope = { projectId: input.projectId, runId: input.runId, stage: input.stage };
|
|
92
|
+
const selected = coordinatorOwner(context, scope, bundle);
|
|
93
|
+
if (selected.prior)
|
|
94
|
+
refuse("coordinator_already_settled");
|
|
95
|
+
if (!selected.session.harness || !selected.session.provider_session_id)
|
|
96
|
+
refuse("physical_owner_missing");
|
|
97
|
+
return { work_id: selected.root.workId, work_session_id: selected.link.id, session_id: selected.link.session_id,
|
|
98
|
+
harness: selected.session.harness, provider_session_id: selected.session.provider_session_id,
|
|
99
|
+
start_hook: selected.link.hook_event_id, association_sha256: hybridDigest(selected.association),
|
|
100
|
+
physical_binding_sha256: hybridDigest(selected.binding), prompt_path: selected.promptPath };
|
|
101
|
+
}
|
|
102
|
+
function owner(context, scope, bundle, finishKey, matchKeys, historicalRead = false) {
|
|
103
|
+
const { input, root, link, session, binding, association, promptPath, prior, start } = coordinatorOwner(context, scope, bundle, historicalRead);
|
|
104
|
+
const assignedId = prior?.binding?.assignedInvocationId;
|
|
105
|
+
const saved = assignedId && context.db.get("SELECT admission_json,status FROM lifecycle_invocations WHERE id=?", [assignedId]);
|
|
106
|
+
const assigned = prior
|
|
107
|
+
? saved && saved.status === "settled" && saved.admission_json ? JSON.parse(saved.admission_json) : null
|
|
108
|
+
: currentAssignedLifecycle(context);
|
|
109
|
+
if (assigned && assigned.work_session_id === link.id && assigned.operation === "stage_finish" && assigned.scope.runId === scope.runId && assigned.stage === scope.stage) {
|
|
110
|
+
return { input, workId: root.workId, workSessionId: link.id, sessionId: link.session_id,
|
|
111
|
+
startHook: link.hook_event_id, finishKey: null, finishId: null, finishDaemon: assigned.scope.daemonId,
|
|
112
|
+
assignedInvocationId: assigned.invocation_id, associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
|
|
113
|
+
promptPath, prior, linkResultPath: link.result_path };
|
|
114
|
+
}
|
|
77
115
|
const finish = context.db.get("SELECT id,daemon_id,session_id,harness,provider_session_id,match_key,status FROM hook_events WHERE project_id=? AND event_key=? AND event_name='PreToolUse'", [scope.projectId, finishKey]);
|
|
78
116
|
if (!start || !finish || finish.status !== (prior ? "claimed" : "observed")
|
|
79
117
|
|| start.session_id !== link.session_id || finish.session_id !== link.session_id
|
|
@@ -83,13 +121,14 @@ function owner(context, scope, bundle, finishKey, matchKeys) {
|
|
|
83
121
|
refuse("finish_owner_mismatch");
|
|
84
122
|
return { input, workId: root.workId, workSessionId: link.id, sessionId: link.session_id,
|
|
85
123
|
startHook: link.hook_event_id, finishKey, finishId: finish.id, finishDaemon: finish.daemon_id,
|
|
86
|
-
associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
|
|
124
|
+
assignedInvocationId: undefined, associationSha256: hybridDigest(association), physicalBindingSha256: hybridDigest(binding),
|
|
87
125
|
promptPath, prior, linkResultPath: link.result_path };
|
|
88
126
|
}
|
|
89
127
|
function stableOwner(value) {
|
|
90
128
|
return { input: value.input, workId: value.workId, workSessionId: value.workSessionId, sessionId: value.sessionId,
|
|
91
129
|
startHook: value.startHook, finishKey: value.finishKey, finishId: value.finishId, finishDaemon: value.finishDaemon,
|
|
92
|
-
associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath
|
|
130
|
+
associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath,
|
|
131
|
+
...(value.assignedInvocationId ? { assignedInvocationId: value.assignedInvocationId } : {}) };
|
|
93
132
|
}
|
|
94
133
|
/** SQL-only accepted terminal custody reader, also after root closure. Match
|
|
95
134
|
* keys must be prepared outside the writer because lifecycle key preparation
|
|
@@ -100,16 +139,17 @@ export function readAcceptedDescriptorStageSettlement(context, input, bundle, ma
|
|
|
100
139
|
// the retained coordinator binding; source/policy owners are not scope keys.
|
|
101
140
|
const scope = { projectId: input.projectId, runId: input.runId, stage: input.stage };
|
|
102
141
|
const receipt = receipts(context, scope, event);
|
|
103
|
-
if (!receipt || typeof receipt.binding?.finishKey !== "string")
|
|
142
|
+
if (!receipt || typeof receipt.binding?.finishKey !== "string" && typeof receipt.binding?.assignedInvocationId !== "string")
|
|
104
143
|
refuse("settlement_receipt_missing");
|
|
105
|
-
const selected = owner(context, scope, bundle, receipt.binding.finishKey, matchKeys);
|
|
144
|
+
const selected = owner(context, scope, bundle, receipt.binding.finishKey, matchKeys, true);
|
|
106
145
|
const data = readAcceptedStageData(context, scope);
|
|
107
146
|
const expected = { run_id: scope.runId, stage: scope.stage, work_id: selected.workId, work_session_id: selected.workSessionId, binding: {
|
|
108
147
|
workId: selected.workId, workSessionId: selected.workSessionId, sessionId: selected.sessionId, startHook: selected.startHook,
|
|
109
148
|
finishKey: selected.finishKey, finishId: selected.finishId, finishDaemon: selected.finishDaemon,
|
|
110
149
|
associationSha256: selected.associationSha256, physicalBindingSha256: selected.physicalBindingSha256, promptPath: selected.promptPath,
|
|
111
150
|
scope, attempt: selected.input.attempt, input_sha256: selected.input.inputSha256, bundle_sha256: bundle.sha256,
|
|
112
|
-
result_sha256: data.result.sha256, result_path: selected.linkResultPath, data_schema_id: data.data_schema_id
|
|
151
|
+
result_sha256: data.result.sha256, result_path: selected.linkResultPath, data_schema_id: data.data_schema_id,
|
|
152
|
+
...(selected.assignedInvocationId ? { assignedInvocationId: selected.assignedInvocationId } : {})
|
|
113
153
|
} };
|
|
114
154
|
if (hybridDigest(expected) !== hybridDigest(receipt) || data.input_sha256 !== selected.input.inputSha256)
|
|
115
155
|
refuse("settlement_changed");
|
|
@@ -168,8 +208,12 @@ export class PreparedDescriptorStageSettlement {
|
|
|
168
208
|
finishKey: value.finishKey, finishId: value.finishId, finishDaemon: value.finishDaemon,
|
|
169
209
|
associationSha256: value.associationSha256, physicalBindingSha256: value.physicalBindingSha256, promptPath: value.promptPath,
|
|
170
210
|
scope: this.#scope, attempt: value.input.attempt, input_sha256: value.input.inputSha256, bundle_sha256: this.#bundle.sha256,
|
|
171
|
-
result_sha256: this.#packet.sha256, result_path: this.#packet.file, data_schema_id: this.#schemaId
|
|
211
|
+
result_sha256: this.#packet.sha256, result_path: this.#packet.file, data_schema_id: this.#schemaId,
|
|
212
|
+
...(value.assignedInvocationId ? { assignedInvocationId: value.assignedInvocationId } : {}) };
|
|
172
213
|
}
|
|
214
|
+
/** Identity already admitted against physical hook/coordinator custody.
|
|
215
|
+
* A family may compare it with a prior writer; this grants no settlement. */
|
|
216
|
+
coordinatorSessionId() { return this.#owner.sessionId; }
|
|
173
217
|
/** The optional handler fence must be SQL-only, prepared before this call.
|
|
174
218
|
* It keeps bootstrap/check/reviewer custody in the same acceptance writer.
|
|
175
219
|
*/
|
|
@@ -197,7 +241,8 @@ export class PreparedDescriptorStageSettlement {
|
|
|
197
241
|
const activeChildren = [...stageWorkIds(context, this.#scope.projectId, this.#scope.runId, this.#scope.stage)].filter(id => id !== this.#owner.workId && context.db.get("SELECT 1 FROM works WHERE work_id=? AND status IN ('created','running','paused')", [id]));
|
|
198
242
|
if (activeChildren.length)
|
|
199
243
|
refuse("stage_children_active");
|
|
200
|
-
|
|
244
|
+
assertAssignedSettlementCurrent(context, this.#owner.workSessionId);
|
|
245
|
+
if (this.#owner.finishId !== null && context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND status='observed'", [context.now(), this.#owner.finishId]).changes !== 1)
|
|
201
246
|
refuse("finish_claim_changed");
|
|
202
247
|
acceptStagePacket(context, { ...this.#scope, prepared: this.#packet });
|
|
203
248
|
completeFlowRunStage(context, { projectRoot: this.#completion.run.project_root, runId: this.#scope.runId, stage: this.#scope.stage, status: "done", data: "result.json", dataSchemaId: this.#schemaId, dataSha256: this.#packet.sha256 }, this.#completion);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { readFlowRunExecutionBundle, resolveRun } from "./runs.js";
|
|
8
|
+
import { readAcceptedStageData, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
9
|
+
import { selectDiagnosticFindings } from "./diagnostic-report.js";
|
|
10
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
11
|
+
import { assertMemoryRepairWritePaths } from "./memory-review-correction-plan.js";
|
|
12
|
+
import { readMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("repair_source_invalid", "Repair intake requires a completed diagnostic RUN and its accepted report", 2, { reason, effect: "no_effect" });
|
|
15
|
+
}
|
|
16
|
+
export function readDiagnosticRepairIntake(context, input) {
|
|
17
|
+
return context.db.readSnapshot(() => {
|
|
18
|
+
try {
|
|
19
|
+
return readIntake(context, input);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
23
|
+
refuse("malformed_source_custody");
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function readMemoryFixUserInputs(context, input, retainedBundle) {
|
|
29
|
+
return readMemoryFlowUserInputs(context, input, "mb-fix", retainedBundle);
|
|
30
|
+
}
|
|
31
|
+
/** Semantic intake proposal only: no write, closure, migration or SDLC launch
|
|
32
|
+
* authority. The registered flow must separately bind actual user approval,
|
|
33
|
+
* observe current source drift and admit its writer/independent verifier.
|
|
34
|
+
* Never classify prose by keywords or infer selection from severity. */
|
|
35
|
+
export function prepareMemoryFixIntake(context, input) {
|
|
36
|
+
const source = readDiagnosticRepairIntake(context, input);
|
|
37
|
+
const routing = validateMemoryFixRouting(source.selected_findings.map(finding => finding.id), input.decisions);
|
|
38
|
+
return { schema_id: "dd-flow/mb-fix-intake@1", ...source, routing,
|
|
39
|
+
repair_finding_ids: routing.filter(decision => decision.route === "memory_repair").map(decision => decision.finding_id) };
|
|
40
|
+
}
|
|
41
|
+
/** Shared semantic routing checks; this does not admit a source RUN or user approval. */
|
|
42
|
+
export function validateMemoryFixRouting(findingIds, decisions) {
|
|
43
|
+
const invalid = (reason) => {
|
|
44
|
+
throw new AppError("memory_fix_intake_invalid", "Memory repair intake requires one bounded routing decision per selected finding", 2, { reason, effect: "no_effect" });
|
|
45
|
+
};
|
|
46
|
+
if (!Array.isArray(findingIds) || !findingIds.length || findingIds.length > 4096
|
|
47
|
+
|| findingIds.some(id => typeof id !== "string" || !id.trim()) || new Set(findingIds).size !== findingIds.length
|
|
48
|
+
|| !Array.isArray(decisions) || decisions.length !== findingIds.length)
|
|
49
|
+
invalid("complete_routing_required");
|
|
50
|
+
const selected = new Set(findingIds);
|
|
51
|
+
const byId = new Map();
|
|
52
|
+
for (const value of decisions) {
|
|
53
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
54
|
+
|| Object.keys(value).sort().join() !== "finding_id,reason,route,write_paths")
|
|
55
|
+
invalid("invalid_routing_decision");
|
|
56
|
+
const decision = value;
|
|
57
|
+
if (typeof decision.finding_id !== "string" || !selected.has(decision.finding_id) || byId.has(decision.finding_id))
|
|
58
|
+
invalid("foreign_or_duplicate_finding");
|
|
59
|
+
if (!["memory_repair", "review_fix", "upgrade", "defer"].includes(decision.route)
|
|
60
|
+
|| typeof decision.reason !== "string" || !decision.reason.trim() || decision.reason.includes("\0")
|
|
61
|
+
|| Buffer.byteLength(decision.reason) > 16 * 1024 || !Array.isArray(decision.write_paths))
|
|
62
|
+
invalid("invalid_routing_decision");
|
|
63
|
+
if (decision.route === "memory_repair")
|
|
64
|
+
assertMemoryRepairWritePaths(decision.write_paths);
|
|
65
|
+
else if (decision.write_paths.length)
|
|
66
|
+
invalid("non_memory_write_scope");
|
|
67
|
+
byId.set(decision.finding_id, structuredClone(decision));
|
|
68
|
+
}
|
|
69
|
+
return findingIds.map(id => byId.get(id));
|
|
70
|
+
}
|
|
71
|
+
function readIntake(context, input) {
|
|
72
|
+
const projectRoot = resolveProjectRoot(input.projectRoot), project = requireProjectByRoot(context, projectRoot);
|
|
73
|
+
const run = resolveRun(context, project.id, input.sourceRunId);
|
|
74
|
+
if (run.status !== "done" || !isDiagnosticFlow(run.flow_kind))
|
|
75
|
+
refuse("completed_diagnostic_required");
|
|
76
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot, runId: run.id });
|
|
77
|
+
if (!bundle || bundle.manifest.flow_id !== run.flow_kind)
|
|
78
|
+
refuse("diagnostic_bundle_required");
|
|
79
|
+
const scope = { projectId: project.id, runId: run.id, stage: "report" };
|
|
80
|
+
const accepted = readAcceptedStageData(context, scope);
|
|
81
|
+
if (accepted.data_schema_id !== "dd-flow/diagnostic-stage-result@1")
|
|
82
|
+
refuse("unsupported_source_result");
|
|
83
|
+
const result = JSON.parse(accepted.bytes.toString("utf8"));
|
|
84
|
+
if (result.schema_id !== accepted.data_schema_id || result.run_id !== run.id || result.flow_id !== run.flow_kind
|
|
85
|
+
|| result.stage !== "report" || result.input_sha256 !== accepted.input_sha256)
|
|
86
|
+
refuse("foreign_source_result");
|
|
87
|
+
const stage = JSON.parse(run.index_json).stage_runs.find((item) => item.stage === "report");
|
|
88
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='diagnostic.report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [project.id, run.id, stage.attempt]);
|
|
89
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 16 * 1024)
|
|
90
|
+
refuse("report_publication_missing");
|
|
91
|
+
const publication = JSON.parse(rows[0].payload_json);
|
|
92
|
+
const reportPath = `${stage.dir}/diagnostic-report.json`;
|
|
93
|
+
if (publication.schema_id !== "dd-flow/diagnostic-report-publication@1" || publication.input_sha256 !== accepted.input_sha256
|
|
94
|
+
|| !Array.isArray(publication.artifacts))
|
|
95
|
+
refuse("foreign_report_publication");
|
|
96
|
+
const artifacts = publication.artifacts.filter((item) => item.path === reportPath);
|
|
97
|
+
if (artifacts.length !== 1 || !/^[a-f0-9]{64}$/.test(artifacts[0].sha256))
|
|
98
|
+
refuse("report_artifact_missing");
|
|
99
|
+
const packets = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='report' AND json_extract(payload_json,'$.attempt')=? AND json_extract(payload_json,'$.sha256')=? LIMIT 2", [project.id, run.id, stage.attempt, artifacts[0].sha256]);
|
|
100
|
+
if (packets.length !== 1 || Buffer.byteLength(packets[0].payload_json) > 6 * 1024 * 1024)
|
|
101
|
+
refuse("report_packet_missing");
|
|
102
|
+
const packet = JSON.parse(packets[0].payload_json);
|
|
103
|
+
const destination = path.resolve(run.run_root ?? run.run_dir, reportPath);
|
|
104
|
+
if (typeof packet.file !== "string" || resolveStagePublicationPath(context, project.id, run.id, packet.file) !== destination
|
|
105
|
+
|| typeof packet.base64 !== "string")
|
|
106
|
+
refuse("foreign_report_packet");
|
|
107
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
108
|
+
if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== packet.base64
|
|
109
|
+
|| createHash("sha256").update(bytes).digest("hex") !== artifacts[0].sha256)
|
|
110
|
+
refuse("report_packet_changed");
|
|
111
|
+
const report = JSON.parse(bytes.toString("utf8"));
|
|
112
|
+
if (report.schema_id !== "dd-flow/diagnostic-report@1" || report.run.id !== run.id || report.run.flow_id !== run.flow_kind
|
|
113
|
+
|| report.run.snapshot_sha256 !== result.snapshot_sha256 || report.summary !== result.summary
|
|
114
|
+
|| hybridDigest(report.review_focus) !== hybridDigest(result.details.focus)
|
|
115
|
+
|| hybridDigest(report.aspect_coverage) !== hybridDigest(result.details.coverage)
|
|
116
|
+
|| hybridDigest(report.limitations) !== hybridDigest(result.details.limitations)
|
|
117
|
+
|| !Array.isArray(report.findings_register)
|
|
118
|
+
|| hybridDigest(report.findings_register.map(({ origin: _origin, resolution: _resolution, critic_decisions: _critics, ...finding }) => finding)) !== hybridDigest(result.details.findings)
|
|
119
|
+
|| hybridDigest(report.findings_register.map(finding => finding.resolution)) !== hybridDigest(result.details.decisions))
|
|
120
|
+
refuse("report_result_changed");
|
|
121
|
+
return { ...selectDiagnosticFindings(report, input.findingIds),
|
|
122
|
+
source_stage: { ...accepted.result, authority_sha256: accepted.authority_sha256 } };
|
|
123
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
4
|
+
import { readDiagnosticRepairIntake } from "./diagnostic-repair-intake.js";
|
|
5
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
|
+
const logical = "runtime/repair-intake.json", limit = 4 * 1024 * 1024;
|
|
9
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
10
|
+
const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
11
|
+
const targets = {
|
|
12
|
+
"mb-fix": { schema: "dd-flow/mb-fix-source-intake@1", error: "memory_fix_intake_custody_invalid", message: "Memory repair requires its own frozen selected source intake", bundle: "memory_fix_bundle_required" },
|
|
13
|
+
"review-fix": { schema: "dd-flow/review-fix-source-intake@1", error: "review_fix_intake_custody_invalid", message: "Review-fix requires its own frozen selected source intake", bundle: "review_fix_bundle_required" }
|
|
14
|
+
};
|
|
15
|
+
function refuse(flowId, reason) {
|
|
16
|
+
const owner = Object.hasOwn(targets, flowId) ? targets[flowId] : targets["mb-fix"];
|
|
17
|
+
throw new AppError(owner.error, owner.message, 2, { reason, effect: "no_effect" });
|
|
18
|
+
}
|
|
19
|
+
/** SQL/in-memory fence for generic Stage writers; performs no filesystem lookup. */
|
|
20
|
+
export function assertDiagnosticRepairSourceIntakeCustody(context, scope, bundle, expectedSha256, flowId) {
|
|
21
|
+
if (!Object.hasOwn(targets, flowId))
|
|
22
|
+
refuse(flowId, "unsupported_flow");
|
|
23
|
+
if (bundle.manifest.flow_id !== flowId || bundle.manifest.run_id !== scope.runId)
|
|
24
|
+
refuse(flowId, targets[flowId].bundle);
|
|
25
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
26
|
+
const references = bundle.manifest.inventory.filter(ref => ref.path === logical);
|
|
27
|
+
if (references.length !== 1 || references[0].sha256 !== expectedSha256 || hash(bundle.blob(logical)) !== expectedSha256)
|
|
28
|
+
refuse(flowId, "source_intake_changed");
|
|
29
|
+
}
|
|
30
|
+
/** Initial selection comes from the completed diagnostic's SQL custody, never
|
|
31
|
+
* an agent-authored report. The RUN bundle factory must retain these exact bytes.
|
|
32
|
+
* No RUN allocation, write approval or mutation happens here.
|
|
33
|
+
*/
|
|
34
|
+
export function prepareDiagnosticRepairSourceIntake(context, input, flowId) {
|
|
35
|
+
if (!Object.hasOwn(targets, flowId))
|
|
36
|
+
refuse(flowId, "unsupported_flow");
|
|
37
|
+
const intake = readDiagnosticRepairIntake(context, input);
|
|
38
|
+
const bytes = Buffer.from(JSON.stringify({ schema_id: targets[flowId].schema, intake }));
|
|
39
|
+
if (bytes.length > limit)
|
|
40
|
+
refuse(flowId, "intake_exceeds_bound");
|
|
41
|
+
return { path: logical, sha256: hash(bytes), bytes };
|
|
42
|
+
}
|
|
43
|
+
/** Existing RUNs read their own retained input, not neighboring audit storage.
|
|
44
|
+
* Original source-stage paths are provenance only, never paths in this RUN.
|
|
45
|
+
* Frozen-bundle SQL authority still belongs to the existing generic runtime.
|
|
46
|
+
*/
|
|
47
|
+
export function readDiagnosticRepairSourceIntake(context, input, flowId, retainedBundle) {
|
|
48
|
+
if (!Object.hasOwn(targets, flowId))
|
|
49
|
+
refuse(flowId, "unsupported_flow");
|
|
50
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
51
|
+
const bundle = retainedBundle ?? readFlowRunExecutionBundle(context, input);
|
|
52
|
+
if (!bundle || bundle.manifest.flow_id !== flowId || bundle.manifest.run_id !== input.runId)
|
|
53
|
+
refuse(flowId, targets[flowId].bundle);
|
|
54
|
+
return context.db.readSnapshot(() => {
|
|
55
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: input.runId }, bundle);
|
|
56
|
+
const references = bundle.manifest.inventory.filter(ref => ref.path === logical);
|
|
57
|
+
if (references.length !== 1)
|
|
58
|
+
refuse(flowId, "source_intake_reference_missing");
|
|
59
|
+
const bytes = bundle.blob(logical);
|
|
60
|
+
if (!bytes.length || bytes.length > limit || hash(bytes) !== references[0].sha256)
|
|
61
|
+
refuse(flowId, "source_intake_changed");
|
|
62
|
+
let value;
|
|
63
|
+
try {
|
|
64
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return refuse(flowId, "invalid_source_intake");
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const intake = value.intake;
|
|
71
|
+
if (value.schema_id !== targets[flowId].schema || Object.keys(value).sort().join() !== "intake,schema_id"
|
|
72
|
+
|| intake.authorization !== "not_granted" || !/^RUN-[0-9]+-[a-z0-9-]+$/.test(intake.source.id)
|
|
73
|
+
|| !isDiagnosticFlow(intake.source.flow_id) || !digest(intake.source.snapshot_sha256) || !digest(intake.source.report_sha256)
|
|
74
|
+
|| !digest(intake.source_stage.sha256) || !digest(intake.source_stage.authority_sha256)
|
|
75
|
+
|| typeof intake.source_stage.path !== "string" || !intake.source_stage.path
|
|
76
|
+
|| !Array.isArray(intake.selected_findings) || !intake.selected_findings.length || intake.selected_findings.length > 4096
|
|
77
|
+
|| !Array.isArray(intake.unselected_ids) || intake.unselected_ids.length > 4096
|
|
78
|
+
|| !Array.isArray(intake.limitations) || intake.limitations.some(item => typeof item !== "string"))
|
|
79
|
+
refuse(flowId, "invalid_source_intake");
|
|
80
|
+
const ids = intake.selected_findings.map(finding => finding.id);
|
|
81
|
+
if (ids.some(id => typeof id !== "string" || !id.trim()) || new Set(ids).size !== ids.length
|
|
82
|
+
|| new Set(intake.unselected_ids).size !== intake.unselected_ids.length
|
|
83
|
+
|| intake.unselected_ids.some(id => typeof id !== "string" || !id.trim() || ids.includes(id)))
|
|
84
|
+
refuse(flowId, "invalid_source_selection");
|
|
85
|
+
for (const finding of intake.selected_findings) {
|
|
86
|
+
if (finding.origin.id !== finding.id || typeof finding.origin.work_id !== "string"
|
|
87
|
+
|| finding.resolution.finding_id !== finding.id
|
|
88
|
+
|| !["accepted", "downgraded", "def_candidate", "review_fix_candidate"].includes(finding.resolution.disposition)
|
|
89
|
+
|| !Array.isArray(finding.evidence) || !Array.isArray(finding.resolution.evidence) || !Array.isArray(finding.critic_decisions)
|
|
90
|
+
|| finding.critic_decisions.some(critic => critic.decision.finding_id !== finding.id || !Array.isArray(critic.decision.evidence)))
|
|
91
|
+
refuse(flowId, "invalid_source_selection");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof AppError)
|
|
96
|
+
throw error;
|
|
97
|
+
return refuse(flowId, "invalid_source_intake");
|
|
98
|
+
}
|
|
99
|
+
return { sha256: references[0].sha256, reference: { root: "run", path: `execution-bundle/${logical}`, sha256: references[0].sha256 },
|
|
100
|
+
intake: structuredClone(value.intake) };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -55,6 +55,36 @@ export function prepareDiagnosticReport(result, analysis, critics) {
|
|
|
55
55
|
dashboard_links: critics.map(work => ({ work_id: work.workId, result: `run://${result.run_id}/works/${work.workId}/result.json` }))
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
/** Pure repair-intake selection, not acceptance or mutation authority. The
|
|
59
|
+
* caller must obtain report bytes from the completed RUN's committed custody,
|
|
60
|
+
* then separately bind the user's approved write scope. Never infer selection
|
|
61
|
+
* from severity, recommendations or a non-empty findings register. */
|
|
62
|
+
export function selectDiagnosticFindings(report, findingIds) {
|
|
63
|
+
const invalid = (reason) => {
|
|
64
|
+
throw new AppError("repair_selection_invalid", "Select exact reviewed finding IDs before preparing repair scope", 2, { reason, effect: "no_effect" });
|
|
65
|
+
};
|
|
66
|
+
if (!Array.isArray(findingIds) || !findingIds.length || findingIds.length > 4096
|
|
67
|
+
|| findingIds.some(id => typeof id !== "string" || !id.trim()) || new Set(findingIds).size !== findingIds.length)
|
|
68
|
+
invalid("explicit_unique_selection_required");
|
|
69
|
+
const register = new Map(report.findings_register.map(finding => [finding.id, finding]));
|
|
70
|
+
if (register.size !== report.findings_register.length)
|
|
71
|
+
invalid("ambiguous_finding_register");
|
|
72
|
+
const selected = findingIds.map(id => {
|
|
73
|
+
const finding = register.get(id);
|
|
74
|
+
if (!finding)
|
|
75
|
+
return invalid("unknown_finding");
|
|
76
|
+
if (!["accepted", "downgraded", "def_candidate", "review_fix_candidate"].includes(finding.resolution.disposition))
|
|
77
|
+
invalid("finding_not_accepted");
|
|
78
|
+
return structuredClone(finding);
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
source: { ...structuredClone(report.run), report_sha256: hybridDigest(report) },
|
|
82
|
+
selected_findings: selected,
|
|
83
|
+
unselected_ids: report.findings_register.filter(finding => !findingIds.includes(finding.id)).map(finding => finding.id),
|
|
84
|
+
limitations: [...report.limitations],
|
|
85
|
+
authorization: "not_granted"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
58
88
|
/** HTML embeds exactly the authoritative JSON value; untrusted prose is text,
|
|
59
89
|
* never HTML or executable script. Markdown remains a compact companion. */
|
|
60
90
|
export function renderDiagnosticReport(report) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { AppError } from "../shared/errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
4
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
4
5
|
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
5
6
|
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
6
7
|
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
@@ -10,34 +11,65 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
10
11
|
import { nextDescriptorRunId } from "./descriptor-run-start.js";
|
|
11
12
|
import { bundledHandlerContext } from "./stage-handlers.js";
|
|
12
13
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
14
|
+
import { readDiagnosticRequest } from "./diagnostic-start.js";
|
|
13
15
|
const bytes = (value) => Buffer.from(JSON.stringify(value));
|
|
14
16
|
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
15
17
|
function refuse(reason) { throw new AppError("diagnostic_run_bundle_invalid", "Diagnostic bundle requires its frozen source, execution and snapshot", 2, { reason, effect: "no_effect" }); }
|
|
16
18
|
/** Initial construction only. Retry after allocation restores the retained RUN
|
|
17
19
|
* bundle, never current operator settings/canon. boundAt belongs to the request. */
|
|
18
20
|
export function prepareDiagnosticRunBundle(context, input) {
|
|
21
|
+
return prepareRequestRunBundle(context, input, "source_snapshot");
|
|
22
|
+
}
|
|
23
|
+
/** Delivery shares immutable request/engine/profile retention, not diagnosis's
|
|
24
|
+
* read-only cloned workspace or its permission model. No effect is admitted. */
|
|
25
|
+
export function prepareDeliveryRunBundle(context, input) {
|
|
26
|
+
return prepareRequestRunBundle(context, input, "delivery");
|
|
27
|
+
}
|
|
28
|
+
function prepareRequestRunBundle(context, input, mode) {
|
|
19
29
|
if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
|
|
20
30
|
|| !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
|
|
21
31
|
refuse("unprepared_selection");
|
|
22
32
|
const frozen = input.execution.snapshot(), flowId = frozen.settings.flow_id;
|
|
23
|
-
if (!
|
|
24
|
-
refuse("
|
|
33
|
+
if (mode === "delivery" ? !isDeliveryFlow(flowId) : !isSourceSnapshotFlow(flowId))
|
|
34
|
+
refuse("registered_request_flow_required");
|
|
35
|
+
if (mode === "delivery" && frozen.settings.stage_session_mode === "same_session")
|
|
36
|
+
refuse("independent_delivery_verifier_session_required");
|
|
37
|
+
if (flowId === "review-fix" && frozen.settings.stage_session_mode === "same_session")
|
|
38
|
+
refuse("independent_scope_review_session_required");
|
|
39
|
+
if ((mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix") && (typeof input.requestText !== "string" || !input.requestText.trim()
|
|
40
|
+
|| Buffer.byteLength(input.requestText) > 64 * 1024))
|
|
41
|
+
refuse(mode === "delivery" ? "original_delivery_request_required" : flowId === "mb-distill" ? "original_distill_request_required" : "original_review_fix_request_required");
|
|
42
|
+
const request = flowId === "review-fix" ? readDiagnosticRequest(context, { projectRoot: frozen.scope.projectRoot,
|
|
43
|
+
requestId: frozen.scope.requestId, flowId }) : null;
|
|
44
|
+
if (flowId === "review-fix" && (!request?.repair_source || request.created_at !== input.boundAt
|
|
45
|
+
|| request.objective !== input.requestText || request.source_sha256 !== input.source.sha256
|
|
46
|
+
|| request.execution_sha256 !== input.execution.sha256))
|
|
47
|
+
refuse("original_repair_request_required");
|
|
25
48
|
const project = requireProjectByRoot(context, frozen.scope.projectRoot);
|
|
26
|
-
const snapshot = readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
|
|
27
|
-
sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
|
|
28
|
-
if (!snapshot || snapshot.root !== frozen.scope.workspace)
|
|
49
|
+
const snapshot = isSourceSnapshotFlow(flowId) ? readDiagnosticSnapshot(context, { projectId: project.id, projectRoot: frozen.scope.projectRoot,
|
|
50
|
+
sourceRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId }) : null;
|
|
51
|
+
if (mode === "source_snapshot" && (!snapshot || snapshot.root !== frozen.scope.workspace))
|
|
29
52
|
refuse("snapshot_selection_changed");
|
|
30
53
|
input.source.revalidate();
|
|
31
54
|
const flow = input.source.resolveRegisteredFlow();
|
|
32
55
|
if (flow.descriptor.id !== flowId || flow.sha256 !== frozen.settings.descriptor_sha256)
|
|
33
56
|
refuse("flow_selection_changed");
|
|
34
|
-
|
|
57
|
+
if (isDeliveryFlow(flowId))
|
|
58
|
+
input.engine.admitDeliveryFlow(input.source, flowId);
|
|
59
|
+
else if (flowId === "review-fix")
|
|
60
|
+
input.engine.admitReviewFixFlow(input.source);
|
|
61
|
+
else if (flowId === "mb-distill")
|
|
62
|
+
input.engine.admitDistillFlow(input.source);
|
|
63
|
+
else if (flowId === "mb-audit" || flowId === "project-review")
|
|
64
|
+
input.engine.admitDiagnosticFlow(input.source, flowId);
|
|
65
|
+
else
|
|
66
|
+
refuse("registered_request_flow_required");
|
|
35
67
|
const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
|
|
36
68
|
if (typeof checksum !== "string")
|
|
37
69
|
refuse("engine_identity_missing");
|
|
38
70
|
const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: flowId });
|
|
39
71
|
const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
|
|
40
|
-
project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: "Request-bound read-only diagnostic",
|
|
72
|
+
project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: isDeliveryFlow(flowId) ? "Request-bound delivery; startup grants no productive mutation approval" : flowId === "review-fix" ? "Request-bound selected finding scope and explicit formalization handoff" : flowId === "mb-distill" ? "Request-bound practice distillation with separate destination authority" : "Request-bound read-only diagnostic",
|
|
41
73
|
engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
|
|
42
74
|
integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
|
|
43
75
|
const blobs = input.source.blobs();
|
|
@@ -47,6 +79,12 @@ export function prepareDiagnosticRunBundle(context, input) {
|
|
|
47
79
|
blobs.set(logical, content);
|
|
48
80
|
return { path: logical, sha256: hash(content) };
|
|
49
81
|
};
|
|
82
|
+
// Use the retained request verbatim, not the coordinator's interpretation or
|
|
83
|
+
// a later mutable intake file. It is task data, never implicit write scope.
|
|
84
|
+
if (mode === "delivery" || flowId === "mb-distill" || flowId === "review-fix")
|
|
85
|
+
put("runtime/source-request.md", Buffer.from(input.requestText));
|
|
86
|
+
if (request?.repair_source)
|
|
87
|
+
put(request.repair_source.path, Buffer.from(request.repair_source.base64, "base64"));
|
|
50
88
|
for (const ref of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
|
|
51
89
|
if (!ref)
|
|
52
90
|
continue;
|
|
@@ -60,7 +98,8 @@ export function prepareDiagnosticRunBundle(context, input) {
|
|
|
60
98
|
const execution = put("runtime/execution.json", bytes(frozen.settings));
|
|
61
99
|
const engineBinding = put("runtime/engine-binding.json", bytes(binding));
|
|
62
100
|
const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
|
|
63
|
-
|
|
101
|
+
if (snapshot)
|
|
102
|
+
put("runtime/diagnostic-snapshot.json", bytes(snapshot));
|
|
64
103
|
const provenance = { canon: input.source.canonProvenance(), project_pack: null };
|
|
65
104
|
const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: flowId,
|
|
66
105
|
descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
7
|
import { appendAudit } from "./audit.js";
|
|
8
8
|
import { reviewSourceHash, reviewSourceFilter, reviewExecutionCopyHash } from "./review-copy.js";
|
|
@@ -32,7 +32,7 @@ export function diagnosticSnapshotPaths(context, value, runId) {
|
|
|
32
32
|
}
|
|
33
33
|
function validate(context, input, runId) {
|
|
34
34
|
const physical = physicalSnapshot(context, input, runId);
|
|
35
|
-
if (!
|
|
35
|
+
if (!isSourceSnapshotFlow(input.flowId) || !input.requestId?.trim() || input.requestId.length > 200
|
|
36
36
|
|| !path.isAbsolute(physical.projectRoot) || physical.projectRoot !== physical.sourceRoot
|
|
37
37
|
|| fs.realpathSync(physical.sourceRoot) !== physical.sourceRoot)
|
|
38
38
|
refuse("invalid_selection");
|
|
@@ -8,7 +8,7 @@ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
|
8
8
|
import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
9
9
|
import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
10
10
|
import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
|
|
11
|
-
import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
|
|
11
|
+
import { assertDiagnosticCriticResolution, assertDiagnosticReportData, assertDiagnosticStageScope, materializeDiagnosticRetainedFields, sealDiagnosticStageLimitations, validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
|
|
12
12
|
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
13
13
|
import { registerDiagnosticStageGraph } from "./diagnostic-work-registration.js";
|
|
14
14
|
import { PreparedDiagnosticWorkCollection } from "./diagnostic-work-collection.js";
|
|
@@ -53,7 +53,8 @@ export function finishDiagnosticStage(context, input) {
|
|
|
53
53
|
const previous = previousStage ? readAcceptedStageData(context, { ...scope, stage: previousStage }) : null;
|
|
54
54
|
const prior = previous ? JSON.parse(previous.bytes.toString("utf8")) : null;
|
|
55
55
|
const collection = ["audit", "analyze", "review"].includes(input.stage) ? PreparedDiagnosticWorkCollection.prepare(context, input) : null;
|
|
56
|
-
const
|
|
56
|
+
const proposal = materializeDiagnosticRetainedFields(input.bytes, prior, input.stage);
|
|
57
|
+
const candidates = ["audit", "analyze"].includes(input.stage) ? sealDiagnosticCandidateIds(proposal, collection.results()) : proposal;
|
|
57
58
|
const bytes = sealDiagnosticStageLimitations(candidates, [
|
|
58
59
|
...(prior?.details.limitations ?? []), ...(collection?.results() ?? []).flatMap(work => work.result.limitations)
|
|
59
60
|
], prior?.details.missing_expected_sources ?? []);
|
|
@@ -7,6 +7,32 @@ import { verifyDiagnosticCoverage, verifyDiagnosticSources } from "./diagnostic-
|
|
|
7
7
|
const refuse = (reason) => {
|
|
8
8
|
throw new AppError("diagnostic_stage_result_invalid", "Diagnostic result does not match its frozen stage", 2, { reason, effect: "no_effect", ...(reason === "result_schema_invalid" ? { phase: "prepare", recoverable: true } : {}) });
|
|
9
9
|
};
|
|
10
|
+
/** Retained fields are CLI data, not a transcription task for the coordinator. */
|
|
11
|
+
export function materializeDiagnosticRetainedFields(bytes, prior, stage) {
|
|
12
|
+
if (!prior || !bytes.length || bytes.length > 1024 * 1024)
|
|
13
|
+
return bytes;
|
|
14
|
+
let value;
|
|
15
|
+
try {
|
|
16
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
if (!value || typeof value !== "object" || !value.details || typeof value.details !== "object" || Array.isArray(value.details))
|
|
22
|
+
return bytes;
|
|
23
|
+
const retained = stage === "report" ? Object.entries(prior.details).filter(([key]) => key !== "limitations")
|
|
24
|
+
: ["focus", "coverage"].map(key => [key, prior.details[key]]);
|
|
25
|
+
for (const [key, expected] of retained) {
|
|
26
|
+
if (Object.hasOwn(value.details, key) && hybridDigest(value.details[key]) !== hybridDigest(expected))
|
|
27
|
+
throw new AppError("diagnostic_stage_invalid", "A proposal cannot replace CLI-retained predecessor data", 2, { reason: `accepted_${key}_changed`, effect: "no_effect", phase: "prepare", recoverable: true });
|
|
28
|
+
value.details[key] = structuredClone(expected);
|
|
29
|
+
}
|
|
30
|
+
if (!Object.hasOwn(value.details, "limitations"))
|
|
31
|
+
value.details.limitations = [];
|
|
32
|
+
if (!Object.hasOwn(value.details, "missing_expected_sources"))
|
|
33
|
+
value.details.missing_expected_sources = structuredClone(prior.details.missing_expected_sources);
|
|
34
|
+
return Buffer.from(JSON.stringify(value));
|
|
35
|
+
}
|
|
10
36
|
/** Accepted limitations and missing-source observations belong to predecessor/Work custody, not a coordinator's
|
|
11
37
|
* transcription. Keep its annotations too; validate the complete sealed result
|
|
12
38
|
* with the frozen schema afterwards, without truncating overflowing data. */
|