@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100
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 +561 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +449 -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 +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +941 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +267 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +136 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +569 -16
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -6,31 +7,39 @@ import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths
|
|
|
6
7
|
import { previewNextEntityId } from "./ids.js";
|
|
7
8
|
import { registerProtocol } from "./protocols.js";
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
10
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, prepareFlowRunStageAttachment, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
11
|
+
import { resolveStageTransition } from "./execution-policy.js";
|
|
10
12
|
import { validateSchema } from "./schema-validation.js";
|
|
11
13
|
import { assertStageStartHookEvent, hookSessionIdentity } from "./hooks.js";
|
|
12
14
|
import { bindStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
13
15
|
import { runTargetedMemoryBankLint } from "./stage-lifecycle.js";
|
|
14
|
-
import { bootstrapManagedWorktree, createManagedWorktree, recordManagedWorktree } from "./worktrees.js";
|
|
16
|
+
import { bootstrapManagedWorktree, createManagedWorktree, prepareManagedWorktree, prepareManagedWorktreeBootstrapSources, recordManagedWorktree } from "./worktrees.js";
|
|
15
17
|
import { loadVnextWorkspacePolicy, requireVnextWorkspaceRoute, vnextRunBranch } from "./vnext-workspace-policy.js";
|
|
16
18
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
17
19
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
18
20
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
20
23
|
const stage = "protocolize";
|
|
21
24
|
function stageSessionMode(context, run) {
|
|
22
25
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
23
26
|
const value = row ? JSON.parse(row.index_json) : null;
|
|
24
|
-
const
|
|
27
|
+
const profile = value?.execution_profile;
|
|
28
|
+
const mode = profile?.settings.stage_session_mode;
|
|
25
29
|
if (mode !== "same_session" && mode !== "new_session")
|
|
26
30
|
throw new AppError("execution_profile_invalid", "RUN is missing its frozen stage session mode", 1, { run_id: run.id });
|
|
31
|
+
if (profile?.settings.execution) {
|
|
32
|
+
if (!profile.agent_profiles)
|
|
33
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
34
|
+
return resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, fromStage: "protocolize", stage: "plan", stageSessionMode: mode, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
35
|
+
}
|
|
27
36
|
return mode;
|
|
28
37
|
}
|
|
29
38
|
export function isVnextProtocolizeRun(context, input) {
|
|
30
39
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
31
40
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
32
41
|
}
|
|
33
|
-
export function
|
|
42
|
+
export function prepareVnextProtocolizeArtifacts(context, input) {
|
|
34
43
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
35
44
|
const project = requireProjectByRoot(context, projectRoot);
|
|
36
45
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -42,45 +51,50 @@ export function prepareVnextProtocolize(context, input) {
|
|
|
42
51
|
const specifyPath = path.join(runHome, "01-specify", "specify.json");
|
|
43
52
|
const specifyMarkdownPath = path.join(runHome, "01-specify", "specify.md");
|
|
44
53
|
const obligations = acceptedSpecifyObligations(context, projectRoot, run.id, specifyPath);
|
|
45
|
-
fs.mkdirSync(stageRoot, { recursive: true });
|
|
46
54
|
const resultPath = path.join(stageRoot, "protocolize-result.json");
|
|
47
55
|
const promptPath = path.join(stageRoot, "prompt.md");
|
|
48
|
-
const
|
|
56
|
+
const templatePath = path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "protocolize.md");
|
|
57
|
+
if (!fs.existsSync(templatePath))
|
|
58
|
+
throw new AppError("runtime_artifact_missing", "vNext PROTOCOLIZE prompt template is missing", 1, { path: templatePath });
|
|
59
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
60
|
+
const projectCatalog = catalog(projectRoot);
|
|
61
|
+
if (!["same_session", "new_session"].includes(input.handoff?.stage_handoff?.effective))
|
|
62
|
+
throw new AppError("runtime_context_invalid", "PROTOCOLIZE requires a frozen handoff policy", 1);
|
|
63
|
+
for (const file of [promptPath, path.join(stageRoot, "work-context.json"), ...(!fs.existsSync(resultPath) ? [resultPath] : [])])
|
|
64
|
+
prepareOutputFile(file);
|
|
65
|
+
const workspace = prepareWorkspaceForProtocolize(context, projectRoot, project.id, run, stageRoot);
|
|
66
|
+
return { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog, workspace };
|
|
67
|
+
}
|
|
68
|
+
/** Materialize the next-stage packet; this is execution, not read-only CLI preparation. */
|
|
69
|
+
export function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
|
|
70
|
+
const { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog } = prepared;
|
|
71
|
+
fs.mkdirSync(stageRoot, { recursive: true });
|
|
72
|
+
const policy = provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
|
|
49
73
|
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:
|
|
74
|
+
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
75
|
if (!fs.existsSync(resultPath))
|
|
52
76
|
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
77
|
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:
|
|
78
|
+
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`) });
|
|
58
79
|
fs.writeFileSync(promptPath, prompt);
|
|
59
|
-
//
|
|
60
|
-
// command atomically installs external context, binds the worker and opens
|
|
61
|
-
// the lifecycle Stage. Do not make this prompt an implicit running stage.
|
|
80
|
+
// Packet publication does not itself bind a Session or open the Stage.
|
|
62
81
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
|
|
63
|
-
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: prompt } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json` };
|
|
82
|
+
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: prompt } : { 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
83
|
return { directive, prompt_path: promptPath };
|
|
65
84
|
}
|
|
66
|
-
export function
|
|
85
|
+
export function prepareVnextProtocolizeStart(context, input) {
|
|
67
86
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
68
87
|
const project = requireProjectByRoot(context, projectRoot);
|
|
69
88
|
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
89
|
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
90
|
if (!work || work.status !== "running")
|
|
73
91
|
throw new AppError("invalid_work_state", "PROTOCOLIZE is not ready for an Agent WorkSession", 1, { run_id: run.id });
|
|
74
92
|
const protocolizeRoot = path.join(requireRunHome(run), "02-protocolize");
|
|
75
93
|
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]);
|
|
94
|
+
const frozenContext = fs.existsSync(contextPath) ? readProtocolizeContext(contextPath) : null;
|
|
95
|
+
const handoff = frozenContext?.system?.handoff ?? specifyHandoff(path.join(requireRunHome(run), "01-specify", "work-context.json"), work.work_id);
|
|
96
|
+
const packetInput = { projectRoot, runId: run.id, workId: work.work_id, handoff };
|
|
97
|
+
const packet = frozenContext ? null : prepareVnextProtocolizeArtifacts(context, packetInput);
|
|
84
98
|
const stageAttached = (() => {
|
|
85
99
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
86
100
|
try {
|
|
@@ -88,12 +102,35 @@ export function startVnextProtocolize(context, input) {
|
|
|
88
102
|
return index.stage_runs?.some((item) => item.stage === stage) ?? false;
|
|
89
103
|
}
|
|
90
104
|
catch {
|
|
91
|
-
|
|
105
|
+
throw new AppError("runtime_artifact_invalid", "RUN stage index is malformed", 1);
|
|
92
106
|
}
|
|
93
107
|
})();
|
|
94
|
-
|
|
108
|
+
if (!stageAttached)
|
|
109
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: "02-protocolize", status: "running", dataSchemaId: "dd-flow/vnext-protocolize-result@3" });
|
|
110
|
+
const mode = handoff.stage_handoff.effective;
|
|
95
111
|
const promptPath = path.join(protocolizeRoot, "prompt.md");
|
|
96
|
-
|
|
112
|
+
if (frozenContext)
|
|
113
|
+
fs.readFileSync(promptPath, "utf8");
|
|
114
|
+
if (input.externalContext) {
|
|
115
|
+
prepareExternalStageContext({ stageRoot: protocolizeRoot, loaded: input.externalContext });
|
|
116
|
+
prepareOutputFile(promptPath);
|
|
117
|
+
}
|
|
118
|
+
return { projectRoot, project, run, work, protocolizeRoot, contextPath, frozenContext, packetInput, packet, stageAttached, mode, promptPath };
|
|
119
|
+
}
|
|
120
|
+
function readProtocolizeContext(file) {
|
|
121
|
+
try {
|
|
122
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
123
|
+
if (["same_session", "new_session"].includes(value?.system?.handoff?.stage_handoff?.effective))
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
catch { /* retained context is not a caller argument */ }
|
|
127
|
+
throw new AppError("runtime_context_invalid", "PROTOCOLIZE context is missing a valid frozen handoff", 1, { file });
|
|
128
|
+
}
|
|
129
|
+
export function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
|
|
130
|
+
const { projectRoot, project, run, work, protocolizeRoot, contextPath, packetInput, packet, stageAttached, mode, promptPath } = prepared;
|
|
131
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
132
|
+
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]);
|
|
133
|
+
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
134
|
const sessionId = hookSessionIdentity(context, project.id, input.hookEventId).sessionId;
|
|
98
135
|
// A stage-entry eval snapshot deliberately stops the canonical binding. Its
|
|
99
136
|
// Subject starts from a fork of the captured starter Session, so the stale
|
|
@@ -103,6 +140,10 @@ export function startVnextProtocolize(context, input) {
|
|
|
103
140
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to continue in the SPECIFY session", 1);
|
|
104
141
|
if (mode === "new_session" && previous?.session_id === sessionId)
|
|
105
142
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to start in a fresh session", 1);
|
|
143
|
+
if (packet)
|
|
144
|
+
prepareVnextProtocolize(context, packetInput, packet);
|
|
145
|
+
const frozenContext = prepared.frozenContext ?? readProtocolizeContext(contextPath);
|
|
146
|
+
const externalContext = applyExternalStageContext({ stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
106
147
|
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
148
|
// Same-session handoff deliberately reuses the root Work/Session that
|
|
108
149
|
// SPECIFY opened. That is not evidence that PROTOCOLIZE is already attached.
|
|
@@ -133,10 +174,9 @@ export function finishVnextProtocolize(context, input) {
|
|
|
133
174
|
if (!work)
|
|
134
175
|
throw new AppError("not_found", "RUN has no PROTOCOLIZE Work", 1, { run_id: run.id });
|
|
135
176
|
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 });
|
|
139
|
-
const result = readResult(resultFile);
|
|
177
|
+
const resultFile = prepareProtocolizeResultPath(requireRunHome(run), input.resultFile);
|
|
178
|
+
const result = input.result ?? readResult(resultFile, { projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
179
|
+
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, data: result, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
140
180
|
if (result.outcome !== "protocolized")
|
|
141
181
|
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
142
182
|
const acceptedObligations = acceptedSpecifyObligations(context, projectRoot, run.id, path.join(requireRunHome(run), "01-specify", "specify.json"));
|
|
@@ -162,12 +202,12 @@ export function finishVnextProtocolize(context, input) {
|
|
|
162
202
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
163
203
|
const reportHtml = path.join(root, "stage-report.html");
|
|
164
204
|
writeStageReport(root, report);
|
|
165
|
-
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
205
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
166
206
|
completeFlowRunStage(context, { 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" });
|
|
167
207
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "protocol_documents_materialized", work_id: work.work_id, protocol_ids: protocolIds, stage });
|
|
168
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
208
|
+
advanceFlowRun(context, { settlementStage: "protocolize", projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
169
209
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
170
|
-
const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json
|
|
210
|
+
const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
171
211
|
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", next_command: nextCommand, next: { kind: "start_stage", stage: "plan", command: nextCommand, cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode }, 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
212
|
}
|
|
173
213
|
function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
@@ -309,21 +349,27 @@ function appendIndexEntry(indexPath, childPath, label) {
|
|
|
309
349
|
}
|
|
310
350
|
function epicRootFor(projectRoot, value) { const resolved = path.resolve(projectRoot, value); const relative = path.relative(projectRoot, resolved); if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
311
351
|
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;
|
|
352
|
+
function readResult(file, context) {
|
|
353
|
+
return prepareJsonSchema(file, "PROTOCOLIZE result", {
|
|
354
|
+
schemaName: "vnext-protocolize-result", projectRoot: context.projectRoot, ddFlowHome: context.ddFlowHome, runId: context.runId, runRoot: context.runRoot
|
|
355
|
+
}).value;
|
|
317
356
|
}
|
|
318
357
|
function acceptedSpecifyObligations(context, projectRoot, runId, specifyPath) {
|
|
319
358
|
if (!fs.existsSync(specifyPath))
|
|
320
|
-
throw new AppError("
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
359
|
+
throw new AppError("runtime_artifact_missing", "PROTOCOLIZE requires accepted specify.json", 1, { path: specifyPath });
|
|
360
|
+
try {
|
|
361
|
+
const specify = readVnextSpecifyResult(specifyPath);
|
|
362
|
+
validateSchema({ schemaName: "vnext-specify", file: specifyPath, data: specify, projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: requireRunHome(requireRun(context, projectRoot, runId)) });
|
|
363
|
+
return [
|
|
364
|
+
...specify.requirements.map((obligation) => ({ ...obligation, kind: "requirement" })),
|
|
365
|
+
...specify.acceptance_criteria.map((obligation) => ({ ...obligation, kind: "acceptance" }))
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (!(error instanceof AppError) || !["validation", "schema_validation", "invalid_json"].includes(error.code))
|
|
370
|
+
throw error;
|
|
371
|
+
throw new AppError("runtime_artifact_invalid", "Accepted SPECIFY result is invalid", 1, { path: specifyPath, cause_code: error.code });
|
|
372
|
+
}
|
|
327
373
|
}
|
|
328
374
|
/** SPECIFY is historical input; only a HITL-backed protocol amendment may alter its effective wording. */
|
|
329
375
|
function effectiveObligations(result, accepted) {
|
|
@@ -435,18 +481,19 @@ function resultTemplate(obligations = []) {
|
|
|
435
481
|
}
|
|
436
482
|
function renderPrompt(input) {
|
|
437
483
|
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\`\`\``, "
|
|
484
|
+
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}`, "</output_contract>", ""].join("\n");
|
|
439
485
|
}
|
|
440
|
-
function
|
|
486
|
+
function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, stageRoot) {
|
|
441
487
|
const receiptPath = path.join(stageRoot, "workspace-route.json");
|
|
442
488
|
if (fs.existsSync(receiptPath))
|
|
443
|
-
return readWorkspaceRouteReceipt(stageRoot).policy;
|
|
489
|
+
return { kind: "retained", policy: readWorkspaceRouteReceipt(stageRoot).policy };
|
|
490
|
+
prepareOutputFile(receiptPath);
|
|
444
491
|
const config = loadVnextWorkspacePolicy(projectRoot);
|
|
445
492
|
const stableFacts = gitFacts(projectRoot);
|
|
493
|
+
const sessionMode = stageSessionMode(context, run);
|
|
446
494
|
if (config.workspace.route === "integration_branch_direct") {
|
|
447
|
-
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:
|
|
448
|
-
|
|
449
|
-
return policy;
|
|
495
|
+
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" };
|
|
496
|
+
return { kind: "direct", receiptPath, policy };
|
|
450
497
|
}
|
|
451
498
|
if (stableFacts.status === "unavailable" || !stableFacts.head || stableFacts.branch !== config.workspace.integration_branch) {
|
|
452
499
|
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 +501,31 @@ function provisionWorkspaceForProtocolize(context, projectRoot, projectId, run,
|
|
|
454
501
|
const branch = vnextRunBranch(config, run);
|
|
455
502
|
const holder = path.basename(projectRoot).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
456
503
|
const worktreePath = projectFeatureWorktreePath(context.ddFlowHome, projectId, `RUN-${run.short_id}`, holder);
|
|
457
|
-
|
|
504
|
+
let create;
|
|
505
|
+
try {
|
|
506
|
+
create = prepareManagedWorktree({ projectRoot, branch, base: stableFacts.head, path: worktreePath });
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
510
|
+
throw new AppError("workspace_route_invalid", "The frozen RUN workspace route cannot be provisioned", 1, { cause_code: error.code, run_id: run.id });
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
prepareManagedWorktreeBootstrapSources(projectRoot);
|
|
514
|
+
return { kind: "feature", receiptPath, config, stableFacts, sessionMode, branch, create };
|
|
515
|
+
}
|
|
516
|
+
function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
|
|
517
|
+
if (prepared.kind === "retained")
|
|
518
|
+
return prepared.policy;
|
|
519
|
+
const { receiptPath } = prepared;
|
|
520
|
+
if (prepared.kind === "direct") {
|
|
521
|
+
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy: prepared.policy, created: null, bootstrap: null });
|
|
522
|
+
return prepared.policy;
|
|
523
|
+
}
|
|
524
|
+
const { config, stableFacts, sessionMode, branch, create } = prepared;
|
|
525
|
+
const created = createManagedWorktree(context, create, create);
|
|
458
526
|
const bootstrap = bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
|
|
459
527
|
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:
|
|
528
|
+
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
529
|
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy, created, bootstrap });
|
|
462
530
|
return policy;
|
|
463
531
|
}
|
|
@@ -498,11 +566,16 @@ function featureIndex(epicRoot, featureSlug) {
|
|
|
498
566
|
}
|
|
499
567
|
function requireWork(context, id) { const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]); if (!work)
|
|
500
568
|
throw new AppError("not_found", "Work is not registered", 1, { work_id: id }); return work; }
|
|
501
|
-
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root,
|
|
569
|
+
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
502
570
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: id }); return run; }
|
|
503
|
-
function requireRunHome(run) { if (!run.
|
|
504
|
-
throw new AppError("runtime_missing", "RUN
|
|
505
|
-
function
|
|
571
|
+
function requireRunHome(run) { if (!run.run_root)
|
|
572
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
573
|
+
export function prepareProtocolizeResultPath(runHome, file) {
|
|
574
|
+
const resolved = path.resolve(file);
|
|
575
|
+
inside(path.join(runHome, "02-protocolize"), resolved);
|
|
576
|
+
return resolved;
|
|
577
|
+
}
|
|
578
|
+
function inside(root, file) { const relative = path.relative(root, file); if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
506
579
|
throw new AppError("path_escape", "Result must be inside the protocolize workspace", 2); }
|
|
507
580
|
function writeJson(file, value) { const tmp = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); }
|
|
508
581
|
function slug(value) { const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); if (!normalized)
|