@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.110

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 (162) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +68 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +77 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/native-hook-ingress.js +48 -0
  9. package/dist/cli/run-cli.js +1465 -384
  10. package/dist/cli.js +8 -2
  11. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  12. package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
  13. package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
  14. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  15. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  16. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  17. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  18. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  19. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  20. package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
  21. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  22. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
  23. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  24. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  25. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
  26. package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
  27. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  28. package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +388 -0
  30. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  31. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
  32. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
  35. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  36. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  37. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  38. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  39. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  40. package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
  41. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  42. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
  43. package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
  44. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  45. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  46. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  47. package/dist/harness-runtime/lib/process-json.mjs +71 -0
  48. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  49. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  50. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  51. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  52. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  53. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  54. package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
  55. package/dist/protocol/local-files.js +18 -9
  56. package/dist/runtime/context.js +2 -2
  57. package/dist/schemas/agent-profile.schema.json +1 -1
  58. package/dist/schemas/code-review-result.schema.json +1 -1
  59. package/dist/schemas/code-work-batch.schema.json +3 -3
  60. package/dist/schemas/code-work-result.schema.json +3 -3
  61. package/dist/schemas/plan-review-result.schema.json +2 -2
  62. package/dist/schemas/run-control-receipt.schema.json +99 -0
  63. package/dist/schemas/run-control-request.schema.json +36 -0
  64. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  65. package/dist/services/canon.js +11 -8
  66. package/dist/services/cleanup.js +132 -39
  67. package/dist/services/cli-operation-classifier.js +22 -24
  68. package/dist/services/code-checks.js +589 -106
  69. package/dist/services/codex-hook-delivery.js +28 -0
  70. package/dist/services/command-context.js +80 -0
  71. package/dist/services/config.js +11 -8
  72. package/dist/services/continuation-outcome.js +20 -0
  73. package/dist/services/controller-fanout.js +173 -0
  74. package/dist/services/dashboard.js +103 -31
  75. package/dist/services/engines.js +121 -76
  76. package/dist/services/eval-snapshots.js +994 -97
  77. package/dist/services/execution-policy.js +181 -0
  78. package/dist/services/external-work-launch.js +166 -0
  79. package/dist/services/harness-adapter.js +147 -24
  80. package/dist/services/harness-config.js +2 -0
  81. package/dist/services/hooks.js +702 -327
  82. package/dist/services/lanes.js +78 -65
  83. package/dist/services/lifecycle-command.js +113 -18
  84. package/dist/services/lifecycle-contract.js +30 -0
  85. package/dist/services/lifecycle-invocations.js +1379 -0
  86. package/dist/services/managed-daemon-binding.js +38 -0
  87. package/dist/services/managed-processes.js +302 -34
  88. package/dist/services/merge-queue.js +228 -149
  89. package/dist/services/merge-server.js +66 -28
  90. package/dist/services/migrations.js +37 -16
  91. package/dist/services/native-daemon-history.js +49 -0
  92. package/dist/services/native-session-control.js +39 -0
  93. package/dist/services/plan-runtime.js +26 -11
  94. package/dist/services/plans.js +28 -25
  95. package/dist/services/projects.js +20 -15
  96. package/dist/services/prompts.js +25 -16
  97. package/dist/services/protocols.js +160 -86
  98. package/dist/services/recovery-observation-budget.js +61 -0
  99. package/dist/services/recovery-snapshot-database.js +142 -0
  100. package/dist/services/repair-intents.js +40 -0
  101. package/dist/services/review-copy.js +136 -0
  102. package/dist/services/run-control-receipt.js +56 -0
  103. package/dist/services/run-control-worker.js +351 -0
  104. package/dist/services/run-control.js +888 -0
  105. package/dist/services/run-controller-adapter.js +231 -0
  106. package/dist/services/run-controller-capture.js +167 -0
  107. package/dist/services/run-controller-process.js +180 -0
  108. package/dist/services/run-controller-recovery.js +221 -0
  109. package/dist/services/run-controller-state.js +36 -0
  110. package/dist/services/run-controller.js +798 -0
  111. package/dist/services/run-engine-bindings.js +1 -1
  112. package/dist/services/run-fork.js +142 -0
  113. package/dist/services/run-observations.js +112 -0
  114. package/dist/services/run-projection.js +22 -64
  115. package/dist/services/run-recovery-runtime.js +80 -0
  116. package/dist/services/run-recovery.js +345 -0
  117. package/dist/services/runs.js +536 -181
  118. package/dist/services/runtime-budget.js +413 -0
  119. package/dist/services/runtime-command.js +43 -0
  120. package/dist/services/runtime-scope-capture.js +52 -0
  121. package/dist/services/runtime-scope-control.js +460 -0
  122. package/dist/services/runtime-scope-resume.js +577 -0
  123. package/dist/services/runtime-scope-stop.js +159 -0
  124. package/dist/services/runtime-scope-worker.js +242 -0
  125. package/dist/services/runtime-service.js +97 -0
  126. package/dist/services/schema-validation.js +10 -8
  127. package/dist/services/scope-writer-cleanup.js +26 -0
  128. package/dist/services/sessions.js +35 -69
  129. package/dist/services/stage-blocker.js +17 -7
  130. package/dist/services/stage-context.js +50 -19
  131. package/dist/services/stage-lifecycle.js +100 -103
  132. package/dist/services/stage-pause.js +88 -75
  133. package/dist/services/stage-report-renderer.js +116 -6
  134. package/dist/services/stage-work-graph.js +35 -0
  135. package/dist/services/usage.js +35 -98
  136. package/dist/services/vnext-code-review.js +197 -84
  137. package/dist/services/vnext-code.js +278 -251
  138. package/dist/services/vnext-execution-profile.js +5 -3
  139. package/dist/services/vnext-fanout.js +172 -17
  140. package/dist/services/vnext-merge.js +345 -111
  141. package/dist/services/vnext-plan-review.js +171 -111
  142. package/dist/services/vnext-plan.js +180 -87
  143. package/dist/services/vnext-protocolize.js +320 -74
  144. package/dist/services/vnext-specify.js +115 -77
  145. package/dist/services/work-registry.js +1001 -187
  146. package/dist/services/workspace-bootstrap.js +76 -0
  147. package/dist/services/workspace-files.js +83 -0
  148. package/dist/services/worktrees.js +88 -36
  149. package/dist/shared/entity-references.js +8 -0
  150. package/dist/shared/errors.js +12 -0
  151. package/dist/storage/database.js +692 -35
  152. package/dist/storage/foreign-key-transaction.js +39 -0
  153. package/dist/storage/immutable-input.js +45 -0
  154. package/dist/storage/import-guard.js +102 -0
  155. package/dist/storage/native-cleanup.js +52 -0
  156. package/dist/storage/paths.js +17 -4
  157. package/dist/storage/projection-lock.js +73 -0
  158. package/dist/storage/work-references.js +23 -0
  159. package/dist/storage/writer-contract.js +77 -0
  160. package/dist/storage/writer-migration.js +85 -0
  161. package/package.json +24 -13
  162. package/tools/repair-paused-run-status.mjs +84 -0
