@deksden-com/dd-flow-cli 0.9.0-beta.121 → 0.9.0-beta.122

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 (33) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/hook-ingress.js +4 -3
  4. package/dist/cli/native-hook-ingress.js +34 -2
  5. package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
  6. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
  7. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
  8. package/dist/harness-runtime/lib/dd-zcode.mjs +25 -12
  9. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  10. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  11. package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
  12. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  13. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  14. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  15. package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
  16. package/dist/runtime/context.js +1 -1
  17. package/dist/schemas/flow-run.schema.json +1 -0
  18. package/dist/services/codex-hook-delivery.js +6 -2
  19. package/dist/services/hooks.js +51 -59
  20. package/dist/services/lifecycle-command.js +1 -383
  21. package/dist/services/lifecycle-contract.js +1 -30
  22. package/dist/services/lifecycle-invocations.js +34 -13
  23. package/dist/services/runs.js +60 -47
  24. package/dist/services/stage-report-renderer.js +19 -0
  25. package/dist/services/verification-matrix.js +325 -0
  26. package/dist/services/vnext-code-review.js +25 -8
  27. package/dist/services/vnext-code.js +31 -10
  28. package/dist/services/vnext-merge.js +30 -7
  29. package/dist/services/vnext-plan-review.js +22 -6
  30. package/dist/services/vnext-plan.js +16 -3
  31. package/dist/services/work-registry.js +13 -1
  32. package/dist/storage/database.js +43 -9
  33. package/package.json +4 -1
@@ -25,6 +25,7 @@ import { prepareError, prepareJsonFile } from "../cli/input-preparation.js";
25
25
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
26
26
  import { acceptedDecisionPrecedence } from "./stage-context.js";
27
27
  import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
28
+ import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
28
29
  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";
29
30
  export function ensureWorkRegistry(context) { context.db.exec("SELECT 1 FROM works LIMIT 1"); context.db.exec("SELECT 1 FROM work_sessions LIMIT 1"); }
30
31
  function ownsPhysicalWorkSession(context, projectId, owner, identity) {
@@ -986,6 +987,14 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
986
987
  if (value.cycle !== payload?.review_cycle)
987
988
  throw new AppError("review_input_invalid", "CODE-REVIEW cycle differs from its Work", 1);
988
989
  }
990
+ const matrix = latestRunVerificationMatrix(context, work.project_id, work.run_id, String(kind), "input");
991
+ const marked = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id]).index_json).verification_matrix_contract === verificationMatrixContract;
992
+ if (marked && !matrix)
993
+ throw new AppError("review_input_contract_missing", "Marked reviewer Stage lacks its immutable matrix baseline", 1, { stage: kind });
994
+ if (matrix) {
995
+ add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
996
+ add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
997
+ }
989
998
  const runInputs = [...expected].map(([key, sha]) => { const [root, ...parts] = key.split(":"); return { root: root, path: parts.join(":"), sha256: sha }; }).sort((a, b) => `${a.root}:${a.path}`.localeCompare(`${b.root}:${b.path}`));
990
999
  for (const item of runInputs) {
991
1000
  const target = reviewInputPath(item.root === "run" ? home : run.workspace_root, item.path);
@@ -1236,7 +1245,10 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
1236
1245
  }
1237
1246
  function renderWorkerPrompt(context, work, run, dependencies, commands) {
1238
1247
  const body = renderWorkerPromptBody(context, work, run, dependencies, commands);
1239
- return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body].join("\n");
1248
+ const kind = parsePayload(work)?.kind;
1249
+ const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
1250
+ const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
1251
+ return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1240
1252
  }
