@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74

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 (52) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli/help.js +2 -0
  3. package/dist/cli/run-cli.js +15 -0
  4. package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
  5. package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
  6. package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
  7. package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
  8. package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
  9. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  10. package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
  11. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
  12. package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
  13. package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
  15. package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
  16. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
  17. package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
  18. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
  19. package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
  20. package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
  21. package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
  22. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  23. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  24. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  25. package/dist/services/code-checks.js +120 -19
  26. package/dist/services/controller-fanout.js +11 -0
  27. package/dist/services/engines.js +40 -9
  28. package/dist/services/eval-snapshots.js +30 -47
  29. package/dist/services/harness-adapter.js +12 -6
  30. package/dist/services/lifecycle-invocations.js +12 -8
  31. package/dist/services/native-session-control.js +6 -0
  32. package/dist/services/projects.js +6 -2
  33. package/dist/services/recovery-observation-budget.js +9 -1
  34. package/dist/services/run-control-worker.js +19 -0
  35. package/dist/services/run-control.js +87 -13
  36. package/dist/services/run-controller-adapter.js +134 -7
  37. package/dist/services/run-controller.js +11 -3
  38. package/dist/services/run-engine-bindings.js +1 -1
  39. package/dist/services/run-fork.js +124 -0
  40. package/dist/services/run-observations.js +112 -0
  41. package/dist/services/runs.js +1 -11
  42. package/dist/services/schema-validation.js +1 -1
  43. package/dist/services/stage-lifecycle.js +14 -2
  44. package/dist/services/usage.js +20 -87
  45. package/dist/services/vnext-code-review.js +35 -27
  46. package/dist/services/vnext-code.js +62 -26
  47. package/dist/services/vnext-merge.js +8 -8
  48. package/dist/services/vnext-plan-review.js +16 -16
  49. package/dist/services/vnext-plan.js +51 -11
  50. package/dist/services/work-registry.js +48 -27
  51. package/dist/shared/errors.js +12 -0
  52. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
7
7
  import { validateSchema } from "./schema-validation.js";
8
8
  import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
9
9
  import { refreshRunSessionProjection } from "./run-projection.js";
10
- import { readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
10
+ import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
11
11
  import { nextWorkId, nextWorkIds } from "./ids.js";
12
12
  import { appendFlowRunTimelineEvent } from "./runs.js";
13
13
  import { resolveStageTransition } from "./execution-policy.js";
@@ -188,23 +188,6 @@ export function listWorks(context, input) {
188
188
  }) };
189
189
  }
190
190
  export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
191
- /** Inspect materializations without changing either the accepted receipt or conflicting bytes. */
192
- export function runWorkResultArtifactIssues(context, projectId, runId, materializedPath = (file) => file) {
193
- const works = context.db.all(`SELECT w.work_id, w.result, (SELECT ws.result_path FROM work_sessions ws WHERE ws.work_id = w.work_id ORDER BY ws.created_at DESC LIMIT 1) AS result_path FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.result_schema IS NOT NULL AND w.result IS NOT NULL AND w.status = 'completed'`, [projectId, runId]);
194
- const issues = [];
195
- for (const work of works) {
196
- const file = work.result_path ? materializedPath(work.result_path) : null;
197
- const actual = file && fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
198
- if (actual !== work.result)
199
- issues.push({ code: actual === null ? "work_result_artifact_missing" : "work_result_artifact_drift", work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: actual === null ? null : crypto.createHash("sha256").update(actual).digest("hex") });
200
- }
201
- return issues;
202
- }
203
- export function assertRunWorkResultArtifacts(context, projectId, runId) {
204
- const issue = runWorkResultArtifactIssues(context, projectId, runId)[0];
205
- if (issue)
206
- throw new AppError(issue.code, issue.code === "work_result_artifact_missing" ? "Completed structured Work has no retained result artifact" : "Completed Work result artifact no longer matches its authoritative receipt", 1, issue);
207
- }
208
191
  function receiptIntentPath(resultPath, sessionId) {
209
192
  return `${resultPath}.pending-${crypto.createHash("sha256").update(sessionId).digest("hex")}`;
210
193
  }
@@ -495,9 +478,9 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
495
478
  // Give each new-generation start its own immutable publication destination.
496
479
  const packetDirectory = generation > 0 ? path.join(directory, "starts", linkId) : directory;
497
480
  const promptPath = path.join(packetDirectory, "prompt.md"), resultPath = path.join(directory, "result.json");
498
- // The expensive filesystem observation is not performed under the writer lock.
481
+ // The first reviewer freezes shared inputs in the same transaction as its claim.
499
482
  const readOnly = parsePayload(work)?.read_only === true;
