@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139
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 +19 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/runtime-process.js +1 -1
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +56 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
- package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
- package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
- package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-children.d.mts +2 -0
- package/dist/harness-runtime/lib/native-children.mjs +20 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +26 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +18 -2
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +93 -15
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/managed-processes.js +31 -10
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +296 -0
- package/dist/services/memory-init-preflight.js +330 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +178 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/merge-server.js +2 -4
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
- package/dist/services/recovery-snapshot-database.js +417 -2
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +5 -5
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +158 -64
- package/dist/services/run-recovery-guard.js +5 -3
- package/dist/services/run-recovery-runtime.js +3 -3
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +413 -30
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +6 -6
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +73 -11
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +7 -5
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/vnext-protocolize.js +20 -6
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
|
@@ -5,6 +5,8 @@ import { AppError } from "../shared/errors.js";
|
|
|
5
5
|
import { harnessKey } from "./harness-config.js";
|
|
6
6
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
7
7
|
import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
|
|
8
|
+
import { operatorSettingsPath, readOperatorProfiles, readOperatorProfileInput, resolveOperatorProfile } from "./operator-profiles.js";
|
|
9
|
+
import { selectOnboardingExecutor } from "./executor-onboarding.js";
|
|
8
10
|
/** Read once so the frozen policy and its provenance describe the same bytes. */
|
|
9
11
|
export function loadExecutionRoutingInput(file) {
|
|
10
12
|
const { file: source_path, bytes } = prepareFileBytes(file, "execution routing");
|
|
@@ -48,23 +50,20 @@ export function isExecutionRouting(value) {
|
|
|
48
50
|
export function loadAgentProfile(ddFlowHome, id) {
|
|
49
51
|
if (typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id))
|
|
50
52
|
throw new AppError("agent_profile_invalid", "Agent profile id is invalid", 2, { id });
|
|
53
|
+
if (fs.existsSync(operatorSettingsPath(ddFlowHome)))
|
|
54
|
+
return resolveOperatorProfile(readOperatorProfiles(ddFlowHome), id);
|
|
51
55
|
const file = path.join(ddFlowHome, "agent-profiles", `${id}.json`);
|
|
52
|
-
|
|
56
|
+
if (!fs.existsSync(file))
|
|
57
|
+
throw new AppError("agent_profile_missing", "Agent profile is unavailable", 1, { id });
|
|
58
|
+
let profile;
|
|
53
59
|
try {
|
|
54
|
-
|
|
60
|
+
profile = readOperatorProfileInput(file);
|
|
55
61
|
}
|
|
56
|
-
catch
|
|
57
|
-
throw new AppError("
|
|
62
|
+
catch {
|
|
63
|
+
throw new AppError("agent_profile_invalid", "Agent profile must be a bounded valid regular JSON file", 2, { id });
|
|
58
64
|
}
|
|
59
|
-
if (
|
|
60
|
-
throw new AppError("agent_profile_invalid", "Agent profile
|
|
61
|
-
const profile = value;
|
|
62
|
-
if (profile.schema_id !== "dd-flow/agent-profile@1" || profile.id !== id
|
|
63
|
-
|| Object.keys(profile).some(key => !["schema_id", "id", "harness", "provider", "model", "reasoning", "mode", "permission"].includes(key))
|
|
64
|
-
|| !["codex", "zcode", "agy", "opencode", "grok", "droid"].includes(String(profile.harness))
|
|
65
|
-
|| !nonempty(profile.provider) || !nonempty(profile.model) || !nonempty(profile.reasoning) || !nonempty(profile.mode)
|
|
66
|
-
|| (profile.permission !== "allow" && profile.permission !== "deny")) {
|
|
67
|
-
throw new AppError("agent_profile_invalid", "Agent profile does not match dd-flow/agent-profile@1", 2, { id, file });
|
|
65
|
+
if (profile.id !== id) {
|
|
66
|
+
throw new AppError("agent_profile_invalid", "Agent profile does not match its selected ID", 2, { id });
|
|
68
67
|
}
|
|
69
68
|
// Only execution settings enter RUN evidence; never copy arbitrary profile fields.
|
|
70
69
|
return { schema_id: profile.schema_id, id, harness: profile.harness, provider: profile.provider, model: profile.model, reasoning: profile.reasoning, mode: profile.mode, permission: profile.permission };
|
|
@@ -73,10 +72,16 @@ export function snapshotAgentProfiles(ddFlowHome, policy) {
|
|
|
73
72
|
if (!isExecutionRouting(policy))
|
|
74
73
|
throw new AppError("execution_profile_invalid", "Execution routing contains an invalid stage, profile or delegation policy", 2);
|
|
75
74
|
const profiles = Object.create(null);
|
|
75
|
+
const settings = fs.existsSync(operatorSettingsPath(ddFlowHome)) ? readOperatorProfiles(ddFlowHome) : undefined;
|
|
76
76
|
for (const override of [policy, ...Object.values(policy.stage_overrides ?? {})]) {
|
|
77
77
|
for (const id of [override.agent_profile_id, override.delegation?.agent_profile_id]) {
|
|
78
|
-
if (id !== undefined && !Object.hasOwn(profiles, id))
|
|
79
|
-
|
|
78
|
+
if (id !== undefined && !Object.hasOwn(profiles, id)) {
|
|
79
|
+
const profile = settings ? selectOnboardingExecutor({}, () => settings, { allowSkip: false, projectProfileId: id }).profile : loadAgentProfile(ddFlowHome, id);
|
|
80
|
+
profiles[id] = profile;
|
|
81
|
+
// Routing labels may be aliases. Default worker resolution uses the
|
|
82
|
+
// complete coordinator's canonical ID, which must also be frozen.
|
|
83
|
+
profiles[profile.id] = profile;
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
// Validate every declared route now, including stages not reached by this run.
|
|
@@ -177,5 +182,4 @@ export function validateExecutionOverride(value) {
|
|
|
177
182
|
if (override.delegation !== undefined)
|
|
178
183
|
validateDelegation(override.delegation);
|
|
179
184
|
}
|
|
180
|
-
function nonempty(value) { return typeof value === "string" && Boolean(value.trim()); }
|
|
181
185
|
function digest(value) { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { prepareOperatorProfile, readOperatorProfileInput, resolveOperatorProfile, validateProfileCreationIntent } from "./operator-profiles.js";
|
|
4
|
+
function refuse(code, message, exitCode = 2) {
|
|
5
|
+
throw new AppError(code, message, exitCode, { phase: "executor_onboarding", effect: "no_effect", help: "profiles" });
|
|
6
|
+
}
|
|
7
|
+
/** Shared setup/first-flow admission; settings are read lazily, never written. */
|
|
8
|
+
export function selectOnboardingExecutor(choice, readSettings, policy) {
|
|
9
|
+
if (!choice || typeof choice !== "object" || Array.isArray(choice)
|
|
10
|
+
|| Object.keys(choice).some(key => !["profileId", "profile", "profileFile", "skip", "profileCreation"].includes(key))
|
|
11
|
+
|| choice.profileFile !== undefined && (typeof choice.profileFile !== "string" || !choice.profileFile.trim())
|
|
12
|
+
|| choice.skip !== undefined && typeof choice.skip !== "boolean"
|
|
13
|
+
|| choice.profileId !== undefined && (typeof choice.profileId !== "string" || !choice.profileId.trim())) {
|
|
14
|
+
refuse("usage", "Invalid executor selection");
|
|
15
|
+
}
|
|
16
|
+
if ([choice.profileId !== undefined, choice.profile !== undefined, choice.profileFile !== undefined, choice.skip === true].filter(Boolean).length > 1) {
|
|
17
|
+
refuse("usage", "Executor profile, profile ID and skip are mutually exclusive");
|
|
18
|
+
}
|
|
19
|
+
if (choice.profileCreation !== undefined) {
|
|
20
|
+
validateProfileCreationIntent(choice.profileCreation);
|
|
21
|
+
if (choice.profile === undefined || hybridDigest(choice.profile) !== hybridDigest(choice.profileCreation.profile))
|
|
22
|
+
refuse("usage", "Profile save intent requires its exact explicit profile");
|
|
23
|
+
}
|
|
24
|
+
if (choice.skip) {
|
|
25
|
+
if (!policy.allowSkip)
|
|
26
|
+
refuse("agent_profile_missing", "This operation requires an executor profile", 1);
|
|
27
|
+
return { profile: null, revision: null, source: "skip" };
|
|
28
|
+
}
|
|
29
|
+
const explicit = choice.profile !== undefined || choice.profileFile !== undefined || choice.profileId !== undefined;
|
|
30
|
+
if (!explicit && policy.projectProfileId !== undefined && (typeof policy.projectProfileId !== "string" || !policy.projectProfileId.trim())) {
|
|
31
|
+
refuse("usage", "Invalid project executor profile selection");
|
|
32
|
+
}
|
|
33
|
+
const profile = prepareOperatorProfile(choice.profile !== undefined ? choice.profile : choice.profileFile !== undefined
|
|
34
|
+
? readOperatorProfileInput(choice.profileFile) : resolveOperatorProfile(readSettings(), choice.profileId ?? policy.projectProfileId));
|
|
35
|
+
return { profile: structuredClone(profile), revision: hybridDigest(profile),
|
|
36
|
+
...(choice.profileCreation ? { profileCreation: structuredClone(choice.profileCreation) } : {}),
|
|
37
|
+
source: choice.profile !== undefined ? "explicit-profile" : choice.profileFile !== undefined ? "explicit-file" : choice.profileId !== undefined ? "explicit-id" : policy.projectProfileId !== undefined ? "project-default" : "default" };
|
|
38
|
+
}
|
|
39
|
+
/** Interactive decisions use exactly the same admission as headless choices. */
|
|
40
|
+
export async function onboardExecutor(choice, readSettings, policy, ask) {
|
|
41
|
+
try {
|
|
42
|
+
return selectOnboardingExecutor(choice, readSettings, policy);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!(error instanceof AppError) || error.code !== "agent_profile_missing"
|
|
46
|
+
|| choice.profileId !== undefined || choice.profile !== undefined || choice.profileFile !== undefined || choice.skip === true
|
|
47
|
+
|| policy.projectProfileId !== undefined || policy.nonInteractive || !ask)
|
|
48
|
+
throw error;
|
|
49
|
+
// No speculative defaults or writes: the human supplies a concrete choice.
|
|
50
|
+
const decision = await ask(structuredClone(readSettings()));
|
|
51
|
+
if (decision.profileId === undefined && decision.profile === undefined && decision.profileFile === undefined && decision.skip !== true) {
|
|
52
|
+
refuse("agent_profile_missing", "Onboarding requires an explicit choice", 1);
|
|
53
|
+
}
|
|
54
|
+
return selectOnboardingExecutor(decision, readSettings, policy);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -16,8 +16,12 @@ import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSo
|
|
|
16
16
|
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
17
|
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
18
18
|
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { continueProviderCapacity } from "./provider-capacity.js";
|
|
20
20
|
import { errorRecord } from "./run-controller-state.js";
|
|
21
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
22
|
+
import { stageWorkIds } from "./stage-work-graph.js";
|
|
23
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
24
|
+
import { prepareMemoryRepairDispatchFromStage, prepareMemorySecondRepairDispatchFromStage, prepareExternalMemoryRepairCopyFromStage, prepareMemorySecondRepairLaunchFromStage } from "./memory-review-handler.js";
|
|
21
25
|
/**
|
|
22
26
|
* Creates an external provider root for one already-created Work. The launch
|
|
23
27
|
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
@@ -38,8 +42,6 @@ export async function launchExternalWork(context, input) {
|
|
|
38
42
|
throw new AppError("invalid_run_state", "External Work dispatch requires a running RUN", 2, { run_id: work.run_id, status: run.status });
|
|
39
43
|
const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
|
|
40
44
|
const snapshot = JSON.parse(run.index_json);
|
|
41
|
-
if (!snapshot.execution_profile?.agent_profiles)
|
|
42
|
-
throw new AppError("execution_profile_not_frozen", "External dispatch requires a RUN with frozen agent profiles; recreate the RUN with the current engine", 1, { work_id: work.work_id });
|
|
43
45
|
const override = workOverride(work.payload_json);
|
|
44
46
|
const policy = snapshot.execution_profile?.settings?.execution ?? {};
|
|
45
47
|
const stage = snapshot.current_stage ?? [...(snapshot.stage_runs ?? [])].reverse().find((entry) => entry.status === "running")?.stage;
|
|
@@ -47,12 +49,66 @@ export async function launchExternalWork(context, input) {
|
|
|
47
49
|
throw new AppError("execution_stage_mismatch", "Requested stage does not match the RUN's current stage", 2, { requested_stage: input.stage, current_stage: stage ?? null });
|
|
48
50
|
if (!stage && Object.keys(policy.stage_overrides ?? {}).length)
|
|
49
51
|
throw new AppError("execution_stage_missing", "Stage overrides require an authoritative RUN stage", 1, { run_id: work.run_id });
|
|
50
|
-
const
|
|
52
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
|
|
53
|
+
let resolved;
|
|
54
|
+
if (bundle) {
|
|
55
|
+
if (!stage)
|
|
56
|
+
throw new AppError("execution_stage_missing", "Bound Work requires an authoritative RUN stage", 1);
|
|
57
|
+
if (snapshot.current_stage !== stage || !snapshot.stage_runs?.some(entry => entry.stage === stage && entry.status === "running")
|
|
58
|
+
|| !stageWorkIds(context, work.project_id, work.run_id, stage).has(work.work_id)) {
|
|
59
|
+
throw new AppError("execution_stage_mismatch", "Bound Work does not belong to the active running stage", 2);
|
|
60
|
+
}
|
|
61
|
+
if (override?.delegation !== undefined)
|
|
62
|
+
throw new AppError("generic_execution_invalid", "Work cannot replace the RUN's frozen delegation", 2);
|
|
63
|
+
const route = bundle.execution.work(stage, override?.agent_profile_id);
|
|
64
|
+
const selected = { coordinator: route.coordinator, worker: route.worker,
|
|
65
|
+
delegation: { mode: route.delegation.mode, max_parallel: route.delegation.mode === "external" ? route.delegation.max_parallel : null } };
|
|
66
|
+
resolved = { ...selected, profile_hash: crypto.createHash("sha256").update(JSON.stringify(selected)).digest("hex") };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (!snapshot.execution_profile?.agent_profiles)
|
|
70
|
+
throw new AppError("execution_profile_not_frozen", "External dispatch requires a RUN with frozen agent profiles; recreate the RUN with the current engine", 1, { work_id: work.work_id });
|
|
71
|
+
resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
|
|
72
|
+
}
|
|
51
73
|
if (resolved.delegation.mode !== "external")
|
|
52
74
|
throw new AppError("external_launch_not_selected", "This Work's frozen policy selects native delegation", 2, { work_id: work.work_id });
|
|
75
|
+
let payload = null;
|
|
76
|
+
try {
|
|
77
|
+
payload = work.payload_json ? JSON.parse(work.payload_json) : null;
|
|
78
|
+
}
|
|
79
|
+
catch { /* Legacy non-memory payload handling remains owned by the Work registry. */ }
|
|
80
|
+
const repairWave = payload?.kind === "memory-repair" ? payload.memory_repair?.wave : undefined;
|
|
81
|
+
if (payload?.kind === "memory-repair" && (payload.read_only !== false || ![1, 2].includes(repairWave ?? 0)
|
|
82
|
+
|| bundle?.descriptor.descriptor.id !== "mb-init" || stage !== "review")) {
|
|
83
|
+
throw new AppError("memory_repair_launch_invalid", "Repair dispatch requires its registered memory review wave", 2, { phase: "prepare", effect: "no_effect" });
|
|
84
|
+
}
|
|
85
|
+
const repairScope = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id };
|
|
86
|
+
const prepareRepairPacket = repairWave === 1 ? prepareMemoryRepairDispatchFromStage : prepareMemorySecondRepairDispatchFromStage;
|
|
87
|
+
const repairPacket = repairWave ? prepareRepairPacket(context, repairScope) : null;
|
|
88
|
+
const assertRepairPacket = (effect = "no_effect") => {
|
|
89
|
+
if (!repairPacket)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
const expected = { coordinator: resolved.coordinator, worker: resolved.worker, delegation: resolved.delegation };
|
|
93
|
+
const actual = { coordinator: repairPacket.profile.coordinator, worker: repairPacket.profile.worker, delegation: repairPacket.profile.delegation };
|
|
94
|
+
if (repairPacket.work_id !== work.work_id || repairPacket.profile.profile_hash !== resolved.profile_hash
|
|
95
|
+
|| hybridDigest(actual) !== hybridDigest(expected) || hybridDigest(prepareRepairPacket(context, repairScope)) !== hybridDigest(repairPacket)) {
|
|
96
|
+
throw new AppError("memory_repair_launch_invalid", "Repair ready copy or frozen route changed before dispatch", 2, { phase: "prepare", effect: "no_effect" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (effect === "partial")
|
|
101
|
+
throw new AppError("memory_repair_launch_pending", "Repair launch files are retained but dispatch custody changed", 1, { effect, work_id: work.work_id, operation_id: operationId, state_dir: root, next_action: "reconcile_exact_request", cause: errorRecord(error) });
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
assertRepairPacket();
|
|
53
106
|
const agyWorker = runtimeHarness(resolved.worker) === "antigravity-cli";
|
|
107
|
+
if (agyWorker && repairPacket)
|
|
108
|
+
throw new AppError("memory_repair_launch_invalid", "Repair copy hook qualification is required before Antigravity dispatch", 2, { phase: "prepare", effect: "no_effect" });
|
|
54
109
|
if (agyWorker)
|
|
55
110
|
await qualifyAgyHooks(run.workspace_root);
|
|
111
|
+
assertRepairPacket();
|
|
56
112
|
const operationId = `OPR-${crypto.randomUUID()}`;
|
|
57
113
|
const root = path.join(run.run_root, "external-launches", operationId);
|
|
58
114
|
const journal = path.join(root, "adapter.events.jsonl");
|
|
@@ -76,8 +132,10 @@ export async function launchExternalWork(context, input) {
|
|
|
76
132
|
reviewCopy = { root: path.relative(run.run_root, isolatedWorkspace).split(path.sep).join("/"), sha256: reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, true, reviewInputs, reviewBase), source_sha256: after };
|
|
77
133
|
}
|
|
78
134
|
const now = context.now();
|
|
79
|
-
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
80
|
-
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined,
|
|
135
|
+
const flowExecutable = prepareHarnessFlowExecutable(context, root, bundle?.engine());
|
|
136
|
+
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined,
|
|
137
|
+
DD_FLOW_MEMORY_REPAIR_WORKSPACE: repairPacket?.copy_root, ...(repairPacket ? { DD_FLOW_READ_ONLY_WORKSPACE: undefined } : {}),
|
|
138
|
+
...(isolatedWorkspace ? { DD_FLOW_READ_ONLY_WORKSPACE: isolatedWorkspace } : {}) };
|
|
81
139
|
// An external Codex root is a fresh thread, so it does not inherit the
|
|
82
140
|
// coordinator's managed PreToolUse hook. Its first `work start` must emit
|
|
83
141
|
// that hook event to bind the Work to the physical session.
|
|
@@ -89,8 +147,9 @@ export async function launchExternalWork(context, input) {
|
|
|
89
147
|
let runtimeContext = { ...context, env };
|
|
90
148
|
const runtimeOwner = { schema_id: "dd-flow/runtime-owner@1", owner_id: `run:${work.run_id}:work:${work.work_id}`, role: "worker", project_id: work.project_id, run_id: work.run_id, work_id: work.work_id, operation_id: operationId, state_dir: root, dd_flow_home: context.ddFlowHome, dd_flow_bin: flowExecutable, resource_home: context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, budget: runtimeBudgetForRun(context, work.project_id, work.run_id) };
|
|
91
149
|
const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
|
|
92
|
-
|
|
93
|
-
|
|
150
|
+
assertRepairPacket("partial");
|
|
151
|
+
context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner }, ...(reviewCopy ? { review_copy: reviewCopy } : {}), ...(repairPacket ? { memory_repair_copy: repairPacket.profile.memory_repair_copy } : {}) }), resolved.profile_hash, root, journal, now, now]);
|
|
152
|
+
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", repairPacket?.copy_root ?? isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
94
153
|
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
95
154
|
try {
|
|
96
155
|
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, harness: runtimeHarness(resolved.worker), env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence, onNativeOutcome: value => {
|
|
@@ -111,7 +170,15 @@ export async function launchExternalWork(context, input) {
|
|
|
111
170
|
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(resolved.worker), stateDir: root, sessionId,
|
|
112
171
|
projectRoot: run.project_root, runId: work.run_id, generation });
|
|
113
172
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
114
|
-
assertWorkLaunchReady(
|
|
173
|
+
assertWorkLaunchReady(runtimeContext, work.work_id);
|
|
174
|
+
if (repairPacket) {
|
|
175
|
+
const owner = repairWave === 1
|
|
176
|
+
? prepareExternalMemoryRepairCopyFromStage(context, { ...repairScope, workspaceRoot: repairPacket.copy_root })
|
|
177
|
+
: prepareMemorySecondRepairLaunchFromStage(context, repairScope);
|
|
178
|
+
if (owner.root() !== repairPacket.copy_root)
|
|
179
|
+
throw new AppError("memory_repair_launch_invalid", "Physical repair launch differs from the prepared copy", 1, { effect: "partial", next_action: "reconcile_exact_request" });
|
|
180
|
+
owner.revalidate(context);
|
|
181
|
+
}
|
|
115
182
|
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
116
183
|
throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
|
|
117
184
|
const promptFile = path.join(root, "launch.md");
|
|
@@ -123,8 +190,6 @@ export async function launchExternalWork(context, input) {
|
|
|
123
190
|
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null);
|
|
124
191
|
}
|
|
125
192
|
catch (error) {
|
|
126
|
-
if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(resolved.worker)))
|
|
127
|
-
throw error;
|
|
128
193
|
const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
129
194
|
const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
|
|
130
195
|
const capacityFailures = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_failures ?? [] : [];
|
|
@@ -133,7 +198,7 @@ export async function launchExternalWork(context, input) {
|
|
|
133
198
|
if (changed.changes !== 1)
|
|
134
199
|
throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
|
|
135
200
|
};
|
|
136
|
-
const resumed = await
|
|
201
|
+
const resumed = await continueProviderCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
|
|
137
202
|
...(input.signal ? { signal: input.signal } : {}),
|
|
138
203
|
continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
|
|
139
204
|
retainedIntents: () => capacityIntents,
|
|
@@ -178,8 +243,15 @@ export async function launchExternalWork(context, input) {
|
|
|
178
243
|
}
|
|
179
244
|
catch (error) {
|
|
180
245
|
const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
let failure = error;
|
|
247
|
+
if (repairPacket) {
|
|
248
|
+
const physical = context.db.get("SELECT provider_session_id FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
249
|
+
failure = new AppError("memory_repair_launch_pending", "Repair launch retains runtime effects; reconcile the exact request before continuing", 1, { effect: error instanceof AppError && error.details.effect === "unknown" ? "unknown" : "partial",
|
|
250
|
+
work_id: work.work_id, operation_id: operationId, provider_session_id: physical?.provider_session_id ?? null,
|
|
251
|
+
next_action: "reconcile_exact_request", cause: errorRecord(error) });
|
|
252
|
+
}
|
|
253
|
+
context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(failure) }), context.now(), operationId]);
|
|
254
|
+
throw failure;
|
|
183
255
|
}
|
|
184
256
|
}
|
|
185
257
|
export function writeReviewGitEvidence(source, copy, base, explicitInputs = []) {
|
|
@@ -192,7 +264,13 @@ export function writeReviewGitEvidence(source, copy, base, explicitInputs = [])
|
|
|
192
264
|
if (!base || base === "unknown")
|
|
193
265
|
throw new AppError("review_git_evidence_missing", "Git review source has no frozen base commit", 1, { workspace_root: source });
|
|
194
266
|
const paths = (reviewSourcePaths(source, explicitInputs, base) ?? []).map(file => `:(literal)${file}`);
|
|
195
|
-
|
|
267
|
+
const commands = [["status.txt", ["status", "--short", "--untracked-files=all"]],
|
|
268
|
+
["diff.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary", ...(base === "unborn" ? ["--cached"] : [base])]]];
|
|
269
|
+
// No revision exists in an unborn repository. Retain both index additions
|
|
270
|
+
// and subsequent working edits; untracked bytes are in the filtered copy.
|
|
271
|
+
if (base === "unborn")
|
|
272
|
+
commands.push(["diff-working.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary"]]);
|
|
273
|
+
for (const [name, args] of commands) {
|
|
196
274
|
const result = paths.length ? spawnSync("git", ["-C", source, ...args, "--", ...paths], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : { status: 0, stdout: "", stderr: "", error: undefined };
|
|
197
275
|
if (result.status !== 0 || result.error)
|
|
198
276
|
throw new AppError("review_git_evidence_failed", "Could not collect read-only Git evidence for reviewer", 1, { workspace_root: source, command: name, cause: result.error?.message ?? result.stderr });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { vnextStages } from "../domain/stage-catalog.js";
|
|
2
|
+
import { sdlcHandlerIdentities, memoryInitHandlerIdentities } from "../domain/stage-handler-identities.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
/** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
|
|
5
|
+
export function listBundledFlows() {
|
|
6
|
+
return [
|
|
7
|
+
{ id: "sdlc", handler_status: "registered", entry_stage: "specify", terminal_stages: ["merge"],
|
|
8
|
+
stages: vnextStages.map(stage => ({ id: stage.id, directory: stage.directory,
|
|
9
|
+
entry_handler: sdlcHandlerIdentities[stage.id].entryId, finish_handler: sdlcHandlerIdentities[stage.id].finishId })),
|
|
10
|
+
edges: vnextStages.flatMap(stage => stage.next.map((to, index) => ({ from: stage.id, to, on: index ? "not-applicable" : "accepted" }))) },
|
|
11
|
+
{ id: "mb-init", handler_status: "registered", entry_stage: "preflight", terminal_stages: ["finish"],
|
|
12
|
+
stages: Object.values(memoryInitHandlerIdentities).map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
|
|
13
|
+
edges: ["preflight", "discover", "compose", "verify", "review"].flatMap((from, index) => [{ from, to: ["discover", "compose", "verify", "review", "finish"][index], on: "accepted" }, ...(from === "verify" ? [{ from, to: "review", on: "needs_repair" }] : [])]) }
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
export function showBundledFlow(id) {
|
|
17
|
+
const flow = listBundledFlows().find(candidate => candidate.id === id);
|
|
18
|
+
if (!flow)
|
|
19
|
+
throw new AppError("flow_not_found", "Unknown bundled flow", 2, { effect: "no_effect", flow_id: id });
|
|
20
|
+
return flow;
|
|
21
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { validateFlowDescriptor, validateHybridShape } from "./hybrid-contracts.js";
|
|
4
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
5
|
+
const blobLimit = 1024 * 1024;
|
|
6
|
+
function freeze(value) {
|
|
7
|
+
if (value !== null && typeof value === "object") {
|
|
8
|
+
for (const child of Object.values(value))
|
|
9
|
+
freeze(child);
|
|
10
|
+
Object.freeze(value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Admitted retained bytes only. This is not a RUN/storage/dispatch permission. */
|
|
14
|
+
export class ResolvedFlowDescriptor {
|
|
15
|
+
sha256;
|
|
16
|
+
descriptor;
|
|
17
|
+
#bytes;
|
|
18
|
+
#blobs;
|
|
19
|
+
constructor(bytes, descriptor, blobs) {
|
|
20
|
+
this.sha256 = digest(bytes);
|
|
21
|
+
this.#bytes = bytes;
|
|
22
|
+
this.#blobs = blobs;
|
|
23
|
+
freeze(descriptor);
|
|
24
|
+
this.descriptor = descriptor;
|
|
25
|
+
Object.freeze(this);
|
|
26
|
+
}
|
|
27
|
+
/** Context comes from the engine materializer, never descriptor-owned allowlists. */
|
|
28
|
+
static admit(input, expectedSha256, context) {
|
|
29
|
+
const details = { phase: "prepare", effect: "no_effect" };
|
|
30
|
+
if (input.byteLength > blobLimit || !/^[a-f0-9]{64}$/.test(expectedSha256)) {
|
|
31
|
+
throw new AppError("flow_descriptor_invalid", "Flow descriptor identity or size is invalid", 2, details);
|
|
32
|
+
}
|
|
33
|
+
const bytes = Buffer.from(input);
|
|
34
|
+
if (digest(bytes) !== expectedSha256)
|
|
35
|
+
throw new AppError("flow_descriptor_integrity", "Flow descriptor bytes do not match the selected identity", 1, details);
|
|
36
|
+
let value;
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new AppError("flow_descriptor_invalid", "Flow descriptor must be valid UTF-8 JSON", 2, details);
|
|
42
|
+
}
|
|
43
|
+
const shape = validateHybridShape("flow-descriptor", value);
|
|
44
|
+
if (shape.length)
|
|
45
|
+
throw new AppError("flow_descriptor_invalid", "Flow descriptor shape is invalid", 2, { ...details, issues: shape });
|
|
46
|
+
const blobs = new Map();
|
|
47
|
+
let total = 0;
|
|
48
|
+
for (const [name, source] of context.blobs) {
|
|
49
|
+
total += source.byteLength;
|
|
50
|
+
if (source.byteLength > blobLimit || total > 32 * blobLimit)
|
|
51
|
+
throw new AppError("flow_descriptor_invalid", "Flow descriptor blobs exceed the bounded inventory", 2, details);
|
|
52
|
+
blobs.set(name, Buffer.from(source));
|
|
53
|
+
}
|
|
54
|
+
const issues = validateFlowDescriptor(value, { ...context, blobs });
|
|
55
|
+
if (issues.length)
|
|
56
|
+
throw new AppError("flow_descriptor_invalid", "Flow descriptor context is invalid", 2, { ...details, issues });
|
|
57
|
+
return new ResolvedFlowDescriptor(bytes, value, blobs);
|
|
58
|
+
}
|
|
59
|
+
stage(id) {
|
|
60
|
+
const stage = this.descriptor.stages.find(candidate => candidate.id === id);
|
|
61
|
+
if (!stage)
|
|
62
|
+
throw new AppError("flow_stage_invalid", "Stage does not belong to the resolved flow", 2, { flow_id: this.descriptor.id, stage: id, effect: "no_effect" });
|
|
63
|
+
return stage;
|
|
64
|
+
}
|
|
65
|
+
successor(id, outcome) {
|
|
66
|
+
this.stage(id);
|
|
67
|
+
if (outcome !== "accepted" && outcome !== "not-applicable" && outcome !== "needs_repair") {
|
|
68
|
+
throw new AppError("flow_transition_invalid", "Unknown stage outcome", 2, { flow_id: this.descriptor.id, stage: id, outcome, effect: "no_effect" });
|
|
69
|
+
}
|
|
70
|
+
const edge = this.descriptor.edges.find(candidate => candidate.from === id && candidate.on === outcome);
|
|
71
|
+
if (edge)
|
|
72
|
+
return edge.to;
|
|
73
|
+
if (this.descriptor.terminal_stages.includes(id))
|
|
74
|
+
return null;
|
|
75
|
+
throw new AppError("flow_transition_invalid", "No declared successor for this stage outcome", 2, { flow_id: this.descriptor.id, stage: id, outcome, effect: "no_effect" });
|
|
76
|
+
}
|
|
77
|
+
descriptorBytes() { return Buffer.from(this.#bytes); }
|
|
78
|
+
stageBlob(id, kind) {
|
|
79
|
+
const reference = this.stage(id)[kind];
|
|
80
|
+
return Buffer.from(this.#blobs.get(reference.path));
|
|
81
|
+
}
|
|
82
|
+
}
|