@deksden-com/dd-flow-cli 0.9.0-beta.92 → 0.9.0-beta.94

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.94
4
+
5
+ ### Patch Changes
6
+
7
+ - Admit public ZCode lifecycle commands through their native receipt without exposing runtime-owned invocation IDs.
8
+
9
+ ## 0.9.0-beta.93
10
+
11
+ ### Patch Changes
12
+
13
+ - Move native ZCode request usage accounting into dd-zcode, preserve causal native errors, and distinguish bridge contract support from exact-tuple runtime qualification.
14
+
3
15
  ## 0.9.0-beta.92
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # dd-flow-cli
2
2
 
3
+ See [dd-zcode integration](src/harness-runtime/DD-ZCODE.md) for native transport,
4
+ adapter ownership and the versioned bridge contract.
5
+
3
6
  ## Managed temporary HTTP services
4
7
 
5
8
  For an interactive scenario, `runtime process start --run <RUN-ID>
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.92",
4
- "cli_commit": "c1017bf64843d6979b9dee222dc0831baff6ad0a",
5
- "built_at": "2026-09-22T04:33:57.178Z",
3
+ "cli_version": "0.9.0-beta.94",
4
+ "cli_commit": "852591e0c29473e4bc7bef90aa2f83d66b8792de",
5
+ "built_at": "2026-09-22T06:20:13.963Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "d1a6081ab15ab92ac917ff5d037121a40c709db1",
@@ -0,0 +1,87 @@
1
+ # dd-zcode integration boundary
2
+
3
+ `bin/dd-zcode.mjs`, `lib/dd-zcode.mjs` and `lib/dd-zcode-daemon.mjs` implement the
4
+ dd-flow adapter over ACP. The maintained bridge is
5
+ [deksden-com/zcode-acp](https://github.com/deksden-com/zcode-acp), based on upstream
6
+ [william0wang/zcode-acp](https://github.com/william0wang/zcode-acp).
7
+
8
+ | Owner | Responsibility |
9
+ | --- | --- |
10
+ | Upstream bridge | ZCode protocol compatibility, ACP transport, native event translation and general session mechanics. |
11
+ | Downstream bridge extensions | Expose native identity, evidence and control primitives missing upstream; preserve native errors and side-effect semantics. |
12
+ | dd-zcode | Requested/observed profile checks, native-to-dd-flow identity mapping, event normalization, lifecycle forwarding, execution daemon ownership, cancellation/settlement verification and usage forwarding. |
13
+ | dd-flow services | Durable lifecycle decisions, Work/RUN state and usage ingestion/accounting. |
14
+ | dd-eval | Qualify the complete tuple, pin profiles/artifacts and run/monitor evaluations. |
15
+
16
+ Native turn completion is evidence, not proof of semantic Work success. A
17
+ closed transport is not proof that its native descendants stopped. The adapter
18
+ must retain causal errors, missing observations and usage completeness rather
19
+ than turn them into successful/zero-valued results.
20
+
21
+ ## Bridge contract
22
+
23
+ The identity check uses `--dd-harness-version` (`dd-zcode-harness@1` for existing
24
+ qualified builds, `dd-zcode-harness@2` for the native-usage candidate) and
25
+ `--dd-harness-commit`, alongside native and bridge versions. This is the current
26
+ mechanism, not a general capability negotiation protocol. `zcodeLifecycleQualification`
27
+ also admits exact native-binary SHA / bridge-commit tuples; a new tuple requires
28
+ behavioral evidence before admission. Merely changing an allowlist is not that
29
+ evidence.
30
+
31
+ The adapter consumes `zcode/session/resolve`, `read`, `subagents`, `usage`,
32
+ `close`, `resident` and `retainedSubagents`, plus standard/existing session
33
+ operations. Resolution must preserve the session's trusted workspace.
34
+ Residency and retained-tree reads after close must not implicitly resume it.
35
+ Cancellation must target owned sessions and verify resulting tree state.
36
+ Profile facts must come from native readback. Session updates supply tool events.
37
+ The adapter forwards a standalone public lifecycle command to `dd-flow zcode
38
+ event handle`; native Session/tool identity binds it to the one exact issued
39
+ assignment in that daemon scope. The command never exposes the runtime-owned
40
+ invocation ID. Its shell inherits `DD_FLOW_DAEMON_ID`, finds the assignment and
41
+ waits for the committed receipt before any lifecycle mutation can execute.
42
+
43
+ The execution-scoped daemon preserves the live native handles needed for
44
+ background children. Inspection remains available during productive operations;
45
+ a lost owner cannot be repaired by pretending retained topology is a live tree.
46
+ Keep process identity, native identity and adapter control locator distinct.
47
+
48
+ ## Usage and contract @2
49
+
50
+ The @2 bridge returns native usage unchanged. `lib/zcode-usage.mjs` reads native
51
+ session evidence and computes request counters in the adapter. It deduplicates
52
+ identified requests, rejects conflicting/invalid measurements and records read
53
+ failures as incomplete evidence. Full history is required for cumulative usage;
54
+ turn-liveness history limits must not be applied here. Immutable @1 bridges
55
+ remain supported through their existing measured projection. All adapter usage
56
+ paths use the same helper; dd-flow services still own accounting deltas.
57
+
58
+ Contract support does not admit a new binary/commit tuple automatically. The
59
+ v0.46.7 @2 candidate is not added to production qualification without live
60
+ receipts. Preserve that fail-closed distinction when preparing the next eval.
61
+
62
+ Future consumer-only retry, recovery, readiness and accounting rules belong
63
+ here or in the owning dd-flow service. Bridge changes are justified when native
64
+ access or protocol mechanics cannot be implemented through its public interface.
65
+ Review existing patches individually; do not claim that all policy has already
66
+ been extracted from the bridge.
67
+
68
+ ## Verification and operations
69
+
70
+ Relevant adapter regressions include `test/zcode-invocation-observer.test.ts`;
71
+ select the additional daemon, usage and lifecycle tests affected by a change.
72
+ Bridge tests check transport facts; adapter tests check their interpretation;
73
+ live qualification checks the actual installed tuple. Release the adapter with
74
+ dd-flow when adapter code or qualification admission changes. A bridge-only
75
+ update does not inherently require an adapter release, but current exact-commit
76
+ admission may require one.
77
+
78
+ Follow the [ZCode upgrade runbook](https://github.com/deksden-com/dd-eval/blob/main/runbooks/update-zcode.md)
79
+ for release discovery, qualification, rollout and rollback. Fork patch maintenance
80
+ is documented in the fork itself. Neither procedure is duplicated here.
81
+
82
+ Adapter changes use short-lived `feat/`, `fix/` or `docs/` branches from `main`,
83
+ reviewed with affected checks before integration (normally squash merge).
84
+ Releases identify verified immutable artifacts, not moving main HEAD. See the
85
+ [cross-repository Git workflow](https://github.com/deksden-com/dd-eval/blob/main/runbooks/git-workflow.md)
86
+ for upgrade ordering, candidate acceptance and hotfixes. Branch-protection
87
+ configuration is separate from this documented policy.
@@ -602,7 +602,7 @@ export async function serveDaemon(stateDir) {
602
602
  const paths = locations(stateDir);
603
603
  const state = await readState(paths.dir);
604
604
  if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible");
605
- const bridge = new AcpBridge(state.config);
605
+ const bridge = new AcpBridge({ ...state.config, env: { ...state.config.env, DD_FLOW_DAEMON_ID: state.daemon_id } });
606
606
  const initialized = await bridge.start();
607
607
  const runtime = new DaemonRuntime(paths, state, bridge, initialized);
608
608
  await removeSocket(paths.socket, paths.dir);
@@ -9,7 +9,9 @@ import readline from "node:readline";
9
9
  import { stopProcessGroup } from "./managed-daemon.mjs";
10
10
  import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
11
11
 
12
+ import { readZcodeUsage } from "./zcode-usage.mjs";
12
13
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
14
+ export const supportedZcodeHarnessContract = contract => [ZCODE_HARNESS_CONTRACT, "dd-zcode-harness@2"].includes(contract);
13
15
  // A deadline is an observation boundary, not permission to replay an ACP
14
16
  // request. Keep the original correlation slot briefly so a slow native
15
17
  // response (especially cold-start/session creation) can still settle it.
@@ -263,7 +265,7 @@ export class AcpBridge {
263
265
  const providerDetails = provider?.message ?? null;
264
266
  const messageText = [message.error.message, details, providerDetails].filter(Boolean).map(value => typeof value === "string" ? value : JSON.stringify(value)).join(": ") || JSON.stringify(message.error);
265
267
  const error = new Error(messageText);
266
- const nativeCode = typeof data?.code === "string" ? data.code : null;
268
+ const nativeCode = typeof data?.code === "string" ? data.code : typeof data?.native_code === "string" ? data.native_code : null;
267
269
  const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
268
270
  error.code = unknown ? nativeCode : /rate limit/i.test(messageText) ? "provider_rate_limited" : /payment required|usage balance exhausted/i.test(messageText) ? "provider_quota_exhausted" : "acp_request_failed";
269
271
  error.retryable = !unknown && /rate limit/i.test(messageText);
@@ -459,7 +461,7 @@ async function inspect(bridge, sessionId) {
459
461
  bridge.request("zcode/session/subagents", { sessionId }),
460
462
  ]);
461
463
  let usage = null, usage_observation_error = null;
462
- try { usage = await bridge.request("zcode/session/usage", { sessionId }); }
464
+ try { usage = await readZcodeUsage(bridge, sessionId); }
463
465
  catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
464
466
  const observed = observedProfile(read);
465
467
  await bridge.recordModel?.(sessionId, observed, "zcode/session/read");
@@ -708,7 +710,7 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
708
710
  const { update, native, session } = facts;
709
711
  const command = update.rawInput?.command ?? update.rawInput?.cmd;
710
712
  const toolName = update._meta?.claudeCode?.toolName;
711
- if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string" || !/(?:^|\s)--invocation-id\s/.test(command)) return;
713
+ if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
712
714
  const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
713
715
  if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
714
716
  await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
@@ -768,12 +770,12 @@ export async function doctor(options = {}) {
768
770
  const zcodeAcp = await commandOutput(options.bin, ["--version"]);
769
771
  const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
770
772
  const ddHarness = await commandOutput(options.bin, ["--dd-harness-version"]);
771
- const zcodePath = options.zcodePath ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
773
+ const zcodePath = options.zcodePath ?? process.env.ZCODE_BIN ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
772
774
  const zcode = await commandOutput(zcodePath, ["--version"]);
773
775
  const native = await readFile(zcodePath);
774
776
  const sha256 = createHash("sha256").update(native).digest("hex");
775
777
  const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
776
- return { compatible: qualification.status === "qualified", lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
778
+ return { compatible: qualification.status === "qualified", contract_supported: supportedZcodeHarnessContract(ddHarness), lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
777
779
  }
778
780
 
779
781
  export async function createSession(options) {
@@ -808,7 +810,7 @@ export async function createSessionWithBridge(bridge, options, initialized) {
808
810
  const profile_receipt = await applyProfile(bridge, created.sessionId, requested);
809
811
  forward = flowForwarder(options, providerSessionId, bridge);
810
812
  let usage = null, usage_observation_error = null;
811
- try { usage = await bridge.request("zcode/session/usage", { sessionId: created.sessionId }); }
813
+ try { usage = await readZcodeUsage(bridge, created.sessionId); }
812
814
  catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
813
815
  const usage_ingest_error = await forwardUsageBestEffort(options, providerSessionId, usage, bridge.toolSummary(created.sessionId));
814
816
  await bridge.flush();
@@ -836,7 +838,7 @@ export async function promptSessionWithBridge(bridge, options) {
836
838
  const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
837
839
  options.assertDispatch?.();
838
840
  let initial_usage_error = null;
839
- try { initial_usage_error = await forwardUsageBestEffort(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId)); }
841
+ try { initial_usage_error = await forwardUsageBestEffort(options, identity.providerSessionId, await readZcodeUsage(bridge, identity.adapterSessionId), bridge.toolSummary(identity.adapterSessionId)); }
840
842
  catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
841
843
  // This is the request budget. ACP silence is observed separately and must
842
844
  // never turn a long native child request into a fabricated provider failure.
@@ -0,0 +1,54 @@
1
+ /** Accounting policy over native evidence; the bridge only exposes native RPCs. */
2
+ export function normalizeZcodeUsage(usage, read) {
3
+ const result = { ...usage, requestUsageStatus: "unavailable" };
4
+ const totals = { requestTotalTokens: 0, requestInputTokens: 0, requestOutputTokens: 0,
5
+ requestReasoningTokens: 0, requestCacheCreationTokens: 0, requestCacheReadTokens: 0, requestCount: 0 };
6
+ const seen = new Map();
7
+ let incomplete = !Array.isArray(read?.messages);
8
+ for (const message of Array.isArray(read?.messages) ? read.messages : []) {
9
+ const info = message?.info;
10
+ if (info?.role !== "assistant") continue;
11
+ // Native history includes system timeline markers with role=assistant and
12
+ // zero token placeholders, but no model request (and no total counter).
13
+ if (info.semantics?.origin === "system" && info.semantics?.kind === "timeline_event") continue;
14
+ if (!info.tokens) { incomplete = true; continue; }
15
+ const tokens = info.tokens;
16
+ const id = info.messageId ?? info.id ?? message.id;
17
+ if (id) {
18
+ const signature = JSON.stringify(tokens);
19
+ if (seen.has(id)) { if (seen.get(id) !== signature) incomplete = true; continue; }
20
+ seen.set(id, signature);
21
+ }
22
+ const values = [tokens.total, tokens.input, tokens.output];
23
+ if (!values.every(valid)) { incomplete = true; continue; }
24
+ totals.requestTotalTokens += tokens.total;
25
+ totals.requestInputTokens += tokens.input;
26
+ totals.requestOutputTokens += tokens.output;
27
+ for (const [key, value] of [["requestReasoningTokens", tokens.reasoning],
28
+ ["requestCacheCreationTokens", tokens.cache?.write], ["requestCacheReadTokens", tokens.cache?.read]]) {
29
+ if (value !== undefined && !valid(value)) incomplete = true;
30
+ else totals[key] += value ?? 0;
31
+ }
32
+ totals.requestCount++;
33
+ }
34
+ if (totals.requestCount && !incomplete) Object.assign(result, totals, { requestUsageStatus: "measured" });
35
+ if (incomplete) result.usageIsIncomplete = true;
36
+ return result;
37
+ }
38
+
39
+ function valid(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
40
+
41
+ export async function readZcodeUsage(bridge, sessionId) {
42
+ const usage = await bridge.request("zcode/session/usage", { sessionId });
43
+ // Compatibility with immutable @1 bridges: they already expose this projection.
44
+ if (usage?.requestUsageStatus === "measured") return usage;
45
+ try {
46
+ // Unbounded native read is intentional: accounting needs the full history,
47
+ // unlike turn liveness reads which may use messageLimit: 1.
48
+ const read = await bridge.request("zcode/session/read", { sessionId });
49
+ return normalizeZcodeUsage(usage, read);
50
+ } catch (error) {
51
+ return { ...usage, requestUsageStatus: "unavailable", usageIsIncomplete: true,
52
+ requestUsageError: { code: error.code ?? "usage_evidence_failed", message: error.message, ...(error.details ? { details: error.details } : {}) } };
53
+ }
54
+ }
@@ -493,7 +493,7 @@ export function handleZcodeEvent(context, input) {
493
493
  return { ok: true, observed: false, reason: "non_bash_tool" };
494
494
  const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd);
495
495
  const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
496
- if (parsed.kind === "none" || !commandOption(parsed.invocation, "invocation-id"))
496
+ if (parsed.kind === "none")
497
497
  return { ok: true, observed: false, reason: "acp_evidence_only" };
498
498
  const controlled = objectRecord(objectRecord(notification._meta).ddZcode);
499
499
  const providerSessionId = stringValue(native.childSessionId) ?? stringValue(controlled.rootProviderSessionId);
@@ -920,7 +920,9 @@ export function observedLifecycleInvocation(context, command, daemonId) {
920
920
  const analysis = parseLifecycleCommand(command);
921
921
  if (analysis.kind !== "standalone" || analysis.invocation.wrapped)
922
922
  return null;
923
- const matches = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('observed','executing') ORDER BY rowid DESC")
923
+ // `issued` is a rendezvous, not authority: the CLI still blocks in
924
+ // awaitLifecycleInvocation until the native observer commits the receipt.
925
+ const matches = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing') ORDER BY rowid DESC")
924
926
  .filter(row => {
925
927
  try {
926
928
  const scope = JSON.parse(row.scope_json);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.92",
3
+ "version": "0.9.0-beta.94",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {