@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -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 +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -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/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- 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/driver-recovery.mjs +9 -8
- 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.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -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 +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- 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/project-flow-pack-manifest.schema.json +1 -1
- 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/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -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 +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- 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 +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- 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-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- 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-tool-probe.js +25 -0
- 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 +92 -27
- 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/native-daemon-history.js +2 -1
- 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 +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- 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-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- 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/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- 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 +13 -6
|
@@ -9,7 +9,9 @@ import { finishVnextMerge, startVnextMerge } from "./vnext-merge.js";
|
|
|
9
9
|
import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
|
|
10
10
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
11
11
|
import { sdlcHandlerIdentities as ids } from "../domain/stage-handler-identities.js";
|
|
12
|
-
import { memoryInitStageHandlers } from "./memory-stage-dispatch.js";
|
|
12
|
+
import { memoryInitStageHandlers, memoryUpgradeStageHandlers } from "./memory-stage-dispatch.js";
|
|
13
|
+
import { diagnosticStageHandlers } from "./diagnostic-stage-handlers.js";
|
|
14
|
+
import { isDiagnosticFlow, diagnosticFlowStages } from "../domain/diagnostic-flows.js";
|
|
13
15
|
/** Engine-owned code, not a plugin registration surface or executable descriptor expression. */
|
|
14
16
|
export const sdlcStageHandlers = Object.freeze({
|
|
15
17
|
specify: Object.freeze({ ...ids.specify, entry: launchVnextSpecify, finish: finishVnextSpecifyStage }),
|
|
@@ -23,8 +25,8 @@ export const sdlcStageHandlers = Object.freeze({
|
|
|
23
25
|
/** A fresh inventory cannot mutate the registered handler functions. */
|
|
24
26
|
export function bundledHandlerContext(blobs) {
|
|
25
27
|
return {
|
|
26
|
-
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.entryId)),
|
|
27
|
-
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.finishId)), blobs
|
|
28
|
+
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.entryId)),
|
|
29
|
+
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.finishId)), blobs
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
/** Handler identity is also stage/flow scoped: a known CODE handler is not a memory handler. */
|
|
@@ -40,8 +42,9 @@ export function admitBundledFlow(input, sha256, blobs) {
|
|
|
40
42
|
throw new AppError("flow_handler_scope_invalid", "SDLC handlers require the registered seven-stage layout and transitions", 2, { phase: "prepare", effect: "no_effect" });
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
if (flow.descriptor.id === "mb-init") {
|
|
44
|
-
const
|
|
45
|
+
if (flow.descriptor.id === "mb-init" || flow.descriptor.id === "mb-upgrade") {
|
|
46
|
+
const upgrade = flow.descriptor.id === "mb-upgrade";
|
|
47
|
+
const stages = Object.keys(upgrade ? memoryUpgradeStageHandlers : memoryInitStageHandlers);
|
|
45
48
|
const edges = stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" }));
|
|
46
49
|
// Historical packs have only accepted transitions; diagnostic VERIFY uses
|
|
47
50
|
// the same REVIEW handler without granting another route or stage.
|
|
@@ -59,12 +62,24 @@ export function admitBundledFlow(input, sha256, blobs) {
|
|
|
59
62
|
schemaId = JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id;
|
|
60
63
|
}
|
|
61
64
|
catch { /* Invalid schema cannot authorize this stage handler. */ }
|
|
62
|
-
if (schemaId !== `dd-flow/mb-init-${stage.id}-result@1`)
|
|
65
|
+
if (schemaId !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : `dd-flow/mb-init-${stage.id}-result@1`))
|
|
63
66
|
throw new AppError("flow_handler_scope_invalid", "Memory init stage requires its own registered result schema", 2, { phase: "prepare", effect: "no_effect", stage: stage.id });
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
if (isDiagnosticFlow(flow.descriptor.id)) {
|
|
70
|
+
const stages = diagnosticFlowStages[flow.descriptor.id];
|
|
71
|
+
if (flow.descriptor.entry_stage !== "preflight" || flow.descriptor.terminal_stages.length !== 1 || flow.descriptor.terminal_stages[0] !== "report"
|
|
72
|
+
|| flow.descriptor.stages.length !== stages.length || flow.descriptor.edges.length !== stages.length - 1
|
|
73
|
+
|| flow.descriptor.stages.some((stage, index) => stage.id !== stages[index] || stage.directory !== `${String(index + 1).padStart(2, "0")}-${stage.id}`)
|
|
74
|
+
|| stages.slice(0, -1).some((stage, index) => !flow.descriptor.edges.some(edge => edge.from === stage && edge.to === stages[index + 1] && edge.on === "accepted"))
|
|
75
|
+
|| flow.descriptor.stages.some(stage => JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id !== "dd-flow/diagnostic-stage-result@1")) {
|
|
76
|
+
throw new AppError("flow_handler_scope_invalid", "Diagnostic handlers require their registered four-stage layout and result schema", 2, { phase: "prepare", effect: "no_effect" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
for (const stage of flow.descriptor.stages) {
|
|
67
|
-
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
|
|
80
|
+
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
|
|
81
|
+
: flow.descriptor.id === "mb-upgrade" ? Object.values(memoryUpgradeStageHandlers)
|
|
82
|
+
: diagnosticStageHandlers.filter(handler => handler.flowId === flow.descriptor.id);
|
|
68
83
|
const handler = handlers.find(candidate => candidate.stage === stage.id);
|
|
69
84
|
if (!handler || stage.entry_handler !== handler.entryId || stage.finish_handler !== handler.finishId) {
|
|
70
85
|
throw new AppError("flow_handler_scope_invalid", "Registered handlers do not belong to this flow stage", 2, { phase: "prepare", effect: "no_effect", flow_id: flow.descriptor.id, stage: stage.id });
|
|
@@ -17,8 +17,11 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
|
17
17
|
import { runtimeCommand } from "./runtime-command.js";
|
|
18
18
|
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
19
19
|
import { latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
20
|
-
import {
|
|
20
|
+
import { acceptedIntentReviewFingerprint, prepareAcceptedIntentSources, validatePreparedAcceptedIntentReview } from "./accepted-intent.js";
|
|
21
21
|
export { flowCommand } from "./runtime-command.js";
|
|
22
|
+
export function stageResumeSourcePath(pause, answer) {
|
|
23
|
+
return path.join(path.dirname(pause.question_path), `resume-${crypto.createHash("sha256").update(`${pause.id}\0${answer}`).digest("hex")}.json`);
|
|
24
|
+
}
|
|
22
25
|
export function prepareStagePause(context, input) {
|
|
23
26
|
if (typeof input.question !== "string" || !input.question.trim())
|
|
24
27
|
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
@@ -116,7 +119,31 @@ export function resumeStageAfterUser(context, input) {
|
|
|
116
119
|
// reserving SQLite's writer; an unreferenced candidate has no flow authority.
|
|
117
120
|
publishImmutableInput(answerPath, Buffer.from(input.answer), "hitl_answer_conflict");
|
|
118
121
|
const answerPacket = prepareStagePacket(answerPath, Buffer.from(input.answer));
|
|
119
|
-
|
|
122
|
+
// Capture the exact continuation inputs and already-issued commands before
|
|
123
|
+
// accepting the answer. A publication retry must not issue new authority.
|
|
124
|
+
const retryCommands = lifecycleRetryCommands(context, stage.stage);
|
|
125
|
+
const resumeSourcePath = stageResumeSourcePath(pause, input.answer);
|
|
126
|
+
let review;
|
|
127
|
+
let reviewPreparationError;
|
|
128
|
+
const previousAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
|
|
129
|
+
if (["plan-review", "code-review"].includes(stage.stage)) {
|
|
130
|
+
try {
|
|
131
|
+
const home = requireRunHome(run), source = latestRunVerificationMatrix(context, project.id, run.id, stage.stage, "input");
|
|
132
|
+
const reviewContext = path.join(home, stage.stage === "plan-review" ? "04-plan-review/work-context.json" : "06-code-review/review-context.json");
|
|
133
|
+
const retainedContext = fs.existsSync(reviewContext) ? JSON.parse(fs.readFileSync(reviewContext, "utf8")) : null;
|
|
134
|
+
const currentSources = acceptedIntentReviewFingerprint(source?.metadata.accepted_sources) ? prepareAcceptedIntentSources(context, { projectId: project.id, runId: run.id, home, stageDirectory: "intake", marked: true }).sources : [];
|
|
135
|
+
review = { home, source, retainedContext, currentSources };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!(error instanceof AppError))
|
|
139
|
+
throw error;
|
|
140
|
+
reviewPreparationError = error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return context.db.writeTransaction(() => {
|
|
144
|
+
const currentAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
|
|
145
|
+
if (currentAnswer !== previousAnswer)
|
|
146
|
+
throw new AppError("runtime_state_changed", "Accepted inputs changed during resume preparation", 1);
|
|
120
147
|
const identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
|
|
121
148
|
if (workSession.session_id !== identity.sessionId) {
|
|
122
149
|
bindSessionForResume(context, work.work_id, identity, now);
|
|
@@ -130,49 +157,56 @@ export function resumeStageAfterUser(context, input) {
|
|
|
130
157
|
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: answerPacket });
|
|
131
158
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
|
|
132
159
|
context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
const prompt = evidence.prompt;
|
|
161
|
+
const intentInstructions = [];
|
|
162
|
+
if (["plan-review", "code-review"].includes(stage.stage)) {
|
|
163
|
+
try {
|
|
164
|
+
if (reviewPreparationError)
|
|
165
|
+
throw reviewPreparationError;
|
|
166
|
+
if (review) {
|
|
167
|
+
const id = context.db.get("SELECT id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.pause_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, pause.id]).id;
|
|
168
|
+
const identity = `accepted_hitl:${String(id).padStart(12, "0")}:${pause.id}`;
|
|
169
|
+
const sources = [...review.currentSources, { role: `${identity}:0-question`, sha256: crypto.createHash("sha256").update(evidence.question).digest("hex") }, { role: `${identity}:1-answer`, sha256: crypto.createHash("sha256").update(input.answer).digest("hex") }];
|
|
170
|
+
validatePreparedAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home: review.home, stage: stage.stage, decisionFrozen: Boolean(review.retainedContext?.accepted_decision) }, review.source?.metadata.accepted_sources, review.retainedContext?.accepted_intent_review_fingerprint, sources);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (!(error instanceof AppError))
|
|
175
|
+
throw error;
|
|
176
|
+
intentInstructions.push(`Accepted-input review boundary: ${error.code}: ${error.message}`);
|
|
177
|
+
if (error.code === "accepted_intent_review_ack_required")
|
|
178
|
+
intentInstructions.push(`Apply/classify this own-stage clarification through the existing coordinator correction/repair route. Include this exact field in the decision: ${JSON.stringify({ accepted_intent_acknowledgement: error.details.accepted_intent_acknowledgement })}. Independent reviewer results prove only the earlier retained inputs; do not claim they reviewed the new answer.`);
|
|
179
|
+
else
|
|
180
|
+
intentInstructions.push("Do not accept stale independent review as coverage of this answer; report the actionable upstream revision/evidence blocker.");
|
|
181
|
+
}
|
|
143
182
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
""
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"</user_answer>",
|
|
172
|
-
"</hitl_resume>",
|
|
173
|
-
""
|
|
174
|
-
].join("\n");
|
|
175
|
-
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
183
|
+
const continuation = [
|
|
184
|
+
prompt.trimEnd(),
|
|
185
|
+
"",
|
|
186
|
+
"<hitl_resume>",
|
|
187
|
+
`- Pause: ${pause.id}`,
|
|
188
|
+
`- Question source: ${pause.question_path}`,
|
|
189
|
+
`- Answer source: ${answerPath}`,
|
|
190
|
+
"- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
|
|
191
|
+
...intentInstructions,
|
|
192
|
+
...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
|
|
193
|
+
"",
|
|
194
|
+
"<user_question>",
|
|
195
|
+
evidence.question,
|
|
196
|
+
"</user_question>",
|
|
197
|
+
"",
|
|
198
|
+
"<user_answer>",
|
|
199
|
+
input.answer,
|
|
200
|
+
"</user_answer>",
|
|
201
|
+
"</hitl_resume>",
|
|
202
|
+
""
|
|
203
|
+
].join("\n");
|
|
204
|
+
const result = { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, resume_source_path: resumeSourcePath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
205
|
+
const scope = context.env.DD_FLOW_INVOCATION_SCOPE ? JSON.parse(context.env.DD_FLOW_INVOCATION_SCOPE) : null;
|
|
206
|
+
const bytes = Buffer.from(`${JSON.stringify({ invocation_id: context.env.DD_FLOW_CURRENT_INVOCATION ?? null, generation: scope?.generation ?? null, result })}\n`);
|
|
207
|
+
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: { file: resumeSourcePath, bytes, sha256: crypto.createHash("sha256").update(bytes).digest("hex") } });
|
|
208
|
+
return result;
|
|
209
|
+
});
|
|
176
210
|
}
|
|
177
211
|
export function stagePauseCommand(context, input) {
|
|
178
212
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -180,6 +214,39 @@ export function stagePauseCommand(context, input) {
|
|
|
180
214
|
const questionInputPath = prepareAgentOutputFile(path.join(requireRunHome(run), "works", input.workId, "question-input.md"));
|
|
181
215
|
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
|
|
182
216
|
}
|
|
217
|
+
/** Recover only the exact resume committed with this invocation; never resume again. */
|
|
218
|
+
export function recoverCommittedStageResume(context, input) {
|
|
219
|
+
const project = requireProjectByRoot(context, input.projectRoot), run = requireRun(context, project.id, input.runId);
|
|
220
|
+
const work = requireWork(context, project.id, run.id, input.workId);
|
|
221
|
+
const index = JSON.parse(run.index_json);
|
|
222
|
+
const current = index.stage_runs.find(stage => stage.stage === input.stage);
|
|
223
|
+
if (!current || current.status !== "running" || index.current_stage && index.current_stage !== input.stage || work.status !== "running")
|
|
224
|
+
return null;
|
|
225
|
+
const resumed = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.work_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, input.stage, work.work_id]);
|
|
226
|
+
if (!resumed)
|
|
227
|
+
return null;
|
|
228
|
+
const acceptedPause = JSON.parse(resumed.payload_json);
|
|
229
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.attempt') = ? AND json_extract(payload_json, '$.file') LIKE '%/resume-%.json' ORDER BY id DESC LIMIT 257", [project.id, run.id, input.stage, current.attempt]);
|
|
230
|
+
if (rows.length > 256)
|
|
231
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt count exceeds its bound", 1);
|
|
232
|
+
let result = null;
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
const packet = JSON.parse(row.payload_json);
|
|
235
|
+
const bytes = Buffer.from(packet.base64, "base64");
|
|
236
|
+
if (bytes.length > 8 * 1024 * 1024 || bytes.toString("base64") !== packet.base64 || crypto.createHash("sha256").update(bytes).digest("hex") !== packet.sha256)
|
|
237
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt custody differs from its committed bytes", 1);
|
|
238
|
+
const retained = JSON.parse(bytes.toString("utf8"));
|
|
239
|
+
if (retained.invocation_id !== input.id)
|
|
240
|
+
continue;
|
|
241
|
+
const value = retained.result;
|
|
242
|
+
if (retained.generation !== input.generation || !value || value.run_id !== run.id || value.work_id !== work.work_id || value.stage !== input.stage
|
|
243
|
+
|| value.pause_id !== acceptedPause.pause_id || value.answer_path !== acceptedPause.answer_path || value.resume_source_path !== packet.file || typeof value.worker_prompt_markdown !== "string"
|
|
244
|
+
|| result && JSON.stringify(result) !== JSON.stringify(value))
|
|
245
|
+
throw new AppError("stage_resume_receipt_invalid", "Resume receipt does not match the current invocation and accepted pause", 1);
|
|
246
|
+
result = value;
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
183
250
|
/** Render separate file-write and lifecycle-call instructions for HITL. */
|
|
184
251
|
export function stagePauseCommandTemplate(command) {
|
|
185
252
|
const questionFile = command.match(/--question-file\s+(?:'([^']*)'|"([^"]*)"|(\S+))/)?.slice(1).find(Boolean) ?? "<question-file-from-command>";
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
const canonicalOnlyFlowNames = ["mb-init.md", "mb-upgrade.md", "mb-distill.md"];
|
|
5
|
+
export const canonicalOnlyFlowNames = Object.freeze(["mb-init.md", "mb-upgrade.md", "mb-distill.md", "mb-audit.md", "review.md"]);
|
|
6
6
|
export function resolveStatusProjectRoot(input) {
|
|
7
7
|
if (input.requestedRoot) {
|
|
8
8
|
const absolute = path.resolve(input.requestedRoot);
|
|
@@ -194,7 +194,7 @@ export async function finishVnextCode(context, input) {
|
|
|
194
194
|
const retainedMatrix = latestRunVerificationMatrix(context, project.id, run.id, stage);
|
|
195
195
|
const retainedMatrixInstruction = verificationMatrixInstruction(retainedMatrix?.metadata ?? null, home, "the current semantic verification and repair");
|
|
196
196
|
if (verification.verdict === "blocked") {
|
|
197
|
-
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke
|
|
197
|
+
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke next.finish_command. Do not repeat while the blocker remains unresolved.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
|
|
198
198
|
}
|
|
199
199
|
if (verification.verdict === "passed" && verification.unresolved.length > 0) {
|
|
200
200
|
throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
|
|
@@ -209,7 +209,7 @@ export async function finishVnextCode(context, input) {
|
|
|
209
209
|
verification,
|
|
210
210
|
objective: verification.summary
|
|
211
211
|
});
|
|
212
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke
|
|
212
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke next.finish_command.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
|
|
213
213
|
}
|
|
214
214
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
215
215
|
if (input.retryCheckId) {
|
|
@@ -15,6 +15,8 @@ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/del
|
|
|
15
15
|
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
16
16
|
import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
|
|
17
17
|
import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
|
|
18
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
19
|
+
import { readRegisteredDiagnosticGraph } from "./diagnostic-work-registration.js";
|
|
18
20
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
19
21
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
20
22
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -105,7 +107,7 @@ export function verifiedNativeWorkFailure(context, input) {
|
|
|
105
107
|
}
|
|
106
108
|
export function fanoutState(input) {
|
|
107
109
|
if (!input.hasWork)
|
|
108
|
-
return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
|
|
110
|
+
return input.registeredEmpty ? "children_settled" : input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
|
|
109
111
|
if (input.capacityRequired && !input.capacityKnown)
|
|
110
112
|
return "capacity_required";
|
|
111
113
|
if (input.created || input.running)
|
|
@@ -133,14 +135,17 @@ export function readFanoutDescriptor(stageRoot) {
|
|
|
133
135
|
* productive assignments, including reviewer/correction waves.
|
|
134
136
|
*/
|
|
135
137
|
export function memoryStageFanoutDescriptor(context, input) {
|
|
136
|
-
if (!["compose", "review"].includes(input.stage))
|
|
138
|
+
if (!["compose", "upgrade", "review"].includes(input.stage))
|
|
137
139
|
return null;
|
|
138
140
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
139
141
|
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
140
|
-
if (run
|
|
142
|
+
if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
|
|
143
|
+
return null;
|
|
144
|
+
const authoringStage = run.flow_kind === "mb-init" ? "compose" : "upgrade";
|
|
145
|
+
if (input.stage !== authoringStage && input.stage !== "review")
|
|
141
146
|
return null;
|
|
142
147
|
const bundle = readFlowRunExecutionBundle(context, input);
|
|
143
|
-
if (!bundle || bundle.descriptor.descriptor.id !==
|
|
148
|
+
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind)
|
|
144
149
|
throw new AppError("stage_graph_compatibility_required", "Memory graph requires its admitted RUN bundle", 1);
|
|
145
150
|
const index = JSON.parse(run.index_json);
|
|
146
151
|
if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
|
|
@@ -153,7 +158,7 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
153
158
|
if (id === root.workId)
|
|
154
159
|
continue;
|
|
155
160
|
const work = context.db.get("SELECT parent_work_id,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [project.id, input.runId, id]);
|
|
156
|
-
const schemas = input.stage ===
|
|
161
|
+
const schemas = input.stage === authoringStage ? ["dd-flow/memory-composition-work-result@1"] : ["dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"];
|
|
157
162
|
if (!work || work.parent_work_id !== root.workId || !schemas.includes(work.result_schema ?? ""))
|
|
158
163
|
throw new AppError("stage_graph_conflict", "Memory graph contains an unassigned Work", 1);
|
|
159
164
|
const custody = readWorkBatchCustody(context, id);
|
|
@@ -161,11 +166,11 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
161
166
|
|| custody.scope.cycle !== scope.cycle || custody.scope.stage !== input.stage || custody.scope.parent_work_id !== root.workId)
|
|
162
167
|
throw new AppError("stage_graph_conflict", "Memory graph contains foreign allocation custody", 1);
|
|
163
168
|
}
|
|
164
|
-
if (input.stage ===
|
|
169
|
+
if (input.stage === authoringStage) {
|
|
165
170
|
const children = context.db.all("SELECT work_id FROM works WHERE project_id=? AND run_id=? AND parent_work_id=?", [project.id, input.runId, root.workId]).filter(work => members.has(work.work_id));
|
|
166
171
|
const registered = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? LIMIT 1", [project.id, input.runId, scope.attempt, scope.cycle]);
|
|
167
172
|
if (children.length || registered) {
|
|
168
|
-
const registration = selectMemoryCompositionRegistration(context, { ...scope, stage:
|
|
173
|
+
const registration = selectMemoryCompositionRegistration(context, { ...scope, stage: authoringStage, parent_work_id: root.workId });
|
|
169
174
|
const ids = new Set(Object.values(registration.work_ids));
|
|
170
175
|
if (ids.size !== children.length || children.some(work => !ids.has(work.work_id)))
|
|
171
176
|
throw new AppError("stage_graph_conflict", "Memory composition differs from its registered graph", 1);
|
|
@@ -173,6 +178,30 @@ export function memoryStageFanoutDescriptor(context, input) {
|
|
|
173
178
|
}
|
|
174
179
|
return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope, stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
|
|
175
180
|
}
|
|
181
|
+
/** Diagnostic graphs are registered by the Stage handler, never fanout.json. */
|
|
182
|
+
export function diagnosticStageFanoutDescriptor(context, input) {
|
|
183
|
+
if (!["audit", "analyze", "review"].includes(input.stage))
|
|
184
|
+
return null;
|
|
185
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
186
|
+
const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
|
|
187
|
+
if (!run || !isDiagnosticFlow(run.flow_kind))
|
|
188
|
+
return null;
|
|
189
|
+
const index = JSON.parse(run.index_json);
|
|
190
|
+
if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
|
|
191
|
+
return null;
|
|
192
|
+
const bundle = readFlowRunExecutionBundle(context, input);
|
|
193
|
+
if (!bundle || bundle.manifest.flow_id !== run.flow_kind)
|
|
194
|
+
throw new AppError("stage_graph_conflict", "Diagnostic graph requires its frozen bundle", 1);
|
|
195
|
+
const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
196
|
+
const root = readDescriptorRootWork(context, scope, bundle), graph = readRegisteredDiagnosticGraph(context, scope);
|
|
197
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
198
|
+
if (!graph || graph.parent_work_id !== root.workId || graph.bundle_sha256 !== bundle.sha256 || root.status !== "running"
|
|
199
|
+
|| !members.has(root.workId) || members.size !== Object.keys(graph.work_ids).length + 1
|
|
200
|
+
|| Object.values(graph.work_ids).some(id => !members.has(id)))
|
|
201
|
+
throw new AppError("stage_graph_conflict", "Diagnostic graph requires its exact registered Work; replay the issued Stage entry if registration was interrupted", 1);
|
|
202
|
+
return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope: stageGraphScope(context, project.id, input.runId, input.stage),
|
|
203
|
+
stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
|
|
204
|
+
}
|
|
176
205
|
/** Read-only projection for an external orchestrator. dd-flow keeps the graph authority. */
|
|
177
206
|
export function getVnextFanoutStatus(context, input) {
|
|
178
207
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -185,7 +214,9 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
185
214
|
const index = JSON.parse(run.index_json);
|
|
186
215
|
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
|
|
187
216
|
const stageRoot = path.join(run.run_root, bundle ? bundle.descriptor.stage(input.stage).directory : vnextStageDirectory(input.stage));
|
|
188
|
-
|
|
217
|
+
const memoryDescriptor = memoryStageFanoutDescriptor(context, input);
|
|
218
|
+
const diagnosticDescriptor = diagnosticStageFanoutDescriptor(context, input);
|
|
219
|
+
let descriptor = memoryDescriptor ?? diagnosticDescriptor ?? (bundle && isDiagnosticFlow(bundle.manifest.flow_id) ? null : readFanoutDescriptor(stageRoot));
|
|
189
220
|
// Registration is authoritative even if projection publication was interrupted,
|
|
190
221
|
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
191
222
|
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
@@ -211,6 +242,17 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
211
242
|
const ready = { works: all.works.filter(work => work.ready) };
|
|
212
243
|
const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
|
|
213
244
|
const hasWork = all.works.length > 0;
|
|
245
|
+
// Only SQL-owned memory registration can attest a deliberately empty
|
|
246
|
+
// graph. A mutable fanout.json must never turn missing Work into success.
|
|
247
|
+
let registeredEmpty = false;
|
|
248
|
+
if (diagnosticDescriptor && !hasWork) {
|
|
249
|
+
registeredEmpty = Object.keys(readRegisteredDiagnosticGraph(context, { projectId: project.id, runId: input.runId, stage: input.stage }).work_ids).length === 0;
|
|
250
|
+
}
|
|
251
|
+
if (memoryDescriptor && input.stage === "upgrade" && !hasWork
|
|
252
|
+
&& context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='upgrade' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 1", [project.id, input.runId, currentScope.attempt, currentScope.cycle, descriptor.parent_work_id])) {
|
|
253
|
+
const registered = selectMemoryCompositionRegistration(context, { ...currentScope, stage: "upgrade", parent_work_id: descriptor.parent_work_id });
|
|
254
|
+
registeredEmpty = Object.keys(registered.work_ids).length === 0;
|
|
255
|
+
}
|
|
214
256
|
const capacityKnown = capacity.available_slots !== null;
|
|
215
257
|
return {
|
|
216
258
|
ok: true,
|
|
@@ -221,7 +263,7 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
221
263
|
// PLAN-REVIEW has a deterministic dispatcher. CODE and CODE-REVIEW do
|
|
222
264
|
// not: their coordinator first materializes the Work graph from its
|
|
223
265
|
// authoritative prompt. An empty graph is therefore not a success.
|
|
224
|
-
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
266
|
+
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, registeredEmpty, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
225
267
|
capacity,
|
|
226
268
|
graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
|
|
227
269
|
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
|
|
@@ -217,8 +217,6 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
217
217
|
const waves = plannedReviewWaves(groups, capacity.availableSlots);
|
|
218
218
|
return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots, source: capacity.source }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
|
|
219
219
|
}
|
|
220
|
-
// Capacity is a RUN fact shared by every fan-out stage.
|
|
221
|
-
const reviewCapacityKey = subagentCapacityKey;
|
|
222
220
|
/** Capacity is observed immediately before this stage dispatches reviewers. */
|
|
223
221
|
export function recordVnextPlanReviewCapacity(context, input) {
|
|
224
222
|
if (!Number.isInteger(input.availableSlots) || input.availableSlots < 0 || input.availableSlots > 15) {
|
|
@@ -228,15 +226,15 @@ export function recordVnextPlanReviewCapacity(context, input) {
|
|
|
228
226
|
const project = requireProjectByRoot(context, projectRoot);
|
|
229
227
|
const run = requireRun(context, projectRoot, input.runId);
|
|
230
228
|
const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
231
|
-
const prior = variables.variables[
|
|
229
|
+
const prior = variables.variables[subagentCapacityKey];
|
|
232
230
|
if (typeof prior === "number") {
|
|
233
231
|
if (prior === input.availableSlots)
|
|
234
232
|
return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
|
|
235
|
-
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key:
|
|
233
|
+
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: subagentCapacityKey, value: input.availableSlots });
|
|
236
234
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
|
|
237
235
|
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
|
|
238
236
|
}
|
|
239
|
-
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key:
|
|
237
|
+
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: subagentCapacityKey, value: input.availableSlots });
|
|
240
238
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
|
|
241
239
|
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
|
|
242
240
|
}
|