@@ -7,7 +7,7 @@
7
7
  "properties": {
8
8
  "schema_id": {"const": "dd-flow/agent-profile@1"},
9
9
  "id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"},
10
- "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok"]},
10
+ "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok", "droid"]},
11
11
  "provider": {"type": "string", "minLength": 1},
12
12
  "model": {"type": "string", "minLength": 1},
13
13
  "reasoning": {"type": "string", "minLength": 1},
@@ -8,7 +8,7 @@
8
8
  "schema_id": {"const": "dd-flow/code-review-result@1"},
9
9
  "verdict": {"enum": ["pass", "findings", "blocked"]},
10
10
  "summary": {"type": "string", "minLength": 1},
11
- "aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "items": {"type": "string"}}}}},
11
+ "aspects": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["aspect_id", "verdict", "summary", "evidence_refs"], "properties": {"aspect_id": {"type": "string", "minLength": 1}, "verdict": {"enum": ["pass", "findings", "blocked"]}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}},
12
12
  "findings": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["finding_id", "aspect_id", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs"], "properties": {"finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"}, "aspect_id": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "obligation_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}}}}
13
13
  }
14
14
  }
@@ -16,10 +16,10 @@
16
16
  "spine": {"type": "object", "additionalProperties": false, "required": ["user_outcome", "component_responsibility", "must_preserve", "non_goals", "acceptance_contribution"], "properties": {"user_outcome": {"type": "string", "minLength": 1}, "component_responsibility": {"type": "string", "minLength": 1}, "must_preserve": {"$ref": "#/$defs/strings"}, "non_goals": {"$ref": "#/$defs/stringList"}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
17
17
  "obligation": {"type": "object", "additionalProperties": false, "required": ["id", "statement"], "properties": {"id": {"type": "string", "minLength": 1}, "statement": {"type": "string", "minLength": 1}}},
18
18
  "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"$ref": "#/$defs/stringList"}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"type": "string", "minLength": 1}}},
19
- "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
19
+ "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "minLength": 1}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}, "inputs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "reuse": {"const": "deterministic"}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}]},
20
20
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
21
21
  "reviewFinding": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs", "decision_reason", "check_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"$ref": "#/$defs/strings"}, "obligation_refs": {"$ref": "#/$defs/strings"}, "decision_reason": {"type": "string", "minLength": 1}, "check_refs": {"$ref": "#/$defs/strings"}}},
22
- "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
23
- "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@2"}}}
22
+ "repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids", "verification_check_refs"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}, "verification_check_refs": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/check"}}}},
23
+ "work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@3"}}}
24
24
  }
