@deksden-com/dd-flow-cli 0.9.0-beta.142 → 0.9.0-beta.143

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.
@@ -892,7 +892,42 @@ export function nativeInventoryVersion(directory, operations = retainedDaemonOpe
892
892
  const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
893
893
  // Only these native verbs are read-only. Unknown verbs remain guarded.
894
894
  const productive = operations.filter(operation => !isObservationOperation(operation.operation));
895
- return crypto.createHash("sha256").update(JSON.stringify([Object.fromEntries(keys.map(key => [key, state[key]])), productive])).digest("hex");
895
+ const knownSchema = ["dd-opencode/daemon-state@2", "dd-codex/daemon-state@1", "dd-grok/daemon-state@1", "dd-zcode/daemon-state@1", "dd-agy/daemon-state@1", "dd-droid/daemon-state@1"].includes(String(state.schema_id));
896
+ const inventory = knownSchema ? Object.fromEntries(keys.map(key => [key, state[key]])) : { ...state };
897
+ const canonical = (value) => Array.isArray(value) ? value.map(canonical)
898
+ : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonical(item)])) : value;
899
+ // Sets have no observation order. Unknown schemas/fields remain guarded;
900
+ // only the proven derived OpenCode adapter alias is not ownership authority.
901
+ for (const key of ["sessions", "descendants", "owned_processes"]) {
902
+ const values = inventory[key];
903
+ if (!Array.isArray(values))
904
+ continue;
905
+ const seen = new Set();
906
+ inventory[key] = values.map(value => {
907
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
908
+ if (state.schema_id === "dd-opencode/daemon-state@2")
909
+ throw new AppError("native_inventory_invalid", `Invalid native ${key} record`, 1);
910
+ return value;
911
+ }
912
+ const record = value;
913
+ const identity = record.provider_session_id ?? record.process_id ?? record.pid;
914
+ if (identity === undefined && state.schema_id !== "dd-opencode/daemon-state@2")
915
+ return record;
916
+ if (!(typeof identity === "string" && identity.length > 0 || typeof identity === "number" && Number.isSafeInteger(identity) && identity > 0) || seen.has(String(identity)))
917
+ throw new AppError("native_inventory_invalid", `Invalid or duplicate native ${key} identity`, 1);
918
+ seen.add(String(identity));
919
+ if (state.schema_id === "dd-opencode/daemon-state@2" && key === "sessions") {
920
+ if (record.adapter_session_id !== undefined && record.adapter_session_id !== record.provider_session_id)
921
+ throw new AppError("native_inventory_invalid", "Native Session adapter identity differs", 1);
922
+ const authority = { ...record };
923
+ delete authority.adapter_session_id;
924
+ return authority;
925
+ }
926
+ return record;
927
+ }).map(canonical).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
928
+ }
929
+ const productiveInventory = productive.map(canonical).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
930
+ return crypto.createHash("sha256").update(JSON.stringify(canonical([inventory, productiveInventory]))).digest("hex");
896
931
  }
