@deksden-com/dd-flow-cli 0.9.0-beta.111 → 0.9.0-beta.113

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.113
4
+
5
+ ### Patch Changes
6
+
7
+ - Allow sibling read-only reviewers to start concurrently when one updates the shared RUN projection; retain Work, generation, dependency, and immutable review-input checks.
8
+
9
+ ## 0.9.0-beta.112
10
+
11
+ ### Patch Changes
12
+
13
+ - Give ZCode native Session creation a bounded end-to-end budget and admit the matching tested bridge commit, reject bare long-running service scripts as checks, run managed checks with the engine's Node architecture, and serialize concurrent Work checks within one RUN. Classify Git's expected EPIPE on a gitless check-ignore probe without hiding other Git failures.
14
+
3
15
  ## 0.9.0-beta.111
4
16
 
5
17
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.111",
4
- "cli_commit": "fde200161dc8fa1fd32de74ea5bad4b8b8e0aed6",
5
- "built_at": "2026-09-27T10:00:38.751Z",
3
+ "cli_version": "0.9.0-beta.113",
4
+ "cli_commit": "97203d7f7def0313962f58ed3476cde9d9f57a74",
5
+ "built_at": "2026-09-27T15:24:27.962Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
@@ -804,6 +804,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
804
804
  ["f4d47b517073f9bdb8d4eba3d0d59ce66c6a2833", "dd-zcode-harness@2"],
805
805
  ["5a8c89ab716a4b2ccc50df81cb4d204ff5c06368", "dd-zcode-harness@2"],
806
806
  ["91a1470650f5fd6105f9863fd31d1cad7b90a109", "dd-zcode-harness@2"],
807
+ ["b9e0f5106dddb439b76186278bd9be5f45dbf3ef", "dd-zcode-harness@2"],
807
808
  ]);
808
809
  const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
809
810
  return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
@@ -845,7 +846,8 @@ export async function createSessionWithBridge(bridge, options, initialized) {
845
846
  if (identity.cwd !== undefined && identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
846
847
  await options.onSessionAllocated?.({ ...identity, cwd });
847
848
  });
848
- const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId });
849
+ // Two provider syncs (10s each) and native create (90s) run inside resolve.
850
+ const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId }, 150_000);
849
851
  const providerSessionId = resolved.providerSessionId;
850
852
  // Expose both identities before profile/usage bookkeeping. The owning
851
853
  // daemon can persist this allocation even when a later step fails.
@@ -15,9 +15,9 @@ const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "co
15
15
  const receiptStartingGraceMs = 30_000;
16
16
  export function codeExecutionEnvironment(workspaceRoot) {
17
17
  const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
18
- // Managed checks retain the launcher's executable resolution. A login shell
19
- // or NODE_PATH can silently replace it, including with another architecture.
20
- const environment = { ...process.env, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
18
+ // The managed CLI's Node must win over a provider app's PATH: a Rosetta
19
+ // provider can otherwise run x64 checks against arm64-installed modules.
20
+ const environment = { ...process.env, PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
21
21
  delete environment.NODE_PATH;
22
22
  return environment;
23
23
  }
@@ -102,6 +102,8 @@ export function validateCheckDeclaration(workspaceRoot, check) {
102
102
  if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
103
103
  throw new AppError("unsupported_check_gate", "Check requires an autonomous executable gate; replan the incompatible declaration", 2, { check_id: check.id, run_at: check.run_at });
104
104
  const { profile } = readCodeCheckProfile(workspaceRoot);
105
+ const resolvedCommand = profile?.aliases[check.command] ?? check.command;
106
+ validateTerminatingCheckCommand(resolvedCommand, check.id);
105
107
  if (check.availability === "planned" && check.command.startsWith("@check/") && !profile?.aliases[check.command])
106
108
  throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
107
109
  validateCodeCheckCommands(workspaceRoot, [check.command]);
@@ -119,6 +121,26 @@ export function validateCheckDeclaration(workspaceRoot, check) {
119
121
  if (!profile?.aliases[check.command])
120
122
  throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
121
123
  }
124
+ export function validateTerminatingCheckCommand(command, checkId) {
125
+ if (isBareServiceScript(command))
126
+ throw new AppError("non_terminating_check", "A check must terminate; run a finite test instead of a bare dev/start/serve/watch service script", 2, { check_id: checkId, command });
127
+ }
128
+ function isBareServiceScript(command) {
129
+ const words = parse(command.trim());
130
+ if (words.some((word) => typeof word !== "string"))
131
+ return false;
132
+ let args = words;
133
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0] ?? ""))
134
+ args = args.slice(1);
135
+ const manager = path.basename(args.shift() ?? "");
136
+ if (!["pnpm", "npm", "yarn", "bun"].includes(manager))
137
+ return false;
138
+ while (args.length && ["--filter", "-F", "--dir", "-C", "--cwd"].includes(args[0]))
139
+ args.splice(0, 2);
140
+ if (args[0] === "run")
141
+ args.shift();
142
+ return ["dev", "start", "serve", "watch"].includes(args[0] ?? "");
143
+ }
122
144
  /** Reject a plan-time typo or a fictitious "manual" command before it becomes
123
145
  * a late shell exit. Compound shell programs remain valid: only a simple,
124
146
  * directly named executable is preflighted here. */
@@ -148,18 +170,32 @@ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
148
170
  throw new AppError("check_command_unavailable", "Available CODE check names an executable that is unavailable in this environment", 2, { check_id: checkId, command, executable: program });
149
171
  }
150
172
  export async function runCodeChecks(context, input) {
151
- reconcileUnfinishedChecks(context, input);
152
- reconcileWorkspaceOwnership(context, input.workspaceRoot);
153
- const gate = acquireWorkspaceGate(context, input);
173
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
174
+ let gate;
175
+ for (;;) {
176
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
177
+ reconcileUnfinishedChecks(context, input);
178
+ reconcileWorkspaceOwnership(context, input.workspaceRoot);
179
+ try {
180
+ gate = acquireWorkspaceGate(context, input);
181
+ break;
182
+ }
183
+ catch (error) {
184
+ const sameRunGate = managedProcessStatus(context).find(item => item.kind === "workspace-gate" && item.operation_id === workspaceOperationId(input.workspaceRoot) && ["starting", "running", "stopping"].includes(item.state) && item.project_id === input.projectId && item.run_id === input.runId);
185
+ if (input.scope !== "work" || !input.workId || !(error instanceof AppError) || !["check_in_progress", "managed_operation_in_progress"].includes(error.code) || !sameRunGate)
186
+ throw error;
187
+ await new Promise(resolve => setTimeout(resolve, 500));
188
+ }
189
+ }
154
190
  try {
155
- return await runCodeChecksOwned(context, input, gate.id);
191
+ return await runCodeChecksOwned(context, input, gate.id, generation);
156
192
  }
157
193
  finally {
158
194
  finishManagedProcess(context, { id: gate.id, leaseToken: gate.lease_token, state: "stopped", reason: "workspace_gate_settled" });
159
195
  }
160
196
  }
