@deksden-com/dd-flow-cli 0.9.0-beta.75 → 0.9.0-beta.79
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/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +84 -41
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +195 -92
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
|
@@ -7,18 +7,19 @@ import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths
|
|
|
7
7
|
import { previewNextEntityId } from "./ids.js";
|
|
8
8
|
import { registerProtocol } from "./protocols.js";
|
|
9
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
10
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
10
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, prepareFlowRunStageAttachment, gitFacts, rebindFlowRunWorkspace } from "./runs.js";
|
|
11
11
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
12
12
|
import { validateSchema } from "./schema-validation.js";
|
|
13
13
|
import { assertStageStartHookEvent, hookSessionIdentity } from "./hooks.js";
|
|
14
14
|
import { bindStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
15
15
|
import { runTargetedMemoryBankLint } from "./stage-lifecycle.js";
|
|
16
|
-
import { bootstrapManagedWorktree, createManagedWorktree, recordManagedWorktree } from "./worktrees.js";
|
|
16
|
+
import { bootstrapManagedWorktree, createManagedWorktree, prepareManagedWorktree, prepareManagedWorktreeBootstrapSources, recordManagedWorktree } from "./worktrees.js";
|
|
17
17
|
import { loadVnextWorkspacePolicy, requireVnextWorkspaceRoute, vnextRunBranch } from "./vnext-workspace-policy.js";
|
|
18
18
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
19
19
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
20
20
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
21
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
23
|
const stage = "protocolize";
|
|
23
24
|
function stageSessionMode(context, run) {
|
|
24
25
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
@@ -38,7 +39,7 @@ export function isVnextProtocolizeRun(context, input) {
|
|
|
38
39
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
39
40
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
40
41
|
}
|
|
41
|
-
export function
|
|
42
|
+
export function prepareVnextProtocolizeArtifacts(context, input) {
|
|
42
43
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
43
44
|
const project = requireProjectByRoot(context, projectRoot);
|
|
44
45
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -50,44 +51,50 @@ export function prepareVnextProtocolize(context, input) {
|
|
|
50
51
|
const specifyPath = path.join(runHome, "01-specify", "specify.json");
|
|
51
52
|
const specifyMarkdownPath = path.join(runHome, "01-specify", "specify.md");
|
|
52
53
|
const obligations = acceptedSpecifyObligations(context, projectRoot, run.id, specifyPath);
|
|
53
|
-
fs.mkdirSync(stageRoot, { recursive: true });
|
|
54
54
|
const resultPath = path.join(stageRoot, "protocolize-result.json");
|
|
55
55
|
const promptPath = path.join(stageRoot, "prompt.md");
|
|
56
|
-
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);
|
|
57
73
|
const routedRun = requireRun(context, projectRoot, run.id);
|
|
58
|
-
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 } });
|
|
59
75
|
if (!fs.existsSync(resultPath))
|
|
60
76
|
writeJson(resultPath, resultTemplate(obligations));
|
|
61
|
-
const templatePath = path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "protocolize.md");
|
|
62
|
-
if (!fs.existsSync(templatePath))
|
|
63
|
-
throw new AppError("not_found", "vNext PROTOCOLIZE prompt template is missing", 1, { path: templatePath });
|
|
64
77
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId: work.work_id, projectRoot });
|
|
65
|
-
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`) });
|
|
66
79
|
fs.writeFileSync(promptPath, prompt);
|
|
67
|
-
//
|
|
68
|
-
// command atomically installs external context, binds the worker and opens
|
|
69
|
-
// 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.
|
|
70
81
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
|
|
71
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`) };
|
|
72
83
|
return { directive, prompt_path: promptPath };
|
|
73
84
|
}
|
|
74
|
-
export function
|
|
85
|
+
export function prepareVnextProtocolizeStart(context, input) {
|
|
75
86
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
76
87
|
const project = requireProjectByRoot(context, projectRoot);
|
|
77
88
|
const run = requireRun(context, projectRoot, input.runId);
|
|
78
|
-
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
79
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]);
|
|
80
90
|
if (!work || work.status !== "running")
|
|
81
91
|
throw new AppError("invalid_work_state", "PROTOCOLIZE is not ready for an Agent WorkSession", 1, { run_id: run.id });
|
|
82
92
|
const protocolizeRoot = path.join(requireRunHome(run), "02-protocolize");
|
|
83
93
|
const contextPath = path.join(protocolizeRoot, "work-context.json");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const frozenContext = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
89
|
-
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]);
|
|
90
|
-
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);
|
|
91
98
|
const stageAttached = (() => {
|
|
92
99
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
93
100
|
try {
|
|
@@ -95,12 +102,35 @@ export function startVnextProtocolize(context, input) {
|
|
|
95
102
|
return index.stage_runs?.some((item) => item.stage === stage) ?? false;
|
|
96
103
|
}
|
|
97
104
|
catch {
|
|
98
|
-
|
|
105
|
+
throw new AppError("runtime_artifact_invalid", "RUN stage index is malformed", 1);
|
|
99
106
|
}
|
|
100
107
|
})();
|
|
101
|
-
|
|
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;
|
|
102
111
|
const promptPath = path.join(protocolizeRoot, "prompt.md");
|
|
103
|
-
|
|
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]);
|
|
104
134
|
const sessionId = hookSessionIdentity(context, project.id, input.hookEventId).sessionId;
|
|
105
135
|
// A stage-entry eval snapshot deliberately stops the canonical binding. Its
|
|
106
136
|
// Subject starts from a fork of the captured starter Session, so the stale
|
|
@@ -110,6 +140,10 @@ export function startVnextProtocolize(context, input) {
|
|
|
110
140
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to continue in the SPECIFY session", 1);
|
|
111
141
|
if (mode === "new_session" && previous?.session_id === sessionId)
|
|
112
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 } : {}) });
|
|
113
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 } : {}) });
|
|
114
148
|
// Same-session handoff deliberately reuses the root Work/Session that
|
|
115
149
|
// SPECIFY opened. That is not evidence that PROTOCOLIZE is already attached.
|
|
@@ -140,10 +174,9 @@ export function finishVnextProtocolize(context, input) {
|
|
|
140
174
|
if (!work)
|
|
141
175
|
throw new AppError("not_found", "RUN has no PROTOCOLIZE Work", 1, { run_id: run.id });
|
|
142
176
|
const root = path.join(requireRunHome(run), "02-protocolize");
|
|
143
|
-
const resultFile =
|
|
144
|
-
|
|
145
|
-
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
146
|
-
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) });
|
|
147
180
|
if (result.outcome !== "protocolized")
|
|
148
181
|
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
149
182
|
const acceptedObligations = acceptedSpecifyObligations(context, projectRoot, run.id, path.join(requireRunHome(run), "01-specify", "specify.json"));
|
|
@@ -316,21 +349,27 @@ function appendIndexEntry(indexPath, childPath, label) {
|
|
|
316
349
|
}
|
|
317
350
|
function epicRootFor(projectRoot, value) { const resolved = path.resolve(projectRoot, value); const relative = path.relative(projectRoot, resolved); if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
318
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; }
|
|
319
|
-
function readResult(file) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
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;
|
|
324
356
|
}
|
|
325
357
|
function acceptedSpecifyObligations(context, projectRoot, runId, specifyPath) {
|
|
326
358
|
if (!fs.existsSync(specifyPath))
|
|
327
|
-
throw new AppError("
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
}
|
|
334
373
|
}
|
|
335
374
|
/** SPECIFY is historical input; only a HITL-backed protocol amendment may alter its effective wording. */
|
|
336
375
|
function effectiveObligations(result, accepted) {
|
|
@@ -444,16 +483,17 @@ function renderPrompt(input) {
|
|
|
444
483
|
const obligationList = input.obligations.map((obligation) => `- ${obligation.id}: ${obligation.statement}`).join("\n");
|
|
445
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(), null, 2)}\n\`\`\``, "Allocate every supplied R-* and AC-* exactly once in obligation_ownership. 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. Run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second 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");
|
|
446
485
|
}
|
|
447
|
-
function
|
|
486
|
+
function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, stageRoot) {
|
|
448
487
|
const receiptPath = path.join(stageRoot, "workspace-route.json");
|
|
449
488
|
if (fs.existsSync(receiptPath))
|
|
450
|
-
return readWorkspaceRouteReceipt(stageRoot).policy;
|
|
489
|
+
return { kind: "retained", policy: readWorkspaceRouteReceipt(stageRoot).policy };
|
|
490
|
+
prepareOutputFile(receiptPath);
|
|
451
491
|
const config = loadVnextWorkspacePolicy(projectRoot);
|
|
452
492
|
const stableFacts = gitFacts(projectRoot);
|
|
493
|
+
const sessionMode = stageSessionMode(context, run);
|
|
453
494
|
if (config.workspace.route === "integration_branch_direct") {
|
|
454
|
-
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:
|
|
455
|
-
|
|
456
|
-
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 };
|
|
457
497
|
}
|
|
458
498
|
if (stableFacts.status === "unavailable" || !stableFacts.head || stableFacts.branch !== config.workspace.integration_branch) {
|
|
459
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 });
|
|
@@ -461,10 +501,31 @@ function provisionWorkspaceForProtocolize(context, projectRoot, projectId, run,
|
|
|
461
501
|
const branch = vnextRunBranch(config, run);
|
|
462
502
|
const holder = path.basename(projectRoot).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
463
503
|
const worktreePath = projectFeatureWorktreePath(context.ddFlowHome, projectId, `RUN-${run.short_id}`, holder);
|
|
464
|
-
|
|
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);
|
|
465
526
|
const bootstrap = bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
|
|
466
527
|
rebindFlowRunWorkspace(context, { projectRoot, runId: run.id, workspaceRoot: created.worktreePath, reason: "PROTOCOLIZE start provisioned the configured feature-worktree route" });
|
|
467
|
-
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 };
|
|
468
529
|
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy, created, bootstrap });
|
|
469
530
|
return policy;
|
|
470
531
|
}
|
|
@@ -509,7 +570,12 @@ function requireRun(context, projectRoot, id) { const project = requireProjectBy
|
|
|
509
570
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: id }); return run; }
|
|
510
571
|
function requireRunHome(run) { if (!run.run_root)
|
|
511
572
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
512
|
-
function
|
|
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))
|
|
513
579
|
throw new AppError("path_escape", "Result must be inside the protocolize workspace", 2); }
|
|
514
580
|
function writeJson(file, value) { const tmp = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); }
|
|
515
581
|
function slug(value) { const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); if (!normalized)
|
|
@@ -4,18 +4,21 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
7
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
8
8
|
import { ensureVnextWorkStorage } from "../storage/database.js";
|
|
9
|
-
import { resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
|
|
9
|
+
import { projectHome, resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
|
|
10
10
|
import { preflightMemoryPermissions } from "./memory-permissions.js";
|
|
11
11
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
12
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
12
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, prepareFlowRunStart, startFlowRun } from "./runs.js";
|
|
13
13
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
14
14
|
import { validateSchema } from "./schema-validation.js";
|
|
15
15
|
import { bindRunningWorkSession, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
16
|
import { nextWorkId } from "./ids.js";
|
|
17
17
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
18
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
|
+
import { prepareError, prepareJsonSchema, prepareJsonText, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
|
|
20
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
21
|
+
import { assertRecoveryHookCurrent, assertRunMutationAllowed } from "./run-recovery.js";
|
|
19
22
|
const flowId = "mb-sdlc-vnext-specify";
|
|
20
23
|
const protocolizeFlowId = "mb-sdlc-vnext-protocolize";
|
|
21
24
|
const flowVersion = 4;
|
|
@@ -29,28 +32,57 @@ export function isVnextSpecifyRun(context, input) {
|
|
|
29
32
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
30
33
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind IN ('vnext_specify', 'vnext_protocolize')", [project.id, input.runId]));
|
|
31
34
|
}
|
|
32
|
-
export function
|
|
33
|
-
ensureVnextWorkStorage(context.db);
|
|
35
|
+
export function prepareVnextSpecifyStart(context, input) {
|
|
34
36
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
35
|
-
registerProject(context, { root: projectRoot });
|
|
36
37
|
const intake = readIntake(input);
|
|
37
38
|
const discussion = intake.markdown.trim();
|
|
38
39
|
if (!discussion)
|
|
39
40
|
throw new AppError("validation", "SPECIFY intake must not be empty", 2);
|
|
40
41
|
const flow = assertFlowDefinition(projectRoot);
|
|
41
42
|
const template = readRequiredFile(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "specify.md"), "vNext SPECIFY prompt template");
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const startInput = { projectRoot, flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify", subjectType: "discussion", subjectId: "SPECIFY", slug: input.slug ?? "specify", nextAction: "await_work_session" };
|
|
44
|
+
const start = input.runId ? undefined : prepareFlowRunStart(context, startInput);
|
|
45
|
+
if (input.runId)
|
|
46
|
+
assertSpecifyNotStarted(context, projectRoot, input.runId);
|
|
47
|
+
const run = input.runId ? requireRun(context, projectRoot, input.runId) : undefined;
|
|
48
|
+
if (!run) {
|
|
49
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
50
|
+
prepareOutputDirectory(project ? path.join(projectHome(context.ddFlowHome, project.id), "runs") : path.join(context.ddFlowHome, "projects"));
|
|
51
|
+
}
|
|
52
|
+
if (run) {
|
|
53
|
+
assertRunMutationAllowed(context, run.project_id, run.id);
|
|
54
|
+
const home = requiredRunHome(run), stageRoot = path.join(home, "01-specify");
|
|
55
|
+
for (const relative of ["intake/discussion.md", "01-specify/prompt.md", "01-specify/work-context.json"])
|
|
56
|
+
prepareOutputFile(path.join(home, relative));
|
|
57
|
+
if (input.externalContext)
|
|
58
|
+
prepareExternalStageContext({ stageRoot, loaded: input.externalContext });
|
|
59
|
+
}
|
|
60
|
+
const preflight = preflightMemoryPermissions({ root: projectRoot, memoryBank: ".memory-bank", flow: "custom", mode: "read", targets: [{ path: ".memory-bank", mode: "read" }] });
|
|
61
|
+
if (preflight.ok !== true)
|
|
62
|
+
throw new AppError("permission_preflight_failed", "SPECIFY project context is not readable", 1, { preflight });
|
|
63
|
+
const grounding = projectGrounding(projectRoot);
|
|
64
|
+
const handoff = run ? executionSnapshot(context, run.project_id, run.id) : executionSnapshotFromProfile(context, start?.executionProfile);
|
|
65
|
+
return { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff };
|
|
66
|
+
}
|
|
67
|
+
function assertSpecifyNotStarted(context, projectRoot, runId) {
|
|
68
|
+
const existing = getFlowRunStatus(context, { projectRoot, runId });
|
|
69
|
+
if (existing.index?.stage_runs?.some(entry => entry.stage === stageId))
|
|
70
|
+
throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: runId });
|
|
71
|
+
}
|
|
72
|
+
export function launchVnextSpecify(context, input, prepared = prepareVnextSpecifyStart(context, input)) {
|
|
73
|
+
const { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff } = prepared;
|
|
74
|
+
if (input.runId)
|
|
75
|
+
assertSpecifyNotStarted(context, projectRoot, input.runId);
|
|
76
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
77
|
+
const identity = hookSessionIdentity(context, project.id, input.hookEventId);
|
|
78
|
+
if (input.runId) {
|
|
79
|
+
assertRunMutationAllowed(context, project.id, input.runId);
|
|
80
|
+
assertRecoveryHookCurrent(context, project.id, input.runId, identity);
|
|
81
|
+
}
|
|
82
|
+
ensureVnextWorkStorage(context.db);
|
|
83
|
+
registerProject(context, { root: projectRoot });
|
|
84
|
+
const started = input.runId ? null : startFlowRun(context, startInput, start);
|
|
50
85
|
const run = requireRun(context, projectRoot, input.runId ?? started.run.id);
|
|
51
|
-
const existingStages = getFlowRunStatus(context, { projectRoot, runId: run.id });
|
|
52
|
-
if (existingStages.index?.stage_runs?.some((entry) => entry.stage === stageId))
|
|
53
|
-
throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: run.id });
|
|
54
86
|
const runHome = requiredRunHome(run);
|
|
55
87
|
const stageRoot = path.join(runHome, "01-specify");
|
|
56
88
|
const intakeDir = path.join(runHome, "intake");
|
|
@@ -63,11 +95,6 @@ export function launchVnextSpecify(context, input) {
|
|
|
63
95
|
const contextPath = path.join(stageRoot, "work-context.json");
|
|
64
96
|
const workId = nextWorkId(context, run.project_id, "root");
|
|
65
97
|
const now = context.now();
|
|
66
|
-
const preflight = writablePreflight(projectRoot, stageRoot);
|
|
67
|
-
if (preflight.ok !== true)
|
|
68
|
-
throw new AppError("permission_preflight_failed", "SPECIFY workspace is not writable", 1, { preflight });
|
|
69
|
-
const grounding = projectGrounding(projectRoot);
|
|
70
|
-
const handoff = executionSnapshot(context, run.project_id, run.id);
|
|
71
98
|
const workContext = {
|
|
72
99
|
schema_id: "dd-flow/work-context@1",
|
|
73
100
|
system: { run_id: run.id, work_id: workId, stage: stageId },
|
|
@@ -245,13 +272,23 @@ export function finishVnextSpecifyStage(context, input) {
|
|
|
245
272
|
const run = requireRun(context, projectRoot, input.runId);
|
|
246
273
|
const stageRoot = path.join(requiredRunHome(run), "01-specify");
|
|
247
274
|
const acceptedResult = path.join(stageRoot, "specify.json");
|
|
248
|
-
const
|
|
275
|
+
const source = input.resultFile ? path.resolve(input.resultFile) : acceptedResult;
|
|
276
|
+
const prepared = input.result
|
|
277
|
+
? { file: source, value: input.result }
|
|
278
|
+
: input.resultJson !== undefined
|
|
279
|
+
? prepareJsonText(input.resultJson, { file: "<stdin>", label: "SPECIFY result" })
|
|
280
|
+
: prepareJsonSchema(source, "SPECIFY result", { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
|
|
281
|
+
try {
|
|
282
|
+
validateSchema({ schemaName: "vnext-specify", file: prepared.file, data: prepared.value, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
|
|
283
|
+
validateObligations(prepared.value, prepared.file);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
return prepareError(error, { file: prepared.file, label: "SPECIFY result" });
|
|
287
|
+
}
|
|
249
288
|
const candidate = path.join(stageRoot, `.specify-candidate-${crypto.randomUUID()}.json`);
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
if (input.resultFile && path.resolve(input.resultFile) === acceptedResult)
|
|
254
|
-
fs.rmSync(acceptedResult, { force: true });
|
|
289
|
+
// Candidate storage starts only after the exact input has passed schema and
|
|
290
|
+
// semantic checks; it is never the caller's source file.
|
|
291
|
+
writeText(candidate, `${JSON.stringify(prepared.value, null, 2)}\n`);
|
|
255
292
|
try {
|
|
256
293
|
return submitVnextSpecify(context, { projectRoot, workId: work.work_id, resultFile: candidate, outcome: input.outcome });
|
|
257
294
|
}
|
|
@@ -264,11 +301,7 @@ function readIntake(input) {
|
|
|
264
301
|
return { markdown: input.intakeMarkdown };
|
|
265
302
|
if (!input.intakeFile)
|
|
266
303
|
throw new AppError("usage", "SPECIFY intake is required", 2);
|
|
267
|
-
|
|
268
|
-
if (!fs.existsSync(intakeFile) || !fs.statSync(intakeFile).isFile()) {
|
|
269
|
-
throw new AppError("not_found", "--intake-file must point to an existing file", 1, { intake_file: input.intakeFile });
|
|
270
|
-
}
|
|
271
|
-
return { markdown: fs.readFileSync(intakeFile, "utf8") };
|
|
304
|
+
return { markdown: prepareTextFile(input.intakeFile, "SPECIFY intake").text };
|
|
272
305
|
}
|
|
273
306
|
function requireRun(context, projectRoot, runId) {
|
|
274
307
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -303,28 +336,6 @@ function requireWorkSession(context, workSessionId) {
|
|
|
303
336
|
function latestWorkSession(context, workId) {
|
|
304
337
|
return context.db.get("SELECT * FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC, id DESC LIMIT 1", [workId]) ?? null;
|
|
305
338
|
}
|
|
306
|
-
function writablePreflight(projectRoot, stageRoot) {
|
|
307
|
-
const memoryBank = preflightMemoryPermissions({
|
|
308
|
-
root: projectRoot,
|
|
309
|
-
memoryBank: ".memory-bank",
|
|
310
|
-
flow: "custom",
|
|
311
|
-
mode: "write",
|
|
312
|
-
targets: [{ path: ".memory-bank", mode: "read" }]
|
|
313
|
-
});
|
|
314
|
-
const probe = path.join(stageRoot, `.dd-flow-probe-${crypto.randomUUID()}`);
|
|
315
|
-
try {
|
|
316
|
-
fs.writeFileSync(probe, "");
|
|
317
|
-
fs.rmSync(probe);
|
|
318
|
-
return { ...memoryBank, ok: memoryBank.ok === true, stage_workspace: { path: stageRoot, status: "passed" } };
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
try {
|
|
322
|
-
fs.rmSync(probe, { force: true });
|
|
323
|
-
}
|
|
324
|
-
catch { /* best effort */ }
|
|
325
|
-
return { ...memoryBank, ok: false, stage_workspace: { path: stageRoot, status: "failed", message: String(error) } };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
339
|
function projectGrounding(projectRoot) {
|
|
329
340
|
return [
|
|
330
341
|
".memory-bank/index.md",
|
|
@@ -402,7 +413,7 @@ function renderPrompt(input) {
|
|
|
402
413
|
"```",
|
|
403
414
|
"The response tells you exactly what to ask and how to resume this same stage. Ask its user_message and stop the Turn.",
|
|
404
415
|
`Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.finishCommand}\`.`,
|
|
405
|
-
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. If finish reports a validation error, correct the same result file and use its returned retry_command in this Session
|
|
416
|
+
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. If finish reports a no-effect validation error, correct the same result file and use its returned retry_command in this Session. Internal invocation IDs are bound by the runtime and must not be copied or invented. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
|
|
406
417
|
"</output_contract>",
|
|
407
418
|
""
|
|
408
419
|
].join("\n");
|
|
@@ -569,7 +580,9 @@ export function readVnextFlowDefinition(projectRoot) {
|
|
|
569
580
|
function executionSnapshot(context, projectId, runId) {
|
|
570
581
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
571
582
|
const snapshot = row ? JSON.parse(row.index_json) : null;
|
|
572
|
-
|
|
583
|
+
return executionSnapshotFromProfile(context, snapshot?.execution_profile, runId);
|
|
584
|
+
}
|
|
585
|
+
function executionSnapshotFromProfile(context, profile, runId) {
|
|
573
586
|
let handoff = profile?.settings.stage_session_mode;
|
|
574
587
|
if (handoff !== "same_session" && handoff !== "new_session")
|
|
575
588
|
throw new AppError("execution_profile_invalid", "RUN has no frozen stage session mode", 1, { run_id: runId });
|