897
932
  export function isObservationOperation(operation) {
898
933
  return ["session.inspect", "daemon.status"].includes(String(operation));
@@ -7,6 +7,7 @@ import { AppError } from "../shared/errors.js";
7
7
  import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
8
8
  import { resolveProjectRoot } from "../storage/paths.js";
9
9
  import { publishImmutableInput } from "../storage/immutable-input.js";
10
+ import { acceptStagePacket, prepareStagePacket } from "./stage-report-renderer.js";
10
11
  import { resolveWorkReference } from "../storage/work-references.js";
11
12
  import { findRecentMatchingHookEvent, claimStageResumeHookEvent, stageResumeMatchKey } from "./hooks.js";
12
13
  import { requireProjectByRoot } from "./projects.js";
@@ -15,6 +16,8 @@ import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.
15
16
  import { assertRunMutationAllowed } from "./run-recovery.js";
16
17
  import { runtimeCommand } from "./runtime-command.js";
17
18
  import { prepareAgentOutputFile } from "../storage/agent-output.js";
19
+ import { latestRunVerificationMatrix } from "./verification-matrix.js";
20
+ import { validateAcceptedIntentReview } from "./accepted-intent.js";
18
21
  export { flowCommand } from "./runtime-command.js";
19
22
  export function prepareStagePause(context, input) {
20
23
  if (typeof input.question !== "string" || !input.question.trim())
@@ -28,6 +31,8 @@ export function prepareStagePause(context, input) {
28
31
  if (work.status !== "running")
29
32
  throw new AppError("invalid_work_state", "Only a running Work can pause", 1, { work_id: work.work_id, status: work.status });
30
33
  const question = input.question;
34
+ if (Buffer.byteLength(question) > 1024 * 1024)
35
+ throw new AppError("accepted_intent_source_invalid", "HITL question exceeds the bounded source contract", 2);
31
36
  const stage = requireStage(run, input.stage, "running");
32
37
  requireRunHome(run);
33
38
  return { projectRoot, project, run, work, question, stage };
@@ -39,6 +44,7 @@ export function pauseStageForUser(context, input) {
39
44
  const questionPath = path.join(pauseRoot, "question.md");
40
45
  fs.mkdirSync(pauseRoot, { recursive: true });
41
46
  publishImmutableInput(questionPath, Buffer.from(question), "hitl_question_conflict");
47
+ const questionPacket = prepareStagePacket(questionPath, Buffer.from(question));
42
48
  const answerInputPath = prepareAgentOutputFile(path.join(pauseRoot, "answer-input.md"));
43
49
  const now = context.now();
44
50
  context.db.writeTransaction(() => {
@@ -46,6 +52,7 @@ export function pauseStageForUser(context, input) {
46
52
  if (paused.changes !== 1)
47
53
  throw new AppError("invalid_work_state", "Work changed before pause could be accepted", 1, { work_id: work.work_id });
48
54
  pauseFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, pauseId, questionPath });
55
+ acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: questionPacket });
49
56
  context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
50
57
  });
51
58
  const resume = stageResumeCommand(context, { runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, answerInputPath });
@@ -103,9 +110,12 @@ export function resumeStageAfterUser(context, input) {
103
110
  }).eventKey;
104
111
  const now = context.now();
105
112
  const answerPath = path.join(path.dirname(pause.question_path), `answer-${crypto.createHash("sha256").update(`${pause.id}\0${input.answer}`).digest("hex")}.md`);
113
+ if (Buffer.byteLength(input.answer) > 1024 * 1024)
114
+ throw new AppError("accepted_intent_source_invalid", "Delivered user answer exceeds the bounded source contract", 2);
106
115
  // The answer is an immutable, content-addressed input. Prepare it before
107
116
  // reserving SQLite's writer; an unreferenced candidate has no flow authority.
108
117
  publishImmutableInput(answerPath, Buffer.from(input.answer), "hitl_answer_conflict");
118
+ const answerPacket = prepareStagePacket(answerPath, Buffer.from(input.answer));
109
119
  context.db.writeTransaction(() => {
110
120
  const identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
111
121
  if (workSession.session_id !== identity.sessionId) {
@@ -117,11 +127,30 @@ export function resumeStageAfterUser(context, input) {
117
127
  if (resumed.changes !== 1)
118
128
  throw new AppError("invalid_work_state", "Work changed before resume could be accepted", 1, { work_id: work.work_id });
119
129
  resumeFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, answerPath });
130
+ acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: answerPacket });
120
131
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
121
132
  context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
122
133
  });
123
134
  const prompt = evidence.prompt;
124
135
  const retryCommands = lifecycleRetryCommands(context, stage.stage);