25
25
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "dd-flow/code-work-result@2",
3
+ "$id": "dd-flow/code-work-result@3",
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "required": ["schema_id", "summary", "changed_paths", "evidence", "deviations", "blockers"],
7
7
  "properties": {
8
- "schema_id": {"const": "dd-flow/code-work-result@2"},
8
+ "schema_id": {"const": "dd-flow/code-work-result@3"},
9
9
  "summary": {"type": "string", "minLength": 1},
10
10
  "changed_paths": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
11
- "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "refs"], "properties": {"criterion_id": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
11
+ "evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["obligation_ref", "refs"], "properties": {"obligation_ref": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
12
12
  "deviations": {"type": "array", "items": {"type": "string", "minLength": 1}},
13
13
  "blockers": {"type": "array", "items": {"type": "string", "minLength": 1}},
14
14
  "resolutions": {"type": "array", "uniqueItems": true, "items": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "summary", "evidence_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}}
@@ -20,7 +20,7 @@
20
20
  "aspect_id": {"type": "string", "minLength": 1},
21
21
  "verdict": {"enum": ["pass", "watch", "needs_changes", "blocked"]},
22
22
  "summary": {"type": "string", "minLength": 1},
23
- "evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}},
23
+ "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
24
24
  "findings": {
25
25
  "type": "array",
26
26
  "items": {
@@ -31,7 +31,7 @@
31
31
  "finding_id": {"type": "string", "pattern": "^FIND-[0-9]{3}$"},
32
32
  "severity": {"enum": ["blocker", "high", "medium", "low", "info"]},
33
33
  "summary": {"type": "string", "minLength": 1},
34
- "evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}}
34
+ "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
35
35
  }
36
36
  }
37
37
  }
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "dd-flow/run-control-receipt@1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["schema_id", "ok", "request_id", "control_id", "scope", "generation", "requested_mode", "applied_mode", "state", "settled", "nodes", "unsettled_reasons", "resumability"],
7
+ "properties": {
8
+ "schema_id": { "const": "dd-flow/run-control-receipt@1" },
9
+ "ok": { "const": true },
10
+ "request_id": { "type": ["string", "null"], "minLength": 1, "maxLength": 128 },
11
+ "control_id": { "type": ["string", "null"], "pattern": "^CTL-[A-Za-z0-9][A-Za-z0-9._-]*$" },
12
+ "scope": {
13
+ "type": "object", "additionalProperties": false,
14
+ "required": ["project_id", "run_id"],
15
+ "properties": {
16
+ "project_id": { "type": "string", "minLength": 1 },
17
+ "run_id": { "type": "string", "pattern": "^RUN-[A-Za-z0-9][A-Za-z0-9._-]*$" }
18
+ }
19
+ },
20
+ "generation": { "type": "integer", "minimum": 0 },
21
+ "requested_mode": { "enum": [null, "pause", "stop", "resume"] },
22
+ "applied_mode": { "enum": [null, "pause", "stop", "resume"] },
23
+ "state": { "enum": ["not_requested", "pause_requested", "paused", "stop_requested", "stopped", "resuming", "running", "terminal_noop", "superseded", "recovery_required"] },
24
+ "settled": { "type": "boolean" },
25
+ "nodes": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object", "additionalProperties": false,
29
+ "required": ["id", "role", "status", "acknowledged", "settled", "reason"],
30
+ "properties": {
31
+ "id": { "type": "string", "minLength": 1 },
32
+ "role": { "enum": ["subject", "worker", "merge", "judge", "probe", "check", "process", "unknown"] },
33
+ "status": { "enum": ["pending", "unknown", "idle", "stopped", "completed"] },
34
+ "acknowledged": { "type": "boolean" },
35
+ "settled": { "type": "boolean" },
36
+ "reason": { "type": ["string", "null"] },
37
+ "receipt_path": { "type": "string", "minLength": 1 },
38
+ "details": { "type": "object" }
39
+ },
40
+ "allOf": [{
41
+ "if": { "properties": { "settled": { "const": true } } },
42
+ "then": { "properties": { "status": { "enum": ["idle", "stopped", "completed"] } } }
43
+ }]
44
+ }
45
+ },
46
+ "unsettled_reasons": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
47
+ "resumability": { "enum": ["pending", "same_session", "replacement_allowed", "recovery_required", "not_applicable"] },
48
+ "recovery_id": { "type": ["string", "null"] },
49
+ "reused": { "type": "boolean" },
50
+ "next_cursor": { "type": "integer", "minimum": 0 },
51
+ "run_status": { "type": "string" },
52
+ "control": { "type": ["object", "null"] },
53
+ "operations": { "type": "array", "items": { "type": "object" } },
54
+ "worker": { "type": ["object", "null"] },
55
+ "resume_operation": { "type": ["object", "null"] },
56
+ "controller": { "type": "object" },
57
+ "operation_id": { "type": "string", "minLength": 1 },
58
+ "status": { "type": "string" },
59
+ "action": { "enum": ["pause", "stop", "resume"] },
60
+ "progress": {
61
+ "type": "array",
62
+ "items": { "type": "object", "additionalProperties": false, "required": ["phase", "message", "at"], "properties": { "phase": { "enum": ["start", "running", "done", "failed"] }, "message": { "type": "string" }, "at": { "type": "string" }, "detail": { "type": "object" } } }
63
+ },
64
+ "warnings": { "type": "array", "items": { "type": "object" } },
65
+ "response_file": { "type": "string", "minLength": 1 },
66
+ "response_file_error": { "type": "string" }
67
+ },
68
+ "allOf": [
69
+ {
70
+ "if": { "properties": { "state": { "not": { "enum": ["not_requested", "terminal_noop"] } } } },
71
+ "then": { "properties": { "control_id": { "type": "string" }, "request_id": { "type": "string" }, "requested_mode": { "type": "string" } } }
72
+ },
73
+ {
74
+ "if": { "properties": { "state": { "enum": ["paused", "stopped"] } } },
75
+ "then": { "properties": { "settled": { "const": true } } }
76
+ },
77
+ {
78
+ "if": { "properties": { "state": { "const": "paused" } } },
79
+ "then": { "properties": { "applied_mode": { "const": "pause" } } }
80
+ },
81
+ {
82
+ "if": { "properties": { "state": { "const": "stopped" } } },
83
+ "then": { "properties": { "applied_mode": { "const": "stop" } } }
84
+ },
85
+ {
86
+ "if": { "properties": { "settled": { "const": true } } },
87
+ "then": {
88
+ "properties": {
89
+ "nodes": { "items": { "properties": { "settled": { "const": true } } } },
90
+ "unsettled_reasons": { "maxItems": 0 }
91
+ }
92
+ }
93
+ },
94
+ {
95
+ "if": { "properties": { "settled": { "const": false } } },
96
+ "then": { "properties": { "unsettled_reasons": { "minItems": 1 }, "resumability": { "enum": ["pending", "recovery_required", "not_applicable"] } } }
97
+ }
98
+ ]
99
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "dd-flow/run-control-request@1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["schema_id", "request_id", "project_root", "run_id", "action"],
7
+ "properties": {
8
+ "schema_id": { "const": "dd-flow/run-control-request@1" },
9
+ "request_id": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
10
+ "project_root": { "type": "string", "minLength": 1, "pattern": "^(?:/|[A-Za-z]:[\\\\/]|\\\\\\\\)" },
11
+ "run_id": { "type": "string", "pattern": "^RUN-[A-Za-z0-9][A-Za-z0-9._-]*$" },
12
+ "action": { "enum": ["pause", "stop", "resume"] },
13
+ "wait_ms": { "type": "integer", "minimum": 0, "maximum": 60000, "default": 0 },
14
+ "grace_ms": { "type": "integer", "minimum": 0, "maximum": 3600000, "default": 30000 },
15
+ "escalate": { "const": "interrupt" },
16
+ "force": { "type": "boolean" },
17
+ "from": { "type": "string", "pattern": "^CTL-[A-Za-z0-9][A-Za-z0-9._-]*$" }
18
+ },
19
+ "allOf": [
20
+ {
21
+ "if": { "properties": { "action": { "const": "pause" } } },
22
+ "then": { "not": { "anyOf": [{ "required": ["force"] }, { "required": ["from"] }] } }
23
+ },
24
+ {
25
+ "if": { "properties": { "action": { "const": "stop" } } },
26
+ "then": { "not": { "anyOf": [{ "required": ["grace_ms"] }, { "required": ["escalate"] }, { "required": ["from"] }] } }
27
+ },
28
+ {
29
+ "if": { "properties": { "action": { "const": "resume" } } },
30
+ "then": {
31
+ "required": ["from"],
32
+ "not": { "anyOf": [{ "required": ["grace_ms"] }, { "required": ["escalate"] }, { "required": ["force"] }] }
33
+ }
34
+ }
35
+ ]
36
+ }
@@ -29,9 +29,9 @@
29
29
  "documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}}},
