@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.133
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 +666 -0
- package/README.md +101 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +306 -0
- package/dist/cli/help.js +88 -12
- package/dist/cli/hook-ingress.js +78 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +80 -0
- package/dist/cli/run-cli.js +1465 -450
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli.js +17 -2
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1108 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
- package/dist/harness-runtime/lib/model-observations.mjs +101 -0
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/flow-run.schema.json +2 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +11 -8
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +851 -109
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +13 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +119 -0
- package/dist/services/dashboard.js +135 -56
- package/dist/services/engines.js +245 -119
- package/dist/services/eval-snapshots.js +1146 -105
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +230 -0
- package/dist/services/harness-adapter.js +329 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +747 -325
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +1774 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +409 -51
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +145 -0
- package/dist/services/repair-intents.js +42 -0
- package/dist/services/review-copy.js +157 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +411 -0
- package/dist/services/run-control.js +985 -0
- package/dist/services/run-controller-adapter.js +399 -0
- package/dist/services/run-controller-capture.js +179 -0
- package/dist/services/run-controller-process.js +187 -0
- package/dist/services/run-controller-recovery.js +289 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1035 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +349 -0
- package/dist/services/runs.js +558 -217
- package/dist/services/runtime-budget.js +430 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +599 -0
- package/dist/services/runtime-scope-stop.js +180 -0
- package/dist/services/runtime-scope-worker.js +287 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +185 -19
- package/dist/services/stage-lifecycle.js +98 -107
- package/dist/services/stage-pause.js +89 -75
- package/dist/services/stage-report-renderer.js +158 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/tooling.js +32 -23
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +374 -0
- package/dist/services/vnext-code-review.js +222 -87
- package/dist/services/vnext-code.js +324 -251
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +79 -4
- package/dist/services/vnext-fanout.js +177 -17
- package/dist/services/vnext-merge.js +335 -113
- package/dist/services/vnext-plan-review.js +276 -126
- package/dist/services/vnext-plan.js +227 -102
- package/dist/services/vnext-protocolize.js +335 -82
- package/dist/services/vnext-specify.js +119 -81
- package/dist/services/work-registry.js +1038 -192
- package/dist/services/workspace-bootstrap.js +87 -0
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +98 -44
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +781 -37
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +30 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -1,36 +1,50 @@
|
|
|
1
|
+
import { isControllerManagedInvocation, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { stageSuccessor } from "./vnext-contracts.js";
|
|
1
3
|
import crypto from "node:crypto";
|
|
2
4
|
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { AppError } from "../shared/errors.js";
|
|
5
8
|
import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths.js";
|
|
6
9
|
import { previewNextEntityId } from "./ids.js";
|
|
7
10
|
import { registerProtocol } from "./protocols.js";
|
|
8
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
12
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion, withStageSettlement, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
13
|
+
import { resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
|
|
10
14
|
import { validateSchema } from "./schema-validation.js";
|
|
11
15
|
import { assertStageStartHookEvent, hookSessionIdentity } from "./hooks.js";
|
|
12
16
|
import { bindStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
13
17
|
import { runTargetedMemoryBankLint } from "./stage-lifecycle.js";
|
|
14
|
-
import { bootstrapManagedWorktree, createManagedWorktree, recordManagedWorktree } from "./worktrees.js";
|
|
18
|
+
import { bootstrapManagedWorktree, createManagedWorktree, prepareManagedWorktree, prepareManagedWorktreeBootstrapSources, recordManagedWorktree } from "./worktrees.js";
|
|
15
19
|
import { loadVnextWorkspacePolicy, requireVnextWorkspaceRoute, vnextRunBranch } from "./vnext-workspace-policy.js";
|
|
16
20
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
17
21
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
18
|
-
import {
|
|
19
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
22
|
+
import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
|
|
23
|
+
import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext, renderRunOperationalContext } from "./stage-context.js";
|
|
24
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
25
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
26
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
20
27
|
const stage = "protocolize";
|
|
21
|
-
function stageSessionMode(context, run) {
|
|
28
|
+
function stageSessionMode(context, run, projectRoot, targetWorkspaceRoot) {
|
|
22
29
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
23
30
|
const value = row ? JSON.parse(row.index_json) : null;
|
|
24
|
-
const
|
|
31
|
+
const profile = value?.execution_profile;
|
|
32
|
+
const mode = profile?.settings.stage_session_mode;
|
|
25
33
|
if (mode !== "same_session" && mode !== "new_session")
|
|
26
34
|
throw new AppError("execution_profile_invalid", "RUN is missing its frozen stage session mode", 1, { run_id: run.id });
|
|
27
|
-
|
|
35
|
+
if (profile?.settings.execution) {
|
|
36
|
+
if (!profile.agent_profiles)
|
|
37
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
38
|
+
return resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot, workspaceRoot: targetWorkspaceRoot, fromStage: "protocolize", stage: "plan", stageSessionMode: mode, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
39
|
+
}
|
|
40
|
+
return stageExecutionCwd({ stage: "protocolize", projectRoot, workspaceRoot: targetWorkspaceRoot })
|
|
41
|
+
=== stageExecutionCwd({ stage: "plan", projectRoot, workspaceRoot: targetWorkspaceRoot }) ? mode : "new_session";
|
|
28
42
|
}
|
|
29
43
|
export function isVnextProtocolizeRun(context, input) {
|
|
30
44
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
31
45
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
32
46
|
}
|
|
33
|
-
export function
|
|
47
|
+
export function prepareVnextProtocolizeArtifacts(context, input) {
|
|
34
48
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
35
49
|
const project = requireProjectByRoot(context, projectRoot);
|
|
36
50
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -42,45 +56,57 @@ export function prepareVnextProtocolize(context, input) {
|
|
|
42
56
|
const specifyPath = path.join(runHome, "01-specify", "specify.json");
|
|
43
57
|
const specifyMarkdownPath = path.join(runHome, "01-specify", "specify.md");
|
|
44
58
|
const obligations = acceptedSpecifyObligations(context, projectRoot, run.id, specifyPath);
|
|
45
|
-
fs.mkdirSync(stageRoot, { recursive: true });
|
|
46
59
|
const resultPath = path.join(stageRoot, "protocolize-result.json");
|
|
47
60
|
const promptPath = path.join(stageRoot, "prompt.md");
|
|
48
|
-
const
|
|
61
|
+
const templatePath = path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "protocolize.md");
|
|
62
|
+
if (!fs.existsSync(templatePath))
|
|
63
|
+
throw new AppError("runtime_artifact_missing", "vNext PROTOCOLIZE prompt template is missing", 1, { path: templatePath });
|
|
64
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
65
|
+
const projectCatalog = catalog(projectRoot);
|
|
66
|
+
if (!["same_session", "new_session"].includes(input.handoff?.stage_handoff?.effective))
|
|
67
|
+
throw new AppError("runtime_context_invalid", "PROTOCOLIZE requires a frozen handoff policy", 1);
|
|
68
|
+
for (const file of [promptPath, path.join(stageRoot, "work-context.json"), ...(!fs.existsSync(resultPath) ? [resultPath] : [])])
|
|
69
|
+
prepareOutputFile(file);
|
|
70
|
+
const workspace = prepareWorkspaceForProtocolize(context, projectRoot, project.id, run, stageRoot);
|
|
71
|
+
return { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog, workspace };
|
|
72
|
+
}
|
|
73
|
+
/** Materialize the next-stage packet; this is execution, not read-only CLI preparation. */
|
|
74
|
+
export async function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
|
|
75
|
+
const { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog } = prepared;
|
|
76
|
+
fs.mkdirSync(stageRoot, { recursive: true });
|
|
77
|
+
const policy = await provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
|
|
49
78
|
const routedRun = requireRun(context, projectRoot, run.id);
|
|
50
|
-
writeJson(path.join(stageRoot, "work-context.json"), { schema_id: "dd-flow/vnext-protocolize-context@2", system: { run_id: run.id, work_id: work.work_id, stage, handoff: input.handoff }, input: { accepted_specify_json: specifyPath, accepted_specify_markdown: specifyMarkdownPath }, catalog:
|
|
79
|
+
writeJson(path.join(stageRoot, "work-context.json"), { schema_id: "dd-flow/vnext-protocolize-context@2", system: { run_id: run.id, work_id: work.work_id, stage, handoff: input.handoff }, input: { accepted_specify_json: specifyPath, accepted_specify_markdown: specifyMarkdownPath }, catalog: projectCatalog, workspace: { project_root: projectRoot, workspace_root: routedRun.workspace_root, stage_root: stageRoot, git_policy: policy } });
|
|
51
80
|
if (!fs.existsSync(resultPath))
|
|
52
81
|
writeJson(resultPath, resultTemplate(obligations));
|
|
53
|
-
const templatePath = path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "protocolize.md");
|
|
54
|
-
if (!fs.existsSync(templatePath))
|
|
55
|
-
throw new AppError("not_found", "vNext PROTOCOLIZE prompt template is missing", 1, { path: templatePath });
|
|
56
82
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId: work.work_id, projectRoot });
|
|
57
|
-
const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog: projectCatalog, policy, template, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), finishCommand: managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --result-file ${JSON.stringify(resultPath)} --json`) });
|
|
84
|
+
const workspaceContract = ["<workspace_contract>", `- stage artifact directory: ${stageRoot}`, `- provisioned project workspace for later stages: ${policy.worktree_path ?? projectRoot}`, `- this PROTOCOLIZE Session remains in its launch directory: ${projectRoot}`, `Write only the named result file in the stage artifact directory; do not write project files, another RUN, another native home, branches, worktrees or Git commits. Do not change the Session directory to the newly provisioned worktree.`, "The CLI performs all protocol document materialization after the result is accepted.", "</workspace_contract>"].join("\n");
|
|
85
|
+
const profile = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE id = ? AND project_id = ?", [run.id, project.id]).index_json).execution_profile;
|
|
86
|
+
const operational = profile?.operational_contract ? renderRunOperationalContext(requireRunHome(run), profile.operational_contract.sha256, profile.operational_contract.path) : "";
|
|
87
|
+
const promptWithWorkspaceContract = `${operational}\n\n${prompt.replace("</work_context>", `</work_context>\n\n${workspaceContract}`)}`;
|
|
88
|
+
fs.writeFileSync(promptPath, promptWithWorkspaceContract);
|
|
89
|
+
// Packet publication does not itself bind a Session or open the Stage.
|
|
62
90
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
|
|
63
|
-
|
|
91
|
+
// The controller has already selected and bound this Stage's Session. The
|
|
92
|
+
// frozen handoff remains evidence, not authority to open another Session.
|
|
93
|
+
const currentSession = input.handoff.stage_handoff.effective === "same_session" || isControllerManagedInvocation(context, projectRoot, run.id);
|
|
94
|
+
const directive = currentSession ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: promptWithWorkspaceContract } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`) };
|
|
64
95
|
return { directive, prompt_path: promptPath };
|
|
65
96
|
}
|
|
66
|
-
export function
|
|
97
|
+
export function prepareVnextProtocolizeStart(context, input) {
|
|
67
98
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
68
99
|
const project = requireProjectByRoot(context, projectRoot);
|
|
69
100
|
const run = requireRun(context, projectRoot, input.runId);
|
|
70
|
-
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
71
101
|
const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [project.id, run.id]);
|
|
72
102
|
if (!work || work.status !== "running")
|
|
73
103
|
throw new AppError("invalid_work_state", "PROTOCOLIZE is not ready for an Agent WorkSession", 1, { run_id: run.id });
|
|
74
104
|
const protocolizeRoot = path.join(requireRunHome(run), "02-protocolize");
|
|
75
105
|
const contextPath = path.join(protocolizeRoot, "work-context.json");
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const handoff = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
81
|
-
const frozenContext = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
82
|
-
const previous = context.db.get("SELECT ws.session_id, s.stop_reason FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, work.work_id]);
|
|
83
|
-
const activeWorkSession = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
106
|
+
const frozenContext = fs.existsSync(contextPath) ? readProtocolizeContext(contextPath) : null;
|
|
107
|
+
const handoff = frozenContext?.system?.handoff ?? specifyHandoff(path.join(requireRunHome(run), "01-specify", "work-context.json"), work.work_id);
|
|
108
|
+
const packetInput = { projectRoot, runId: run.id, workId: work.work_id, handoff };
|
|
109
|
+
const packet = frozenContext ? null : prepareVnextProtocolizeArtifacts(context, packetInput);
|
|
84
110
|
const stageAttached = (() => {
|
|
85
111
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
86
112
|
try {
|
|
@@ -88,12 +114,35 @@ export function startVnextProtocolize(context, input) {
|
|
|
88
114
|
return index.stage_runs?.some((item) => item.stage === stage) ?? false;
|
|
89
115
|
}
|
|
90
116
|
catch {
|
|
91
|
-
|
|
117
|
+
throw new AppError("runtime_artifact_invalid", "RUN stage index is malformed", 1);
|
|
92
118
|
}
|
|
93
119
|
})();
|
|
94
|
-
|
|
120
|
+
if (!stageAttached)
|
|
121
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: "02-protocolize", status: "running", dataSchemaId: "dd-flow/vnext-protocolize-result@3" });
|
|
122
|
+
const mode = handoff.stage_handoff.effective;
|
|
95
123
|
const promptPath = path.join(protocolizeRoot, "prompt.md");
|
|
96
|
-
|
|
124
|
+
if (frozenContext)
|
|
125
|
+
fs.readFileSync(promptPath, "utf8");
|
|
126
|
+
if (input.externalContext) {
|
|
127
|
+
prepareExternalStageContext({ stageRoot: protocolizeRoot, loaded: input.externalContext });
|
|
128
|
+
prepareOutputFile(promptPath);
|
|
129
|
+
}
|
|
130
|
+
return { projectRoot, project, run, work, protocolizeRoot, contextPath, frozenContext, packetInput, packet, stageAttached, mode, promptPath };
|
|
131
|
+
}
|
|
132
|
+
function readProtocolizeContext(file) {
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
135
|
+
if (["same_session", "new_session"].includes(value?.system?.handoff?.stage_handoff?.effective))
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
catch { /* retained context is not a caller argument */ }
|
|
139
|
+
throw new AppError("runtime_context_invalid", "PROTOCOLIZE context is missing a valid frozen handoff", 1, { file });
|
|
140
|
+
}
|
|
141
|
+
export async function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
|
|
142
|
+
const { projectRoot, project, run, work, protocolizeRoot, contextPath, packetInput, packet, stageAttached, mode, promptPath } = prepared;
|
|
143
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
144
|
+
const previous = context.db.get("SELECT ws.session_id, s.stop_reason FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, work.work_id]);
|
|
145
|
+
const activeWorkSession = context.db.get("SELECT * FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
97
146
|
const sessionId = hookSessionIdentity(context, project.id, input.hookEventId).sessionId;
|
|
98
147
|
// A stage-entry eval snapshot deliberately stops the canonical binding. Its
|
|
99
148
|
// Subject starts from a fork of the captured starter Session, so the stale
|
|
@@ -103,6 +152,10 @@ export function startVnextProtocolize(context, input) {
|
|
|
103
152
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to continue in the SPECIFY session", 1);
|
|
104
153
|
if (mode === "new_session" && previous?.session_id === sessionId)
|
|
105
154
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to start in a fresh session", 1);
|
|
155
|
+
if (packet)
|
|
156
|
+
await prepareVnextProtocolize(context, packetInput, packet);
|
|
157
|
+
const frozenContext = prepared.frozenContext ?? readProtocolizeContext(contextPath);
|
|
158
|
+
const externalContext = applyExternalStageContext({ context, stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
106
159
|
const binding = bindStageCoordinatorWork(context, { workId: work.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(requireRunHome(run), "02-protocolize", "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
107
160
|
// Same-session handoff deliberately reuses the root Work/Session that
|
|
108
161
|
// SPECIFY opened. That is not evidence that PROTOCOLIZE is already attached.
|
|
@@ -110,11 +163,11 @@ export function startVnextProtocolize(context, input) {
|
|
|
110
163
|
if (!stageAttached)
|
|
111
164
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: "02-protocolize", status: "running", dataSchemaId: "dd-flow/vnext-protocolize-result@3" });
|
|
112
165
|
if (mode === "same_session" && activeWorkSession?.session_id === sessionId) {
|
|
113
|
-
return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: activeWorkSession.id,
|
|
166
|
+
return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: activeWorkSession.id, git_policy: frozenContext.workspace?.git_policy ?? null, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}) };
|
|
114
167
|
}
|
|
115
168
|
const workSessionId = String(binding.work_session_id);
|
|
116
169
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", work_id: work.work_id, id: workSessionId, stage, session_id: sessionId });
|
|
117
|
-
return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: workSessionId,
|
|
170
|
+
return { ok: true, outcome: "work_session_required", run_id: run.id, work_id: work.work_id, id: workSessionId, git_policy: frozenContext.workspace?.git_policy ?? null, ...currentStagePrompt(context, promptPath), ...(externalContext ? { external_context: externalContext } : {}) };
|
|
118
171
|
}
|
|
119
172
|
function specifyHandoff(file, workId) {
|
|
120
173
|
try {
|
|
@@ -125,7 +178,7 @@ function specifyHandoff(file, workId) {
|
|
|
125
178
|
catch { /* reported below */ }
|
|
126
179
|
throw new AppError("runtime_context_invalid", "SPECIFY Work is missing its immutable handoff policy", 1, { work_id: workId });
|
|
127
180
|
}
|
|
128
|
-
export function finishVnextProtocolize(context, input) {
|
|
181
|
+
export async function finishVnextProtocolize(context, input) {
|
|
129
182
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
130
183
|
const project = requireProjectByRoot(context, projectRoot);
|
|
131
184
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -133,26 +186,34 @@ export function finishVnextProtocolize(context, input) {
|
|
|
133
186
|
if (!work)
|
|
134
187
|
throw new AppError("not_found", "RUN has no PROTOCOLIZE Work", 1, { run_id: run.id });
|
|
135
188
|
const root = path.join(requireRunHome(run), "02-protocolize");
|
|
136
|
-
const resultFile =
|
|
137
|
-
|
|
138
|
-
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
139
|
-
const result = readResult(resultFile);
|
|
140
|
-
if (result.outcome !== "protocolized")
|
|
141
|
-
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
189
|
+
const resultFile = prepareProtocolizeResultPath(requireRunHome(run), input.resultFile);
|
|
190
|
+
const result = input.result ?? readResult(resultFile, { projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
191
|
+
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, data: result, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
142
192
|
const acceptedObligations = acceptedSpecifyObligations(context, projectRoot, run.id, path.join(requireRunHome(run), "01-specify", "specify.json"));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
let obligations;
|
|
194
|
+
try {
|
|
195
|
+
if (result.outcome !== "protocolized")
|
|
196
|
+
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
197
|
+
obligations = effectiveObligations(result, acceptedObligations);
|
|
198
|
+
validateDelivery(projectRoot, result, obligations);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (!(error instanceof AppError) || error.code !== "validation" || error.exitCode !== 2)
|
|
202
|
+
throw error;
|
|
203
|
+
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
204
|
+
}
|
|
147
205
|
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
148
206
|
const receipt = readWorkspaceRouteReceipt(root);
|
|
149
207
|
const workspaceRoot = run.workspace_root;
|
|
150
|
-
const
|
|
208
|
+
const intent = await prepareProtocolizeIntent(context, run, resultFile, result, obligations);
|
|
209
|
+
publishProtocolizeIntent(context, project.id, intent);
|
|
210
|
+
const protocolIds = intent.protocol_ids;
|
|
211
|
+
const changedFiles = intent.changed_files;
|
|
151
212
|
for (const protocolId of protocolIds)
|
|
152
213
|
registerProtocol(context, { handshakeId: protocolId, projectRoot, workspacePath: workspaceRoot });
|
|
153
214
|
if (receipt.created && receipt.bootstrap) {
|
|
154
215
|
for (const protocolId of protocolIds)
|
|
155
|
-
|
|
216
|
+
ensureManagedWorktreeRecorded(context, { protocolId, projectRoot, branch: requiredPolicyValue(resolvedPolicy.feature_branch, "feature branch"), base: requiredPolicyValue(resolvedPolicy.base_ref, "base ref"), created: receipt.created, bootstrap: receipt.bootstrap });
|
|
156
217
|
}
|
|
157
218
|
const now = context.now();
|
|
158
219
|
const workSession = completeWorkSession(context, work.work_id, resultFile, now);
|
|
@@ -161,16 +222,152 @@ export function finishVnextProtocolize(context, input) {
|
|
|
161
222
|
const reportJson = path.join(root, "stage-report.json");
|
|
162
223
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
163
224
|
const reportHtml = path.join(root, "stage-report.html");
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
225
|
+
const currentIndex = JSON.parse(context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]).index_json);
|
|
226
|
+
const stageDone = currentIndex.stage_runs.some(item => item.stage === stage && item.status === "done");
|
|
227
|
+
if (stageDone) {
|
|
228
|
+
publishStageReport(context, project.id, run.id, stage, root);
|
|
229
|
+
if (!fs.existsSync(reportJson))
|
|
230
|
+
throw new AppError("protocolize_report_missing", "Completed PROTOCOLIZE has no accepted report to replay", 1, { run_id: run.id, report_path: reportJson });
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const preparedReport = prepareStageReport(report);
|
|
234
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
235
|
+
const completion = { projectRoot, runId: run.id, stage, status: "done", data: "protocolize-result.json", dataSchemaId: "dd-flow/vnext-protocolize-result@3", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: crypto.createHash("sha256").update(fs.readFileSync(resultFile)).digest("hex") };
|
|
236
|
+
const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
|
|
237
|
+
withStageSettlement(context, () => {
|
|
238
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
239
|
+
completeFlowRunStage(context, completion, preparedCompletion);
|
|
240
|
+
});
|
|
241
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "protocol_documents_materialized", work_id: work.work_id, protocol_ids: protocolIds, stage });
|
|
242
|
+
}
|
|
243
|
+
if (currentIndex.verdict !== "protocolized")
|
|
244
|
+
advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
169
245
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
170
|
-
const
|
|
171
|
-
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan",
|
|
246
|
+
const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode } });
|
|
247
|
+
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", ...(next.command ? { next_command: next.command } : {}), next, artifacts: { result: resultFile, report_json: reportJson, report_markdown: reportMarkdown, report_html: reportHtml, protocols: protocolIds.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "summary.md")) } };
|
|
172
248
|
}
|
|
173
|
-
function
|
|
249
|
+
async function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
|
|
250
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
251
|
+
const index = row ? JSON.parse(row.index_json) : null;
|
|
252
|
+
const attempt = index?.stage_runs?.find(item => item.stage === stage)?.attempt;
|
|
253
|
+
if (!attempt)
|
|
254
|
+
throw new AppError("protocolize_owner_missing", "PROTOCOLIZE has no owning Stage attempt", 1, { run_id: run.id });
|
|
255
|
+
const resultSha = sha256(fs.readFileSync(resultFile));
|
|
256
|
+
const retained = readProtocolizeIntent(context, run.project_id, run.id, attempt);
|
|
257
|
+
if (retained) {
|
|
258
|
+
if (retained.workspace_root !== run.workspace_root || retained.result_sha256 !== resultSha)
|
|
259
|
+
throw new AppError("protocolize_intent_conflict", "Accepted PROTOCOLIZE publication belongs to different result or workspace bytes", 1, { run_id: run.id, attempt });
|
|
260
|
+
return retained;
|
|
261
|
+
}
|
|
262
|
+
const projectRoot = run.workspace_root;
|
|
263
|
+
const protocolIds = allocateProtocolIds(context, run.project_id, projectRoot, result.delivery.members.map(member => member.slug));
|
|
264
|
+
const psetId = result.delivery.kind === "protocol_set" ? allocatePsetId(context, run.project_id, projectRoot, result.delivery.slug) : null;
|
|
265
|
+
const draft = fs.mkdtempSync(path.join(os.tmpdir(), "dd-flow-protocolize-"));
|
|
266
|
+
try {
|
|
267
|
+
// A draft must never follow a workspace symlink back into the live tree.
|
|
268
|
+
fs.cpSync(path.join(projectRoot, ".memory-bank"), path.join(draft, ".memory-bank"), { recursive: true, filter: source => !fs.lstatSync(source).isSymbolicLink() });
|
|
269
|
+
const feature = result.feature.epic_path && path.isAbsolute(result.feature.epic_path)
|
|
270
|
+
? { ...result.feature, epic_path: path.join(draft, path.relative(projectRoot, result.feature.epic_path)) }
|
|
271
|
+
: result.feature;
|
|
272
|
+
const draftResult = { ...result, feature };
|
|
273
|
+
const materialized = await materialize(draft, run.id, requireRunHome(run), draftResult, obligations, protocolIds, psetId);
|
|
274
|
+
const changedFiles = materialized.files;
|
|
275
|
+
const files = changedFiles.map(relative => {
|
|
276
|
+
const target = path.join(draft, relative);
|
|
277
|
+
const bytes = fs.readFileSync(target, "utf8");
|
|
278
|
+
return { relative, before_sha256: materialized.before[path.relative(draft, target)] ?? null, after_sha256: sha256(bytes), bytes };
|
|
279
|
+
});
|
|
280
|
+
const candidate = { run_id: run.id, attempt, workspace_root: projectRoot, result_sha256: resultSha, protocol_ids: protocolIds, pset_id: psetId, changed_files: changedFiles, files };
|
|
281
|
+
return context.db.writeTransaction(() => {
|
|
282
|
+
const raced = readProtocolizeIntent(context, run.project_id, run.id, attempt);
|
|
283
|
+
if (raced) {
|
|
284
|
+
if (raced.workspace_root !== projectRoot || raced.result_sha256 !== resultSha)
|
|
285
|
+
throw new AppError("protocolize_intent_conflict", "A competing PROTOCOLIZE publication was accepted", 1, { run_id: run.id, attempt });
|
|
286
|
+
return raced;
|
|
287
|
+
}
|
|
288
|
+
const reserved = new Set(context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.protocolize_intent'", [run.project_id])
|
|
289
|
+
.flatMap(row => { const prior = JSON.parse(row.payload_json); return [...prior.protocol_ids, ...(prior.pset_id ? [prior.pset_id] : [])]; }));
|
|
290
|
+
if ([...protocolIds, ...(psetId ? [psetId] : [])].some(id => reserved.has(id)))
|
|
291
|
+
throw new AppError("protocolize_id_reserved", "Another PROTOCOLIZE attempt reserved the selected document ID; retry to allocate a new one", 1, { run_id: run.id });
|
|
292
|
+
context.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.protocolize_intent', 0, ?, ?)", [run.project_id, JSON.stringify(candidate), context.now()]);
|
|
293
|
+
return candidate;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
fs.rmSync(draft, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function readProtocolizeIntent(context, projectId, runId, attempt) {
|
|
301
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.protocolize_intent' ORDER BY id DESC", [projectId]);
|
|
302
|
+
return rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.attempt === attempt) ?? null;
|
|
303
|
+
}
|
|
304
|
+
function publishProtocolizeIntent(context, projectId, intent) {
|
|
305
|
+
const memory = path.join(intent.workspace_root, ".memory-bank");
|
|
306
|
+
withProjectionLock(path.join(context.ddFlowHome, "locks", `protocolize-${projectId}.publish-lock`), () => {
|
|
307
|
+
for (const file of intent.files) {
|
|
308
|
+
if (!file.relative.startsWith(".memory-bank/") || sha256(file.bytes) !== file.after_sha256)
|
|
309
|
+
throw new AppError("protocolize_intent_invalid", "Accepted PROTOCOLIZE file is invalid", 1, { relative: file.relative });
|
|
310
|
+
const target = path.resolve(intent.workspace_root, file.relative);
|
|
311
|
+
inside(memory, target);
|
|
312
|
+
assertNoSymlinkPath(memory, target);
|
|
313
|
+
const current = fs.existsSync(target) ? sha256(fs.readFileSync(target)) : null;
|
|
314
|
+
if (current !== file.before_sha256 && current !== file.after_sha256)
|
|
315
|
+
throw new AppError("protocolize_publication_conflict", "PROTOCOLIZE target changed since publication was accepted", 1, { target });
|
|
316
|
+
}
|
|
317
|
+
for (const file of intent.files) {
|
|
318
|
+
const target = path.resolve(intent.workspace_root, file.relative);
|
|
319
|
+
if (fs.existsSync(target) && sha256(fs.readFileSync(target)) === file.after_sha256)
|
|
320
|
+
continue;
|
|
321
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
322
|
+
const temporary = `${target}.${crypto.randomUUID()}.tmp`;
|
|
323
|
+
try {
|
|
324
|
+
fs.writeFileSync(temporary, file.bytes, { flag: "wx" });
|
|
325
|
+
if (file.before_sha256 === null) {
|
|
326
|
+
try {
|
|
327
|
+
fs.linkSync(temporary, target);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error.code !== "EEXIST" || sha256(fs.readFileSync(target)) !== file.after_sha256)
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
if (sha256(fs.readFileSync(target)) !== file.before_sha256)
|
|
336
|
+
throw new AppError("protocolize_publication_conflict", "PROTOCOLIZE target changed during publication", 1, { target });
|
|
337
|
+
fs.renameSync(temporary, target);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
fs.rmSync(temporary, { force: true });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function assertNoSymlinkPath(memory, target) {
|
|
347
|
+
for (let cursor = memory;; cursor = path.join(cursor, path.relative(cursor, target).split(path.sep)[0])) {
|
|
348
|
+
try {
|
|
349
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
350
|
+
throw new AppError("protocolize_publication_conflict", "PROTOCOLIZE publication cannot follow a workspace symlink", 1, { target, symlink: cursor });
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (error.code !== "ENOENT")
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
if (cursor === target)
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function ensureManagedWorktreeRecorded(context, input) {
|
|
361
|
+
const existing = context.db.get("SELECT wr.feature_branch, wr.base_ref, wr.worktree_path FROM worktree_records wr JOIN protocols p ON p.project_id = wr.project_id AND p.id = wr.protocol_id WHERE p.project_root = ? AND p.id = ? AND wr.status = 'active' ORDER BY wr.created_at DESC LIMIT 1", [input.projectRoot, input.protocolId]);
|
|
362
|
+
if (existing) {
|
|
363
|
+
if (existing.feature_branch !== input.branch || existing.base_ref !== input.base || existing.worktree_path !== input.created.worktreePath)
|
|
364
|
+
throw new AppError("worktree_already_exists", "Existing managed worktree record conflicts with the accepted PROTOCOLIZE route", 1, { protocol_id: input.protocolId });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
recordManagedWorktree(context, input);
|
|
368
|
+
}
|
|
369
|
+
function sha256(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
|
|
370
|
+
async function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
174
371
|
const psetPath = psetId ? path.join(projectRoot, ".memory-bank", "protocol", "_set", `${psetId}.md`) : null;
|
|
175
372
|
const created = [];
|
|
176
373
|
const changed = new Set();
|
|
@@ -208,8 +405,8 @@ function materialize(projectRoot, runId, runHome, result, obligations, ids, pset
|
|
|
208
405
|
const absolute = path.resolve(projectRoot, file);
|
|
209
406
|
return created.some((directory) => absolute.startsWith(`${directory}${path.sep}`)) || absolute === psetPath;
|
|
210
407
|
});
|
|
211
|
-
runTargetedMemoryBankLint(projectRoot, materializedFiles);
|
|
212
|
-
return files;
|
|
408
|
+
await runTargetedMemoryBankLint(projectRoot, materializedFiles);
|
|
409
|
+
return { files, before: Object.fromEntries([...originals].map(([file, bytes]) => [path.relative(projectRoot, file), bytes === null ? null : sha256(bytes)])) };
|
|
213
410
|
}
|
|
214
411
|
catch (error) {
|
|
215
412
|
for (const [file, content] of originals) {
|
|
@@ -309,21 +506,27 @@ function appendIndexEntry(indexPath, childPath, label) {
|
|
|
309
506
|
}
|
|
310
507
|
function epicRootFor(projectRoot, value) { const resolved = path.resolve(projectRoot, value); const relative = path.relative(projectRoot, resolved); if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
311
508
|
throw new AppError("path_escape", "epic_path must stay inside the project", 2, { epic_path: value }); return path.basename(resolved) === "index.md" ? path.dirname(resolved) : resolved; }
|
|
312
|
-
function readResult(file) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return value;
|
|
509
|
+
function readResult(file, context) {
|
|
510
|
+
return prepareJsonSchema(file, "PROTOCOLIZE result", {
|
|
511
|
+
schemaName: "vnext-protocolize-result", projectRoot: context.projectRoot, ddFlowHome: context.ddFlowHome, runId: context.runId, runRoot: context.runRoot
|
|
512
|
+
}).value;
|
|
317
513
|
}
|
|
318
514
|
function acceptedSpecifyObligations(context, projectRoot, runId, specifyPath) {
|
|
319
515
|
if (!fs.existsSync(specifyPath))
|
|
320
|
-
throw new AppError("
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
516
|
+
throw new AppError("runtime_artifact_missing", "PROTOCOLIZE requires accepted specify.json", 1, { path: specifyPath });
|
|
517
|
+
try {
|
|
518
|
+
const specify = readVnextSpecifyResult(specifyPath);
|
|
519
|
+
validateSchema({ schemaName: "vnext-specify", file: specifyPath, data: specify, projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: requireRunHome(requireRun(context, projectRoot, runId)) });
|
|
520
|
+
return [
|
|
521
|
+
...specify.requirements.map((obligation) => ({ ...obligation, kind: "requirement" })),
|
|
522
|
+
...specify.acceptance_criteria.map((obligation) => ({ ...obligation, kind: "acceptance" }))
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (!(error instanceof AppError) || !["validation", "schema_validation", "invalid_json"].includes(error.code))
|
|
527
|
+
throw error;
|
|
528
|
+
throw new AppError("runtime_artifact_invalid", "Accepted SPECIFY result is invalid", 1, { path: specifyPath, cause_code: error.code });
|
|
529
|
+
}
|
|
327
530
|
}
|
|
328
531
|
/** SPECIFY is historical input; only a HITL-backed protocol amendment may alter its effective wording. */
|
|
329
532
|
function effectiveObligations(result, accepted) {
|
|
@@ -435,18 +638,19 @@ function resultTemplate(obligations = []) {
|
|
|
435
638
|
}
|
|
436
639
|
function renderPrompt(input) {
|
|
437
640
|
const obligationList = input.obligations.map((obligation) => `- ${obligation.id}: ${obligation.statement}`).join("\n");
|
|
438
|
-
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(), null, 2)}\n\`\`\``, "
|
|
641
|
+
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(input.obligations), null, 2)}\n\`\`\``, "obligation_id values and their one-per-obligation entries are prefilled by the CLI. Preserve those IDs exactly; change only member_keys when semantic ownership requires it. The CLI rejects unknown, duplicate or missing obligations, and every member must own at least one AC-*.", "If PROTOCOLIZE itself exposes a material user decision with no reasonable default, do not finish and do not return to SPECIFY. Write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message and stop. Resume this same PROTOCOLIZE Work using the exact command returned by pause.", `When the answer is incorporated and the result is protocolized, finish:\n\n${input.finishCommand}`, stageLifecycleInstruction, "</output_contract>", ""].join("\n");
|
|
439
642
|
}
|
|
440
|
-
function
|
|
643
|
+
function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, stageRoot) {
|
|
441
644
|
const receiptPath = path.join(stageRoot, "workspace-route.json");
|
|
442
645
|
if (fs.existsSync(receiptPath))
|
|
443
|
-
return readWorkspaceRouteReceipt(stageRoot).policy;
|
|
646
|
+
return { kind: "retained", policy: readWorkspaceRouteReceipt(stageRoot).policy };
|
|
647
|
+
prepareOutputFile(receiptPath);
|
|
444
648
|
const config = loadVnextWorkspacePolicy(projectRoot);
|
|
445
649
|
const stableFacts = gitFacts(projectRoot);
|
|
446
650
|
if (config.workspace.route === "integration_branch_direct") {
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
return policy;
|
|
651
|
+
const sessionMode = stageSessionMode(context, run, projectRoot, projectRoot);
|
|
652
|
+
const policy = { route: "integration_branch_direct", integration_branch: config.workspace.integration_branch, base_ref: stableFacts.head, feature_branch: null, worktree_path: null, reason: "Project workspace configuration explicitly selects the integration checkout.", provision_stage: "protocolize_start", stage_session_mode: sessionMode, provisioned: true, bootstrap_status: "not_applicable" };
|
|
653
|
+
return { kind: "direct", receiptPath, policy };
|
|
450
654
|
}
|
|
451
655
|
if (stableFacts.status === "unavailable" || !stableFacts.head || stableFacts.branch !== config.workspace.integration_branch) {
|
|
452
656
|
throw new AppError("workspace_route_invalid", "Configured feature worktree requires a clean, known integration checkout", 1, { expected_integration_branch: config.workspace.integration_branch, actual: stableFacts, project_root: projectRoot });
|
|
@@ -454,10 +658,32 @@ function provisionWorkspaceForProtocolize(context, projectRoot, projectId, run,
|
|
|
454
658
|
const branch = vnextRunBranch(config, run);
|
|
455
659
|
const holder = path.basename(projectRoot).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
456
660
|
const worktreePath = projectFeatureWorktreePath(context.ddFlowHome, projectId, `RUN-${run.short_id}`, holder);
|
|
457
|
-
const
|
|
458
|
-
|
|
661
|
+
const sessionMode = stageSessionMode(context, run, projectRoot, worktreePath);
|
|
662
|
+
let create;
|
|
663
|
+
try {
|
|
664
|
+
create = prepareManagedWorktree({ projectRoot, branch, base: stableFacts.head, path: worktreePath });
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
668
|
+
throw new AppError("workspace_route_invalid", "The frozen RUN workspace route cannot be provisioned", 1, { cause_code: error.code, run_id: run.id });
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
prepareManagedWorktreeBootstrapSources(projectRoot);
|
|
672
|
+
return { kind: "feature", receiptPath, config, stableFacts, sessionMode, branch, create };
|
|
673
|
+
}
|
|
674
|
+
async function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
|
|
675
|
+
if (prepared.kind === "retained")
|
|
676
|
+
return prepared.policy;
|
|
677
|
+
const { receiptPath } = prepared;
|
|
678
|
+
if (prepared.kind === "direct") {
|
|
679
|
+
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy: prepared.policy, created: null, bootstrap: null });
|
|
680
|
+
return prepared.policy;
|
|
681
|
+
}
|
|
682
|
+
const { config, stableFacts, sessionMode, branch, create } = prepared;
|
|
683
|
+
const created = await createManagedWorktree(context, create, create);
|
|
684
|
+
const bootstrap = await bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
|
|
459
685
|
rebindFlowRunWorkspace(context, { projectRoot, runId: run.id, workspaceRoot: created.worktreePath, reason: "PROTOCOLIZE start provisioned the configured feature-worktree route" });
|
|
460
|
-
const policy = { route: "feature_worktree", integration_branch: config.workspace.integration_branch, base_ref: stableFacts.head, feature_branch: branch, worktree_path: created.worktreePath, reason: "Project workspace configuration requires a managed feature worktree before durable protocol documents exist.", provision_stage: "protocolize_start", stage_session_mode:
|
|
686
|
+
const policy = { route: "feature_worktree", integration_branch: config.workspace.integration_branch, base_ref: stableFacts.head, feature_branch: branch, worktree_path: created.worktreePath, reason: "Project workspace configuration requires a managed feature worktree before durable protocol documents exist.", provision_stage: "protocolize_start", stage_session_mode: sessionMode, provisioned: true, bootstrap_status: bootstrap.status };
|
|
461
687
|
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy, created, bootstrap });
|
|
462
688
|
return policy;
|
|
463
689
|
}
|
|
@@ -475,8 +701,27 @@ function readWorkspaceRouteReceipt(root) {
|
|
|
475
701
|
}
|
|
476
702
|
function requiredPolicyValue(value, label) { if (!value)
|
|
477
703
|
throw new AppError("runtime_missing", `PROTOCOLIZE Git policy is missing ${label}`, 1); return value; }
|
|
478
|
-
function
|
|
479
|
-
|
|
704
|
+
function allocateProtocolIds(context, projectId, projectRoot, names) {
|
|
705
|
+
if (!names.length)
|
|
706
|
+
return [];
|
|
707
|
+
const first = previewNextEntityId(context, { projectRoot, type: "protocol", slug: names[0] }).entity.id;
|
|
708
|
+
const used = protocolizeReservedNumbers(context, projectId, "protocol_ids", /^PRT-(\d+)-/);
|
|
709
|
+
const sequence = Math.max(Number(/^PRT-(\d+)-/.exec(first)?.[1] ?? 1) - 1, ...used) + 1;
|
|
710
|
+
return names.map((name, index) => `PRT-${String(sequence + index).padStart(3, "0")}-${slug(name)}`);
|
|
711
|
+
}
|
|
712
|
+
function allocatePsetId(context, projectId, projectRoot, value) {
|
|
713
|
+
const root = path.join(projectRoot, ".memory-bank", "protocol", "_set");
|
|
714
|
+
const used = fs.existsSync(root) ? fs.readdirSync(root).map((file) => /^PSET-(\d+)-/.exec(file)?.[1]).filter((item) => Boolean(item)).map(Number) : [];
|
|
715
|
+
return `PSET-${String(Math.max(0, ...used, ...protocolizeReservedNumbers(context, projectId, "pset_id", /^PSET-(\d+)-/)) + 1).padStart(3, "0")}-${slug(value)}`;
|
|
716
|
+
}
|
|
717
|
+
function protocolizeReservedNumbers(context, projectId, field, pattern) {
|
|
718
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.protocolize_intent'", [projectId]);
|
|
719
|
+
return rows.flatMap(row => {
|
|
720
|
+
const value = JSON.parse(row.payload_json);
|
|
721
|
+
const ids = field === "protocol_ids" ? value.protocol_ids : value.pset_id ? [value.pset_id] : [];
|
|
722
|
+
return ids.map(id => Number(pattern.exec(id)?.[1])).filter(Number.isFinite);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
480
725
|
function catalog(projectRoot) {
|
|
481
726
|
const memory = path.join(projectRoot, ".memory-bank");
|
|
482
727
|
const candidates = [
|
|
@@ -502,7 +747,15 @@ function requireRun(context, projectRoot, id) { const project = requireProjectBy
|
|
|
502
747
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: id }); return run; }
|
|
503
748
|
function requireRunHome(run) { if (!run.run_root)
|
|
504
749
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
505
|
-
function
|
|
750
|
+
export function prepareProtocolizeResultPath(runHome, file) {
|
|
751
|
+
const resolved = path.resolve(file);
|
|
752
|
+
inside(path.join(runHome, "02-protocolize"), resolved);
|
|
753
|
+
const canonical = path.join(runHome, "02-protocolize", "protocolize-result.json");
|
|
754
|
+
if (resolved !== canonical)
|
|
755
|
+
throw new AppError("protocolize_result_path_invalid", "PROTOCOLIZE result must be the canonical Stage data file named by its completion receipt", 2, { result_file: resolved, expected: canonical });
|
|
756
|
+
return resolved;
|
|
757
|
+
}
|
|
758
|
+
function inside(root, file) { const relative = path.relative(root, file); if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
506
759
|
throw new AppError("path_escape", "Result must be inside the protocolize workspace", 2); }
|
|
507
760
|
function writeJson(file, value) { const tmp = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); }
|
|
508
761
|
function slug(value) { const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); if (!normalized)
|