500
- const fingerprint = readOnly ? workspaceFingerprint(run.workspace_root) : undefined;
483
+ let fingerprint;
501
484
  let binding;
502
485
  try {
503
486
  binding = context.db.writeTransaction(() => {
@@ -518,6 +501,8 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
518
501
  let prompt = renderWorkerPrompt(context, current, run, dependencyResults);
519
502
  if (recovery)
520
503
  prompt = `${recoveryPrompt(recovery)}\n\n${prompt}`;
504
+ if (readOnly)
505
+ fingerprint = sharedReviewInput(work, run, true) ?? workspaceFingerprint(run.workspace_root);
521
506
  const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: run.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint } : {}) };
522
507
  const contextBytes = `${JSON.stringify(workContext, null, 2)}\n`;
523
508
  const response = { ok: true, work_id: current.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, ...(recovery ? { recovery } : {}), session_binding: { source: "PreToolUse", session: { harness_id: identity.harness, session_id: identity.nativeSessionId } } };
@@ -671,6 +656,27 @@ export function retryWork(context, id, reason) {
671
656
  refreshRunWorkProjection(context, work.project_id, work.run_id);
672
657
  return { ok: true, work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason };
673
658
  }
659
+ function sharedReviewInput(work, run, initialize = false) {
660
+ const kind = parsePayload(work)?.kind;
661
+ const relative = kind === "code-review" ? "06-code-review/review-context.json" : kind === "plan-review" ? "04-plan-review/work-context.json" : null;
662
+ if (!relative || !work.parent_work_id)
663
+ return null;
664
+ const file = path.join(requireRunHome(run), relative);
665
+ if (!fs.existsSync(file))
666
+ return null;
667
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
668
+ if (value.read_only_input?.parent_work_id !== work.parent_work_id)
669
+ return null;
670
+ if (!value.read_only_input.fingerprint && initialize) {
671
+ // Called under the Work-start writer reservation, after stage setup has
672
+ // finished. The first reviewer accepts the input shared by this group.
673
+ value.read_only_input.fingerprint = workspaceFingerprint(run.workspace_root);
674
+ const temporary = `${file}.tmp-${crypto.randomUUID()}`;
675
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
676
+ fs.renameSync(temporary, file);
677
+ }
678
+ return typeof value.read_only_input.fingerprint === "string" ? value.read_only_input.fingerprint : null;
679
+ }
674
680
  async function settle(context, id, status, result, progress, retry) {
675
681
  const work = requireWork(context, id);
676
682
  assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
@@ -685,14 +691,21 @@ async function settle(context, id, status, result, progress, retry) {
685
691
  throw new AppError("runtime_missing", "Running Work has no open Work/Session link", 1, { work_id: id });
686
692
  let receipts = [];
687
693
  let coordinationDrift = [];
688
- if (status === "completed") {
689
- if (parsePayload(work)?.read_only === true) {
694
+ const assertReadOnlyInput = () => {
695
+ if (status === "completed" && parsePayload(work)?.read_only === true) {
696
+ const shared = sharedReviewInput(work, run);
697
+ // The final reviewer commits the shared input boundary under the writer
698
+ // reservation. Concurrent finishes cannot both skip this verification.
699
+ if (shared && context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND work_id <> ? AND status IN ('created','running','paused') AND json_extract(payload_json, '$.read_only') = 1 AND json_extract(payload_json, '$.kind') = ? LIMIT 1", [work.project_id, work.run_id, work.parent_work_id, work.work_id, parsePayload(work)?.kind]))
700
+ return;
690
701
  const contextFile = link ? path.join(path.dirname(link.prompt_path), "context.json") : "";
691
- const baseline = contextFile && fs.existsSync(contextFile) ? JSON.parse(fs.readFileSync(contextFile, "utf8")).workspace_fingerprint : null;
702
+ const baseline = shared ?? (contextFile && fs.existsSync(contextFile) ? JSON.parse(fs.readFileSync(contextFile, "utf8")).workspace_fingerprint : null);
692
703
  const current = workspaceFingerprint(run.workspace_root);
693
704
  if (typeof baseline !== "string" || baseline !== current)
694
- throw new AppError("read_only_work_mutated_workspace", "Read-only Work changed the accepted workspace", 2, { work_id: work.work_id, baseline, current });
705
+ throw new AppError("read_only_input_changed", "The workspace changed during read-only Work; its review input is stale (the writer is unknown)", 2, { work_id: work.work_id, baseline, current });
695
706
  }
707
+ };
708
+ if (status === "completed") {
696
709
  validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
697
710
  const packet = codePacket(work);
698
711
  if (packet) {
@@ -716,6 +729,7 @@ async function settle(context, id, status, result, progress, retry) {
716
729
  const now = context.now();
717
730
  context.db.beginWriteTransaction();
718
731
  try {
732
+ assertReadOnlyInput();
719
733
  assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
720
734
  if (link && !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND status = 'running'", [link.id]))
721
735
  throw new AppError("work_generation_stale", "Work Session was interrupted or superseded while checks ran", 1, { work_id: id });
@@ -749,7 +763,10 @@ function plannedAreaDrift(packet, result) {
749
763
  const changed = JSON.parse(result).changed_paths;
750
764
  if (!Array.isArray(changed) || packet.planned_write_areas.length === 0)
751
765
  return [];
752
- return changed.filter((item) => typeof item === "string" && !packet.planned_write_areas.some((area) => item === area || item.startsWith(`${area}/`)));
766
+ return changed.filter((item) => typeof item === "string" && !packet.planned_write_areas.some((area) => {
767
+ const normalized = area.replace(/[\\/]+$/, "");
768
+ return item === normalized || item.startsWith(`${normalized}/`);
769
+ }));
753
770
  }
754
771
  function bindSession(context, work, run, identity, now) {
755
772
  const priorWorkSession = context.db.get("SELECT session_id FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC LIMIT 1", [work.work_id])?.session_id ?? null;
@@ -811,6 +828,10 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
811
828
  }
812
829
  function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
813
830
  const result = value;
831
+ const changedPaths = (result.changed_paths ?? []).map((item) => normalizeWorkspacePath(projectRoot, item));
832
+ const ignored = ignoredWorkspacePaths(projectRoot, changedPaths);
833
+ if (ignored.length)
834
+ throw new AppError("environment_repair_not_product_change", "CODE Work must record ignored environment repair separately, not as a product changed_path", 2, { work_id: work.work_id, paths: ignored });
814
835
  if ((result.deviations?.length ?? 0) > 0 || (result.blockers?.length ?? 0) > 0)
815
836
  throw new AppError("work_contract_incomplete", "CODE Work cannot complete with unresolved deviations or blockers; fail the Work and report the contract mismatch", 2, { work_id: work.work_id, deviations: result.deviations ?? [], blockers: result.blockers ?? [] });
816
837
  const packet = codePacket(work);
@@ -829,7 +850,7 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
829
850
  assertPortableArtifactRef(ref, { workspaceRoot: projectRoot, runHome, runId });
830
851
  }
831
852
  const documentUpdates = (packet.document_updates ?? []);
832
- const changed = new Set(result.changed_paths ?? []);
853
+ const changed = new Set(changedPaths);
833
854
  const missingDocuments = documentUpdates.map((entry) => entry.path).filter((entry) => !changed.has(entry));
834
855
  if (missingDocuments.length)
835
856
  throw new AppError("document_update_missing", "CODE Work did not materialize every document update assigned by PLAN", 2, { work_id: work.work_id, missing_paths: missingDocuments });
@@ -840,7 +861,7 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
840
861
  const assigned = packet.repair?.review_findings?.map((finding) => finding.finding_ref) ?? [];
841
862
  validateReviewResolutions(work.work_id, assigned, result.resolutions ?? []);
842
863
  if (assigned.length) {
843
- if ((result.changed_paths?.length ?? 0) === 0)
864
+ if (changedPaths.length === 0)
844
865
  throw new AppError("review_repair_no_change", "Review repair must materialize a project change; a no-op cannot resolve a finding", 2, { work_id: work.work_id, findings: assigned });
845
866
  for (const resolution of result.resolutions ?? [])
846
867
  for (const ref of resolution.evidence_refs ?? [])
@@ -9,6 +9,18 @@ export class AppError extends Error {
9
9
  this.details = details;
10
10
  }
11
11
  }
12
+ /** Error fields must cross JSON boundaries as data: Error.message is not enumerable. */
13
+ export function errorRecord(error, depth = 0) {
14
+ const value = error;
15
+ const details = value?.details && typeof value.details === "object" && !Array.isArray(value.details) ? value.details : {};
16
+ return {
17
+ code: typeof value?.code === "string" ? value.code : "unexpected",
18
+ message: typeof value?.message === "string" ? value.message : String(error),
19
+ exitCode: typeof value?.exitCode === "number" ? value.exitCode : 1,
20
+ details,
21
+ ...(value?.cause && depth < 3 ? { cause: errorRecord(value.cause, depth + 1) } : {})
22
+ };
23
+ }
12
24
  export function isAppError(error) {
13
25
  return error instanceof AppError;
14
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.64",
3
+ "version": "0.9.0-beta.74",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {