@deksden-com/dd-flow-cli 0.9.0-beta.114 → 0.9.0-beta.116
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.116
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Reject ZCode managed lifecycle commands launched as background Bash before execution, and give all harnesses one foreground lifecycle instruction so their native admission receipts remain observable.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.115
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Freeze the reviewed source check profile for MERGE after verifying that it only adds aliases to the integration baseline; keep existing check policy immutable and verify the post-apply profile against that frozen source.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.114
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.116",
|
|
4
|
+
"cli_commit": "c30f54c04930f51d7982c49369a96bca51eabe65",
|
|
5
|
+
"built_at": "2026-09-27T20:17:11.827Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
|
|
@@ -49,7 +49,7 @@ export function delegationContract(harness) {
|
|
|
49
49
|
return `dd-flow/delegation/${key}@${key === "codex" ? 2 : 1}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const lifecycleCommandCompletionInstruction = "A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
52
|
+
export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
|
|
53
53
|
|
|
54
54
|
export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
|
|
55
55
|
|
|
@@ -3,6 +3,7 @@ import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, w
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
7
|
import { parse } from "shell-quote";
|
|
7
8
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
8
9
|
import { AppError } from "../shared/errors.js";
|
|
@@ -66,6 +67,32 @@ export function codeCheckProfileHash(workspaceRoot, checks = []) {
|
|
|
66
67
|
};
|
|
67
68
|
return crypto.createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
68
69
|
}
|
|
70
|
+
/** The integration target starts at its baseline profile; the reviewed source
|
|
71
|
+
* may add aliases, but must not change existing check policy or bindings. */
|
|
72
|
+
export function mergeCheckProfileHash(sourceWorkspace, targetWorkspace, checks) {
|
|
73
|
+
const source = readCodeCheckProfile(sourceWorkspace).profile;
|
|
74
|
+
const target = readCodeCheckProfile(targetWorkspace).profile;
|
|
75
|
+
if (!source && !target)
|
|
76
|
+
return null;
|
|
77
|
+
if (!source || !target)
|
|
78
|
+
throw new AppError("check_profile_drift", "Source and integration check profiles differ", 1);
|
|
79
|
+
const retained = structuredClone(source);
|
|
80
|
+
for (const field of ["aliases", "ports_by_alias", "inputs_by_alias"]) {
|
|
81
|
+
const old = target[field];
|
|
82
|
+
if (!old) {
|
|
83
|
+
delete retained[field];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const current = source[field] ?? {};
|
|
87
|
+
retained[field] = Object.fromEntries(Object.keys(old).filter(key => key in current).map(key => [key, current[key]]));
|
|
88
|
+
}
|
|
89
|
+
const existingAliases = new Set(Object.keys(target.aliases));
|
|
90
|
+
const changedExistingMetadata = ["ports_by_alias", "inputs_by_alias"].some(field => Object.keys(source[field] ?? {}).some(alias => existingAliases.has(alias) && !(alias in (target[field] ?? {}))));
|
|
91
|
+
if (changedExistingMetadata || !isDeepStrictEqual(retained, target)) {
|
|
92
|
+
throw new AppError("check_profile_drift", "Source check profile changes existing integration policy", 1, { source_profile_hash: codeCheckProfileHash(sourceWorkspace, checks), target_profile_hash: codeCheckProfileHash(targetWorkspace, checks) });
|
|
93
|
+
}
|
|
94
|
+
return codeCheckProfileHash(sourceWorkspace, checks);
|
|
95
|
+
}
|
|
69
96
|
export function validateCodeCheckCommands(workspaceRoot, commands) {
|
|
70
97
|
const { profile, file } = readCodeCheckProfile(workspaceRoot);
|
|
71
98
|
return commands.map((command) => {
|
package/dist/services/hooks.js
CHANGED
|
@@ -509,9 +509,9 @@ export function handleZcodeEvent(context, input) {
|
|
|
509
509
|
* admission; the native hook must not create a competing receipt for them.
|
|
510
510
|
*/
|
|
511
511
|
export function handleZcodeHook(context, input) {
|
|
512
|
-
return recordZcodeLifecycle(context, input);
|
|
512
|
+
return recordZcodeLifecycle(context, input, undefined, true);
|
|
513
513
|
}
|
|
514
|
-
function recordZcodeLifecycle(context, input, invocationId) {
|
|
514
|
+
function recordZcodeLifecycle(context, input, invocationId, nativePreTool = false) {
|
|
515
515
|
const hook = parseJsonObject(input.stdin || "{}", "ZCode hook event");
|
|
516
516
|
const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
|
|
517
517
|
if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use")
|
|
@@ -524,6 +524,11 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
524
524
|
const lifecycle = lifecycleFacts(command);
|
|
525
525
|
if (!lifecycle && !continuationCommand(command))
|
|
526
526
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
527
|
+
// ZCode can execute background Bash without emitting the ACP tool_call that admits an issued CLI invocation.
|
|
528
|
+
if (nativePreTool && lifecycle && rawInput.run_in_background === true) {
|
|
529
|
+
const reason = "Run dd-flow lifecycle commands in the foreground; ZCode background Bash does not provide the required admission receipt.";
|
|
530
|
+
return { continue: false, reason, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
531
|
+
}
|
|
527
532
|
if (!invocationId && lifecycle && commandOption(lifecycle.invocation, "invocation-id"))
|
|
528
533
|
return { ok: true, observed: false, reason: "cli_invocation_owns_admission" };
|
|
529
534
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -17,7 +17,7 @@ import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime
|
|
|
17
17
|
import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
|
|
18
18
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
19
19
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
20
|
-
import { checkReceiptEvidenceValid, checkReceipts,
|
|
20
|
+
import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, mergeCheckProfileHash, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
21
21
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
22
|
import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
23
23
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
@@ -71,7 +71,8 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
71
71
|
const freezeFile = path.join(freezeRoot, "source-freeze.json");
|
|
72
72
|
const gateFile = path.join(freezeRoot, "merge-gate.json");
|
|
73
73
|
fs.mkdirSync(freezeRoot, { recursive: true });
|
|
74
|
-
|
|
74
|
+
const profileHash = mergeCheckProfileHash(run.workspace_root, targetWorkspace, effective);
|
|
75
|
+
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash, now: context.now() });
|
|
75
76
|
let sourceCommit;
|
|
76
77
|
let sourcePaths;
|
|
77
78
|
if (fs.existsSync(freezeFile)) {
|
|
@@ -109,7 +110,7 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
109
110
|
return requestView(context, raced);
|
|
110
111
|
const child = createChildWork(context, { parentWorkId: root.work_id, stage: "merge", slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
111
112
|
const requestId = nextMergeRequestId(context);
|
|
112
|
-
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route,
|
|
113
|
+
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, profileHash, replacementOf(run), now, now]);
|
|
113
114
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
|
|
114
115
|
return requestView(context, requireRequest(context, requestId));
|
|
115
116
|
});
|