30
30
  "spine": {"type": "object", "additionalProperties": false, "required": ["user_outcome", "component_responsibility", "must_preserve", "non_goals", "acceptance_contribution"], "properties": {"user_outcome": {"type": "string", "minLength": 1}, "component_responsibility": {"type": "string", "minLength": 1}, "must_preserve": {"$ref": "#/$defs/strings"}, "non_goals": {"type": "array", "items": {"type": "string", "minLength": 1}}, "acceptance_contribution": {"type": "string", "minLength": 1}}},
31
31
  "execution": {"type": "object", "additionalProperties": false, "required": ["required_read", "discovery_boundary", "planned_write_areas", "stop_conditions"], "properties": {"required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources. This is not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas. A worker may inspect other project-local sources when necessary."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Optional file or directory hints used only to coordinate concurrent Works. They never grant or deny write permission."}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Semantic contradictions or external blockers that require the worker to stop."}}},
32
- "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "pattern": "^P[0-9]+$"}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}, {"if": {"properties": {"command": {"pattern": "^@check/"}}}, "then": {"required": ["definition"]}}]},
32
+ "check": {"type": "object", "additionalProperties": false, "required": ["id", "command", "purpose", "run_at", "availability"], "properties": {"id": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}, "command": {"type": "string", "minLength": 1}, "purpose": {"type": "string", "minLength": 1}, "run_at": {"enum": ["work", "code", "readiness", "merge", "release"]}, "availability": {"enum": ["available", "planned"]}, "provided_by": {"type": "string", "pattern": "^P[0-9]+$"}, "definition": {"type": "string", "minLength": 1}, "required_artifacts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "ports": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"}}, "inputs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "reuse": {"const": "deterministic"}}, "allOf": [{"if": {"properties": {"availability": {"const": "planned"}}, "required": ["availability"]}, "then": {"required": ["provided_by", "definition"], "properties": {"command": {"pattern": "^@check/"}}}}, {"if": {"properties": {"command": {"pattern": "^@check/"}}}, "then": {"required": ["definition"]}}]},
33
33
  "verification": {"type": "object", "additionalProperties": false, "required": ["check_refs"], "properties": {"check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}}},
34
34
  "item": {"type": "object", "additionalProperties": false, "required": ["id", "title", "summary", "details", "depends_on", "requirement_refs", "semantic_spine", "execution_context", "verification"], "properties": {"id": {"type": "string", "pattern": "^P[0-9]+$"}, "title": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "details": {"type": "string", "minLength": 1}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "requirement_refs": {"$ref": "#/$defs/strings"}, "semantic_spine": {"$ref": "#/$defs/spine"}, "execution_context": {"$ref": "#/$defs/execution"}, "verification": {"$ref": "#/$defs/verification"}}},
35
- "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "plan_item_ids", "changed_surfaces", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "plan_item_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "changed_surfaces": {"$ref": "#/$defs/strings"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"type": "array", "items": {"type": "string", "minLength": 1}}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"enum": ["work", "code", "readiness", "merge", "release", "external"]}}}
35
+ "acceptance": {"type": "object", "additionalProperties": false, "required": ["criterion_id", "plan_item_ids", "changed_surfaces", "path", "environment", "fixtures", "cleanup", "check_refs", "expected_evidence", "proof_limits", "gate"], "properties": {"criterion_id": {"type": "string", "pattern": "^AC-[0-9]+$"}, "plan_item_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^P[0-9]+$"}}, "changed_surfaces": {"$ref": "#/$defs/strings"}, "path": {"type": "string", "minLength": 1}, "environment": {"type": "string", "minLength": 1}, "fixtures": {"type": "array", "items": {"type": "string", "minLength": 1}}, "cleanup": {"type": "string", "minLength": 1}, "check_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^CHK-[A-Za-z0-9-]+$"}}, "expected_evidence": {"$ref": "#/$defs/strings"}, "proof_limits": {"$ref": "#/$defs/strings"}, "gate": {"enum": ["work", "code", "readiness", "merge", "release"]}}}
36
36
  }