136
+ const intentInstructions = [];
137
+ if (["plan-review", "code-review"].includes(stage.stage)) {
138
+ try {
139
+ const home = requireRunHome(run), source = latestRunVerificationMatrix(context, project.id, run.id, stage.stage, "input");
140
+ const reviewContext = path.join(home, stage.stage === "plan-review" ? "04-plan-review/work-context.json" : "06-code-review/review-context.json");
141
+ const retainedContext = fs.existsSync(reviewContext) ? JSON.parse(fs.readFileSync(reviewContext, "utf8")) : null;
142
+ validateAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home, stage: stage.stage, decisionFrozen: Boolean(retainedContext?.accepted_decision) }, source?.metadata.accepted_sources, retainedContext?.accepted_intent_review_fingerprint);
143
+ }
144
+ catch (error) {
145
+ if (!(error instanceof AppError))
146
+ throw error;
147
+ intentInstructions.push(`Accepted-input review boundary: ${error.code}: ${error.message}`);
148
+ if (error.code === "accepted_intent_review_ack_required")
149
+ intentInstructions.push(`Apply/classify this own-stage clarification through the existing coordinator correction/repair route. Include this exact field in the decision: ${JSON.stringify({ accepted_intent_acknowledgement: error.details.accepted_intent_acknowledgement })}. Independent reviewer results prove only the earlier retained inputs; do not claim they reviewed the new answer.`);
150
+ else
151
+ intentInstructions.push("Do not accept stale independent review as coverage of this answer; report the actionable upstream revision/evidence blocker.");
152
+ }
153
+ }
125
154
  const continuation = [
126
155
  prompt.trimEnd(),
127
156
  "",
@@ -130,6 +159,7 @@ export function resumeStageAfterUser(context, input) {
130
159
  `- Question source: ${pause.question_path}`,
131
160
  `- Answer source: ${answerPath}`,
132
161
  "- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
162
+ ...intentInstructions,
133
163
  ...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
134
164
  "",
135
165
  "<user_question>",
@@ -6,6 +6,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
6
6
  import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations } from "./code-checks.js";
7
7
  import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
8
8
  import { assertRunMutationAllowed } from "./run-recovery.js";
9
+ import { acceptedIntentContract, acceptedIntentSourceRole, prepareAcceptedIntentSources } from "./accepted-intent.js";
9
10
  export const verificationMatrixContract = "dd-flow/verification-matrix@1";
10
11
  export const finalCheckCoverageContract = "dd-flow/final-check-coverage@1";
11
12
  /** A view of already validated facts: never a gate or a receipt selector. */
@@ -55,7 +56,9 @@ export function verificationMatrixInstruction(metadata, runHome, task) {
55
56
  const contract = `<verification_evidence_contract>\nCheck prerequisites must be executable in the check command or supported project policy; purpose prose and list order do not prepare an environment. A readiness gate is final verification, not implicit setup before the first CODE Work. PLAN and PLAN-REVIEW must compare claimed prerequisites with the actual launch path: a description saying migrate-before-check cannot repair a command that checks before migration. Prefer the existing self-contained project verification alias.\nBrowser checks must prove the expected surface exists and the measured set is nonempty before asserting layout geometry. An empty DOM is not evidence of no overlap. Derive navigation from the actual UI and accepted specification. Review whether the test can fail for the intended defect; a green named contour does not prove complete product semantics. Keep native test outcome separate from observation/cleanup: passed assertions with unknown cleanup are neither PASS nor a product assertion failure. Use retained native diagnostics and proof limits; generic timeouts or infrastructure errors do not authorize source repair. Do not patch the candidate product outside the assigned flow Work.\n</verification_evidence_contract>\n`;
56
57
  if (!metadata)
57
58
  return contract;
58
- return `${contract}<verification_matrix>\nRuntime generated the technical verification matrix: ${safePath(runHome, metadata.markdown.path, false)}; structured data: ${safePath(runHome, metadata.json.path, false)}. Source fingerprint: ${metadata.source_fingerprint}; completeness: ${metadata.completeness}. Read it before ${task}. This is a derived view of the named accepted requirements, PLAN and receipts, not semantic acceptance. Do not edit these generated files or manually duplicate receipt IDs/hashes/results. Record insufficient proof or violated criteria in the ordinary Stage/Work result with evidence references; modify only your assigned source/test/result documents. Review inputs remain read-only. A passed check proves only its named contour and limits, not all business semantics or production readiness.\n</verification_matrix>\n`;
59
+ const originals = metadata.accepted_sources?.length ? ["Original accepted inputs (ordered intake, then question/answer pairs):", "A question is retained only to interpret its delivered user answer. Agent recommendations and options are not accepted decisions unless that answer explicitly adopts them.", ...metadata.accepted_sources.map(source => `- ${source.role}: ${safePath(runHome, source.path, false)} (sha256 ${source.sha256})`)].join("\n") : "Original accepted-input coverage is unavailable for this historical RUN. Do not treat SPECIFY as the original user input or claim that semantic preservation was verified.";
60
+ const originalContract = `<accepted_intent_sources>\n${originals}\nCompare original accepted inputs with SPECIFY, effective amendments, PLAN and observable implementation. Preserve material actors, operations, UI/API surfaces, state transitions and exceptions when applicable. API evidence does not prove UI behavior. Later text supersedes an earlier accepted decision only when explicitly accepted as such; unresolved conflicts are material questions, not last-text-wins. Report lost obligations with source and R/AC references. Never edit immutable accepted SPECIFY to conceal a finding. Use the supported upstream correction route; if unavailable or a wholly missing R/AC needs a new accepted revision, report an actionable blocker rather than repairing only PLAN. Do not fabricate HITL references. A structurally complete matrix is not semantic acceptance.\n</accepted_intent_sources>\n`;
61
+ return `${contract}${originalContract}<verification_matrix>\nRuntime generated the technical verification matrix: ${safePath(runHome, metadata.markdown.path, false)}; structured data: ${safePath(runHome, metadata.json.path, false)}. Source fingerprint: ${metadata.source_fingerprint}; completeness: ${metadata.completeness}. Read it before ${task}. This is a derived view of the named accepted requirements, PLAN and receipts, not semantic acceptance. Do not edit these generated files or manually duplicate receipt IDs/hashes/results. Record insufficient proof or violated criteria in the ordinary Stage/Work result with evidence references; modify only your assigned source/test/result documents. Review inputs remain read-only. A passed check proves only its named contour and limits, not all business semantics or production readiness.\n</verification_matrix>\n`;
59
62
  }
60
63
  /** Attachment and packet authority share a short transaction, then both files
61
64
  * must be visible before the prompt claims they are ready. */
@@ -86,6 +89,12 @@ export function prepareRunVerificationMatrix(context, input) {
86
89
  const owning = index.stage_runs?.find(item => item.stage === input.stage);
87
90
  const attempt = input.stageAttempt ?? owning?.attempt ?? "try-001";
88
91
  const sources = [], packets = [];
92
+ const intentContract = index.accepted_intent_contract;
93
+ if (intentContract !== undefined && intentContract !== acceptedIntentContract)
94
+ throw new AppError("accepted_intent_contract_unsupported", "Unknown accepted-input custody contract", 1, { contract: intentContract });
95
+ const intent = prepareAcceptedIntentSources(context, { projectId: input.projectId, runId: input.runId, home, stageDirectory: vnextStageDirectory(input.stage), marked: index.accepted_intent_contract === acceptedIntentContract });
96
+ sources.push(...intent.sources);
97
+ packets.push(...intent.packets);
89
98
  const readSource = (role, root, relative, expected) => {
90
99
  const file = safePath(root === "run" ? home : workspace, relative), bytes = fs.readFileSync(file), sha = digest(bytes);
91
100
  if (expected && sha !== expected)
@@ -258,6 +267,8 @@ export function prepareRunVerificationMatrix(context, input) {
258
267
  const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
259
268
  packets.push(json, markdown);
260
269
  const metadata = { contract: verificationMatrixContract, role: header.role, completeness: header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(home, json.file)), sha256: json.sha256 }, markdown: { path: slash(path.relative(home, markdown.file)), sha256: markdown.sha256 } };
270
+ if (intent.sources.length)
271
+ metadata.accepted_sources = intent.sources;
261
272
  if (index.final_check_coverage_contract === finalCheckCoverageContract)
262
273
  metadata.coverage_contract = finalCheckCoverageContract;
263
274
  const predecessors = (index.stage_runs ?? []).filter(item => item.stage !== input.stage && item.status === "done").map(item => ({ stage: item.stage, ...(item.attempt ? { attempt: item.attempt } : {}), ...(item.data_sha256 ? { data_sha256: item.data_sha256 } : {}) }));
@@ -294,6 +305,8 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
294
305
  if (!run?.run_root)
295
306
  return null;
296
307
  const index = JSON.parse(run.index_json);
308
+ if (index.accepted_intent_contract !== undefined && index.accepted_intent_contract !== acceptedIntentContract)
309
+ throw new AppError("accepted_intent_contract_unsupported", "Unknown accepted-input custody contract", 1, { contract: index.accepted_intent_contract });
297
310
  const attempt = index.stage_runs?.find(item => item.stage === stage)?.attempt;
298
311
  const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
299
312
  for (const row of rows) {
@@ -319,9 +332,14 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
319
332
  const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
320
333
  const json = { file: packet.file, bytes, sha256: packet.sha256 };
321
334
  const metadata = { contract: verificationMatrixContract, role: matrix.header.role, completeness: matrix.header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(run.run_root, packet.file)), sha256: packet.sha256 }, markdown: { path: slash(path.relative(run.run_root, mdFile)), sha256: md.sha256 } };
335
+ const acceptedSources = matrix.sources.filter(source => acceptedIntentSourceRole(source.role)).sort((a, b) => a.role === "accepted_intake" ? -1 : b.role === "accepted_intake" ? 1 : compare(a.role, b.role));
336
+ if (index.accepted_intent_contract === acceptedIntentContract && !acceptedSources.some(source => source.role === "accepted_intake"))
337
+ throw new AppError("accepted_intent_source_gap", "Marked matrix is missing its original accepted intake", 1, { stage, run_id: runId });
338
+ if (acceptedSources.length)
339
+ metadata.accepted_sources = acceptedSources;
322
340
  if (index.final_check_coverage_contract === finalCheckCoverageContract)
323
341
  metadata.coverage_contract = finalCheckCoverageContract;
324
- const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
342
+ const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch" || acceptedIntentSourceRole(source.role)) && source.root === "run").map(source => {
325
343
  const file = safePath(run.run_root, source.path, false);
326
344
  const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file && item.sha256 === source.sha256);
