@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.
Files changed (226) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,148 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
6
+ import { ObservedMemorySources } from "./memory-source-observations.js";
7
+ import { memorySourceKey } from "./memory-stage-results.js";
8
+ import { localEnvironmentAllowed } from "./workspace-files.js";
9
+ function refuse(reason) {
10
+ throw new AppError("interactive_before_versions_invalid", "Interactive must retain selected original versions before changing them", 2, { reason, effect: "no_effect" });
11
+ }
12
+ function safe(name) {
13
+ return typeof name === "string" && !!name && name.length <= 4096 && !path.posix.isAbsolute(name) && !path.win32.isAbsolute(name)
14
+ && !name.includes("\\") && !name.includes("\0") && name.split("/").every(part => part && part !== "." && part !== ".."
15
+ && ![".git", ".dd-flow", "node_modules"].includes(part.normalize("NFC").toLowerCase()) && localEnvironmentAllowed(part));
16
+ }
17
+ function absent(root, logical) {
18
+ let current = root;
19
+ const parts = logical.split("/");
20
+ for (let index = 0; index < parts.length; index++) {
21
+ current = path.join(current, parts[index]);
22
+ try {
23
+ const stat = fs.lstatSync(current);
24
+ if (index === parts.length - 1 || !stat.isDirectory())
25
+ refuse("unobserved_existing_source");
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT")
29
+ return;
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ function rootIdentity(baseline) {
35
+ const value = baseline.snapshot(), actual = fs.lstatSync(value.workspace);
36
+ if (!actual.isDirectory() || fs.realpathSync(value.workspace) !== value.workspace
37
+ || actual.dev !== value.workspace_identity.dev || actual.ino !== value.workspace_identity.ino)
38
+ refuse("workspace_identity_changed");
39
+ return value;
40
+ }
41
+ /** Before-image evidence, not write approval. Reuses the existing bounded,
42
+ * private-file-aware source reader. No backup file, Git object, RUN or project
43
+ * mutation is created; the request/Work owner must retain these bytes first. */
44
+ export class PreparedInteractiveBeforeVersions {
45
+ sha256;
46
+ #value;
47
+ #baseline;
48
+ #observed;
49
+ constructor(value, baseline, observed) {
50
+ if (Buffer.byteLength(JSON.stringify(value)) > 96 * 1024 * 1024)
51
+ refuse("before_versions_exceed_bound");
52
+ this.#value = structuredClone(value);
53
+ this.#baseline = baseline;
54
+ this.#observed = observed;
55
+ this.sha256 = createHash("sha256").update(JSON.stringify(value)).digest("hex");
56
+ Object.freeze(this);
57
+ }
58
+ static capture(baseline, paths) {
59
+ if (!(baseline instanceof PreparedInteractiveGitBaseline) || !Array.isArray(paths) || paths.length > 20_000
60
+ || paths.some(name => !safe(name)) || new Set(paths).size !== paths.length)
61
+ refuse("invalid_selected_paths");
62
+ const source = rootIdentity(baseline), byPath = new Map(source.entries.map(item => [item.path, item]));
63
+ const entries = [], files = [];
64
+ for (const name of [...paths].sort()) {
65
+ const before = byPath.get(name);
66
+ if (!before || before.kind === "missing") {
67
+ absent(source.workspace, name);
68
+ entries.push({ path: name, kind: "missing", mode: null, sha256: null, base64: null });
69
+ }
70
+ else {
71
+ if (before.kind !== "file")
72
+ refuse("regular_file_scope_required");
73
+ const actual = fs.lstatSync(path.join(source.workspace, name));
74
+ if (!actual.isFile() || actual.nlink !== 1 || (actual.mode & 0o7777) !== before.mode)
75
+ refuse("before_version_changed");
76
+ files.push(name);
77
+ entries.push({ path: name, kind: "file", mode: before.mode, sha256: before.sha256, base64: null });
78
+ }
79
+ }
80
+ const observed = ObservedMemorySources.capture({ workspace: source.workspace }, files.map(name => ({ root: "workspace", path: name })));
81
+ const hashes = observed.hashes();
82
+ for (const item of entries.filter(item => item.kind === "file")) {
83
+ if (hashes.get(memorySourceKey("workspace", item.path)) !== item.sha256)
84
+ refuse("before_version_changed");
85
+ item.base64 = observed.bytes({ root: "workspace", path: item.path }).toString("base64");
86
+ }
87
+ const prepared = new PreparedInteractiveBeforeVersions({ schema_id: "dd-flow/interactive-before-versions@1",
88
+ baseline_sha256: baseline.sha256, workspace: source.workspace, entries }, baseline, observed);
89
+ prepared.revalidate();
90
+ return prepared;
91
+ }
92
+ snapshot() { return structuredClone(this.#value); }
93
+ bytes() { return Buffer.from(JSON.stringify(this.#value)); }
94
+ reference() { return { path: "runtime/before-versions.json", sha256: this.sha256 }; }
95
+ revalidate() {
96
+ rootIdentity(this.#baseline);
97
+ this.#observed.revalidate();
98
+ for (const item of this.#value.entries) {
99
+ if (item.kind === "missing")
100
+ absent(this.#value.workspace, item.path);
101
+ else {
102
+ const stat = fs.lstatSync(path.join(this.#value.workspace, item.path));
103
+ if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o7777) !== item.mode)
104
+ refuse("before_version_changed");
105
+ }
106
+ }
107
+ }
108
+ }
109
+ /** Historical parsing against an independently selected retained baseline.
110
+ * Never re-read the now-edited project to reconstruct its original content. */
111
+ export function restoreInteractiveBeforeVersions(bytes, expectedSha256, baseline) {
112
+ if (!(baseline instanceof PreparedInteractiveGitBaseline) || !bytes.length || bytes.length > 96 * 1024 * 1024
113
+ || createHash("sha256").update(bytes).digest("hex") !== expectedSha256)
114
+ refuse("retained_before_versions_changed");
115
+ let value;
116
+ try {
117
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
118
+ }
119
+ catch {
120
+ return refuse("invalid_retained_before_versions");
121
+ }
122
+ const original = baseline.snapshot(), byPath = new Map(original.entries.map(item => [item.path, item]));
123
+ if (!value || Object.keys(value).sort().join() !== "baseline_sha256,entries,schema_id,workspace"
124
+ || value.schema_id !== "dd-flow/interactive-before-versions@1" || value.baseline_sha256 !== baseline.sha256
125
+ || value.workspace !== original.workspace || !Array.isArray(value.entries) || value.entries.length > 20_000)
126
+ refuse("retained_before_scope_changed");
127
+ let previous = "", total = 0;
128
+ for (const item of value.entries) {
129
+ if (!item || Object.keys(item).sort().join() !== "base64,kind,mode,path,sha256" || !safe(item.path) || item.path <= previous)
130
+ refuse("invalid_retained_before_versions");
131
+ const before = byPath.get(item.path);
132
+ if (item.kind === "missing") {
133
+ if (before && before.kind !== "missing" || item.mode !== null || item.sha256 !== null || item.base64 !== null)
134
+ refuse("retained_absence_changed");
135
+ }
136
+ else {
137
+ if (item.kind !== "file" || before?.kind !== "file" || item.mode !== before.mode || item.sha256 !== before.sha256 || typeof item.base64 !== "string")
138
+ refuse("retained_original_changed");
139
+ const content = Buffer.from(item.base64, "base64");
140
+ total += content.length;
141
+ if (content.length > 16 * 1024 * 1024 || total > 64 * 1024 * 1024 || content.toString("base64") !== item.base64
142
+ || createHash("sha256").update(content).digest("hex") !== before.sha256)
143
+ refuse("retained_original_changed");
144
+ }
145
+ previous = item.path;
146
+ }
147
+ return structuredClone(value);
148
+ }
@@ -0,0 +1,85 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ const partLimit = 1024 * 1024, totalLimit = 32 * partLimit;
4
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
+ function refuse(reason) {
6
+ throw new AppError("interactive_bundle_data_invalid", "Interactive data must fit the existing retained bundle limits", 2, { reason, effect: "no_effect" });
7
+ }
8
+ function pathFor(logical, ordinal) { return `${logical}.parts/${String(ordinal).padStart(4, "0")}.bin`; }
9
+ function supported(logical) { if (!["runtime/git-baseline.json", "runtime/before-versions.json", "runtime/work-context.json"].includes(logical))
10
+ refuse("unsupported_data_role"); }
11
+ /** Packaging only, not admission. Keep ordinary small JSON unchanged; large
12
+ * before-images use explicit immutable parts instead of widening global limits. */
13
+ export function putInteractiveBundleData(blobs, logical, input) {
14
+ supported(logical);
15
+ if (!input.length || input.byteLength > totalLimit)
16
+ refuse("unbounded_data");
17
+ const bytes = Buffer.from(input), additions = new Map();
18
+ const sha256 = hash(bytes);
19
+ if (bytes.length <= partLimit)
20
+ additions.set(logical, bytes);
21
+ else {
22
+ const parts = [];
23
+ for (let offset = 0; offset < bytes.length; offset += partLimit) {
24
+ const part = Buffer.from(bytes.subarray(offset, offset + partLimit)), file = pathFor(logical, parts.length);
25
+ additions.set(file, part);
26
+ parts.push({ path: file, bytes: part.length, sha256: hash(part) });
27
+ }
28
+ const index = { schema_id: "dd-flow/interactive-bundle-parts@1", logical_path: logical, bytes: bytes.length, sha256, parts };
29
+ additions.set(logical, Buffer.from(JSON.stringify(index)));
30
+ }
31
+ const existing = new Set([...blobs.keys()].map(name => name.toLowerCase()));
32
+ if ([...additions.keys()].some(name => existing.has(name.toLowerCase())))
33
+ refuse("data_path_collision");
34
+ const total = [...blobs.values(), ...additions.values()].reduce((sum, value) => sum + value.length, 0);
35
+ if (total > totalLimit)
36
+ refuse("bundle_budget_exceeded");
37
+ for (const [file, content] of additions)
38
+ blobs.set(file, content);
39
+ return { sha256, reference: { path: logical, sha256: hash(additions.get(logical)) } };
40
+ }
41
+ /** Reads only the selected inventory, never nearby files or current project
42
+ * bytes. For multipart data reference hashes the physical index; sha256 hashes
43
+ * the reconstructed logical data. They intentionally are not interchangeable. */
44
+ export function readInteractiveBundleData(bundle, logical) {
45
+ supported(logical);
46
+ const physical = (file) => {
47
+ const refs = bundle.manifest.inventory.filter(item => item.path === file);
48
+ if (refs.length !== 1)
49
+ refuse("data_reference_required");
50
+ const bytes = bundle.blob(file);
51
+ if (!bytes.length || bytes.length > partLimit || hash(bytes) !== refs[0].sha256)
52
+ refuse("data_part_changed");
53
+ return { bytes, reference: refs[0] };
54
+ };
55
+ const root = physical(logical);
56
+ let value;
57
+ try {
58
+ value = JSON.parse(root.bytes.toString("utf8"));
59
+ }
60
+ catch {
61
+ value = null;
62
+ }
63
+ if (!value || typeof value !== "object" || value.schema_id !== "dd-flow/interactive-bundle-parts@1")
64
+ return { ...root, sha256: root.reference.sha256 };
65
+ const index = value;
66
+ if (Object.keys(index).sort().join() !== "bytes,logical_path,parts,schema_id,sha256" || index.logical_path !== logical
67
+ || !Number.isSafeInteger(index.bytes) || index.bytes <= partLimit || index.bytes > totalLimit
68
+ || typeof index.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(index.sha256) || !Array.isArray(index.parts)
69
+ || index.parts.length !== Math.ceil(index.bytes / partLimit))
70
+ refuse("invalid_data_index");
71
+ const parts = [];
72
+ for (const [ordinal, part] of index.parts.entries()) {
73
+ if (!part || Object.keys(part).sort().join() !== "bytes,path,sha256" || part.path !== pathFor(logical, ordinal)
74
+ || part.bytes !== Math.min(partLimit, index.bytes - ordinal * partLimit))
75
+ refuse("invalid_data_part");
76
+ const selected = physical(part.path);
77
+ if (selected.reference.sha256 !== part.sha256 || selected.bytes.length !== part.bytes)
78
+ refuse("data_part_changed");
79
+ parts.push(selected.bytes);
80
+ }
81
+ const bytes = Buffer.concat(parts);
82
+ if (bytes.length !== index.bytes || hash(bytes) !== index.sha256)
83
+ refuse("logical_data_changed");
84
+ return { bytes, sha256: index.sha256, reference: root.reference };
85
+ }
@@ -0,0 +1,179 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { hybridDigest } from "./hybrid-contracts.js";
3
+ import { appendAudit } from "./audit.js";
4
+ import { readInteractiveStageScope } from "./interactive-stage-entry.js";
5
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
6
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
+ import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
8
+ import { readInteractiveWorkGit } from "./interactive-work-start.js";
9
+ import { readInteractiveWorkHistory } from "./interactive-work-history.js";
10
+ import { codeCheckProfileHash, projectPolicyCheckDeclarations, runCodeChecks, PreparedCheckEvidence, validateCheckDeclaration } from "./code-checks.js";
11
+ import { memoryCheckEnvironment } from "./memory-verification-checks.js";
12
+ function refuse(reason) {
13
+ throw new AppError("interactive_checks_invalid", "Interactive checks require retained obligations and the exact current patch", 2, { reason, effect: "no_effect" });
14
+ }
15
+ const event = "interactive.check_plan";
16
+ function latest(context, scope) {
17
+ const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, event, scope.runId]);
18
+ if (!row)
19
+ return null;
20
+ if (Buffer.byteLength(row.payload_json) > 1024 * 1024)
21
+ refuse("unbounded_check_plan");
22
+ const value = JSON.parse(row.payload_json);
23
+ if (value.run_id !== scope.runId || value.plan?.run_id !== scope.runId || value.plan.schema_id !== "dd-flow/interactive-check-plan@1"
24
+ || value.sha256 !== hybridDigest(value.plan) || !Array.isArray(value.plan.checks))
25
+ refuse("changed_check_plan");
26
+ return value;
27
+ }
28
+ /** One existing aggregate check runner and receipt store. Selection is retained
29
+ * before execution, independently of the Stage proposal being assessed. */
30
+ export class PreparedInteractiveChecks {
31
+ sha256;
32
+ #selected;
33
+ #packet;
34
+ #changes;
35
+ #history;
36
+ #plan;
37
+ #environment;
38
+ constructor(selected, packet, changes, history, plan, environment) {
39
+ this.#selected = selected;
40
+ this.#packet = packet;
41
+ this.#changes = changes;
42
+ this.#history = history;
43
+ this.#plan = structuredClone(plan);
44
+ this.#environment = environment;
45
+ this.sha256 = hybridDigest(plan);
46
+ Object.freeze(this);
47
+ }
48
+ static prepare(context, input) {
49
+ if (context.db.isOwnedWriteTransaction?.())
50
+ refuse("preparation_outside_writer_required");
51
+ if (!["hardening", "review"].includes(input.stage) || !Array.isArray(input.additionalChecks) || input.additionalChecks.length > 4096)
52
+ refuse("invalid_check_selection");
53
+ const selected = readInteractiveStageScope(context, input), { scope, bundle, run } = selected;
54
+ const packet = readDescriptorStageInput(context, scope, bundle), frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
55
+ if (!run.run_root)
56
+ refuse("run_root_required");
57
+ if (selected.selected.needs_scope_refresh)
58
+ refuse("current_scope_required");
59
+ const checks = [...projectPolicyCheckDeclarations(frozen.workspace, "code"), ...projectPolicyCheckDeclarations(frozen.workspace, "readiness")];
60
+ const ids = new Set(checks.map(check => check.id));
61
+ for (const extra of input.additionalChecks) {
62
+ if (!extra || Object.keys(extra).sort().join() !== "command,id,purpose" || typeof extra.id !== "string" || !/^CHK-[A-Za-z0-9-]{1,64}$/.test(extra.id)
63
+ || ids.has(extra.id) || [extra.command, extra.purpose].some(value => typeof value !== "string" || !value.trim() || value.includes("\0") || Buffer.byteLength(value) > 16384))
64
+ refuse("invalid_check_selection");
65
+ ids.add(extra.id);
66
+ checks.push({ ...extra, source: "plan", availability: "available", run_at: "readiness" });
67
+ }
68
+ checks.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
69
+ for (const check of checks)
70
+ validateCheckDeclaration(frozen.workspace, check);
71
+ if (!checks.length && (typeof input.noChecksReason !== "string" || !input.noChecksReason.trim() || input.noChecksReason.includes("\0") || Buffer.byteLength(input.noChecksReason) > 16384))
72
+ refuse("explicit_no_checks_reason_required");
73
+ if (checks.length && input.noChecksReason !== undefined)
74
+ refuse("no_checks_reason_with_checks");
75
+ const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot,
76
+ workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
77
+ const plan = { schema_id: "dd-flow/interactive-check-plan@1", run_id: input.runId, stage: input.stage, attempt: packet.attempt,
78
+ bundle_sha256: bundle.sha256, input_sha256: packet.inputSha256, user_inputs_sha256: selected.selected.inputs.sha256,
79
+ scope_revision: selected.selected.revision, file_changes_sha256: hybridDigest(changes.snapshot().delta.changes),
80
+ profile_sha256: codeCheckProfileHash(frozen.workspace, checks), checks, no_checks_reason: checks.length ? null : input.noChecksReason };
81
+ if (Buffer.byteLength(JSON.stringify({ run_id: plan.run_id, sha256: "a".repeat(64), plan })) > 1024 * 1024)
82
+ refuse("unbounded_check_plan");
83
+ const prepared = new PreparedInteractiveChecks(selected, packet, changes, hybridDigest(readInteractiveWorkHistory(context, scope, bundle)), plan, memoryCheckEnvironment(context, scope.projectId, scope.runId));
84
+ prepared.revalidate(context);
85
+ return prepared;
86
+ }
87
+ static restore(context, input) {
88
+ const selected = readInteractiveStageScope(context, input), retained = latest(context, selected.scope);
89
+ if (!retained || retained.plan.stage !== input.stage)
90
+ refuse("retained_check_plan_required");
91
+ const prepared = this.prepare(context, { ...input, additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
92
+ .map(({ id, command, purpose }) => ({ id, command, purpose })),
93
+ ...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
94
+ if (prepared.sha256 !== retained.sha256)
95
+ refuse("retained_check_plan_changed");
96
+ return prepared;
97
+ }
98
+ /** Carry declared obligations into the initial critic's current Stage. The
99
+ * common evidence owner still rejects changed task/patch/policy or missing
100
+ * receipts; this does not execute or bless an author's test claims. */
101
+ static prepareReview(context, input) {
102
+ const selected = readInteractiveStageScope(context, { ...input, stage: "review" }), retained = latest(context, selected.scope);
103
+ if (!retained)
104
+ return null;
105
+ return this.prepare(context, { ...input, stage: "review", additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
106
+ .map(({ id, command, purpose }) => ({ id, command, purpose })),
107
+ ...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
108
+ }
109
+ value() { return structuredClone(this.#plan); }
110
+ checkInput() {
111
+ const { bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks } = this.#plan;
112
+ return { projectId: this.#selected.scope.projectId, runId: this.#plan.run_id, runHome: this.#packet.runRoot,
113
+ workspaceRoot: this.#selected.bundle.execution.snapshot().scope.workspace, scope: "aggregate",
114
+ checks: structuredClone(checks), expectedProfileHash: profile_sha256,
115
+ // Stage custody changes do not by themselves invalidate checks of the
116
+ // same task/patch/policy. The common runner still owns reuse eligibility.
117
+ evidenceContext: `interactive:${hybridDigest({ bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks })}`,
118
+ artifactDir: `${this.#selected.bundle.descriptor.stage(this.#plan.stage).directory}/checks/${this.sha256}`,
119
+ ...(this.#environment ? { checkEnvironment: this.#environment } : {}) };
120
+ }
121
+ assertSelection(context) {
122
+ this.#selected.assertCustody(context);
123
+ const { scope, bundle } = this.#selected, run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
124
+ if (run.status !== "running" || index.current_stage !== this.#plan.stage
125
+ || hybridDigest(readDescriptorStageInput(context, scope, bundle)) !== hybridDigest(this.#packet)
126
+ || hybridDigest(readInteractiveWorkHistory(context, scope, bundle)) !== this.#history)
127
+ refuse("check_selection_changed");
128
+ const previous = latest(context, scope);
129
+ if (previous && previous.plan.user_inputs_sha256 === this.#plan.user_inputs_sha256
130
+ && previous.plan.checks.some(check => !this.#plan.checks.some(current => hybridDigest(current) === hybridDigest(check))))
131
+ refuse("check_obligation_dropped");
132
+ }
133
+ revalidate(context) {
134
+ this.#changes.revalidate();
135
+ readInteractiveWorkGit(this.#selected.bundle);
136
+ if (codeCheckProfileHash(this.checkInput().workspaceRoot, this.#plan.checks) !== this.#plan.profile_sha256
137
+ || hybridDigest(memoryCheckEnvironment(context, this.#selected.scope.projectId, this.#plan.run_id) ?? null) !== hybridDigest(this.#environment ?? null))
138
+ refuse("check_policy_changed");
139
+ context.db.readSnapshot(() => this.assertSelection(context));
140
+ }
141
+ retain(context) {
142
+ this.revalidate(context);
143
+ return context.db.writeTransaction(() => {
144
+ this.#changes.assertCustody(context);
145
+ this.assertSelection(context);
146
+ if (latest(context, this.#selected.scope)?.sha256 === this.sha256)
147
+ return { changed: false };
148
+ appendAudit(context, { projectId: this.#selected.scope.projectId, eventType: event, payload: { run_id: this.#plan.run_id, sha256: this.sha256, plan: this.value() } });
149
+ return { changed: true };
150
+ });
151
+ }
152
+ assertCustody(context) {
153
+ this.#changes.assertCustody(context);
154
+ this.assertSelection(context);
155
+ if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
156
+ refuse("retained_check_plan_required");
157
+ }
158
+ async execute(context) {
159
+ this.retain(context);
160
+ if (!this.#plan.checks.length)
161
+ return [];
162
+ const receipts = await runCodeChecks(context, this.checkInput());
163
+ try {
164
+ this.revalidate(context);
165
+ }
166
+ catch (error) {
167
+ if (error instanceof AppError)
168
+ error.details.effect = "committed";
169
+ throw error;
170
+ }
171
+ return receipts;
172
+ }
173
+ evidence(context) {
174
+ this.revalidate(context);
175
+ if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
176
+ refuse("retained_check_plan_required");
177
+ return this.#plan.checks.length ? PreparedCheckEvidence.prepare(context, this.checkInput()) : null;
178
+ }
179
+ }
@@ -0,0 +1,33 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { FrozenGenericExecution } from "./generic-execution.js";
3
+ import { PreparedInteractiveAttachedSession } from "./interactive-attached-session.js";
4
+ import { prepareOperatorProfile, resolveOperatorProfile } from "./operator-profiles.js";
5
+ /** Fresh interactive selection only; continuation restores the retained bundle
6
+ * rather than re-reading current defaults or claiming a different Session. */
7
+ export function prepareInteractiveExecution(context, input) {
8
+ const { scope, flow, profiles, selection } = input;
9
+ if (flow.descriptor.id !== "interactive" || Object.hasOwn(selection, "attachedSessionReceipt")) {
10
+ throw new AppError("interactive_execution_invalid", "Interactive execution must use its registered flow and CLI-owned Session binding", 2, { effect: "no_effect" });
11
+ }
12
+ const mode = selection.execution ?? "attached";
13
+ const stageSessionMode = selection.stageSessionMode ?? (mode === "attached" ? "same_session" : "new_session");
14
+ let attached = null;
15
+ let receipts = new Map();
16
+ const receiptBytes = new Map();
17
+ if (mode === "attached") {
18
+ if (!input.source && !input.launchArgs)
19
+ throw new AppError("interactive_attached_session_required", "Interactive defaults to the current trusted Session; select managed execution explicitly when unavailable", 2, { phase: "prepare", effect: "no_effect", next_action: "provide_matching_attached_session_or_choose_managed" });
20
+ if (stageSessionMode !== "same_session")
21
+ throw new AppError("interactive_execution_invalid", "An attached root cannot claim a new physical Session per Stage; choose managed execution for that route", 2, { effect: "no_effect" });
22
+ const profile = prepareOperatorProfile(typeof selection.rootProfile === "object" ? selection.rootProfile : resolveOperatorProfile(profiles, selection.rootProfile));
23
+ attached = input.source ? PreparedInteractiveAttachedSession.prepare(context, scope, profile, input.source)
24
+ : PreparedInteractiveAttachedSession.prepareCurrent(context, scope, profile, input.launchArgs);
25
+ const evidence = attached.evidence();
26
+ receipts = evidence.receipts;
27
+ receiptBytes.set(evidence.reference.sha256, evidence.bytes);
28
+ }
29
+ const execution = FrozenGenericExecution.prepare(flow, profiles, { ...selection, execution: mode,
30
+ stageSessionMode, delegation: selection.delegation ?? { mode: "external", max_parallel: 1 },
31
+ ...(attached ? { attachedSessionReceipt: attached.evidence().reference } : {}) }, scope, receipts);
32
+ return { execution, attached, receipts, receiptBytes };
33
+ }