37
37
  }
@@ -1,27 +1,30 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { prepareError } from "../cli/input-preparation.js";
4
6
  const canonRootKey = "canon.root";
5
- export function registerCanonRoot(context, input) {
7
+ export function prepareCanonRegistration(context, input) {
8
+ if (typeof input.root !== "string" || !input.root.trim())
9
+ return prepareError(new AppError("validation", "Canon root must be a non-empty path", 2));
6
10
  const resolution = resolveCanonRoot(context, { explicitRoot: input.root, allowRegistered: false });
7
11
  if (!resolution.ok || !resolution.canon) {
8
- return {
9
- ...resolution,
10
- action: "register",
11
- exit_code: 1
12
- };
12
+ return prepareError(new AppError("validation", "Canon root cannot be registered", 2, { root: input.root, blockers: resolution.blockers, candidates: resolution.candidates, bootstrap: resolution.bootstrap }));
13
13
  }
14
+ return resolution.canon;
15
+ }
16
+ export function registerCanonRoot(context, input, canon = prepareCanonRegistration(context, input)) {
14
17
  const now = context.now();
15
18
  context.db.run(`INSERT INTO runtime_config (key, value_json, created_at, updated_at)
16
19
  VALUES (?, ?, ?, ?)
17
20
  ON CONFLICT(key) DO UPDATE SET
18
21
  value_json = excluded.value_json,
19
- updated_at = excluded.updated_at`, [canonRootKey, JSON.stringify({ root: resolution.canon.root }), now, now]);
22
+ updated_at = excluded.updated_at`, [canonRootKey, JSON.stringify({ root: canon.root }), now, now]);
20
23
  return {
21
24
  ok: true,
22
25
  action: "register",
23
26
  canon: {
24
- ...resolution.canon,
27
+ ...canon,
25
28
  source: "registered"
26
29
  },
27
30
  db_path: context.db.path
@@ -2,13 +2,13 @@ import fs from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { parseJsonObject } from "../shared/json.js";
6
- import { ensureReadableFile } from "../storage/database.js";
7
- import { projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
5
+ import { prepareJsonFile } from "../cli/input-preparation.js";
6
+ import { projectCheckoutRoot, resolveProjectRoot, runtimeStateJsonPath } from "../storage/paths.js";
8
7
  import { appendAudit } from "./audit.js";
9
8
  import { requireProjectByRoot } from "./projects.js";
10
- import { protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
9
+ import { persistProtocolState, prepareProtocolRuntimeRepair, protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
11
10
  import { publicSessionIdentity } from "./session-identity.js";
11
+ import { publishRunProjections } from "./runs.js";
12
12
  const actionKinds = new Set([
13
13
  "repair_protocol_state",
14
14
  "cancel_queue_job",
@@ -25,7 +25,8 @@ export function cleanupScan(context, input) {
25
25
  const protocols = context.db.all("SELECT id, status, stage, state_path FROM protocols WHERE project_id = ? ORDER BY id ASC", [project.id]);
26
26
  for (const protocol of protocols) {
27
27
  const terminal = ["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage);
28
- if (!fs.existsSync(protocol.state_path)) {
28
+ const requiresMigration = protocol.state_path !== runtimeStateJsonPath(context.ddFlowHome, project.id, protocol.id);
29
+ if (!fs.existsSync(protocol.state_path) || requiresMigration) {
29
30
  pushFinding(findings, actions, "missing_protocol_runtime_state", "warning", {
30
31
  protocol_id: protocol.id,
31
32
  path: protocol.state_path,
@@ -43,6 +44,8 @@ export function cleanupScan(context, input) {
43
44
  }
44
45
  }
45
46
  const fullProtocol = requireProtocol(context, protocol.id, project.id);
47
+ if (requiresMigration)
48
+ continue;
46
49
  const state = readProtocolRuntimeState(context, fullProtocol).state;
47
50
  const diagnostics = protocolRunDiagnostics(context, fullProtocol, state).diagnostics;
48
51
  for (const diagnostic of diagnostics.filter((entry) => entry.code === "protocol_run_stage_mismatch")) {
@@ -170,36 +173,55 @@ export function cleanupScan(context, input) {
170
173
  plan: { project_root: project.root, project_id: project.id, actions }
171
174
  };
172
175
  }
173
- export function cleanupApply(context, input) {
174
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
176
+ export function prepareCleanupApply(context, input) {
175
177
  const reason = input.reason.trim();
176
178
  if (!reason) {
177
179
  throw new AppError("validation", "cleanup apply requires --reason", 2);
178
180
  }
179
- ensureReadableFile(input.planFile);
180
- const plan = validateCleanupPlan(parseJsonObject(fs.readFileSync(input.planFile, "utf8"), input.planFile), project);
181
+ const value = prepareJsonFile(input.planFile, "cleanup plan").value;
182
+ if (!value || typeof value !== "object" || Array.isArray(value))
183
+ throw new AppError("validation", "cleanup plan must be an object", 2);
184
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
185
+ const plan = validateCleanupPlan(value, project);
181
186
  validateNoDestructiveSurprises(context, plan, input.force);
187
+ return { project, reason, plan };
188
+ }
189
+ export function cleanupApply(context, input) {
190
+ const { project, reason, plan } = input.prepared ?? prepareCleanupApply(context, input);
191
+ // A supplied plan can outlive its initial inspection. Refresh external
192
+ // workspace evidence before reserving the writer, then recheck SQL below.
193
+ if (input.prepared)
194
+ validateNoDestructiveSurprises(context, plan, input.force);
195
+ const prepared = prepareCleanupEffects(context, plan);
182
196
  const now = context.now();
183
197
  const results = [];
184
- context.db.exec("BEGIN IMMEDIATE");
198
+ let executing = false, committed = false;
185
199
  try {
186
- for (const action of plan.actions) {
187
- results.push(applyAction(context, action, reason, input.force, now));
188
- }
189
- const changed = results.filter((result) => result.changed).length;
190
- if (changed > 0) {
191
- appendAudit(context, {
192
- projectId: project.id,
193
- eventType: "cleanup.applied",
194
- reason,
195
- forced: input.force,
196
- payload: { project_id: project.id, action_count: plan.actions.length, changed, results }
197
- });
198
- }
199
- context.db.exec("COMMIT");
200
+ context.db.writeTransaction(() => {
201
+ context.db.afterCommit(() => { committed = true; });
202
+ validateNoDestructiveSurprises(context, plan, input.force, false);
203
+ prepared.assertUnchanged();
204
+ for (const action of plan.actions) {
205
+ executing = true;
206
+ results.push(applyAction(context, action, reason, input.force, now, prepared));
207
+ }
208
+ const changed = results.filter((result) => result.changed).length;
209
+ if (changed > 0) {
210
+ appendAudit(context, {
211
+ projectId: project.id,
212
+ eventType: "cleanup.applied",
213
+ reason,
214
+ forced: input.force,
215
+ payload: { project_id: project.id, action_count: plan.actions.length, changed, results }
216
+ });
217
+ }
218
+ });
200
219
  }
201
220
  catch (error) {
202
- context.db.exec("ROLLBACK");
221
+ if (committed || error instanceof AppError && error.details.effect === "committed")
222
+ throw new AppError(error instanceof AppError ? error.code : "cleanup_publication_failed", error instanceof Error ? error.message : "Committed cleanup publication failed", error instanceof AppError ? error.exitCode : 1, { ...(error instanceof AppError ? error.details : {}), effect: "committed", publication_pending: true, attempted_results: results, cause: String(error) });
223
+ if (executing)
224
+ throw new AppError("cleanup_apply_failed", "Cleanup failed after execution began; inspect retained artifacts before retrying", 1, { effect: "unknown", database_rolled_back: true, attempted_results: results, cause: error instanceof Error ? error.message : String(error), ...(error instanceof AppError ? { cause_code: error.code } : {}) });
203
225
  throw error;
204
226
  }
205
227
  return {
@@ -248,18 +270,36 @@ function validateCleanupPlan(value, project) {
248
270
  throw new AppError("validation", "close_flow_session requires a native session identity", 2);
249
271
  }
250
272
  }
273
+ const requiredField = action.kind === "discard_stale_run" ? "run_id"
274
+ : ["repair_protocol_state", "cancel_queue_job", "close_worktree_record"].includes(String(action.kind)) ? "protocol_id" : undefined;
275
+ if (requiredField && (typeof action[requiredField] !== "string" || !action[requiredField].trim()))
276
+ throw new AppError("validation", `cleanup ${String(action.kind)} requires ${requiredField}`, 2);
277
+ const numericField = action.kind === "expire_lane_lock" ? "lock_id" : action.kind === "expire_lane_waiter" ? "waiter_id" : undefined;
278
+ if (numericField && (!Number.isSafeInteger(action[numericField]) || action[numericField] < 1))
279
+ throw new AppError("validation", `cleanup ${String(action.kind)} requires a positive integer ${numericField}`, 2);
280
+ if (action.worktree_path !== undefined && (typeof action.worktree_path !== "string" || !path.isAbsolute(action.worktree_path)))
281
+ throw new AppError("validation", "cleanup worktree_path must be an absolute path", 2);
251
282
  return action;
252
283
  });
253
284
  return { project_root: project.root, project_id: project.id, actions };
254
285
  }
255
- function validateNoDestructiveSurprises(context, plan, force) {
256
- const destructive = plan.actions.find((action) => action.kind === "close_worktree_record" && action.worktree_path && dirtyGit(action.worktree_path));
286
+ function validateNoDestructiveSurprises(context, plan, force, inspectFiles = true) {
287
+ for (const action of plan.actions) {
288
+ if (action.kind === "repair_protocol_state")
289
+ requireProtocol(context, action.protocol_id, action.project_id);
290
+ if (action.kind === "discard_stale_run") {
291
+ const run = context.db.get("SELECT index_json, status FROM runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);
292
+ if (run && ["running", "discarded"].includes(run.status))
293
+ cleanupRunIndex(run.index_json);
294
+ }
295
+ }
296
+ const destructive = inspectFiles && plan.actions.find((action) => action.kind === "close_worktree_record" && action.worktree_path && dirtyGit(action.worktree_path));
257
297
  if (destructive && !force) {
258
298
  throw new AppError("worktree_dirty", "cleanup apply refuses dirty worktree actions without --force --reason", 1, {
259
299
  path: destructive.worktree_path
260
300
  });
261
301
  }
262
- const unsafeWorktree = plan.actions.find((action) => action.kind === "close_worktree_record" && action.worktree_path && !safeCleanupWorktreePath(context, action.project_id, action.worktree_path));
302
+ const unsafeWorktree = inspectFiles && plan.actions.find((action) => action.kind === "close_worktree_record" && action.worktree_path && !safeCleanupWorktreePath(context, action.project_id, action.worktree_path));
263
303
  if (unsafeWorktree) {
264
304
  throw new AppError("worktree_cleanup_unsafe", "cleanup apply refuses unsafe worktree path", 1, {
265
305
  path: unsafeWorktree.worktree_path
@@ -278,12 +318,53 @@ function validateNoDestructiveSurprises(context, plan, force) {
278
318
  });
279
319
  }
280
320
  }
281
- function applyAction(context, action, reason, force, now) {
321
+ function prepareCleanupEffects(context, plan) {
322
+ const checks = [], files = new Map();
323
+ const repairs = new Map();
324
+ const read = (sql, params) => {
325
+ const value = context.db.get(sql, params), encoded = JSON.stringify(value);
326
+ checks.push(() => { if (JSON.stringify(context.db.get(sql, params)) !== encoded)
327
+ throw new AppError("cleanup_plan_changed", "Cleanup target changed during preparation", 1, { effect: "no_effect" }); });
328
+ return value;
329
+ };
330
+ const remember = (file) => { if (file !== undefined)
331
+ files.set(file, fs.existsSync(file)); };
332
+ for (const action of plan.actions) {
333
+ if (action.kind === "repair_protocol_state") {
334
+ read("SELECT * FROM protocols WHERE project_id = ? AND id = ?", [action.project_id, action.protocol_id]);
335
+ const protocol = requireProtocol(context, action.protocol_id, action.project_id);
336
+ remember(protocol.state_path);
337
+ repairs.set(action, prepareProtocolRuntimeRepair(context, protocol));
338
+ }
339
+ if (action.kind === "discard_stale_run") {
340
+ const run = read("SELECT * FROM runs WHERE project_id = ? AND id = ?", [action.project_id, action.run_id]);
341
+ if (run) {
342
+ remember(run.runtime_path);
343
+ remember(run.run_dir);
344
+ remember(run.workspace_root);
345
+ }
346
+ }
347
+ if (action.kind === "close_worktree_record") {
348
+ const record = read("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [action.project_id, action.protocol_id]);
349
+ remember(action.worktree_path ?? record?.worktree_path);
350
+ }
351
+ }
352
+ return { repairs, assertUnchanged: () => { for (const check of checks)
353
+ check(); }, exists: (file) => {
354
+ if (!files.has(file))
355
+ throw new AppError("cleanup_plan_changed", "Cleanup target has no prepared filesystem observation", 1, { path: file, effect: "no_effect" });
356
+ return files.get(file);
357
+ } };
358
+ }
359
+ function applyAction(context, action, reason, force, now, prepared) {
282
360
  if (action.kind === "repair_protocol_state" && action.protocol_id) {
283
361
  const protocol = requireProtocol(context, action.protocol_id, action.project_id);
284
362
  const beforePath = protocol.state_path;
285
- const existed = fs.existsSync(beforePath);
286
- readProtocolRuntimeState(context, protocol);
363
+ const existed = prepared.exists(beforePath), repair = prepared.repairs.get(action);
364
+ context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [repair.statePath, repair.planPath, action.project_id, action.protocol_id]);
365
+ persistProtocolState(context, { ...protocol, state_path: repair.statePath, plan_path: repair.planPath }, repair.state);
366
+ protocol.state_path = repair.statePath;
367
+ appendAudit(context, { protocolId: protocol.id, projectId: protocol.project_id, eventType: "protocol.runtime_state_repaired", payload: { protocol_id: protocol.id, path: repair.statePath, repaired_from: repair.previousStatePath, source: "sqlite" } });
287
368
  return {
288
369
  kind: action.kind,
289
370
  changed: !existed || beforePath !== protocol.state_path,
@@ -338,21 +419,19 @@ function applyAction(context, action, reason, force, now) {
338
419
  if (run.status === "discarded") {
339
420
  const index = discardedRunIndex(run.index_json, reason, now);
340
421
  context.db.run("UPDATE runs SET verdict = 'discarded', next_action = ?, index_json = ?, updated_at = ?, completed_at = COALESCE(completed_at, ?) WHERE project_id = ? AND id = ?", [reason, JSON.stringify(index), now, now, action.project_id, action.run_id]);
341
- if (fs.existsSync(run.runtime_path))
342
- fs.writeFileSync(run.runtime_path, `${JSON.stringify(index, null, 2)}\n`);
422
+ context.db.afterCommit(() => publishRunProjections(context, action.project_id, action.run_id));
343
423
  return { kind: action.kind, changed: true, run_id: action.run_id };
344
424
  }
345
425
  if (run.status !== "running") {
346
426
  return { kind: action.kind, changed: false, skipped: true, reason: `already_${run.status}`, run_id: action.run_id };
347
427
  }
348
- if (fs.existsSync(run.runtime_path) || fs.existsSync(run.run_dir) || fs.existsSync(run.workspace_root)) {
428
+ if (prepared.exists(run.runtime_path) || prepared.exists(run.run_dir) || prepared.exists(run.workspace_root)) {
349
429
  return { kind: action.kind, changed: false, skipped: true, reason: "runtime_or_workspace_reappeared", run_id: action.run_id };
350
430
  }
351
431
  const index = discardedRunIndex(run.index_json, reason, now);
352
432
  context.db.run(`UPDATE runs SET status = 'discarded', verdict = 'discarded', next_action = ?, index_json = ?, updated_at = ?, completed_at = ?
353
433
  WHERE project_id = ? AND id = ? AND status = 'running'`, [reason, JSON.stringify(index), now, now, action.project_id, action.run_id]);
354
- fs.mkdirSync(run.run_dir, { recursive: true });
355
- fs.writeFileSync(run.runtime_path, `${JSON.stringify(index, null, 2)}\n`);
434
+ context.db.afterCommit(() => publishRunProjections(context, action.project_id, action.run_id));
356
435
  context.db.run(`UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
357
436
  WHERE project_id = ? AND run_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, action.project_id, action.run_id]);