327
345
  if (!retained)
@@ -28,6 +28,7 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
28
28
  import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
29
29
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
30
30
  import { reviewDefEvidence } from "./review-deferral.js";
31
+ import { acceptedIntentReviewFingerprint, validateAcceptedIntentReview, assertAcceptedIntentReviewCurrent } from "./accepted-intent.js";
31
32
  import { stageSuccessor } from "./vnext-contracts.js";
32
33
  const stage = "code-review";
33
34
  const stageDir = vnextStageDirectory(stage);
@@ -93,7 +94,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
93
94
  freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
94
95
  const promptPath = path.join(root, "stage-prompt.md");
95
96
  const reviewContextPath = path.join(root, "review-context.json");
96
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", reviewer_inputs: prepared.reviewerInputs, cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
97
+ fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", accepted_intent_review_fingerprint: acceptedIntentReviewFingerprint(prepared.matrix?.metadata.accepted_sources), reviewer_inputs: prepared.reviewerInputs, cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
97
98
  if (mode.mode === "off") {
98
99
  fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
99
100
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n${stageLifecycleInstruction}\n`;
@@ -197,6 +198,7 @@ export async function finishVnextCodeReview(context, input) {
197
198
  ? disabledDecision
198
199
  : readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile), input.decision);
199
200
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
201
+ const intentEvidence = mode === "off" ? null : validateAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home, stage, decisionFrozen: Boolean(reviewContext.accepted_decision) }, latestRunVerificationMatrix(context, project.id, run.id, stage, "input")?.metadata.accepted_sources, reviewContext.accepted_intent_review_fingerprint, decision.accepted_intent_acknowledgement);
200
202
  const cycle = reviewContext.cycle ?? "initial";
201
203
  const reviewers = reviewerWorks(context, project.id, run.id, cycle);
202
204
  const defEvidence = decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id));
@@ -218,13 +220,20 @@ export async function finishVnextCodeReview(context, input) {
218
220
  throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
219
221
  const fixIds = canonical.fix_ids;
220
222
  if (fixIds.length && repairs.length === 0) {
223
+ // The fresh repair sees the coordinator's newly accepted clarification,
224
+ // without pretending that the earlier independent review covered it.
225
+ if (intentEvidence?.coordinator_clarification_refs.length) {
226
+ const correctionMatrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionFile });
227
+ context.db.writeTransaction(() => { assertAcceptedIntentReviewCurrent(context, { projectId: project.id, runId: run.id }, intentEvidence); acceptVerificationMatrix(context, correctionMatrix); });
228
+ publishVerificationMatrix(context, correctionMatrix);
229
+ }
221
230
  // Accept the value once. Repairs consume this decision, not a mutable
222
231
  // operator file. Reuse the existing context artifact and review cycle.
223
232
  const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
224
233
  fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision, def_evidence: defEvidence }, null, 2)}\n`);
