@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -2,11 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import { AppError } from "../shared/errors.js";
|
|
3
3
|
import { assertMemoryOrdinaryConfigurationCoverage, assertMemoryOrdinaryConfigurationOutputs } from "./memory-ordinary-defaults.js";
|
|
4
4
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
-
import { runtimeHarness } from "./execution-policy.js";
|
|
6
5
|
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
7
6
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
7
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
9
8
|
import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
9
|
+
import { readDescriptorCoordinatorSession } from "./descriptor-coordinator-session.js";
|
|
10
10
|
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
11
11
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
12
12
|
import { PreparedMemoryResultBinding } from "./memory-result-binding.js";
|
|
@@ -64,27 +64,7 @@ function compositionBaseline(context, bundle) {
|
|
|
64
64
|
* separately. No new Session, hook claim or provider invocation is created.
|
|
65
65
|
*/
|
|
66
66
|
function coordinatorSession(context, scope, bundle, workId) {
|
|
67
|
-
|
|
68
|
-
if (rows.length !== 1)
|
|
69
|
-
refuse("active_coordinator_session_required");
|
|
70
|
-
const session = rows[0], route = bundle.execution.stage(scope.stage);
|
|
71
|
-
const input = readDescriptorStageInput(context, scope, bundle), directory = bundle.descriptor.stage(scope.stage).directory;
|
|
72
|
-
if (session.prompt_path !== path.join(input.runRoot, directory, "coordinator-prompt.md")
|
|
73
|
-
|| session.result_path !== path.join(input.runRoot, directory, "result-input.json"))
|
|
74
|
-
refuse("coordinator_packet_link_changed");
|
|
75
|
-
if (session.harness !== runtimeHarness(route.profile)
|
|
76
|
-
|| !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id]))
|
|
77
|
-
refuse("coordinator_start_custody_missing");
|
|
78
|
-
const bindings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='stage.coordinator_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, session.id]);
|
|
79
|
-
if (!bindings.length || bindings.length > 32 || bindings.some(row => Buffer.byteLength(row.payload_json) > 32768))
|
|
80
|
-
refuse("physical_binding_missing");
|
|
81
|
-
const binding = JSON.parse(bindings[0].payload_json);
|
|
82
|
-
if (bindings.some(row => hybridDigest(JSON.parse(row.payload_json)) !== hybridDigest(binding))
|
|
83
|
-
|| binding.work_id !== workId || binding.session_id !== session.session_id || binding.harness !== session.harness || binding.provider_session_id !== session.provider_session_id
|
|
84
|
-
|| binding.execution_sha256 !== bundle.execution.sha256 || binding.profile_id !== route.profile.id || binding.profile_sha256 !== hybridDigest(route.profile)
|
|
85
|
-
|| binding.session_mode !== route.session_mode || hybridDigest(binding.scope) !== hybridDigest({ ...stageGraphScope(context, scope.projectId, scope.runId, scope.stage), stage: scope.stage }))
|
|
86
|
-
refuse("physical_binding_changed");
|
|
87
|
-
return hybridDigest({ session, binding });
|
|
67
|
+
return readDescriptorCoordinatorSession(context, { scope, bundle, workId }, refuse);
|
|
88
68
|
}
|
|
89
69
|
/** Actual registration path for the composition handler, awaiting complete
|
|
90
70
|
* memory-flow admission. Registers the immutable blueprint before workers run;
|
|
@@ -5,6 +5,7 @@ import { PreparedMemoryFinishReview } from "./memory-finish-handler.js";
|
|
|
5
5
|
import { readMemoryInitBootstrapReadiness } from "./memory-init-bootstrap-readiness.js";
|
|
6
6
|
import { readMemoryInitGitPolicy } from "./memory-init-git-policy.js";
|
|
7
7
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
8
|
+
import { assertReviewedProjectGitInventory, observeReviewedProjectGitIntegration } from "./project-git-integration.js";
|
|
8
9
|
import { observeReviewedMemoryGitIntegration, reviewedMemoryGitFiles, isReviewedMemoryGitFile, assertMemoryGitRemovalSources } from "./memory-git-integration.js";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
11
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
@@ -149,6 +150,20 @@ export function observeMemoryUpgradeFinishInventory(root, expected) {
|
|
|
149
150
|
/** Physical Git operation only; callers must independently select accepted files
|
|
150
151
|
* and retain each mutation's intent. No ambient Git redirection or whole-tree add. */
|
|
151
152
|
export function integrateReviewedMemoryFiles(selection, environment, references, runId, mutation, policy, removals = []) {
|
|
153
|
+
if (!Array.isArray(references) || !Array.isArray(removals) || !references.length
|
|
154
|
+
|| references.length + removals.length > 4096 || !references.some(ref => ref?.path === ".memory-bank/index.md")
|
|
155
|
+
|| [...references, ...removals].some(ref => !isReviewedMemoryGitFile(ref))
|
|
156
|
+
|| removals.some(ref => !ref.path.startsWith(".memory-bank/") || ref.path === ".memory-bank/index.md"))
|
|
157
|
+
refuse("invalid_owned_inventory");
|
|
158
|
+
return integrateReviewedFiles(selection, environment, references, mutation, policy, removals, () => observeReviewedMemoryGitIntegration(selection, environment, references, removals), `chore(${runId.toLowerCase()}): integrate reviewed Memory Bank`);
|
|
159
|
+
}
|
|
160
|
+
/** Physical project-file variant only. Accepted review and the original dirty
|
|
161
|
+
* baseline remain the caller's responsibility; this is not a RUN authority. */
|
|
162
|
+
export function integrateReviewedProjectFiles(selection, environment, references, runId, mutation, policy, removals = []) {
|
|
163
|
+
assertReviewedProjectGitInventory(references, removals);
|
|
164
|
+
return integrateReviewedFiles(selection, environment, references, mutation, policy, removals, () => observeReviewedProjectGitIntegration(selection, environment, references, removals), `chore(${runId.toLowerCase()}): integrate reviewed project changes`);
|
|
165
|
+
}
|
|
166
|
+
function integrateReviewedFiles(selection, environment, references, mutation, policy, removals, observe, message) {
|
|
152
167
|
const env = { ...Object.fromEntries(Object.entries(environment).filter(([key]) => !key.startsWith("GIT_"))), GIT_LITERAL_PATHSPECS: "1" };
|
|
153
168
|
const git = (directory, args, write = false, absent = false) => {
|
|
154
169
|
const result = spawnSync(selection.gitExecutable, ["-c", "core.fsmonitor=false",
|
|
@@ -158,13 +173,8 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
|
|
|
158
173
|
if (result.error || result.signal || result.status !== 0)
|
|
159
174
|
throw new AppError("memory_finish_integration_invalid", write ? "Git integration did not return success; inspect the original operation before retrying" : "Git integration preparation failed", 2, { reason: "git_operation_failed", effect: write ? "unknown" : "no_effect", operation: args[0], stderr: result.stderr,
|
|
160
175
|
next_action: write ? "reconcile_original_integration" : "resolve_git_configuration_or_workspace" });
|
|
161
|
-
return result.stdout.trim();
|
|
176
|
+
return args.includes("-z") ? result.stdout : result.stdout.trim();
|
|
162
177
|
};
|
|
163
|
-
if (!Array.isArray(references) || !Array.isArray(removals) || !references.length
|
|
164
|
-
|| references.length + removals.length > 4096 || !references.some(ref => ref?.path === ".memory-bank/index.md")
|
|
165
|
-
|| [...references, ...removals].some(ref => !isReviewedMemoryGitFile(ref))
|
|
166
|
-
|| removals.some(ref => !ref.path.startsWith(".memory-bank/") || ref.path === ".memory-bank/index.md"))
|
|
167
|
-
refuse("invalid_owned_inventory");
|
|
168
178
|
const paths = [...references, ...removals].map(ref => ref.path);
|
|
169
179
|
if (new Set(paths).size !== paths.length)
|
|
170
180
|
refuse("invalid_owned_inventory");
|
|
@@ -195,10 +205,10 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
|
|
|
195
205
|
if (hashes.get(memorySourceKey(ref.root, ref.path)) !== ref.sha256)
|
|
196
206
|
refuse("reviewed_workspace_bytes_changed");
|
|
197
207
|
try {
|
|
198
|
-
return
|
|
208
|
+
return observe();
|
|
199
209
|
}
|
|
200
210
|
catch (error) {
|
|
201
|
-
if (!(error instanceof AppError) || error.code
|
|
211
|
+
if (!(error instanceof AppError) || !["memory_git_integration_invalid", "project_git_integration_invalid"].includes(error.code))
|
|
202
212
|
throw error;
|
|
203
213
|
}
|
|
204
214
|
const visibility = git(source, ["ls-files", "-v", "--", ...paths]);
|
|
@@ -207,6 +217,12 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
|
|
|
207
217
|
if (git(source, ["diff", "--name-only", "--diff-filter=U"]))
|
|
208
218
|
refuse("unmerged_workspace");
|
|
209
219
|
const sourceHead = git(source, ["rev-parse", "--verify", "HEAD"], false, true);
|
|
220
|
+
let ownedHead = sourceHead;
|
|
221
|
+
const changedCommit = () => {
|
|
222
|
+
throw new AppError("memory_finish_integration_invalid", "The scoped commit changed before integration completed; reconcile its original effect", 2, { reason: "scoped_commit_changed", effect: "unknown", next_action: "reconcile_original_integration" });
|
|
223
|
+
};
|
|
224
|
+
const assertOwnedHead = () => { if (git(source, ["rev-parse", "HEAD"]) !== ownedHead)
|
|
225
|
+
changedCommit(); };
|
|
210
226
|
const deleting = [];
|
|
211
227
|
let removedBytes = 0;
|
|
212
228
|
for (const ref of removals) {
|
|
@@ -237,26 +253,49 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
|
|
|
237
253
|
return bytes.status !== 0 || createHash("sha256").update(bytes.stdout).digest("hex") !== ref.sha256
|
|
238
254
|
|| !entry.startsWith(`${ref.git_mode ?? "100644"} blob `);
|
|
239
255
|
});
|
|
256
|
+
const assertSourceBeforeMutation = () => {
|
|
257
|
+
observed.revalidate();
|
|
258
|
+
assertModes();
|
|
259
|
+
assertMemoryGitRemovalSources(source, removals);
|
|
260
|
+
if (git(source, ["rev-parse", "--verify", "HEAD"], false, true) !== sourceHead
|
|
261
|
+
|| git(source, ["symbolic-ref", "--short", "HEAD"]) !== branch)
|
|
262
|
+
refuse("source_git_baseline_changed");
|
|
263
|
+
};
|
|
240
264
|
if (needsCommit) {
|
|
241
265
|
git(source, ["var", "GIT_AUTHOR_IDENT"]);
|
|
242
266
|
git(source, ["var", "GIT_COMMITTER_IDENT"]);
|
|
243
267
|
mutation("commit", sourceHead, () => {
|
|
244
|
-
|
|
245
|
-
assertModes();
|
|
246
|
-
assertMemoryGitRemovalSources(source, removals);
|
|
247
|
-
if (git(source, ["rev-parse", "--verify", "HEAD"], false, true) !== sourceHead
|
|
248
|
-
|| git(source, ["symbolic-ref", "--short", "HEAD"]) !== branch)
|
|
249
|
-
refuse("source_git_baseline_changed");
|
|
268
|
+
assertSourceBeforeMutation();
|
|
250
269
|
const commitPaths = [...references.map(ref => ref.path), ...deleting];
|
|
251
270
|
git(source, ["add", "-f", "--", ...commitPaths], true);
|
|
252
|
-
git(source, ["commit", "--only", "-m",
|
|
271
|
+
git(source, ["commit", "--only", "-m", message, "--", ...commitPaths], true);
|
|
272
|
+
});
|
|
273
|
+
// Hooks or a concurrent writer may have changed history/index after the
|
|
274
|
+
// original admission. Never carry an unowned commit into the merge.
|
|
275
|
+
ownedHead = git(source, ["rev-parse", "HEAD"]);
|
|
276
|
+
const parentAndSubject = git(source, ["show", "-s", "--format=%P%n%s", ownedHead]);
|
|
277
|
+
const committedPaths = git(source, ["diff-tree", "--root", "--no-commit-id", "--no-renames", "--name-only", "-r", "-z", ownedHead]).split("\0").filter(Boolean);
|
|
278
|
+
if (parentAndSubject !== `${sourceHead}\n${message}`.trim() || committedPaths.some(logical => !paths.includes(logical)))
|
|
279
|
+
changedCommit();
|
|
280
|
+
}
|
|
281
|
+
else if (git(source, ["diff", "--cached", "--name-only", "-z", "--", ...paths])) {
|
|
282
|
+
// The reviewed tree can already equal HEAD while a selected pre-existing
|
|
283
|
+
// staged version differs. Settle only its admitted index effect; do not
|
|
284
|
+
// manufacture an empty commit or disturb unrelated staged entries.
|
|
285
|
+
const indexedRemovals = removals.length ? git(source, ["ls-files", "--cached", "-z", "--", ...removals.map(ref => ref.path)]).split("\0").filter(Boolean) : [];
|
|
286
|
+
mutation("commit", sourceHead, () => {
|
|
287
|
+
assertSourceBeforeMutation();
|
|
288
|
+
git(source, ["add", "-f", "--", ...references.map(ref => ref.path), ...indexedRemovals], true);
|
|
253
289
|
});
|
|
290
|
+
assertOwnedHead();
|
|
254
291
|
}
|
|
255
292
|
observed.revalidate();
|
|
293
|
+
assertOwnedHead();
|
|
256
294
|
if (source !== target) {
|
|
257
|
-
const head =
|
|
295
|
+
const head = ownedHead, targetHead = git(target, ["rev-parse", "HEAD"]);
|
|
258
296
|
if (head !== targetHead) {
|
|
259
297
|
mutation("merge", targetHead, () => {
|
|
298
|
+
assertOwnedHead();
|
|
260
299
|
if (git(target, ["rev-parse", "HEAD"]) !== targetHead
|
|
261
300
|
|| git(target, ["symbolic-ref", "--short", "HEAD"]) !== selection.integrationBranch
|
|
262
301
|
|| git(target, ["diff", "--cached", "--name-only"]))
|
|
@@ -265,7 +304,9 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
|
|
|
265
304
|
});
|
|
266
305
|
}
|
|
267
306
|
}
|
|
268
|
-
|
|
307
|
+
const result = observe();
|
|
308
|
+
assertOwnedHead();
|
|
309
|
+
return result;
|
|
269
310
|
}
|
|
270
311
|
/** The admitted Memory finish operation. A retained intent never licenses a
|
|
271
312
|
* second dispatch: replay first observes completed Git effects, or reports unknown. */
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { observeMemoryGitPolicySelection } from "./memory-init-git-policy.js";
|
|
7
|
+
import { observeReviewedMemoryGitIntegration } from "./memory-git-integration.js";
|
|
8
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
9
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
10
|
+
import { readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
11
|
+
import { readMemoryFixPublication } from "./memory-fix-publication.js";
|
|
12
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
13
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
14
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
15
|
+
const logical = "runtime/git-policy.json";
|
|
16
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
17
|
+
function refuse(reason) {
|
|
18
|
+
throw new AppError("memory_fix_finish_integration_invalid", "Repair finish requires accepted review and project-policy Git readback", 2, { reason, effect: "no_effect" });
|
|
19
|
+
}
|
|
20
|
+
/** Read-only explicit policy preparation before RUN creation. The factory must
|
|
21
|
+
* retain these bytes in the frozen bundle; this grants no commit/merge/push. */
|
|
22
|
+
export function prepareMemoryFixGitPolicy(environment, input) {
|
|
23
|
+
if (!path.isAbsolute(input.gitExecutable))
|
|
24
|
+
refuse("absolute_git_executable_required");
|
|
25
|
+
const gitExecutable = fs.realpathSync(input.gitExecutable);
|
|
26
|
+
const policy = observeMemoryGitPolicySelection(environment, { ...input, gitExecutable });
|
|
27
|
+
const value = { schema_id: "dd-flow/mb-fix-git-policy@1", scope: {
|
|
28
|
+
requestId: input.scope.requestId, projectRoot: input.scope.projectRoot, workspaceRoot: input.scope.workspaceRoot
|
|
29
|
+
},
|
|
30
|
+
git_executable: gitExecutable, git_sha256: hash(readBoundedReleaseFile(gitExecutable, 64 * 1024 * 1024)), policy };
|
|
31
|
+
const bytes = Buffer.from(JSON.stringify(value));
|
|
32
|
+
return { path: logical, bytes, sha256: hash(bytes) };
|
|
33
|
+
}
|
|
34
|
+
/** Frozen/SQL custody only, suitable for the acceptance writer. No ambient
|
|
35
|
+
* main-branch inference, tool discovery or filesystem access in this reader. */
|
|
36
|
+
export function readMemoryFixGitPolicy(context, scope, bundle) {
|
|
37
|
+
if (bundle.manifest.flow_id !== "mb-fix")
|
|
38
|
+
refuse("memory_fix_bundle_required");
|
|
39
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
40
|
+
const references = bundle.manifest.inventory.filter(ref => ref.path === logical), bytes = bundle.blob(logical);
|
|
41
|
+
if (references.length !== 1 || !bytes.length || bytes.length > 32768 || references[0].sha256 !== hash(bytes))
|
|
42
|
+
refuse("frozen_git_policy_required");
|
|
43
|
+
let value;
|
|
44
|
+
try {
|
|
45
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return refuse("invalid_git_policy");
|
|
49
|
+
}
|
|
50
|
+
if (value?.schema_id !== "dd-flow/mb-fix-git-policy@1" || Object.keys(value).sort().join() !== "git_executable,git_sha256,policy,schema_id,scope"
|
|
51
|
+
|| value.scope?.projectRoot !== run.project_root || value.scope?.workspaceRoot !== run.workspace_root
|
|
52
|
+
|| !value.scope.requestId || value.scope.requestId !== bundle.execution.snapshot().scope.requestId
|
|
53
|
+
|| typeof value.git_executable !== "string" || !/^[a-f0-9]{64}$/.test(value.git_sha256)
|
|
54
|
+
|| value.policy?.source !== "explicit_request" || value.policy.push_authorized !== false
|
|
55
|
+
|| !["feature_worktree", "integration_branch_direct"].includes(value.policy.route)
|
|
56
|
+
|| !value.policy.repository || !value.policy.workspace_branch || !value.policy.integration_branch)
|
|
57
|
+
refuse("git_policy_scope_changed");
|
|
58
|
+
return structuredClone(value);
|
|
59
|
+
}
|
|
60
|
+
/** Selected repair changes plus the unchanged mandatory MB index anchor, not
|
|
61
|
+
* a whole-tree commit inventory. Only existing selected files may be removed. */
|
|
62
|
+
export function memoryFixGitInventory(publication) {
|
|
63
|
+
const before = new Map(publication.plan.state(0).entries().map(entry => [entry.path, entry]));
|
|
64
|
+
const after = new Map(publication.plan.state(publication.plan.actions().length).entries().map(entry => [entry.path, entry]));
|
|
65
|
+
const references = [], removals = [];
|
|
66
|
+
for (const logical of new Set([...publication.binding.write_scope.paths, ".memory-bank/index.md"])) {
|
|
67
|
+
const entry = after.get(logical);
|
|
68
|
+
if (entry) {
|
|
69
|
+
if (entry.kind !== "file" || !entry.sha256)
|
|
70
|
+
refuse("regular_reviewed_file_required");
|
|
71
|
+
references.push({ root: "workspace", path: logical, sha256: entry.sha256, ...((entry.mode & 0o111) ? { git_mode: "100755" } : {}) });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const original = before.get(logical);
|
|
75
|
+
if (logical === ".memory-bank/index.md" || !original?.sha256 || original.kind !== "file")
|
|
76
|
+
refuse("retained_removal_required");
|
|
77
|
+
removals.push({ root: "workspace", path: logical, sha256: original.sha256, ...((original.mode & 0o111) ? { git_mode: "100755" } : {}) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { references, removals };
|
|
81
|
+
}
|
|
82
|
+
/** Accepted review/policy/publication inputs for the shared readback and scoped
|
|
83
|
+
* dispatcher. SQL custody only; this does not grant Git mutation by itself. */
|
|
84
|
+
export function prepareMemoryFixReviewedGitInputs(context, scope, bundle) {
|
|
85
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
86
|
+
refuse("preparation_outside_writer_required");
|
|
87
|
+
const selected = readMemoryFixGitPolicy(context, scope, bundle);
|
|
88
|
+
const keys = entityReferenceVariants(scope.runId).map(ref => stageLifecycleMatchKey("stage_finish", ref, "review", selected.scope.projectRoot));
|
|
89
|
+
const read = (writer) => {
|
|
90
|
+
const policy = readMemoryFixGitPolicy(writer, scope, bundle), review = readAcceptedStageData(writer, { ...scope, stage: "review" });
|
|
91
|
+
const settled = readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "review" }, bundle, keys);
|
|
92
|
+
const publication = readMemoryFixPublication(writer, { project_id: scope.projectId, run_id: scope.runId });
|
|
93
|
+
const result = JSON.parse(review.bytes.toString("utf8"));
|
|
94
|
+
if (review.data_schema_id !== "dd-flow/mb-fix-stage-result@1" || result.run_id !== scope.runId || result.stage !== "review"
|
|
95
|
+
|| !publication?.completed || publication.binding.bundle_sha256 !== bundle.sha256
|
|
96
|
+
|| hybridDigest(result.details.write_scope) !== hybridDigest(publication.binding.write_scope)
|
|
97
|
+
|| result.details.verification.some(item => item.verdict !== "resolved")
|
|
98
|
+
|| publication.binding.write_scope.finding_ids.some(id => !result.details.verification.some(item => item.finding_id === id && item.verdict === "resolved")
|
|
99
|
+
|| !result.details.review.some(item => item.finding_id === id && item.disposition === "accept")))
|
|
100
|
+
refuse("accepted_repair_review_required");
|
|
101
|
+
return { policy, review, settled, binding: publication.binding, planSha256: publication.plan.sha256, inventory: memoryFixGitInventory(publication) };
|
|
102
|
+
};
|
|
103
|
+
const original = context.db.readSnapshot(() => read(context));
|
|
104
|
+
return { selected, original, assertCustody: (writer) => {
|
|
105
|
+
if (hybridDigest(read(writer)) !== hybridDigest(original))
|
|
106
|
+
refuse("accepted_integration_custody_changed");
|
|
107
|
+
} };
|
|
108
|
+
}
|
|
109
|
+
/** Current physical readback of independently admitted review/policy inputs. */
|
|
110
|
+
export function prepareMemoryFixFinishIntegration(context, scope, bundle) {
|
|
111
|
+
const inputs = prepareMemoryFixReviewedGitInputs(context, scope, bundle), { selected, original } = inputs;
|
|
112
|
+
const observe = () => {
|
|
113
|
+
if (hash(readBoundedReleaseFile(selected.git_executable, 64 * 1024 * 1024)) !== selected.git_sha256)
|
|
114
|
+
refuse("selected_git_changed");
|
|
115
|
+
const observation = observeReviewedMemoryGitIntegration({ projectRoot: selected.scope.projectRoot, workspaceRoot: selected.scope.workspaceRoot,
|
|
116
|
+
integrationBranch: selected.policy.integration_branch, gitExecutable: selected.git_executable }, context.env, original.inventory.references, original.inventory.removals);
|
|
117
|
+
if (observation.repository !== selected.policy.repository || observation.workspace_branch !== selected.policy.workspace_branch)
|
|
118
|
+
refuse("policy_repository_or_branch_changed");
|
|
119
|
+
return observation;
|
|
120
|
+
};
|
|
121
|
+
const observation = observe();
|
|
122
|
+
return { value: () => structuredClone(observation), assertCustody: inputs.assertCustody, revalidate: () => {
|
|
123
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
124
|
+
refuse("physical_revalidation_outside_writer_required");
|
|
125
|
+
inputs.assertCustody(context);
|
|
126
|
+
if (hybridDigest(observe()) !== hybridDigest(observation))
|
|
127
|
+
refuse("integration_readback_changed");
|
|
128
|
+
} };
|
|
129
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
6
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
7
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
8
|
+
import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
9
|
+
import { prepareMemoryFixReviewedGitInputs, prepareMemoryFixFinishIntegration } from "./memory-fix-finish-integration.js";
|
|
10
|
+
import { integrateReviewedMemoryFiles } from "./memory-finish-integration.js";
|
|
11
|
+
import { readMemoryFixPublication } from "./memory-fix-publication.js";
|
|
12
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
13
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
14
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
15
|
+
import { appendAudit } from "./audit.js";
|
|
16
|
+
const event = "memory_fix.integration_intent", anchorPath = '$.variables."memory_fix.integration"';
|
|
17
|
+
function refuse(reason) {
|
|
18
|
+
throw new AppError("memory_fix_git_dispatch_invalid", "Repair integration requires the original scoped reviewed operation", 2, { reason, effect: "no_effect" });
|
|
19
|
+
}
|
|
20
|
+
function pending(reason) {
|
|
21
|
+
throw new AppError("memory_fix_integration_pending", "Reconcile the original Git operation before redispatch", 1, { reason, effect: "unknown", next_action: "reconcile_original_integration" });
|
|
22
|
+
}
|
|
23
|
+
/** Explicit scoped local commit/merge only. Existing Git mechanics own writes;
|
|
24
|
+
* each phase has retained intent before effects. Never push, reset, autostash or
|
|
25
|
+
* repeat an unresolved phase. A proven commit can continue to its first merge.
|
|
26
|
+
*/
|
|
27
|
+
export function integrateMemoryFixFinish(context, input) {
|
|
28
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
29
|
+
refuse("preparation_outside_writer_required");
|
|
30
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
|
|
31
|
+
if (!bundle || bundle.manifest.flow_id !== "mb-fix")
|
|
32
|
+
refuse("memory_fix_bundle_required");
|
|
33
|
+
const scope = { projectId: project.id, runId: input.runId, stage: "finish" };
|
|
34
|
+
const stage = readDescriptorStageInput(context, scope, bundle), actor = readActiveDescriptorStageCoordinator(context, scope, bundle);
|
|
35
|
+
const reviewed = prepareMemoryFixReviewedGitInputs(context, scope, bundle), { selected, original } = reviewed;
|
|
36
|
+
const delivered = readPreparedMemoryFlowUserInputs(context, scope, "mb-fix", bundle), userDigest = hybridDigest(delivered);
|
|
37
|
+
const agreement = input.agreement ?? { user_inputs_sha256: original.binding.user_inputs_sha256, approval_source: original.binding.write_scope.approval_source };
|
|
38
|
+
if (agreement.user_inputs_sha256 !== userDigest || agreement.approval_source?.root !== "run"
|
|
39
|
+
|| !delivered.sources.some(source => ["request", "answer"].includes(source.role)
|
|
40
|
+
&& source.path === agreement.approval_source.path && source.sha256 === agreement.approval_source.sha256))
|
|
41
|
+
refuse("current_delivered_agreement_required");
|
|
42
|
+
const selection = { projectRoot: selected.scope.projectRoot, workspaceRoot: selected.scope.workspaceRoot,
|
|
43
|
+
integrationBranch: selected.policy.integration_branch, gitExecutable: selected.git_executable };
|
|
44
|
+
const identity = hybridDigest({ scope, bundle_sha256: bundle.sha256, stage, actor, reviewed: original, agreement });
|
|
45
|
+
const intents = (writer) => {
|
|
46
|
+
const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 3", [project.id, event, input.runId]);
|
|
47
|
+
const anchor = writer.db.get("SELECT json_extract(index_json,?) identity FROM runs WHERE project_id=? AND id=?", [anchorPath, project.id, input.runId])?.identity;
|
|
48
|
+
if (rows.length > 2 || rows.some(row => Buffer.byteLength(row.payload_json) > 32768) || !!rows.length !== !!anchor || anchor && anchor !== identity)
|
|
49
|
+
pending("integration_intent_changed");
|
|
50
|
+
const values = rows.map(row => JSON.parse(row.payload_json));
|
|
51
|
+
if (new Set(values.map(value => value.phase)).size !== values.length || values.some(value => value.run_id !== input.runId || value.identity !== identity
|
|
52
|
+
|| !["commit", "merge"].includes(value.phase) || typeof value.baseline !== "string" || value.baseline !== "" && !/^[a-f0-9]{40,64}$/.test(value.baseline)))
|
|
53
|
+
pending("integration_intent_changed");
|
|
54
|
+
return values;
|
|
55
|
+
};
|
|
56
|
+
const assertTool = () => {
|
|
57
|
+
if (createHash("sha256").update(readBoundedReleaseFile(selected.git_executable, 64 * 1024 * 1024)).digest("hex") !== selected.git_sha256)
|
|
58
|
+
refuse("selected_git_changed");
|
|
59
|
+
};
|
|
60
|
+
const assertCustody = (writer) => {
|
|
61
|
+
reviewed.assertCustody(writer);
|
|
62
|
+
if (hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(stage)
|
|
63
|
+
|| hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(actor)
|
|
64
|
+
|| hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)) !== userDigest)
|
|
65
|
+
refuse("integration_authority_changed");
|
|
66
|
+
};
|
|
67
|
+
const assertPublication = () => {
|
|
68
|
+
assertTool();
|
|
69
|
+
assertCustody(context);
|
|
70
|
+
const publication = readMemoryFixPublication(context, { project_id: project.id, run_id: input.runId });
|
|
71
|
+
if (!publication?.completed || publication.plan.sha256 !== original.planSha256)
|
|
72
|
+
refuse("completed_publication_required");
|
|
73
|
+
const actual = publication.plan.classify(selection.workspaceRoot);
|
|
74
|
+
if (actual.completed !== actual.total)
|
|
75
|
+
refuse("publication_not_observed");
|
|
76
|
+
assertUnselectedIndexAnchor();
|
|
77
|
+
};
|
|
78
|
+
const env = Object.fromEntries(Object.entries(context.env).filter(([name]) => !name.startsWith("GIT_")));
|
|
79
|
+
const gitRead = (args) => {
|
|
80
|
+
const result = spawnSync(selected.git_executable, ["-c", "core.fsmonitor=false", "-C", selection.workspaceRoot, ...args], { env: { ...env, GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" }, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
|
|
81
|
+
if (result.error || result.signal || result.status !== 0)
|
|
82
|
+
refuse("git_readback_failed");
|
|
83
|
+
return result.stdout;
|
|
84
|
+
};
|
|
85
|
+
const previous = intents(context);
|
|
86
|
+
assertPublication();
|
|
87
|
+
try {
|
|
88
|
+
const proof = prepareMemoryFixFinishIntegration(context, scope, bundle);
|
|
89
|
+
proof.revalidate();
|
|
90
|
+
return { integrated: true, replayed: true, integration: proof.value() };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof AppError) || error.code !== "memory_git_integration_invalid")
|
|
94
|
+
throw error;
|
|
95
|
+
if (previous.length) {
|
|
96
|
+
const commit = previous.length === 1 && previous[0].phase === "commit" ? previous[0] : null;
|
|
97
|
+
if (!commit || selection.workspaceRoot === selection.projectRoot
|
|
98
|
+
|| gitRead(["log", "-1", "--format=%P%n%s"]).toString("utf8").trim() !== `${commit.baseline}\nchore(${input.runId.toLowerCase()}): integrate reviewed Memory Bank`.trim())
|
|
99
|
+
pending("original_git_effect_not_proven");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// The required index anchor is observation coverage, not permission to add
|
|
103
|
+
// an unrelated dirty/untracked index document to this repair's commit.
|
|
104
|
+
function assertUnselectedIndexAnchor() {
|
|
105
|
+
if (original.binding.write_scope.paths.includes(".memory-bank/index.md"))
|
|
106
|
+
return;
|
|
107
|
+
const index = original.inventory.references.find(ref => ref.path === ".memory-bank/index.md");
|
|
108
|
+
if (createHash("sha256").update(gitRead(["cat-file", "blob", "HEAD:.memory-bank/index.md"])).digest("hex") !== index.sha256
|
|
109
|
+
|| !gitRead(["ls-tree", "-z", "HEAD", "--", ".memory-bank/index.md"]).toString("utf8").startsWith(`${index.git_mode ?? "100644"} blob `))
|
|
110
|
+
refuse("unselected_index_anchor_not_committed");
|
|
111
|
+
if (gitRead(["diff", "--cached", "--name-only", "-z", "--", ".memory-bank/index.md"]).length)
|
|
112
|
+
refuse("unselected_index_anchor_staged");
|
|
113
|
+
}
|
|
114
|
+
let dispatched = false;
|
|
115
|
+
try {
|
|
116
|
+
integrateReviewedMemoryFiles(selection, context.env, original.inventory.references, input.runId, (phase, baseline, action) => {
|
|
117
|
+
assertPublication();
|
|
118
|
+
context.db.writeTransaction(() => {
|
|
119
|
+
assertCustody(context);
|
|
120
|
+
const current = intents(context);
|
|
121
|
+
if (current.some(value => value.phase === phase))
|
|
122
|
+
pending("original_git_phase_unresolved");
|
|
123
|
+
appendAudit(context, { projectId: project.id, eventType: event, payload: { run_id: input.runId, identity, phase, baseline } });
|
|
124
|
+
context.db.run("UPDATE runs SET index_json=json_set(index_json,?,?) WHERE project_id=? AND id=?", [anchorPath, identity, project.id, input.runId]);
|
|
125
|
+
});
|
|
126
|
+
dispatched = true;
|
|
127
|
+
assertUnselectedIndexAnchor();
|
|
128
|
+
action();
|
|
129
|
+
}, selected.policy, original.inventory.removals);
|
|
130
|
+
const proof = prepareMemoryFixFinishIntegration(context, scope, bundle);
|
|
131
|
+
proof.revalidate();
|
|
132
|
+
return { integrated: true, replayed: false, integration: proof.value() };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!dispatched)
|
|
136
|
+
throw error;
|
|
137
|
+
return pending(error instanceof AppError ? error.code : "git_operation_failed");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { assertDiagnosticRepairSourceIntakeCustody, prepareDiagnosticRepairSourceIntake, readDiagnosticRepairSourceIntake } from "./diagnostic-repair-source-intake.js";
|
|
2
|
+
// Preserve this flow's own capsule identity; shared transport grants no approval.
|
|
3
|
+
export function assertMemoryFixSourceIntakeCustody(context, scope, bundle, expectedSha256) {
|
|
4
|
+
return assertDiagnosticRepairSourceIntakeCustody(context, scope, bundle, expectedSha256, "mb-fix");
|
|
5
|
+
}
|
|
6
|
+
export function prepareMemoryFixSourceIntake(context, input) {
|
|
7
|
+
return prepareDiagnosticRepairSourceIntake(context, input, "mb-fix");
|
|
8
|
+
}
|
|
9
|
+
export function readMemoryFixSourceIntake(context, input, retainedBundle) {
|
|
10
|
+
return readDiagnosticRepairSourceIntake(context, input, "mb-fix", retainedBundle);
|
|
11
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
6
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
7
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
8
|
+
import { PreparedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
9
|
+
import { readAcceptedStageData, prepareStagePacket, acceptStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
|
|
10
|
+
import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
|
|
11
|
+
import { prepareMemoryFixScopeEvidence } from "./memory-fix-scope.js";
|
|
12
|
+
import { validateMemoryFixStageResult } from "./memory-fix-stage-results.js";
|
|
13
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
14
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("memory_fix_intake_settlement_invalid", "Repair intake requires accepted scope and original source observations", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Historical SQL read only: repaired files and later user turns must not
|
|
19
|
+
* replace an already accepted intake baseline. This grants no new write scope.
|
|
20
|
+
* Reads accepted Stage bytes itself, never caller-provided result JSON. The
|
|
21
|
+
* caller still needs ordinary coordinator settlement for Stage replay. Retained
|
|
22
|
+
* workspace_root is provenance only after restore, never a fresh write target.
|
|
23
|
+
*/
|
|
24
|
+
export function readAcceptedMemoryFixIntakeEvidence(context, scope, file) {
|
|
25
|
+
const accepted = readAcceptedStageData(context, { ...scope, stage: "intake" });
|
|
26
|
+
const rows = 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')='intake' LIMIT 33", [scope.projectId, scope.runId]);
|
|
27
|
+
if (rows.length > 32)
|
|
28
|
+
refuse("ambiguous_intake_evidence");
|
|
29
|
+
const packets = rows.map(row => {
|
|
30
|
+
if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
|
|
31
|
+
refuse("intake_evidence_exceeds_bound");
|
|
32
|
+
return JSON.parse(row.payload_json);
|
|
33
|
+
}).filter(packet => packet && typeof packet.file === "string"
|
|
34
|
+
&& resolveStagePublicationPath(context, scope.projectId, scope.runId, packet.file) === file);
|
|
35
|
+
if (packets.length !== 1)
|
|
36
|
+
refuse("intake_evidence_missing");
|
|
37
|
+
const packet = packets[0];
|
|
38
|
+
if (typeof packet.base64 !== "string")
|
|
39
|
+
refuse("intake_evidence_changed");
|
|
40
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
41
|
+
if (!bytes.length || bytes.length > 4 * 1024 * 1024 || bytes.toString("base64") !== packet.base64
|
|
42
|
+
|| hash(bytes) !== packet.sha256)
|
|
43
|
+
refuse("intake_evidence_changed");
|
|
44
|
+
let evidence, result;
|
|
45
|
+
try {
|
|
46
|
+
evidence = JSON.parse(bytes.toString("utf8"));
|
|
47
|
+
result = JSON.parse(accepted.bytes.toString("utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return refuse("intake_evidence_changed");
|
|
51
|
+
}
|
|
52
|
+
if (!evidence || !result?.details || accepted.data_schema_id !== "dd-flow/mb-fix-stage-result@1"
|
|
53
|
+
|| result.schema_id !== "dd-flow/mb-fix-stage-result@1" || result.run_id !== scope.runId || result.stage !== "intake"
|
|
54
|
+
|| result.input_sha256 !== accepted.input_sha256 || evidence.intake_sha256 !== result.intake_sha256
|
|
55
|
+
|| evidence.source_observation_sha256 !== result.source_observation_sha256
|
|
56
|
+
|| hybridDigest(evidence.source_observation) !== result.source_observation_sha256
|
|
57
|
+
|| hybridDigest(evidence.routing) !== hybridDigest(result.details.routing)
|
|
58
|
+
|| hybridDigest(evidence.write_scope) !== hybridDigest(result.details.write_scope)
|
|
59
|
+
|| evidence.write_scope && evidence.user_inputs_sha256 !== evidence.write_scope.user_inputs_sha256)
|
|
60
|
+
refuse("accepted_intake_changed");
|
|
61
|
+
return structuredClone(evidence);
|
|
62
|
+
}
|
|
63
|
+
/** Internal family handler. Generic runtime owns hook/Session identity and
|
|
64
|
+
* atomic completion; the scope packet is retained in that same SQL writer.
|
|
65
|
+
* Public registration waits for the complete repair/verification family.
|
|
66
|
+
*/
|
|
67
|
+
export function finishMemoryFixIntake(context, input) {
|
|
68
|
+
if (input.stage !== "intake")
|
|
69
|
+
refuse("intake_stage_required");
|
|
70
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
71
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
72
|
+
if (!bundle || bundle.manifest.flow_id !== "mb-fix")
|
|
73
|
+
refuse("memory_fix_bundle_required");
|
|
74
|
+
const scope = { projectId: project.id, runId: input.runId, stage: "intake" };
|
|
75
|
+
const source = readMemoryFixSourceIntake(context, input, bundle);
|
|
76
|
+
const index = context.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
77
|
+
const replay = JSON.parse(index.index_json).stage_runs.some((stage) => stage.stage === "intake" && stage.status === "done");
|
|
78
|
+
const packet = readDescriptorStageInput(context, scope, bundle, true, replay);
|
|
79
|
+
const file = path.join(packet.runRoot, bundle.descriptor.stage("intake").directory, "intake-evidence.json");
|
|
80
|
+
if (replay) {
|
|
81
|
+
const accepted = readAcceptedStageData(context, scope);
|
|
82
|
+
if (!accepted.bytes.equals(Buffer.from(input.bytes)))
|
|
83
|
+
refuse("accepted_result_changed");
|
|
84
|
+
const evidence = readAcceptedMemoryFixIntakeEvidence(context, scope, file);
|
|
85
|
+
if (evidence.intake_sha256 !== source.sha256)
|
|
86
|
+
refuse("source_intake_changed");
|
|
87
|
+
const settled = PreparedDescriptorStageSettlement.prepare(context, input);
|
|
88
|
+
return settled.apply(context, writer => {
|
|
89
|
+
assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
|
|
90
|
+
const current = readAcceptedStageData(writer, scope);
|
|
91
|
+
if (!current.bytes.equals(accepted.bytes)
|
|
92
|
+
|| hybridDigest(readAcceptedMemoryFixIntakeEvidence(writer, scope, file)) !== hybridDigest(evidence))
|
|
93
|
+
refuse("accepted_intake_changed");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!input.bytes.length || input.bytes.length > 1024 * 1024)
|
|
97
|
+
refuse("result_exceeds_bound");
|
|
98
|
+
let proposed;
|
|
99
|
+
try {
|
|
100
|
+
proposed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return refuse("invalid_result_json");
|
|
104
|
+
}
|
|
105
|
+
if (!proposed?.details || typeof proposed.details !== "object")
|
|
106
|
+
refuse("invalid_result_details");
|
|
107
|
+
const prepared = prepareMemoryFixScopeEvidence(context, { ...input, routing: proposed.details.routing, writeScope: proposed.details.write_scope });
|
|
108
|
+
const evidence = prepared.snapshot();
|
|
109
|
+
validateMemoryFixStageResult(input.bytes, bundle.descriptor.stageBlob("intake", "result_schema"), {
|
|
110
|
+
runId: input.runId, stage: "intake", inputSha256: packet.inputSha256, intakeSha256: source.sha256,
|
|
111
|
+
sourceObservationSha256: evidence.source_observation_sha256, findingIds: source.intake.selected_findings.map(finding => finding.id),
|
|
112
|
+
projectRoot: evidence.workspace_root, runRoot: packet.runRoot, predecessors: [], writeScope: evidence.write_scope, repairReceipt: null
|
|
113
|
+
});
|
|
114
|
+
const evidenceBytes = Buffer.from(JSON.stringify(evidence));
|
|
115
|
+
if (evidenceBytes.length > 4 * 1024 * 1024)
|
|
116
|
+
refuse("intake_evidence_exceeds_bound");
|
|
117
|
+
const retained = prepareStagePacket(file, evidenceBytes);
|
|
118
|
+
const settled = PreparedDescriptorStageSettlement.prepare(context, input);
|
|
119
|
+
prepared.assertSourcesCurrent();
|
|
120
|
+
return settled.apply(context, writer => {
|
|
121
|
+
prepared.assertCustody(writer);
|
|
122
|
+
const current = writer.db.get("SELECT index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
123
|
+
if (JSON.parse(current.index_json).stage_runs.some((stage) => stage.stage === "intake" && stage.status === "done"))
|
|
124
|
+
refuse("intake_already_accepted");
|
|
125
|
+
acceptStagePacket(writer, { ...scope, prepared: retained });
|
|
126
|
+
});
|
|
127
|
+
}
|