@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
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 +526 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- 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"
|
|
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@
|
|
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@
|
|
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@
|
|
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": ["
|
|
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"
|
|
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"
|
|
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
|
}
|
package/dist/services/canon.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
...
|
|
27
|
+
...canon,
|
|
25
28
|
source: "registered"
|
|
26
29
|
},
|
|
27
30
|
db_path: context.db.path
|
package/dist/services/cleanup.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
198
|
+
let executing = false, committed = false;
|
|
185
199
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
286
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
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
|
-
|
|
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)) {
|