225
234
  fs.renameSync(temporary, contextFile);
226
235
  const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
227
- const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
236
+ const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, intentEvidence?.coordinator_clarification_refs.length ? "output" : "input");
228
237
  return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, ...(retained ? { verification_matrix: retained.metadata } : {}), instruction: `The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.\n${verificationMatrixInstruction(retained?.metadata ?? null, home, "the accepted review decision and repair")}`, next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
229
238
  }
230
239
  repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
@@ -273,9 +282,12 @@ export async function finishVnextCodeReview(context, input) {
273
282
  if (JSON.stringify(defEvidence) !== JSON.stringify(decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id))))
274
283
  throw new AppError("deferral_invalid", "DEF evidence changed while final checks ran", 2);
275
284
  Object.assign(report.semantic, { def_evidence: defEvidence });
285
+ if (intentEvidence)
286
+ Object.assign(report.semantic, { accepted_intent_review: intentEvidence });
276
287
  const preparedReport = prepareStageReport(report);
277
288
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
278
289
  withStageSettlement(context, () => {
290
+ assertAcceptedIntentReviewCurrent(context, { projectId: project.id, runId: run.id }, intentEvidence);
279
291
  assertRunMutationAllowed(context, project.id, run.id, generation);
280
292
  const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
281
293
  const currentRepairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
@@ -25,6 +25,7 @@ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-ins
25
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
26
26
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
27
27
  import { reviewDefEvidence } from "./review-deferral.js";
28
+ import { acceptedIntentReviewFingerprint, validateAcceptedIntentReview, assertAcceptedIntentReviewCurrent } from "./accepted-intent.js";
28
29
  import { stageSuccessor } from "./vnext-contracts.js";
29
30
  const stage = "plan-review";
30
31
  const stageDir = vnextStageDirectory(stage);
@@ -171,7 +172,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
171
172
  const workSessionId = String(binding.work_session_id);
172
173
  const sessionId = String(binding.session_id);
173
174
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
174
- writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_sources: readJson(prepared.batch).sources.map(source => ({ ...source, content_sha256: planContentHash(readJson(path.join(run.workspace_root, ".memory-bank", "protocol", source.protocol_id, "plan.json"))) })), plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
175
+ writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", accepted_intent_review_fingerprint: acceptedIntentReviewFingerprint(prepared.matrix?.metadata.accepted_sources), reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_sources: readJson(prepared.batch).sources.map(source => ({ ...source, content_sha256: planContentHash(readJson(path.join(run.workspace_root, ".memory-bank", "protocol", source.protocol_id, "plan.json"))) })), plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
175
176
  refreshRunWorkProjection(context, project.id, run.id);
176
177
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
177
178
  return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
@@ -280,6 +281,7 @@ function finishPlanReviewOwned(context, input) {
280
281
  return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, parentWorkId: parent.work_id, decision, now, decisionReceipt, preparedReport, completionInput, preparedCompletion }));
