@deksden-com/dd-flow-cli 0.9.0-beta.135 → 0.9.0-beta.137
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 +14 -0
- package/README.md +2 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +1 -0
- package/dist/cli/run-cli.js +5 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +14 -1
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +36 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -1
- package/dist/services/cli-operation-classifier.js +4 -0
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-fanout.js +9 -2
- package/dist/services/engines.js +1 -1
- package/dist/services/managed-processes.js +30 -0
- package/dist/services/run-controller.js +84 -8
- package/dist/services/run-health.js +149 -0
- package/dist/services/runs.js +61 -13
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +13 -0
- package/dist/services/vnext-fanout.js +66 -1
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/storage/database.js +18 -3
- package/dist/storage/writer-contract.js +8 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
7
8
|
import { AppError } from "../shared/errors.js";
|
|
8
9
|
import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
10
|
import { previewNextEntityId } from "./ids.js";
|
|
@@ -189,6 +190,29 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
189
190
|
const resultFile = prepareProtocolizeResultPath(requireRunHome(run), input.resultFile);
|
|
190
191
|
const result = input.result ?? readResult(resultFile, { projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
191
192
|
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, data: result, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
193
|
+
const acceptedIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]).index_json);
|
|
194
|
+
if (acceptedIndex.stage_runs.some(item => item.stage === stage && item.status === "done")) {
|
|
195
|
+
publishStageReport(context, project.id, run.id, stage, root);
|
|
196
|
+
const file = path.join(root, "stage-report.json");
|
|
197
|
+
if (!fs.existsSync(file))
|
|
198
|
+
throw new AppError("protocolize_report_missing", "Completed PROTOCOLIZE has no accepted report to replay", 1, { run_id: run.id });
|
|
199
|
+
const accepted = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
200
|
+
const authoredSha = accepted.semantic.materialization?.authored_result?.sha256;
|
|
201
|
+
if (authoredSha && authoredSha !== sha256(fs.readFileSync(resultFile)))
|
|
202
|
+
throw new AppError("protocolize_intent_conflict", "Completed PROTOCOLIZE result differs from accepted input", 1, { run_id: run.id });
|
|
203
|
+
const currentStage = acceptedIndex.current_stage ?? [...acceptedIndex.stage_runs].reverse().find(item => item.status === "running" || item.status === "done")?.stage;
|
|
204
|
+
const activeReplay = currentStage === stage && acceptedIndex.status === "running";
|
|
205
|
+
const resolvedPolicy = activeReplay ? requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" }) : accepted.mechanical?.workspace_route;
|
|
206
|
+
if (activeReplay) {
|
|
207
|
+
if (acceptedIndex.verdict !== "protocolized")
|
|
208
|
+
advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
209
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
210
|
+
}
|
|
211
|
+
const next = activeReplay ? stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: run.workspace_root, session_mode: resolvedPolicy.stage_session_mode } }) : { kind: "complete", outcome: "already_completed" };
|
|
212
|
+
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: accepted.semantic.acceptance, workspace_root: run.workspace_root, git_policy: resolvedPolicy, next_action: activeReplay ? "await_plan" : acceptedIndex.next_action, ...("command" in next && next.command ? { next_command: next.command } : {}), next,
|
|
213
|
+
artifacts: { result: resultFile, report_json: file, report_markdown: path.join(root, "stage-report.md"), report_html: path.join(root, "stage-report.html"), protocols: accepted.semantic.acceptance.map(id => path.join(run.workspace_root, ".memory-bank/protocol", id, "summary.md")) } };
|
|
214
|
+
}
|
|
215
|
+
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
192
216
|
const acceptedObligations = acceptedSpecifyObligations(context, projectRoot, run.id, path.join(requireRunHome(run), "01-specify", "specify.json"));
|
|
193
217
|
let obligations;
|
|
194
218
|
try {
|
|
@@ -202,11 +226,11 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
202
226
|
throw error;
|
|
203
227
|
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
204
228
|
}
|
|
205
|
-
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
206
229
|
const receipt = readWorkspaceRouteReceipt(root);
|
|
207
230
|
const workspaceRoot = run.workspace_root;
|
|
208
231
|
const intent = await prepareProtocolizeIntent(context, run, resultFile, result, obligations);
|
|
209
232
|
publishProtocolizeIntent(context, project.id, intent);
|
|
233
|
+
const materialization = verifyProtocolizeMaterialization(intent, result, path.relative(requireRunHome(run), resultFile));
|
|
210
234
|
const protocolIds = intent.protocol_ids;
|
|
211
235
|
const changedFiles = intent.changed_files;
|
|
212
236
|
for (const protocolId of protocolIds)
|
|
@@ -216,9 +240,10 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
216
240
|
ensureManagedWorktreeRecorded(context, { protocolId, projectRoot, branch: requiredPolicyValue(resolvedPolicy.feature_branch, "feature branch"), base: requiredPolicyValue(resolvedPolicy.base_ref, "base ref"), created: receipt.created, bootstrap: receipt.bootstrap });
|
|
217
241
|
}
|
|
218
242
|
const now = context.now();
|
|
219
|
-
const workSession =
|
|
243
|
+
const workSession = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
220
244
|
const routedRun = requireRun(context, projectRoot, run.id);
|
|
221
245
|
const report = stageReport(context, routedRun, resultFile, protocolIds, changedFiles, workSession?.session_id ?? null, now, resolvedPolicy);
|
|
246
|
+
Object.assign(report.semantic, { materialization });
|
|
222
247
|
const reportJson = path.join(root, "stage-report.json");
|
|
223
248
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
224
249
|
const reportHtml = path.join(root, "stage-report.html");
|
|
@@ -235,6 +260,7 @@ export async function finishVnextProtocolize(context, input) {
|
|
|
235
260
|
const completion = { projectRoot, runId: run.id, stage, status: "done", data: "protocolize-result.json", dataSchemaId: "dd-flow/vnext-protocolize-result@3", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: crypto.createHash("sha256").update(fs.readFileSync(resultFile)).digest("hex") };
|
|
236
261
|
const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
|
|
237
262
|
withStageSettlement(context, () => {
|
|
263
|
+
completeWorkSession(context, work.work_id, resultFile, now);
|
|
238
264
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
239
265
|
completeFlowRunStage(context, completion, preparedCompletion);
|
|
240
266
|
});
|
|
@@ -367,6 +393,38 @@ function ensureManagedWorktreeRecorded(context, input) {
|
|
|
367
393
|
recordManagedWorktree(context, input);
|
|
368
394
|
}
|
|
369
395
|
function sha256(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
|
|
396
|
+
export function verifyProtocolizeMaterialization(intent, result, authoredPath) {
|
|
397
|
+
const documents = intent.files.map(file => {
|
|
398
|
+
const target = path.resolve(intent.workspace_root, file.relative);
|
|
399
|
+
inside(path.join(intent.workspace_root, ".memory-bank"), target);
|
|
400
|
+
assertNoSymlinkPath(path.join(intent.workspace_root, ".memory-bank"), target);
|
|
401
|
+
const bytes = fs.readFileSync(target);
|
|
402
|
+
if (sha256(bytes) !== file.after_sha256)
|
|
403
|
+
throw new AppError("protocolize_materialization_invalid", "Published document differs from accepted intent", 1, { path: file.relative });
|
|
404
|
+
return { path: file.relative, sha256: file.after_sha256 };
|
|
405
|
+
});
|
|
406
|
+
const featurePaths = intent.changed_files.filter(file => /\/features\/FT-[^/]+\/index\.md$/.test(file));
|
|
407
|
+
const featurePath = result.feature.action === "not_applicable" ? null : featurePaths.length === 1 ? featurePaths[0] : null;
|
|
408
|
+
if (result.feature.action !== "not_applicable" && !featurePath)
|
|
409
|
+
throw new AppError("protocolize_materialization_invalid", "Accepted feature output is ambiguous or missing", 1);
|
|
410
|
+
const frontmatter = (relative) => {
|
|
411
|
+
const text = fs.readFileSync(path.join(intent.workspace_root, relative), "utf8");
|
|
412
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
413
|
+
const value = match ? parseYaml(match[1]) : null;
|
|
414
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
415
|
+
throw new AppError("protocolize_materialization_invalid", "Published document lacks frontmatter", 1, { path: relative });
|
|
416
|
+
return value;
|
|
417
|
+
};
|
|
418
|
+
const edges = featurePath ? intent.protocol_ids.map(protocolId => ({ feature_path: featurePath, protocol_id: protocolId, protocol_path: `.memory-bank/protocol/${protocolId}/summary.md` })) : [];
|
|
419
|
+
for (const edge of edges) {
|
|
420
|
+
const protocols = frontmatter(edge.feature_path).related_protocols;
|
|
421
|
+
const features = frontmatter(edge.protocol_path).related_features;
|
|
422
|
+
if (!Array.isArray(protocols) || !protocols.includes(edge.protocol_id) || !Array.isArray(features) || !features.includes(edge.feature_path))
|
|
423
|
+
throw new AppError("protocolize_materialization_invalid", "Owned feature/protocol links are not reciprocal", 1, edge);
|
|
424
|
+
}
|
|
425
|
+
return { schema_id: "dd-flow/protocolize-materialization@1", run_id: intent.run_id, stage: "protocolize", action: result.feature.action, feature_path: featurePath,
|
|
426
|
+
protocol_ids: intent.protocol_ids, authored_result: { role: "authored_input", run_relative_path: authoredPath, sha256: intent.result_sha256 }, documents, owned_links: edges, status: "passed" };
|
|
427
|
+
}
|
|
370
428
|
async function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
371
429
|
const psetPath = psetId ? path.join(projectRoot, ".memory-bank", "protocol", "_set", `${psetId}.md`) : null;
|
|
372
430
|
const created = [];
|
package/dist/storage/database.js
CHANGED
|
@@ -5,7 +5,7 @@ import { channel } from "node:diagnostics_channel";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { ensureDir } from "./paths.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
8
|
+
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, registerReconciliationWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
9
9
|
import { assertImportAdmission } from "./import-guard.js";
|
|
10
10
|
import { withStoreCleanup, rollbackStoreTransaction } from "./native-cleanup.js";
|
|
11
11
|
const workContinuationStorageSql = `
|
|
@@ -354,6 +354,8 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
354
354
|
}
|
|
355
355
|
if (mode === "hook" && !exists)
|
|
356
356
|
throw new AppError("hook_storage_unprepared", "Prepare the runtime store before starting native hooks", 1);
|
|
357
|
+
if (mode === "reconcile" && !exists)
|
|
358
|
+
throw new AppError("reconciliation_storage_unprepared", "Reconciliation cannot create a runtime store", 1);
|
|
357
359
|
if (!exists || mode === "migrate")
|
|
358
360
|
ensureDir(ddFlowHome);
|
|
359
361
|
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
@@ -365,7 +367,10 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
365
367
|
}
|
|
366
368
|
if (mode !== "read_existing") {
|
|
367
369
|
try {
|
|
368
|
-
|
|
370
|
+
if (mode === "reconcile")
|
|
371
|
+
registerReconciliationWriter(db);
|
|
372
|
+
else
|
|
373
|
+
registerStoreWriter(db);
|
|
369
374
|
}
|
|
370
375
|
catch (error) {
|
|
371
376
|
return closeAfterOpenFailure(db, error);
|
|
@@ -379,7 +384,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
379
384
|
}
|
|
380
385
|
// Internal additive schema changes must be available to every write command.
|
|
381
386
|
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
382
|
-
if (mode !== "read_existing") {
|
|
387
|
+
if (mode !== "read_existing" && mode !== "reconcile") {
|
|
383
388
|
try {
|
|
384
389
|
const initialize = () => { migrate(db); ensureRecoveryStorage(db); ensureLifecycleInvocationStorage(db); };
|
|
385
390
|
if (mode !== "hook") {
|
|
@@ -402,6 +407,16 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
402
407
|
}
|
|
403
408
|
if (mode === "read_existing")
|
|
404
409
|
db.exec("PRAGMA query_only = ON");
|
|
410
|
+
if (mode === "reconcile") {
|
|
411
|
+
try {
|
|
412
|
+
db.prepare("SELECT id, project_id, status, verdict, next_action, index_json, updated_at, completed_at FROM runs LIMIT 0");
|
|
413
|
+
db.prepare("SELECT id, root FROM projects LIMIT 0");
|
|
414
|
+
db.prepare("SELECT id, event_type, payload_json FROM audit_events LIMIT 0");
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return closeAfterOpenFailure(db, error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
405
420
|
db.exec("PRAGMA foreign_keys = ON");
|
|
406
421
|
return databaseFacade(dbPath, mode !== "read_existing", db, undefined, deadline);
|
|
407
422
|
}
|
|
@@ -35,6 +35,14 @@ export function registerStoreWriter(db) {
|
|
|
35
35
|
throw new AppError("store_writer_incompatible", "This CLI cannot write the store's required contract", 1, { required: current, supported: version });
|
|
36
36
|
db.function("dd_flow_writer_version", () => version);
|
|
37
37
|
}
|
|
38
|
+
/** Health maintenance uses only verified legacy lifecycle columns and never
|
|
39
|
+
* migrates schema. Keep the retained writer fence, including version 1. */
|
|
40
|
+
export function registerReconciliationWriter(db) {
|
|
41
|
+
const current = readStoreWriterContract(db);
|
|
42
|
+
if (current !== null && current !== 1 && current !== version)
|
|
43
|
+
throw new AppError("store_writer_incompatible", "Unsupported reconciliation writer contract", 1, { required: current, supported: [1, version] });
|
|
44
|
+
db.function("dd_flow_writer_version", () => current ?? version);
|
|
45
|
+
}
|
|
38
46
|
export function readStoreWriterContract(db) {
|
|
39
47
|
if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table))
|
|
40
48
|
return null;
|