358
437
  context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE project_id = ? AND run_id = ? AND ended_at IS NULL", [now, action.project_id, action.run_id]);
@@ -394,7 +473,7 @@ function applyAction(context, action, reason, force, now) {
394
473
  }
395
474
  if (action.kind === "close_worktree_record" && action.protocol_id) {
396
475
  const worktreePath = action.worktree_path ?? context.db.get("SELECT worktree_path FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [action.project_id, action.protocol_id])?.worktree_path;
397
- const status = worktreePath && fs.existsSync(worktreePath) ? "kept" : "removed";
476
+ const status = worktreePath && prepared.exists(worktreePath) ? "kept" : "removed";
398
477
  const result = context.db.run(`UPDATE worktree_records SET status = ?, updated_at = ?, closed_at = ?
399
478
  WHERE project_id = ? AND protocol_id = ? AND status = 'active'`, [status, now, now, action.project_id, action.protocol_id]);
400
479
  return {
@@ -407,22 +486,36 @@ function applyAction(context, action, reason, force, now) {
407
486
  return { kind: action.kind, changed: false, skipped: true, reason: "invalid_action" };
408
487
  }
409
488
  function discardedRunIndex(indexJson, reason, now) {
410
- const index = JSON.parse(indexJson);
489
+ const index = cleanupRunIndex(indexJson);
411
490
  index.schema_id = "dd-flow/flow-run@3";
412
491
  index.status = "discarded";
413
492
  index.verdict = "discarded";
414
493
  index.next_action = reason;
415
494
  index.updated_at = now;
495
+ index.runtime_revision = (typeof index.runtime_revision === "number" ? index.runtime_revision : 0) + 1;
416
496
  index.completed_at ??= now;
417
497
  index.finished_at ??= now;
418
498
  return index;
419
499
  }
500
+ function cleanupRunIndex(indexJson) {
501
+ try {
502
+ const index = JSON.parse(indexJson);
503
+ if (!index || typeof index !== "object" || Array.isArray(index))
504
+ throw new Error("RUN index must be an object");
505
+ return index;
506
+ }
507
+ catch (error) {
508
+ throw new AppError("runtime_state_invalid", "Cleanup cannot read retained RUN state", 1, { cause: String(error) });
509
+ }
510
+ }
420
511
  function dirtyGit(worktreePath) {
421
512
  if (!fs.existsSync(worktreePath)) {
422
513
  return false;
423
514
  }
424
515
  const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8" });
425
- return result.status === 0 && result.stdout.trim().length > 0;
516
+ if (result.error || result.status !== 0)
517
+ throw new AppError("workspace_status_failed", "Cannot establish worktree status before cleanup", 1, { path: worktreePath, cause: result.error?.message ?? result.stderr });
518
+ return result.stdout.trim().length > 0;
426
519
  }
427
520
  function safeCleanupWorktreePath(context, projectId, worktreePath) {
428
521
  if (!fs.existsSync(worktreePath)) {