281
282
  }
282
283
  const latestChildren = latestReviewChildren(children);
284
+ const intentEvidence = validateAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home, stage }, latestRunVerificationMatrix(context, project.id, run.id, stage, "input")?.metadata.accepted_sources, contextFile.accepted_intent_review_fingerprint, decision.accepted_intent_acknowledgement);
283
285
  const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
284
286
  if (evidenceFailures.length)
285
287
  throw new AppError("review_evidence_invalid", "PLAN-REVIEW reviewer evidence is incomplete, stale, or not isolated", 2, { errors: evidenceFailures });
@@ -332,11 +334,14 @@ function finishPlanReviewOwned(context, input) {
332
334
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
333
335
  const completionBundle = readFlowRunExecutionBundle(context, { projectRoot, runId: run.id });
334
336
  return withStageSettlement(context, () => {
337
+ assertAcceptedIntentReviewCurrent(context, { projectId: project.id, runId: run.id }, intentEvidence);
335
338
  const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
336
339
  const now = context.now();
337
340
  settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
338
341
  const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, ...(next.command ? { nextCommand: next.command } : {}) });
339
342
  Object.assign(report.semantic, { def_evidence: defEvidence });
343
+ if (intentEvidence)
344
+ Object.assign(report.semantic, { accepted_intent_review: intentEvidence });
340
345
  applyReviewResults(home, latestChildren, decision);
341
346
  if (matrix)
342
347
  Object.assign(report.semantic, { verification_matrix: matrix.metadata });
@@ -1,6 +1,7 @@
1
1
  import { isControllerManagedInvocation, managedLifecycleCommand } from "./lifecycle-invocations.js";
2
2
  import { stageSuccessor } from "./vnext-contracts.js";
3
3
  import crypto from "node:crypto";
4
+ import { assertDeliveredHitlReferences } from "./accepted-intent.js";
4
5
  import fs from "node:fs";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
@@ -220,6 +221,7 @@ export async function finishVnextProtocolize(context, input) {
220
221
  try {
221
222
  if (result.outcome !== "protocolized")
222
223
  throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
224
+ assertDeliveredHitlReferences(context, { projectId: project.id, runId: run.id }, (result.obligation_amendments ?? []).map(amendment => amendment.hitl_ref));
223
225
  obligations = effectiveObligations(result, acceptedObligations);
224
226
  validateDelivery(projectRoot, result, obligations);
225
227
  }
@@ -1,6 +1,7 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
2
  import { stageSuccessor } from "./vnext-contracts.js";
3
3
  import crypto from "node:crypto";
4
+ import { retainAcceptedIntake } from "./accepted-intent.js";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { spawnSync } from "node:child_process";
@@ -40,6 +41,8 @@ export function prepareVnextSpecifyStart(context, input) {
40
41
  const discussion = intake.markdown.trim();
41
42
  if (!discussion)
42
43
  throw new AppError("validation", "SPECIFY intake must not be empty", 2);
44
+ if (Buffer.byteLength(intake.markdown) > 1024 * 1024)
45
+ throw new AppError("accepted_intent_source_invalid", "Accepted intake exceeds the bounded source contract", 2);
43
46
  const flow = assertFlowDefinition(projectRoot);
44
47
  const template = readRequiredFile(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "specify.md"), "vNext SPECIFY prompt template");
45
48
  const startInput = { projectRoot, flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify", subjectType: "discussion", subjectId: "SPECIFY", slug: input.slug ?? "specify", nextAction: "await_work_session" };
@@ -128,6 +131,7 @@ export function launchVnextSpecify(context, input, prepared = prepareVnextSpecif
128
131
  status: "running",
129
132
  dataSchemaId: "dd-flow/specify@1"
130
133
  });
134
+ retainAcceptedIntake(context, { projectId: run.project_id, runId: run.id, file: discussionPath, bytes: Buffer.from(intake.markdown) });
131
135
  refreshRunWorkProjection(context, run.project_id, run.id);
132
136
  appendFlowRunTimelineEvent(context, run.project_id, run.id, {
133
137
  type: "work_waiting_for_agent",
@@ -39,6 +39,7 @@ import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-ins
39
39
  import { acceptedDecisionPrecedence, loadExternalStageContext, renderRetainedStageContext, renderRunOperationalContext, runContextRelocations } from "./stage-context.js";
40
40
  import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
41
41
  import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
42
+ import { acceptedIntentSourceRole } from "./accepted-intent.js";
42
43
  const workColumns = "work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, payload_json, depends_on_json, status, result, created_at, started_at, updated_at, completed_at";
43
44
  export function ensureWorkRegistry(context) { context.db.get("SELECT 1 FROM works LIMIT 1"); context.db.get("SELECT 1 FROM work_sessions LIMIT 1"); }
44
45
  function ownsPhysicalWorkSession(context, projectId, owner, identity) {
@@ -1418,7 +1419,7 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
1418
1419
  if (matrix) {
1419
1420
  add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
1420
1421
  add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
1421
- for (const source of matrix.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)))
1422
+ for (const source of matrix.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role) || acceptedIntentSourceRole(source.role)))
1422
1423
  add(source.root, source.path, source.sha256);
1423
1424
  }
1424
1425
  for (const item of value.reviewer_inputs ?? [])
@@ -1823,7 +1824,8 @@ function renderWorkerPrompt(context, work, run, dependencies, commands, workspac
1823
1824
  const retainedContext = contextFile ? renderRetainedStageContext({ loaded: loadExternalStageContext({ file: contextFile, expectedSha256: contextInput.sha256, stage: contextStage }), receipts: relocationReceipts, ...(workspaceView ? { workspaceView } : {}) }) : "";
1824
1825
  const body = `${operational}\n\n${retainedContext}${renderWorkerPromptBody(context, work, run, dependencies, commands)}`;
1825
1826
  const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
1826
- const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
1827
+ const reviewRepairMatrix = codePacket(work)?.repair?.review_findings?.length ? latestRunVerificationMatrix(context, work.project_id, work.run_id, "code-review", "output") : null;
1828
+ const matrix = reviewRepairMatrix ?? (matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null);
1827
1829
  const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
1828
1830
  return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1829
1831
  }
@@ -1683,6 +1683,10 @@ function migrate(db) {
1683
1683
  ensureColumn(db, "check_receipts", "after_fingerprint", "ALTER TABLE check_receipts ADD COLUMN after_fingerprint TEXT NOT NULL DEFAULT ''");
1684
1684
  ensureColumn(db, "check_receipts", "profile_hash", "ALTER TABLE check_receipts ADD COLUMN profile_hash TEXT");
1685
1685
  ensureColumn(db, "check_receipts", "mutation_paths_json", "ALTER TABLE check_receipts ADD COLUMN mutation_paths_json TEXT NOT NULL DEFAULT '[]'");
1686
+ ensureColumn(db, "check_receipts", "fingerprint_scope", "ALTER TABLE check_receipts ADD COLUMN fingerprint_scope TEXT");
1687
+ ensureColumn(db, "check_receipts", "input_manifest_json", "ALTER TABLE check_receipts ADD COLUMN input_manifest_json TEXT");
1688
+ ensureColumn(db, "check_receipts", "execution_failures_json", "ALTER TABLE check_receipts ADD COLUMN execution_failures_json TEXT");
1689
+ ensureColumn(db, "check_receipts", "execution_contract_hash", "ALTER TABLE check_receipts ADD COLUMN execution_contract_hash TEXT");
1686
1690
  ensureColumn(db, "hook_events", "event_key", "ALTER TABLE hook_events ADD COLUMN event_key TEXT");
1687
1691
  ensureColumn(db, "hook_events", "match_key", "ALTER TABLE hook_events ADD COLUMN match_key TEXT");
1688
1692
  ensureColumn(db, "hook_events", "claimed_at", "ALTER TABLE hook_events ADD COLUMN claimed_at TEXT");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.142",
3
+ "version": "0.9.0-beta.143",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -0,0 +1,37 @@
1
+ /* global console */
2
+ // Explicit non-generative contract probe. Never uses the operator's Codex home.
3
+ import { mkdtemp, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { CodexBridge, codexReadPhase, readCodexHistory, readCodexRpc } from "../dist/harness-runtime/lib/dd-codex.mjs";
8
+ import { errorRecord } from "../dist/harness-runtime/lib/operation-errors.mjs";
9
+
10
+ const bin = process.argv[2];
11
+ if (!bin || !path.isAbsolute(bin)) throw new Error("Usage: node tools/probe-codex-history.mjs /absolute/native/codex");
12
+ const home = await mkdtemp(path.join(os.tmpdir(), "dd-codex-history-"));
13
+ await writeFile(path.join(home, "config.toml"), "[features]\nplugins = false\nremote_models = false\n");
14
+ const bridge = new CodexBridge({ bin, cwd: home, journal: path.join(home, "native.jsonl"),
15
+ env: { ...Object.fromEntries(Object.keys(process.env).map(key => [key, undefined])),
16
+ PATH: process.env.PATH, HOME: home, CODEX_HOME: home, CODEX_SQLITE_HOME: home, CODEX_LOG_DIR: home } });
17
+ const report = { home, non_generative: true, reads: [] };
18
+ try {
19
+ await bridge.start();
20
+ const created = await bridge.request("thread/start", { cwd: home, ephemeral: false, approvalPolicy: "never", sandbox: "read-only" });
21
+ report.session_id = created.thread.id;
22
+ report.history_mode = created.thread.historyMode;
23
+ const phase = codexReadPhase(bridge);
24
+ const compact = await readCodexRpc(bridge, "thread/read", { threadId: report.session_id, includeTurns: false }, phase);
25
+ report.compact = { id: compact.thread.id, status: compact.thread.status };
26
+ for (const method of ["thread/turns/list", "thread/read"]) {
27
+ const read = await readCodexHistory(bridge, method, { threadId: report.session_id,
28
+ ...(method === "thread/read" ? { includeTurns: true } : { limit: 20, itemsView: "summary" }) }, phase);
29
+ report.reads.push({ method, available: read.available, ...(read.error ? { error: read.error } : {}) });
30
+ }
31
+ } catch (error) { report.error = errorRecord(error); process.exitCode = 1; }
32
+ finally {
33
+ try { await bridge.close(); }
34
+ catch (error) { report.cleanup_error = errorRecord(error); process.exitCode = 1; }
35
+ await writeFile(path.join(home, "result.json"), JSON.stringify(report, null, 2) + "\n");
36
+ }
37
+ console.log(JSON.stringify(report, null, 2));