@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.145
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 +59 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +9 -3
- package/dist/cli/command-metadata.js +33 -14
- package/dist/cli/help.js +32 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +74 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +70 -32
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +12 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +11 -4
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-fanout.js +21 -4
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +48 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-start.js +26 -19
- package/dist/services/descriptor-stage-coordinator.js +30 -10
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +104 -93
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +8 -4
- package/dist/services/frozen-engine.js +113 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +2 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +25 -4
- package/dist/services/lifecycle-invocations.js +33 -5
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +28 -2
- package/dist/services/memory-init-controller-start.js +19 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +21 -3
- package/dist/services/memory-init-native-capacity.js +13 -0
- package/dist/services/memory-init-preflight.js +11 -1
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-layer.js +68 -4
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +77 -23
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +69 -8
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-controller-process.js +2 -6
- package/dist/services/run-controller-recovery.js +6 -1
- package/dist/services/run-controller.js +131 -80
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +9 -7
- package/dist/services/stage-pause.js +108 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-fanout.js +21 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +80 -33
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +8 -4
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"work_id": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
10
10
|
"packet_sha256": { "$ref": "#/$defs/hash" },
|
|
11
11
|
"wave": { "type": "integer", "minimum": 1, "maximum": 2 },
|
|
12
|
-
"outcome": { "enum": ["accepted", "blocked"] },
|
|
12
|
+
"outcome": { "enum": ["accepted", "reconciled", "blocked"] },
|
|
13
13
|
"summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
|
|
14
14
|
"changes": {
|
|
15
15
|
"type": "array", "maxItems": 512,
|
|
@@ -29,12 +29,19 @@
|
|
|
29
29
|
"properties": {
|
|
30
30
|
"finding_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
31
31
|
"summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
|
|
32
|
-
"delta_paths": { "type": "array", "
|
|
32
|
+
"delta_paths": { "type": "array", "maxItems": 512, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } },
|
|
33
33
|
"evidence": { "type": "array", "minItems": 1, "maxItems": 256, "items": { "$ref": "#/$defs/reference" } }
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
+
"allOf": [
|
|
39
|
+
{ "if": { "properties": { "outcome": { "const": "accepted" } } },
|
|
40
|
+
"then": { "properties": { "changes": { "type": "array", "minItems": 1 }, "resolutions": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "delta_paths": { "type": "array", "minItems": 1 } } } } } } },
|
|
41
|
+
{ "if": { "properties": { "outcome": { "const": "reconciled" } } },
|
|
42
|
+
"then": { "properties": { "changes": { "type": "array", "maxItems": 0 }, "resolutions": { "type": "array", "minItems": 1,
|
|
43
|
+
"items": { "type": "object", "properties": { "delta_paths": { "type": "array", "maxItems": 0 } } } } } } }
|
|
44
|
+
],
|
|
38
45
|
"$defs": {
|
|
39
46
|
"hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
40
47
|
"reference": {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"required": ["schema_id", "from_version", "to_version", "minimum_upgrade_mode", "domains", "path_migration", "runtime_migration", "required_checks"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"schema_id": {"const": "dd-flow/release-impact@1"},
|
|
10
|
-
"from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]
|
|
11
|
-
"to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]
|
|
10
|
+
"from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
|
|
11
|
+
"to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
|
|
12
12
|
"minimum_upgrade_mode": {"enum": ["patch", "targeted_minor", "full_migration"]},
|
|
13
|
-
"domains": {"type": "array", "items": {"enum": ["canonical_layer", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
|
|
13
|
+
"domains": {"type": "array", "items": {"enum": ["canonical_layer", "flow_contracts", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
|
|
14
14
|
"path_migration": {"type": "boolean"},
|
|
15
15
|
"runtime_migration": {"type": "boolean"},
|
|
16
16
|
"required_checks": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"stage_root": {"type": "string", "minLength": 1},
|
|
20
20
|
"archive_path": {"type": ["string", "null"]},
|
|
21
21
|
"prompt_path": {"type": "string", "minLength": 1},
|
|
22
|
+
"prompt_delivery": {
|
|
23
|
+
"type": "object", "additionalProperties": false, "required": ["path", "sha256"],
|
|
24
|
+
"properties": {"path": {"type": "string", "minLength": 1}, "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}}
|
|
25
|
+
},
|
|
22
26
|
"aliases": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
23
27
|
"resolved_context": {"type": "object", "additionalProperties": true},
|
|
24
28
|
"plan_ref": {"type": "string", "pattern": "^\\.memory-bank/protocol/[^/]+/plan\\.json$"},
|
|
@@ -15,12 +15,24 @@ export function acceptedIntentReviewFingerprint(sources) {
|
|
|
15
15
|
* clarification may use the existing coordinator correction, explicitly named
|
|
16
16
|
* as such; it never retroactively expands the independent review's coverage. */
|
|
17
17
|
export function validateAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, acknowledgement) {
|
|
18
|
+
// Keep file validation outside callers' SQL writer; the prepared validator
|
|
19
|
+
// below consumes only accepted bytes/ordered SQL events.
|
|
18
20
|
const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
|
|
19
21
|
if (!reviewed)
|
|
20
|
-
return null;
|
|
22
|
+
return null;
|
|
21
23
|
if (contextFingerprint !== reviewed)
|
|
22
24
|
throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
|
|
23
25
|
const currentSources = prepareAcceptedIntentSources(context, { ...scope, stageDirectory: "intake", marked: true }).sources;
|
|
26
|
+
return validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement);
|
|
27
|
+
}
|
|
28
|
+
export function validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement) {
|
|
29
|
+
const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
|
|
30
|
+
if (!reviewed)
|
|
31
|
+
return null; // Historical source contract has no new authority.
|
|
32
|
+
if (contextFingerprint !== reviewed)
|
|
33
|
+
throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
|
|
34
|
+
if (currentSources.length > 256)
|
|
35
|
+
throw new AppError("accepted_intent_source_invalid", "Accepted intent source count exceeds its bound", 1);
|
|
24
36
|
const current = acceptedIntentReviewFingerprint(currentSources);
|
|
25
37
|
const reviewedEvent = Math.max(0, ...(reviewedSources ?? []).filter(source => source.role.startsWith("accepted_hitl:")).map(source => Number(source.role.split(":")[1])));
|
|
26
38
|
const rows = context.db.all("SELECT id, payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND id > ? ORDER BY id LIMIT 256", [scope.projectId, scope.runId, reviewedEvent]);
|
package/dist/services/canon.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { prepareError } from "../cli/input-preparation.js";
|
|
6
|
+
import { resolveDdFlowHome } from "../storage/paths.js";
|
|
7
|
+
import { readSetupStatus } from "./setup.js";
|
|
6
8
|
const canonRootKey = "canon.root";
|
|
7
9
|
export function prepareCanonRegistration(context, input) {
|
|
8
10
|
if (typeof input.root !== "string" || !input.root.trim())
|
|
@@ -41,16 +43,21 @@ export function getCanonStatus(context, input = {}) {
|
|
|
41
43
|
export function resolveCanonRoot(context, input = {}) {
|
|
42
44
|
return resolveCanonCandidates(canonCandidates(context, input));
|
|
43
45
|
}
|
|
44
|
-
/** Read only: resolve
|
|
46
|
+
/** Read only: resolve canonical data without opening SQLite or running migrations. */
|
|
45
47
|
export function resolveCanonRootBeforeContext(env, input = {}) {
|
|
46
48
|
const candidates = [];
|
|
47
49
|
if (input.explicitRoot)
|
|
48
50
|
candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
|
|
49
51
|
else if (env.DD_MEMORYBANK)
|
|
50
52
|
candidates.push(validateCanonCandidate("env", env.DD_MEMORYBANK));
|
|
51
|
-
else
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
else {
|
|
54
|
+
const installed = installedCanonCandidate(resolveDdFlowHome(env));
|
|
55
|
+
if (installed)
|
|
56
|
+
candidates.push(installed);
|
|
57
|
+
else
|
|
58
|
+
for (const known of knownCanonRoots())
|
|
59
|
+
candidates.push(validateCanonCandidate("known", known));
|
|
60
|
+
}
|
|
54
61
|
return resolveCanonCandidates(uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`));
|
|
55
62
|
}
|
|
56
63
|
function resolveCanonCandidates(candidates) {
|
|
@@ -67,7 +74,7 @@ function resolveCanonCandidates(candidates) {
|
|
|
67
74
|
const memorybankRoot = candidate.memorybank_root ?? root;
|
|
68
75
|
const mbbRoot = candidate.mbb_root ?? path.join(memorybankRoot, "mbb");
|
|
69
76
|
const layout = candidate.layout ?? "legacy_root";
|
|
70
|
-
const metadata = readCanonMetadata(root, flowRoot);
|
|
77
|
+
const metadata = candidate.metadata ?? readCanonMetadata(root, flowRoot);
|
|
71
78
|
return {
|
|
72
79
|
ok: true,
|
|
73
80
|
canon: {
|
|
@@ -114,6 +121,30 @@ function resolveCanonCandidates(candidates) {
|
|
|
114
121
|
]
|
|
115
122
|
};
|
|
116
123
|
}
|
|
124
|
+
/** Archive provenance belongs to the selected release, not an ancestor Git
|
|
125
|
+
* checkout. A damaged installation is not permission to select nearby code. */
|
|
126
|
+
function installedCanonCandidate(home) {
|
|
127
|
+
if (!fs.existsSync(path.join(home, "installation.json")))
|
|
128
|
+
return null;
|
|
129
|
+
const status = readSetupStatus(home), installation = status.installation;
|
|
130
|
+
const invalid = (root, reason) => ({
|
|
131
|
+
source: "installed", root, realpath: null, valid: false, blockers: [reason]
|
|
132
|
+
});
|
|
133
|
+
if (!installation || status.installation_readiness !== "installed")
|
|
134
|
+
return invalid(home, "Installed canonical release is degraded; reconcile setup before selecting its canon.");
|
|
135
|
+
const roots = fs.readdirSync(installation.canon_root);
|
|
136
|
+
if (roots.length !== 1)
|
|
137
|
+
return invalid(installation.canon_root, "Installed canonical archive has no unique root.");
|
|
138
|
+
const candidate = validateCanonCandidate("installed", path.join(installation.canon_root, roots[0]));
|
|
139
|
+
if (!candidate.valid)
|
|
140
|
+
return candidate;
|
|
141
|
+
const version = readTrimmed(path.join(candidate.root, "VERSION"));
|
|
142
|
+
const flowContract = readFlowContractId(candidate.flow_root);
|
|
143
|
+
if (version !== installation.manifest.canon.version || !flowContract)
|
|
144
|
+
return invalid(candidate.root, "Installed canonical metadata differs from its release selection.");
|
|
145
|
+
return { ...candidate, metadata: { version, commit: installation.manifest.canon.commit,
|
|
146
|
+
flow_contract: flowContract, status: "present", diagnostics: [] } };
|
|
147
|
+
}
|
|
117
148
|
function canonCandidates(context, input) {
|
|
118
149
|
const candidates = [];
|
|
119
150
|
if (input.explicitRoot) {
|
|
@@ -129,8 +160,13 @@ function canonCandidates(context, input) {
|
|
|
129
160
|
candidates.push(validateCanonCandidate("registered", registered.root));
|
|
130
161
|
}
|
|
131
162
|
}
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
if (!candidates.length) {
|
|
164
|
+
const installed = installedCanonCandidate(context.ddFlowHome);
|
|
165
|
+
if (installed)
|
|
166
|
+
return [installed];
|
|
167
|
+
for (const known of knownCanonRoots()) {
|
|
168
|
+
candidates.push(validateCanonCandidate("known", known));
|
|
169
|
+
}
|
|
134
170
|
}
|
|
135
171
|
return uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`);
|
|
136
172
|
}
|
|
@@ -32,7 +32,7 @@ export function isMbUpgradeCliOperation(args, env = process.env) {
|
|
|
32
32
|
export function requiresExplicitUpgradeAuthorization(args) {
|
|
33
33
|
const positional = positionalArgs(args);
|
|
34
34
|
const [family, command] = positional;
|
|
35
|
-
if (family === "migration" && command
|
|
35
|
+
if (family === "migration" && ["apply", "readiness"].includes(command ?? ""))
|
|
36
36
|
return true;
|
|
37
37
|
if (family === "run" && command === "start" && optionValue(args, "flow-kind") === "mb-upgrade")
|
|
38
38
|
return true;
|
|
@@ -10,7 +10,7 @@ export function isUpgradeAllowlisted(args) {
|
|
|
10
10
|
return true;
|
|
11
11
|
if (family === "engine" && command === "resolve")
|
|
12
12
|
return true;
|
|
13
|
-
if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
13
|
+
if (family === "migration" && ["impact", "readiness", "plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
14
14
|
return true;
|
|
15
15
|
if (family === "memory" && command === "permissions" && parsed.positional[2] === "preflight")
|
|
16
16
|
return true;
|
|
@@ -343,7 +343,13 @@ export class PreparedCheckEvidence {
|
|
|
343
343
|
const references = deduplicate(checks, commands).map(group => {
|
|
344
344
|
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
|
|
345
345
|
const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
|
|
346
|
-
if (!receipt
|
|
346
|
+
if (!receipt)
|
|
347
|
+
throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, {
|
|
348
|
+
effect: "no_effect", reason: "input_identity_mismatch", expected_input_hash: inputHash,
|
|
349
|
+
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
350
|
+
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
351
|
+
});
|
|
352
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
347
353
|
fail();
|
|
348
354
|
passed = passed && receipt.status === "passed";
|
|
349
355
|
for (const artifact of receipt.artifacts)
|
|
@@ -1008,7 +1014,7 @@ export function workspaceFingerprint(root) { return workspaceState(root).fingerp
|
|
|
1008
1014
|
/** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
|
|
1009
1015
|
* records carry a second path; retain both because the old side is a deletion. */
|
|
1010
1016
|
export function workspaceChangedPaths(root) {
|
|
1011
|
-
const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1017
|
+
const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1012
1018
|
// A workspace can be intentionally gitless. Keep the same conservative
|
|
1013
1019
|
// file inventory used for fingerprints instead of making result validation
|
|
1014
1020
|
// fail before the work contract can report its actual error.
|
|
@@ -1041,7 +1047,7 @@ export function ignoredWorkspacePaths(root, paths) {
|
|
|
1041
1047
|
const normalized = [...new Set(paths.map((item) => normalizeWorkspacePath(root, item)))];
|
|
1042
1048
|
if (!normalized.length)
|
|
1043
1049
|
return [];
|
|
1044
|
-
const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
|
|
1050
|
+
const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
|
|
1045
1051
|
// The canonical owned hook has one meaning across Git status, check cache
|
|
1046
1052
|
// and MERGE, without changing a sibling worktree's shared Git excludes.
|
|
1047
1053
|
const gitless = isGitlessWorkspace(root, status);
|
|
@@ -1150,9 +1156,10 @@ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
|
|
|
1150
1156
|
// and tool selection; omit only known process/control bookkeeping.
|
|
1151
1157
|
const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
|
|
1152
1158
|
"DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
|
|
1159
|
+
"DD_FLOW_HOOK_DEADLINE_AT", // Per-call transport budget, not a project check input.
|
|
1153
1160
|
"DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
|
|
1154
1161
|
"DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
|
|
1155
|
-
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
|
|
1162
|
+
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
|
|
1156
1163
|
return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
|
|
1157
1164
|
.filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
|
|
1158
1165
|
&& (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
@@ -13,7 +13,7 @@ export async function preflightCliCompatibility(context, args, env = process.env
|
|
|
13
13
|
if (resolvedProjectRoot === null && parsed.positional[0] === "runtime" && parsed.positional[1] === "process"
|
|
14
14
|
&& !parsed.options.has("project-root") && !parsed.options.has("root"))
|
|
15
15
|
return { ok: true, classification, compatibility: null };
|
|
16
|
-
const { resolveOperationProjectRoot, selectEngine } = await import("./engines.js");
|
|
16
|
+
const { resolveOperationProjectRoot, selectEngine, selectBoundExecutingEngine } = await import("./engines.js");
|
|
17
17
|
const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
|
|
18
18
|
if (!projectRoot) {
|
|
19
19
|
return { ok: true, classification, compatibility: null };
|
|
@@ -21,7 +21,10 @@ export async function preflightCliCompatibility(context, args, env = process.env
|
|
|
21
21
|
// Compatibility checks the executing build/manifest identity. Routing and
|
|
22
22
|
// binding validate snapshot bytes before dispatch; maintenance must not
|
|
23
23
|
// rehash the dependency closure or spawn health probes on every lease ACK.
|
|
24
|
-
|
|
24
|
+
// Read-only probes may intentionally inspect a copied RUN before its new
|
|
25
|
+
// executor is bound. Writer authority is neither required nor granted here.
|
|
26
|
+
const selection = (classification.mode === "read_only_diagnostics" ? null : selectBoundExecutingEngine(context, args, projectRoot, env))
|
|
27
|
+
?? selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
|
|
25
28
|
const compatibility = compatibilityReport(selection, classification);
|
|
26
29
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
27
30
|
return { ok: true, classification, compatibility };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
3
|
import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
3
4
|
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, getStageFanoutCapacity, verifiedNativeWorkFailure } from "./vnext-fanout.js";
|
|
@@ -10,6 +11,7 @@ import { registerMemoryRepairBaselineFromStage, reserveMemoryRepairWriterCopyFro
|
|
|
10
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
12
|
import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
|
|
12
13
|
import { runtimeCommand } from "./runtime-command.js";
|
|
14
|
+
import { promptDeliveryInstruction } from "./prompt-delivery.js";
|
|
13
15
|
function semanticCompletionPrompt(stage, completed) {
|
|
14
16
|
const task = stage === "plan-review"
|
|
15
17
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
@@ -29,7 +31,7 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
29
31
|
return [
|
|
30
32
|
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
31
33
|
command,
|
|
32
|
-
|
|
34
|
+
promptDeliveryInstruction,
|
|
33
35
|
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
34
36
|
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
35
37
|
stageLifecycleInstruction
|
|
@@ -39,7 +41,7 @@ export function controllerStageResumePrompt(pauseId, command) {
|
|
|
39
41
|
return [
|
|
40
42
|
`The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
|
|
41
43
|
command,
|
|
42
|
-
|
|
44
|
+
`Do not alter the answer. ${promptDeliveryInstruction} Follow the returned packet, then stop at this Stage boundary.`,
|
|
43
45
|
stageLifecycleInstruction
|
|
44
46
|
].join("\n\n");
|
|
45
47
|
}
|
|
@@ -173,8 +175,8 @@ export async function nextControllerFanout(context, input) {
|
|
|
173
175
|
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
174
176
|
if (input.stage === "review") {
|
|
175
177
|
const projectId = requireProjectByRoot(context, input.projectRoot).id;
|
|
176
|
-
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
|
|
177
|
-
if (run?.flow_kind === "mb-init") {
|
|
178
|
+
const run = context.db.get("SELECT flow_kind,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
|
|
179
|
+
if (run?.flow_kind === "mb-init" || run?.flow_kind === "mb-upgrade") {
|
|
178
180
|
const attempt = stageAttempt(context, projectId, input.runId, "review");
|
|
179
181
|
const repair = context.db.get(`SELECT CASE event_type WHEN 'memory.second_repair_registration' THEN 2 ELSE 1 END AS wave
|
|
180
182
|
FROM audit_events WHERE project_id=? AND event_type IN ('memory.repair_registration','memory.second_repair_registration')
|
|
@@ -186,6 +188,21 @@ export async function nextControllerFanout(context, input) {
|
|
|
186
188
|
const args = ["--run", input.runId, "--project-root", input.projectRoot, "--wave", String(repair.wave), "--json"];
|
|
187
189
|
return prompt("semantic_completion", `Correction wave ${repair.wave} Work has settled. Before Stage finish, run ${runtimeCommand(context, ["memory", "corrections", "checks", ...args])}, then ${runtimeCommand(context, ["memory", "review", "selective-register", ...args])}. Read both complete responses, then end this Turn so the controller can dispatch the independent selective reviewers. Do not finish the Stage or treat repair Work completion as independent acceptance.`);
|
|
188
190
|
}
|
|
191
|
+
const wave = repair?.wave ?? 0;
|
|
192
|
+
const runHome = run.run_root ?? run.run_dir;
|
|
193
|
+
if (!runHome || !path.isAbsolute(runHome))
|
|
194
|
+
throw new AppError("runtime_missing", "Memory review collection requires its retained absolute RUN home", 1);
|
|
195
|
+
const collectionArgs = ["--run", input.runId, "--project-root", input.projectRoot, ...(wave ? ["--wave", String(wave)] : []),
|
|
196
|
+
"--json", "--progress-jsonl", "--response-file", path.join(runHome, `memory-review-collection-wave-${wave}.json`)];
|
|
197
|
+
const collect = runtimeCommand(context, ["memory", "review", wave ? "selective-collect" : "collect", ...collectionArgs]);
|
|
198
|
+
return prompt("semantic_completion", [
|
|
199
|
+
`All declared memory review Work has settled. First run ${collect}. Read the complete canonical collection from its response_file; do not decide acceptance from a truncated preview or reviewer counts.`,
|
|
200
|
+
"Only if the collection outcome is accepted (projection.outcome = accepted), integrate its accepted results into the original Stage proposal and invoke the exact Stage finish command from the authoritative packet.",
|
|
201
|
+
wave < 2
|
|
202
|
+
? `If the outcome is needs_repair, use the returned canonical findings and correction_plan_binding to author substantive repair tasks with exact owned write_paths. Register that plan with the packet's issued memory corrections register command for wave ${wave + 1}, then end this Turn so the controller can dispatch it. Do not invoke Stage finish before corrections, checks and independent selective review complete.`
|
|
203
|
+
: "If the outcome is needs_repair or blocked, the two correction waves are exhausted. Use the issued pause command for the concrete unresolved blocker; do not mark it accepted.",
|
|
204
|
+
"For blocked collection, use the issued pause command. Do not calculate finding hashes, merge or drop findings, edit project files in the coordinator, create speculative Work or start a successor Stage."
|
|
205
|
+
].join("\n\n"));
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
/** Keep synchronous custody/copy work off the controller's lease event loop.
|
|
4
|
+
* The thread remains inside its registered process; OS-level stop owns both.
|
|
5
|
+
* Do not resolve until exit, including after cancellation or a typed failure. */
|
|
6
|
+
export function runControllerMemoryTask(context, task, signal) {
|
|
7
|
+
const aborted = () => new AppError("controller_memory_task_aborted", "Memory operation was cancelled; reconcile its retained effects", 1, { effect: "unknown" });
|
|
8
|
+
if (signal?.aborted)
|
|
9
|
+
return Promise.reject(aborted());
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
// Both source tests and installed runtimes use the package's compiled worker.
|
|
12
|
+
const worker = new Worker(new URL("../../dist/services/controller-memory-worker.js", import.meta.url), {
|
|
13
|
+
workerData: { task, env: { ...context.env, DD_FLOW_HOME: context.ddFlowHome } }
|
|
14
|
+
});
|
|
15
|
+
let reply, failure;
|
|
16
|
+
const abort = () => worker.postMessage({ abort: true });
|
|
17
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
18
|
+
if (signal?.aborted)
|
|
19
|
+
abort();
|
|
20
|
+
worker.on("message", (value) => {
|
|
21
|
+
const invalid = value && typeof value === "object" && ("error" in value
|
|
22
|
+
? !value.error || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !Number.isInteger(value.error.exitCode)
|
|
23
|
+
|| !value.error.details || typeof value.error.details !== "object" || Array.isArray(value.error.details) || "result" in value
|
|
24
|
+
: !("result" in value) || !value.result || typeof value.result !== "object" || Array.isArray(value.result));
|
|
25
|
+
if (reply || !value || typeof value !== "object" || invalid) {
|
|
26
|
+
failure = new AppError("controller_memory_task_invalid", "Memory worker returned an invalid terminal reply", 1, { effect: "unknown" });
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
reply = value;
|
|
30
|
+
});
|
|
31
|
+
worker.once("error", error => { failure = error; });
|
|
32
|
+
worker.once("exit", code => {
|
|
33
|
+
signal?.removeEventListener("abort", abort);
|
|
34
|
+
if (failure)
|
|
35
|
+
return reject(failure);
|
|
36
|
+
if (reply && "error" in reply)
|
|
37
|
+
return reject(new AppError(reply.error.code, reply.error.message, reply.error.exitCode, reply.error.details));
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
return reject(aborted());
|
|
40
|
+
if (code !== 0 || !reply || !("result" in reply))
|
|
41
|
+
return reject(new AppError("controller_memory_task_failed", "Memory worker exited without its terminal result", 1, { exit_code: code, effect: "unknown" }));
|
|
42
|
+
resolve(reply.result);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createContext } from "../runtime/context.js";
|
|
3
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
4
|
+
import { nextControllerFanout } from "./controller-fanout.js";
|
|
5
|
+
import { launchExternalWork } from "./external-work-launch.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
8
|
+
import { assertManagedProcessLease } from "./managed-processes.js";
|
|
9
|
+
if (!parentPort)
|
|
10
|
+
throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
|
|
11
|
+
const port = parentPort, cancellation = new AbortController();
|
|
12
|
+
port.on("message", message => { if (message?.abort === true)
|
|
13
|
+
cancellation.abort(); });
|
|
14
|
+
const { task, env } = workerData;
|
|
15
|
+
let context;
|
|
16
|
+
let reply;
|
|
17
|
+
try {
|
|
18
|
+
context = createContext(env, "prepared_existing");
|
|
19
|
+
const project = requireProjectByRoot(context, task.input.projectRoot);
|
|
20
|
+
const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
|
|
21
|
+
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
|
|
22
|
+
throw new AppError("controller_memory_task_invalid", "Memory worker requires the selected memory RUN", 1, { effect: "no_effect" });
|
|
23
|
+
if (task.operation === "external") {
|
|
24
|
+
const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
|
|
25
|
+
if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
|
|
26
|
+
throw new AppError("controller_memory_task_invalid", "External Work belongs to another RUN", 1, { effect: "no_effect" });
|
|
27
|
+
}
|
|
28
|
+
const owner = env.DD_FLOW_CONTROLLER_TOKEN ? context.db.get("SELECT * FROM run_controllers WHERE project_id=? AND run_id=? AND owner_token=?", [project.id, task.input.runId, env.DD_FLOW_CONTROLLER_TOKEN]) : undefined;
|
|
29
|
+
if (!owner)
|
|
30
|
+
throw new AppError("controller_owner_mismatch", "Memory task lacks its managed controller owner", 1, { effect: "no_effect" });
|
|
31
|
+
assertControllerLeaseAllowed(context, owner);
|
|
32
|
+
assertManagedProcessLease(context, { id: owner.process_id, leaseToken: owner.process_lease_token, pid: process.pid });
|
|
33
|
+
cancellation.signal.throwIfAborted();
|
|
34
|
+
if (task.operation === "fanout")
|
|
35
|
+
reply = { result: await nextControllerFanout(context, task.input) };
|
|
36
|
+
else if (task.operation === "external")
|
|
37
|
+
reply = { result: await launchExternalWork(context, { ...task.input, signal: cancellation.signal }) };
|
|
38
|
+
else
|
|
39
|
+
throw new AppError("controller_memory_task_invalid", "Unknown memory worker operation", 1, { effect: "no_effect" });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
reply = { error: errorRecord(error) };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
context?.db.close?.();
|
|
46
|
+
}
|
|
47
|
+
port.postMessage(reply);
|
|
48
|
+
port.close();
|
|
@@ -216,10 +216,10 @@ export function autoRefreshDashboards(context, input) {
|
|
|
216
216
|
if (projectRecoveryGuarded(context, project.id))
|
|
217
217
|
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
218
218
|
const configured = readProjectConfig(context, project.id);
|
|
219
|
-
// Automatic observation must not change
|
|
219
|
+
// Automatic observation must not change either memory flow's source baseline.
|
|
220
220
|
// Keep projections in Home; do not hide runtime-created paths in Git or
|
|
221
221
|
// weaken source preservation by excluding user-owned .tasks directories.
|
|
222
|
-
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind
|
|
222
|
+
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
223
|
const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
|
|
224
224
|
markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
|
|
225
225
|
if (!config.dashboard.auto_refresh) {
|
|
@@ -15,9 +15,11 @@ import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
|
15
15
|
import { prepareFlowRunStart, publishRunProjections, readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority, runBundleAuthorityMetadata } from "./runs.js";
|
|
16
16
|
import { readRunStartRequest, refuseRunStart, runStartBytesSha256, runStartPayloadLimit } from "./run-start-custody.js";
|
|
17
17
|
import { readMemoryInitBootstrapReadiness } from "./memory-init-bootstrap-readiness.js";
|
|
18
|
+
import { readMemoryUpgradeBootstrapReadiness } from "./memory-upgrade-bootstrap-readiness.js";
|
|
18
19
|
import { allocateDescriptorRootWork, readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
19
20
|
import { nextRunId } from "./ids.js";
|
|
20
21
|
import { readMemoryInitRunAllocation } from "./memory-init-run-allocation.js";
|
|
22
|
+
import { readMemoryUpgradeEvidence, readMemoryUpgradeTargetEvidence, upgradeEvidenceFilePath, upgradeEvidenceIndexPath, upgradeTargetIndexPath } from "./memory-upgrade-evidence.js";
|
|
21
23
|
const encode = (value) => Buffer.from(JSON.stringify(value));
|
|
22
24
|
const owner = (directory) => {
|
|
23
25
|
const stat = fs.lstatSync(directory);
|
|
@@ -25,10 +27,10 @@ const owner = (directory) => {
|
|
|
25
27
|
refuseRunStart("linked_or_missing_directory");
|
|
26
28
|
return { dev: stat.dev, ino: stat.ino };
|
|
27
29
|
};
|
|
28
|
-
function
|
|
30
|
+
export function observeDescriptorWorkspace(context, projectRoot, workspaceRoot, gitExecutable = "git") {
|
|
29
31
|
const env = Object.fromEntries(Object.entries({ ...process.env, ...context.env }).filter(([key]) => !key.startsWith("GIT_")));
|
|
30
32
|
const read = (root, args, optional = false) => {
|
|
31
|
-
const result = spawnSync(
|
|
33
|
+
const result = spawnSync(gitExecutable, ["-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false", "-C", root, ...args], { env: { ...env, GIT_OPTIONAL_LOCKS: "0" }, encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
32
34
|
if (result.error || result.signal || !optional && result.status !== 0)
|
|
33
35
|
refuseRunStart("git_observation_failed");
|
|
34
36
|
return result.status === 0 ? result.stdout : null;
|
|
@@ -100,19 +102,15 @@ function memoryBootstrap(context, projectId, bundle) {
|
|
|
100
102
|
refuseRunStart("memory_bootstrap_authority_changed");
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
retained
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
if (!retained || retained.schema_id !== "dd-flow/memory-bootstrap-authority@1" || retained.project_root !== scope.projectRoot
|
|
114
|
-
|| retained.workspace_root !== scope.workspace || retained.flow_id !== bundle.manifest.flow_id || retained.readiness !== "ready"
|
|
115
|
-
|| retained.bootstrap_sha256 !== bundle.manifest.bootstrap_receipts[0].sha256)
|
|
105
|
+
if (bundle.descriptor.descriptor.id !== "mb-upgrade")
|
|
106
|
+
refuseRunStart("unsupported_memory_bootstrap");
|
|
107
|
+
const retained = readMemoryUpgradeBootstrapReadiness(context, { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace });
|
|
108
|
+
const reference = bundle.manifest.bootstrap_receipts[0];
|
|
109
|
+
if (!retained || !retained.hasPreservationBaseline || !retained.hasLintCapability || !retained.hasGitPolicy || !retained.hasFlowCapability || retained.projectId !== projectId || bundle.manifest.bootstrap_receipts.length !== 1 || !reference
|
|
110
|
+
|| retained.authority.binding.execution_sha256 !== bundle.execution.sha256
|
|
111
|
+
|| !bundle.blob(bundle.manifest.engine_manifest.path).equals(Buffer.from(retained.authority.binding.engine_manifest_base64, "base64"))
|
|
112
|
+
|| retained.authority.bootstrap_sha256 !== reference.sha256
|
|
113
|
+
|| !bundle.blob(reference.path).equals(encode(retained.authority.receipt)))
|
|
116
114
|
refuseRunStart("memory_bootstrap_authority_changed");
|
|
117
115
|
}
|
|
118
116
|
/** RUN allocation and SQL bundle authority are one request-owned transaction.
|
|
@@ -166,12 +164,16 @@ export class PreparedDescriptorRunStart {
|
|
|
166
164
|
const home = fs.realpathSync(context.ddFlowHome), runRoot = projectRunHome(home, selected.id, bundle.manifest.run_id);
|
|
167
165
|
if (fs.existsSync(runRoot))
|
|
168
166
|
refuseRunStart("foreign_run_root");
|
|
169
|
-
const facts =
|
|
167
|
+
const facts = observeDescriptorWorkspace(context, scope.projectRoot, scope.workspace), now = context.now(), artifacts = [
|
|
170
168
|
{ path: "engine-binding.json", base64: bundle.blob(bundle.manifest.engine_binding.path).toString("base64") }
|
|
171
169
|
];
|
|
172
170
|
const isSdlc = bundle.descriptor.descriptor.id === "sdlc", flowKind = isSdlc ? "vnext_specify" : bundle.descriptor.descriptor.id;
|
|
173
171
|
if (!["vnext_specify", "mb-init", "mb-upgrade"].includes(flowKind))
|
|
174
172
|
refuseRunStart("unsupported_flow_kind");
|
|
173
|
+
if (flowKind === "mb-upgrade") {
|
|
174
|
+
const upgradeScope = { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace };
|
|
175
|
+
artifacts.push(...readMemoryUpgradeEvidence(context, upgradeScope).artifacts, ...readMemoryUpgradeTargetEvidence(context, upgradeScope).artifacts);
|
|
176
|
+
}
|
|
175
177
|
const legacy = isSdlc ? prepareFlowRunStart(context, { projectRoot: scope.projectRoot, workspaceRoot: scope.workspace,
|
|
176
178
|
flowKind, subjectId, subjectType, slug: parsedId.slug }) : undefined;
|
|
177
179
|
const profile = legacy?.executionProfile;
|
|
@@ -248,7 +250,7 @@ export class PreparedDescriptorRunStart {
|
|
|
248
250
|
revalidatePhysical(context) {
|
|
249
251
|
this.revalidateDirectoryOwners(context);
|
|
250
252
|
const p = this.#payload;
|
|
251
|
-
if (hybridDigest(
|
|
253
|
+
if (hybridDigest(observeDescriptorWorkspace(context, p.project_root, p.workspace_root)) !== hybridDigest(p.git))
|
|
252
254
|
refuseRunStart("physical_scope_changed");
|
|
253
255
|
this.#bundle.engine();
|
|
254
256
|
}
|
|
@@ -381,7 +383,9 @@ export class PreparedDescriptorRunStart {
|
|
|
381
383
|
}
|
|
382
384
|
checkpoint?.("root_owned");
|
|
383
385
|
const assertPhysicalOwner = () => {
|
|
384
|
-
|
|
386
|
+
// Every write needs path custody, not another whole-workspace Git probe.
|
|
387
|
+
// Git/engine are fenced around the batch and before SQL acceptance.
|
|
388
|
+
this.revalidateDirectoryOwners(context);
|
|
385
389
|
let ancestor = p.home_root;
|
|
386
390
|
for (const part of path.relative(p.home_root, p.run_root).split(path.sep)) {
|
|
387
391
|
ancestor = path.join(ancestor, part);
|
|
@@ -397,19 +401,22 @@ export class PreparedDescriptorRunStart {
|
|
|
397
401
|
assertPhysicalOwner();
|
|
398
402
|
const file = path.join(p.run_root, artifact.path), bytes = Buffer.from(artifact.base64, "base64");
|
|
399
403
|
if (fs.existsSync(file)) {
|
|
400
|
-
|
|
404
|
+
const limit = [upgradeEvidenceIndexPath, upgradeTargetIndexPath, upgradeEvidenceFilePath(runStartBytesSha256(bytes))].includes(artifact.path) ? 16 * 1024 * 1024 : 1024 * 1024;
|
|
405
|
+
if (fs.lstatSync(file).nlink !== 1 || !readBoundedReleaseFile(file, limit).equals(bytes))
|
|
401
406
|
refuseRunStart("start_artifact_changed");
|
|
402
407
|
}
|
|
403
408
|
else
|
|
404
409
|
fs.writeFileSync(file, bytes, { flag: "wx", mode: 0o600 });
|
|
405
410
|
}
|
|
406
411
|
assertPhysicalOwner();
|
|
412
|
+
this.revalidatePhysical(context);
|
|
407
413
|
this.#bundle.materialize(p.run_root);
|
|
408
414
|
checkpoint?.("bundle_published");
|
|
409
415
|
const physical = readFlowRunExecutionBundle(context, { projectRoot: p.project_root, runId: p.run_id });
|
|
410
416
|
if (physical?.sha256 !== p.bundle_sha256)
|
|
411
417
|
refuseRunStart("bundle_changed");
|
|
412
418
|
assertPhysicalOwner();
|
|
419
|
+
this.revalidatePhysical(context);
|
|
413
420
|
context.db.writeTransaction(() => {
|
|
414
421
|
this.#storage.assertCompatibility(context);
|
|
415
422
|
const retained = readRunStartRequest(context, p.project_id, p.request_id);
|