1241
1253
  function workerLifecycleCommands(context, work, run) {
1242
1254
  const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
@@ -331,7 +331,16 @@ export function ensureWorkLaunchStorage(db) {
331
331
  CREATE INDEX IF NOT EXISTS idx_work_launches_status ON work_launches(status, updated_at DESC);
332
332
  `);
333
333
  }
334
- export function getDatabase(ddFlowHome, mode = "initialize", importPermitId) {
334
+ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hookDeadlineAt) {
335
+ if (hookDeadlineAt !== undefined && (!Number.isSafeInteger(hookDeadlineAt) || mode !== "hook"))
336
+ throw new AppError("hook_deadline_invalid", "Hook deadline must be a finite absolute millisecond deadline", 1, { effect: "unknown" });
337
+ const deadline = hookDeadlineAt === undefined ? undefined : (phase) => {
338
+ const remaining = hookDeadlineAt - Date.now();
339
+ if (remaining <= 0)
340
+ throw new AppError("native_hook_timeout", "Native hook deadline expired before durable admission", 1, { phase, deadline_at: hookDeadlineAt, effect: "unknown" });
341
+ return Math.max(1, Math.min(4000, remaining));
342
+ };
343
+ deadline?.("store_open");
335
344
  if (mode !== "read_existing")
336
345
  assertImportAdmission(ddFlowHome, importPermitId);
337
346
  const dbPath = path.join(ddFlowHome, "db.sqlite");
@@ -344,7 +353,12 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId) {
344
353
  if (!exists || mode === "migrate")
345
354
  ensureDir(ddFlowHome);
346
355
  const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
347
- db.exec("PRAGMA busy_timeout = 4000");
356
+ try {
357
+ db.exec(`PRAGMA busy_timeout = ${deadline?.("store_open") ?? 4000}`);
358
+ }
359
+ catch (error) {
360
+ return closeAfterOpenFailure(db, error);
361
+ }
348
362
  if (mode !== "read_existing") {
349
363
  try {
350
364
  registerStoreWriter(db);
@@ -354,7 +368,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId) {
354
368
  }
355
369
  }
356
370
  try {
357
- configureDatabase(db, mode);
371
+ configureDatabase(db, mode, deadline);
358
372
  }
359
373
  catch (error) {
360
374
  return closeAfterOpenFailure(db, error);
@@ -374,6 +388,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId) {
374
388
  migrateSessionStorageKeys(db, dbPath);
375
389
  }
376
390
  else {
391
+ deadline?.("store_validation");
377
392
  assertHookStoreReady(db);
378
393
  }
379
394
  }
@@ -384,7 +399,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId) {
384
399
  if (mode === "read_existing")
385
400
  db.exec("PRAGMA query_only = ON");
386
401
  db.exec("PRAGMA foreign_keys = ON");
387
- return databaseFacade(dbPath, mode !== "read_existing", db);
402
+ return databaseFacade(dbPath, mode !== "read_existing", db, undefined, deadline);
388
403
  }
389
404
  /**
390
405
  * Hook ingress is a hot, synchronous path. It may write receipts, but it
@@ -554,7 +569,7 @@ function emptyReadOnlyDatabase(dbPath) {
554
569
  all: () => []
555
570
  };
556
571
  }
557
- function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
572
+ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadline) {
558
573
  let reserved = false;
559
574
  let poisoned = false;
560
575
  let closed = false;
@@ -567,6 +582,14 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
567
582
  let manualFrames = [];
568
583
  let readDepth = 0;
569
584
  const writerTiming = channel("dd-flow.sqlite.writer");
585
+ const ingressPhase = channel("dd-flow.sqlite.ingress");
586
+ const bound = (phase) => {
587
+ const remaining = deadline?.(phase);
588
+ if (remaining !== undefined)
589
+ db.exec(`PRAGMA busy_timeout = ${remaining}`);
590
+ if (ingressPhase.hasSubscribers)
591
+ ingressPhase.publish({ phase, ...(remaining !== undefined ? { remaining_ms: remaining } : {}) });
592
+ };
570
593
  const publishCommitted = () => {
571
594
  const pending = afterCommit;
572
595
  afterCommit = [];
@@ -584,8 +607,11 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
584
607
  throw new AppError("read_snapshot_write", "Cannot reserve a writer inside a read snapshot", 1);
585
608
  if (!writable)
586
609
  throw new AppError("read_only_storage", "Read-only access cannot start a write transaction", 1, { path: dbPath });
610
+ bound("writer_reservation");
587
611
  db.exec("BEGIN IMMEDIATE");
588
612
  reserved = true;
613
+ if (ingressPhase.hasSubscribers)
614
+ ingressPhase.publish({ phase: "writer_reserved" });
589
615
  };
590
616
  const rollback = (sql, original) => {
591
617
  try {
@@ -626,6 +652,8 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
626
652
  beginWriteTransaction();
627
653
  }
628
654
  catch (error) {
655
+ if (error instanceof AppError)
656
+ throw error;
629
657
  throw new AppError("storage_write_failed", "Cannot reserve SQLite writer", 1, { cause: errorDetails(error), transaction_phase: "begin", transaction_rolled_back: true });
630
658
  }
631
659
  let phase = "body";
@@ -636,11 +664,14 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
636
664
  throw new AppError("write_transaction_async", "Write transaction callback must be synchronous", 1);
637
665
  }
638
666
  phase = "commit";
667
+ bound("commit");
639
668
  if (nested)
640
669
  db.exec(`RELEASE ${marker}`);
641
670
  else
642
671
  db.exec("COMMIT");
643
672
  phase = "committed";
673
+ if (ingressPhase.hasSubscribers)
674
+ ingressPhase.publish({ phase: "committed" });
644
675
  if (!nested && writerTiming.hasSubscribers)
645
676
  writerTiming.publish({ operation: operationName, wall_time_ms: performance.now() - startedAt, path: dbPath });
646
677
  if (!nested)
@@ -786,22 +817,25 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.()) {
786
817
  executePublications([operation]);
787
818
  } },
788
819
  run: (sql, params = []) => { assertUsable(); if (readDepth)
789
- throw new AppError("read_snapshot_write", "Cannot write inside a read snapshot", 1); return db.prepare(sql).run(...params); },
790
- get: (sql, params = []) => { assertUsable(); return db.prepare(sql).get(...params); },
791
- all: (sql, params = []) => { assertUsable(); return db.prepare(sql).all(...params); }
820
+ throw new AppError("read_snapshot_write", "Cannot write inside a read snapshot", 1); bound("write"); return db.prepare(sql).run(...params); },
821
+ get: (sql, params = []) => { assertUsable(); bound("lookup"); return db.prepare(sql).get(...params); },
822
+ all: (sql, params = []) => { assertUsable(); bound("lookup"); return db.prepare(sql).all(...params); }
792
823
  };
793
824
  }
794
825
  function errorDetails(error) {
795
826
  const value = error;
796
827
  return { message: String(value?.message ?? error), ...(typeof value?.code === "string" ? { code: value.code } : {}), ...(typeof value?.errcode === "number" ? { sqlite_extended_code: value.errcode } : {}) };
797
828
  }
798
- function configureDatabase(db, mode) {
829
+ function configureDatabase(db, mode, ingressDeadline) {
799
830
  if (mode !== "read_existing") {
800
831
  // Concurrent first openers can collide while changing journal mode, even
801
832
  // with busy_timeout set. Retrying this configuration does not replay writes.
802
833
  const deadline = performance.now() + 4000, pause = new Int32Array(new SharedArrayBuffer(4));
803
834
  for (;;) {
804
835
  try {
836
+ const remaining = ingressDeadline?.("store_configuration");
837
+ if (remaining !== undefined)
838
+ db.exec(`PRAGMA busy_timeout = ${remaining}`);
805
839
  const current = db.prepare("PRAGMA journal_mode").get();
806
840
  if (current.journal_mode !== "wal")
807
841
  db.exec("PRAGMA journal_mode = WAL");
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.121",
3
+ "version": "0.9.0-beta.122",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@10.23.0",
6
7
  "bin": {
7
8
  "dd-flow": "dist/cli.js",
8
9
  "dd-codex": "dist/harness-runtime/bin/dd-codex.mjs",
@@ -49,6 +50,8 @@
49
50
  "ajv": "^8.20.0",
50
51
  "shell-quote": "^1.10.0",
51
52
  "smol-toml": "^1.6.1",
53
+ "tree-sitter": "0.25.1",
54
+ "tree-sitter-bash": "0.25.1",
52
55
  "yaml": "^2.9.0"
53
56
  },
54
57
  "publishConfig": {