161
- async function runCodeChecksOwned(context, input, workspaceGateId) {
162
- const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
197
+ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
198
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
163
199
  for (const check of input.checks)
164
200
  validateCheckDeclaration(input.workspaceRoot, check);
165
201
  const { profile } = readCodeCheckProfile(input.workspaceRoot);
@@ -94,7 +94,7 @@ export async function launchExternalWork(context, input) {
94
94
  const call = (args, timeoutMs) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: `${operationId}:${args[0]}.${args[1]}`, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
95
95
  await call(["daemon", "start", ...common]);
96
96
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
97
- const created = await call(["session", "create", ...common]);
97
+ const created = await call(["session", "create", ...common], runtimeHarness(resolved.worker) === "zcode-acp" ? 180_000 : undefined);
98
98
  const sessionId = adapterSessionId(created);
99
99
  if (!sessionId)
100
100
  throw new AppError("harness_adapter_invalid", "Harness adapter did not return a typed Session id", 1, { work_id: work.work_id, harness: runtimeHarness(resolved.worker) });
@@ -112,7 +112,7 @@ async function dispatch(context, input) {
112
112
  try {
113
113
  const adapterCall = (args, timeoutMs, progressMessage) => { assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation); return runHarnessAdapter({ executable, args, env: runtimeContext.env, runtimeOwner, operationId: `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }) }); };
114
114
  await adapterCall(["daemon", "start", ...common]);
115
- const created = await adapterCall(["session", "create", ...common]);
115
+ const created = await adapterCall(["session", "create", ...common], runtimeHarness(input.profile) === "zcode-acp" ? 180_000 : undefined);
116
116
  const sessionId = adapterSessionId(created);
117
117
  if (!sessionId)
118
118
  throw new AppError("merge_adapter_invalid", "Harness adapter did not return a Session id", 1, { harness: input.profile.harness, response: created });
@@ -197,7 +197,9 @@ export async function controllerAdapter(context, row, manifest, session, args, a
197
197
  let receipt;
198
198
  let recoveredOperation = false;
199
199
  try {
200
- receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs: args[0] === "session" && args[1] === "prompt" ? null : 120_000, ...(signal ? { signal } : {}) });
200
+ const timeoutMs = args[0] === "session" && args[1] === "prompt" ? null
201
+ : args[0] === "session" && args[1] === "create" && runtimeHarness(session.profile) === "zcode-acp" ? 180_000 : 120_000;
202
+ receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs, ...(signal ? { signal } : {}) });
201
203
  }
202
204
  catch (error) {
203
205
  const retained = await recoverCompletedOperation(context, manifest, session, executable, args, operationId, signal);
@@ -5,7 +5,7 @@ import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStage
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { AppError } from "../shared/errors.js";
8
- import { effectiveCheckDeclarations, readCodeCheckProfile, validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands } from "./code-checks.js";
8
+ import { effectiveCheckDeclarations, readCodeCheckProfile, validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands, validateTerminatingCheckCommand } from "./code-checks.js";
9
9
  import { requireProjectByRoot } from "./projects.js";
10
10
  import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
11
11
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, getFlowRunVariables, gitFacts } from "./runs.js";
@@ -102,7 +102,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
102
102
  ? [`This RUN must reach MERGE. Project policy already supplies the mandatory merge gate${policyMergeAliases.length === 1 ? "" : "s"}: ${policyMergeAliases.join(", ")}. Do not duplicate them in semantic checks[]. Add another merge check only when the task genuinely needs additional evidence.`]
103
103
  : ["This RUN must reach MERGE and project policy supplies no merge gate. Select at least one real top-level checks[] entry with run_at: merge. It may use an existing project alias or a planned alias materialised by a named P* provider Work. This is a planning obligation: do not defer it to CODE-REVIEW or MERGE."]), "The CLI validates the effective merge gate but never invents one or migrates an incompatible project policy.", "</merge_gate_contract>", ""]
104
104
  : [];
105
- const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, 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", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
105
+ const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, 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", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
106
106
  const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
107
107
  const promptPath = path.join(root, "stage-prompt.md");
108
108
  // This explicit final rule supersedes historical pack wording: a flow gate
@@ -218,6 +218,8 @@ export function prepareVnextPlanArtifacts(context, input) {
218
218
  validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
219
219
  for (const check of value.checks.filter((item) => item.availability === "available"))
220
220
  validateCheckDeclaration(workspaceRoot, check);
221
+ for (const check of value.checks.filter((item) => item.availability === "planned" && item.definition))
222
+ validateTerminatingCheckCommand(check.definition, check.id);
221
223
  // Placement is a PLAN-time rule. A Work may legitimately materialize a
222
224
  // new aggregate alias later, so rechecking against the changed profile
223
225
  // at CODE entry would retroactively invalidate an accepted plan.
@@ -579,7 +579,6 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
579
579
  const workerRun = readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE
580
580
  ? { ...run, workspace_root: path.resolve(context.env.DD_FLOW_READ_ONLY_WORKSPACE) }
581
581
  : run;
582
- const indexBeforePrepare = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id])?.index_json;
583
582
  const preparedReviewInput = readOnly ? assertSharedReviewInput(context, work, run, generation) : null;
584
583
  const reviewPaths = preparedReviewInput?.run_inputs.filter(item => item.root === "workspace").map(item => item.path) ?? [];
585
584
  const preparedCopy = readOnly && context.env.DD_FLOW_READ_ONLY_WORKSPACE ? prepareBoundReviewCopy(context, work, run, context.env.DD_FLOW_READ_ONLY_WORKSPACE, reviewPaths) : null;
@@ -601,8 +600,6 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
601
600
  throw new AppError("conflict", "Work was claimed concurrently", 1, { work_id: current.work_id, status: current.status });
602
601
  if (Object.keys(current).some(key => current[key] !== work[key]))
603
602
  throw new AppError("work_not_ready", "Work inputs changed while preparing its start packet", 2, { work_id: current.work_id });
604
- if (context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [work.project_id, work.run_id])?.index_json !== indexBeforePrepare)
605
- throw new AppError("work_not_ready", "RUN revision changed while preparing the Work start packet", 2, { work_id: current.work_id });
606
603
  if (!isReady(context, current))
607
604
  throw new AppError("work_not_ready", "Work dependencies are not ready", 2, { work_id: current.work_id, blockers: readinessBlockers(context, current) });
608
605
  if (retryPending)
@@ -11,7 +11,9 @@ export function localEnvironmentAllowed(source) {
11
11
  }
12
12
  /** A damaged/inaccessible repository is not an intentional gitless workspace. */
13
13
  export function isGitlessWorkspace(root, result) {
14
- if (result.error || result.status !== 128 || !/not a git repository|outside a repository/i.test(result.stderr ?? ""))
14
+ // git check-ignore can exit 128 before consuming --stdin, yielding EPIPE
15
+ // alongside the authoritative "not a git repository" diagnostic.
16
+ if ((result.error && result.error.code !== "EPIPE") || result.status !== 128 || !/not a git repository|outside a repository/i.test(result.stderr ?? ""))
15
17
  return false;
16
18
  for (let directory = path.resolve(root);; directory = path.dirname(directory)) {
17
19
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.111",
3
+ "version": "0.9.0-beta.113",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {