@deksden-com/dd-flow-cli 0.9.0-beta.95 → 0.9.0-beta.96
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 +6 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +20 -4
- package/dist/harness-runtime/bin/dd-agy.mjs +7 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +19 -4
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -5
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +29 -4
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +21 -2
- package/dist/harness-runtime/lib/dd-opencode.mjs +1 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +15 -6
- package/dist/harness-runtime/lib/managed-daemon.mjs +7 -0
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/hooks.js +8 -1
- package/dist/services/lifecycle-invocations.js +5 -1
- package/dist/services/run-controller.js +26 -16
- package/dist/services/runtime-budget.js +7 -2
- package/dist/services/vnext-code.js +2 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.96
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4769ad6: Settle early lifecycle rejections against their native calls, reconcile retained provider capacity before continuing, and preserve causal outcomes across AGY, Grok, Droid, OpenCode, Codex, and ZCode adapters.
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.95
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.96",
|
|
4
|
+
"cli_commit": "cb6c669ff3f24115ac0a4d2c04c8877eb8683bc6",
|
|
5
|
+
"built_at": "2026-09-22T12:33:21.451Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -89,9 +89,25 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
89
89
|
output = parseOutputOptions(args);
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const rejection = isAppError(error) ? error : new AppError("usage", error instanceof Error ? error.message : String(error), 2);
|
|
93
|
+
Object.assign(rejection.details, { phase: "prepare", effect: "no_effect", recoverable: true, ...rejection.details });
|
|
94
|
+
try {
|
|
95
|
+
const command = quote(["dd-flow", ...args]), event = observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID);
|
|
96
|
+
if (event) {
|
|
97
|
+
const context = createContext({ ...env }, "hook");
|
|
98
|
+
try {
|
|
99
|
+
observeLifecycleCommand(context, command, event)({ error: rejection });
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
context.db.close?.();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (persistence) {
|
|
107
|
+
rejection.details.lifecycle_diagnostic_error = String(persistence);
|
|
108
|
+
}
|
|
109
|
+
writeJson(io.stderr, { ok: false, error: { code: rejection.code, message: rejection.message, details: rejection.details } });
|
|
110
|
+
return rejection.exitCode;
|
|
95
111
|
}
|
|
96
112
|
const progress = progressForCommand(output.args);
|
|
97
113
|
let heartbeat;
|
|
@@ -284,7 +300,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
284
300
|
}
|
|
285
301
|
catch (caught) {
|
|
286
302
|
const error = isAppError(caught) ? caught : new AppError("unexpected", caught instanceof Error ? caught.message : String(caught), 1, { effect: "unknown", cause: { code: caught?.code ?? null, sqlite_extended_code: caught?.errcode ?? null } });
|
|
287
|
-
if (!settleHook && !invocationSettlement
|
|
303
|
+
if (!settleHook && !invocationSettlement) {
|
|
288
304
|
let earlyContext;
|
|
289
305
|
try {
|
|
290
306
|
const command = quote(["dd-flow", ...output.args]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { invokeNativeHook } from "../lib/native-hook-command.mjs";
|
|
5
5
|
import { doctor } from "../lib/dd-agy.mjs";
|
|
6
6
|
import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-agy-daemon.mjs";
|
|
7
7
|
|
|
@@ -10,12 +10,14 @@ const prompt = async options => options["prompt-file"] ? await readFile(options[
|
|
|
10
10
|
async function stdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
|
|
11
11
|
async function hook(options) {
|
|
12
12
|
const payload = JSON.parse(await stdin() || "{}"), event = options.event;
|
|
13
|
-
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload });
|
|
13
|
+
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload }, 10_000);
|
|
14
|
+
if (identity.stale) return event === "PreToolUse" ? { decision: "deny", reason: "agy_stale_hook" } : {};
|
|
14
15
|
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
15
16
|
const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
|
|
16
|
-
const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, execution_num: payload.executionNum, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
|
|
18
|
+
await invokeNativeHook({ bin: options["dd-flow-bin"] ?? identity.dd_flow_bin,
|
|
19
|
+
args: ["agy", "event", "handle", "--project-root", options["project-root"] ?? identity.project_root, "--json"],
|
|
20
|
+
payload: normalized, home: options["dd-flow-home"] ?? identity.dd_flow_home });
|
|
19
21
|
}
|
|
20
22
|
return event === "PreToolUse" ? { decision: "allow" } : {};
|
|
21
23
|
}
|
|
@@ -284,7 +284,10 @@ export class Runtime {
|
|
|
284
284
|
if (result.conversation_id && result.conversation_id !== this.init?.conversation_id) {
|
|
285
285
|
await this.journal("foreign_terminal_observed", { conversation_id: result.conversation_id, result });
|
|
286
286
|
const child = this.descendants.get(result.conversation_id);
|
|
287
|
-
if (child)
|
|
287
|
+
if (child) {
|
|
288
|
+
this.descendants.set(result.conversation_id, { ...child, status: result.status === "SUCCESS" ? "completed" : result.status === "ERROR" ? "failed" : "cancelled" });
|
|
289
|
+
await this.completePendingTurn();
|
|
290
|
+
}
|
|
288
291
|
return;
|
|
289
292
|
}
|
|
290
293
|
const current = this.active;
|
|
@@ -312,7 +315,19 @@ export class Runtime {
|
|
|
312
315
|
: "agy_provider_failed";
|
|
313
316
|
return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
|
|
314
317
|
}
|
|
315
|
-
|
|
318
|
+
await this.completePendingTurn();
|
|
319
|
+
}
|
|
320
|
+
async completePendingTurn() {
|
|
321
|
+
const current = this.active;
|
|
322
|
+
if (!current?.terminalResult || current.terminalResult !== this.lastResult) return;
|
|
323
|
+
await this.persist({ active_tree: !this.receipt().settled });
|
|
324
|
+
// Hooks can invalidate the tree while persistence is pending. Never
|
|
325
|
+
// publish a proof captured before that asynchronous boundary.
|
|
326
|
+
if (this.active !== current) return;
|
|
327
|
+
const receipt = this.receipt();
|
|
328
|
+
if (!receipt.settled) { await this.persist({ active_tree: true }); return; }
|
|
329
|
+
this.active = null;
|
|
330
|
+
current.resolve(receipt);
|
|
316
331
|
}
|
|
317
332
|
receipt(result = this.lastResult) {
|
|
318
333
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
@@ -365,10 +380,10 @@ export class Runtime {
|
|
|
365
380
|
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
366
381
|
return child;
|
|
367
382
|
}
|
|
368
|
-
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.
|
|
383
|
+
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
|
|
369
384
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
370
385
|
await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
|
|
371
|
-
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId,
|
|
386
|
+
const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
|
|
372
387
|
}
|
|
373
388
|
async finalizeProviderForControl() {
|
|
374
389
|
const pending = this.providerFinalization;
|
|
@@ -324,11 +324,8 @@ export class DroidRuntime {
|
|
|
324
324
|
catch (error) {
|
|
325
325
|
receipt = { provider_session_id: binding.provider_session_id, adapter_session_id: binding.provider_session_id, settled: false, settlement_observation_error: errorRecord(error) };
|
|
326
326
|
}
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// durable binding for the normal settlement observer instead of exposing
|
|
330
|
-
// a misleading successful receipt.
|
|
331
|
-
if (receipt.settled !== true) return null;
|
|
327
|
+
// Native outcome and tree settlement are independent. Retain the former
|
|
328
|
+
// even while children are running; settled:false keeps capacity reserved.
|
|
332
329
|
const current = this.active;
|
|
333
330
|
if (current?.operationId === operationId && current.turnId === binding.turn_id) {
|
|
334
331
|
current.outcome = outcome; this.lastResult = outcome;
|
|
@@ -5,7 +5,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
5
5
|
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import net from "node:net";
|
|
8
|
-
import { durableDaemonDispatch } from "./daemon-operations.mjs";
|
|
8
|
+
import { durableDaemonDispatch as dispatchOperation } from "./daemon-operations.mjs";
|
|
9
9
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
@@ -17,6 +17,10 @@ import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmD
|
|
|
17
17
|
const REQUEST_SCHEMA = "dd-grok/daemon-request@1";
|
|
18
18
|
const RESPONSE_SCHEMA = "dd-grok/daemon-response@1";
|
|
19
19
|
const STATE_SCHEMA = "dd-grok/daemon-state@1";
|
|
20
|
+
// Normalize raw socket callers too, before the shared layer reserves capacity.
|
|
21
|
+
export function durableDaemonDispatch(root, request, action, observe) {
|
|
22
|
+
return dispatchOperation(root, request.operation === "session.resume" ? { ...request, operation: "session.inspect" } : request, action, observe);
|
|
23
|
+
}
|
|
20
24
|
class DaemonError extends Error { constructor(code, message, retryable = false, details) { super(message); this.code = code; this.retryable = retryable; this.details = details; } }
|
|
21
25
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); }
|
|
22
26
|
function locations(stateDir) { const dir = absolute(stateDir, "--state-dir"); const localSocket = path.join(dir, "daemon.sock"); return { dir, socket: Buffer.byteLength(localSocket) < 100 ? localSocket : `/tmp/dd-grok-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}.sock`, state: path.join(dir, "daemon.json"), log: path.join(dir, "daemon.log"), home: path.join(dir, "grok-home") }; }
|
|
@@ -142,6 +146,8 @@ async function materializeSessionArchive(paths, archiveDir) {
|
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
149
|
+
// Grok resume observes an already-owned Session; it never starts a turn.
|
|
150
|
+
if (operation === "session.resume") operation = "session.inspect";
|
|
145
151
|
const { socket } = locations(stateDir); const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
146
152
|
return await new Promise((resolve, reject) => {
|
|
147
153
|
const client = net.createConnection(socket); let buffer = ""; let settled = false;
|
|
@@ -233,13 +239,32 @@ export class Runtime {
|
|
|
233
239
|
return running;
|
|
234
240
|
}
|
|
235
241
|
async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
|
|
236
|
-
async productive(name, task
|
|
242
|
+
async productive(name, task, sessionId) {
|
|
243
|
+
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
244
|
+
this.active = name;
|
|
245
|
+
// Only a new native turn invalidates its own root proof. Creation, fork
|
|
246
|
+
// and local archive export must not erase an unrelated Session's receipt.
|
|
247
|
+
if (name === "session.prompt" && this.sessions.has(sessionId)) this.sessions.get(sessionId).native_root_receipt = null;
|
|
248
|
+
try {
|
|
249
|
+
await this.persist({ active_tree: true, active_operation: name });
|
|
250
|
+
const result = await task();
|
|
251
|
+
this.track(result);
|
|
252
|
+
const session = this.sessions.get(result.provider_session_id);
|
|
253
|
+
if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id };
|
|
254
|
+
// Archive requires an idle tree before entering this exclusive section
|
|
255
|
+
// and performs no native work. Its file receipt is not tree evidence.
|
|
256
|
+
const running = name === "session.archive" ? false : this.treeRunning(result);
|
|
257
|
+
await this.persist({ active_tree: running, active_operation: null });
|
|
258
|
+
return { ...result, descendants: this.topology(result.provider_session_id), settled: !running };
|
|
259
|
+
} catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; }
|
|
260
|
+
finally { this.active = null; }
|
|
261
|
+
}
|
|
237
262
|
async dispatch(operation, params) {
|
|
238
263
|
const assertDispatch = captureDispatchGuard(this);
|
|
239
264
|
if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config }; }
|
|
240
|
-
if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root");
|
|
265
|
+
if (operation === "hook.resolve") { const sessionId = String(params.sessionId ?? ""); if (!sessionId) throw new DaemonError("hook_identity_missing", "hook has no sessionId"); let session = this.sessions.get(sessionId); if (!session) { const roots = [...this.sessions.values()].filter((item) => !item.parent_provider_session_id); if (roots.length !== 1) throw new DaemonError("hook_identity_unknown", "unknown hook Session has no unique root"); const observed = await inspectSessionWithBridge(this.bridge, this.options({ sessionId, liveSession: true })); this.track(observed); session = this.sessions.get(sessionId); if (!session || session.root_provider_session_id !== roots[0].provider_session_id) throw new DaemonError("hook_identity_unknown", "hook Session is not a member of the retained native tree"); await this.persist(); } return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} }; }
|
|
241
266
|
if (operation === "session.create") return await this.productive(operation, async () => { const result = await createSessionWithBridge(this.bridge, { ...this.options(params), onSessionCreated: async (session) => { this.track(session); this.loadedSessionId = session.provider_session_id; await this.persist(); } }, this.initialized); this.loadedSessionId = result.provider_session_id; return result; });
|
|
242
|
-
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch }); this.loadedSessionId = params.sessionId; return result; }); }
|
|
267
|
+
if (operation === "session.prompt") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await promptSessionWithBridge(this.bridge, { ...this.options(params), assertDispatch }); this.loadedSessionId = params.sessionId; return result; }, params.sessionId); }
|
|
243
268
|
if (operation === "session.fork") { await this.requireSettled(); return await this.productive(operation, async () => { const result = await forkSessionWithBridge(this.bridge, this.options(params)); this.loadedSessionId = params.sessionId; return result; }); }
|
|
244
269
|
if (operation === "session.inspect" || operation === "session.resume") { const result = await inspectSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); this.track(result); await this.persist(); return { ...result, settled: !this.active && !this.treeRunning(result), descendants: this.topology(result.provider_session_id) }; }
|
|
245
270
|
if (operation === "session.cancel") { if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon"); cancelPendingDispatch(this); const result = await cancelSessionWithBridge(this.bridge, this.options({ ...params, liveSession: true })); await this.persist({ active_tree: Boolean(this.active) || !result.settled }); return result; }
|
|
@@ -34,7 +34,8 @@ function errorPayload(error) { return { code: error.code ?? "operation_failed",
|
|
|
34
34
|
|
|
35
35
|
async function writePlugin(paths, config) {
|
|
36
36
|
const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
37
|
-
const
|
|
37
|
+
const helper = new URL("./native-hook-command.mjs", import.meta.url).href;
|
|
38
|
+
const source = `import { invokeNativeHook } from ${JSON.stringify(helper)};\nconst candidate = command => typeof command === "string" && command.length > 0;\nconst participating = new Set();\nconst invoke = async payload => await invokeNativeHook({bin:process.env.DD_OPENCODE_DD_FLOW_BIN,args:["opencode","event","handle","--project-root",process.env.DD_OPENCODE_PROJECT_ROOT,"--json"],payload,home:process.env.DD_FLOW_HOME,timeoutMs:15000});\nexport const DdFlowPlugin=async({client,project,directory})=>{ const envelope=async(phase,input,extra={})=>{ const response=await client.session.get({path:{id:input.sessionID}}); if(response?.error||!response?.data) throw Object.assign(new Error("OpenCode Session identity is unavailable"),{code:"opencode_session_identity_unavailable"}); const session=response.data; return {schema_id:"dd-flow/opencode-tool-event@1",event_id:[input.sessionID,input.callID,phase].join(":"),phase,daemon_id:process.env.DD_OPENCODE_DAEMON_ID,server_version:process.env.DD_OPENCODE_SERVER_VERSION,plugin_sha256:process.env.DD_OPENCODE_PLUGIN_SHA256,session:{provider_session_id:input.sessionID,parent_provider_session_id:session.parentID??null,agent_id:process.env.DD_OPENCODE_AGENT,directory:session.directory??directory,project_id:session.projectID??project?.id},message_id:input.messageID??null,tool_call_id:input.callID,tool:input.tool,profile:{provider:process.env.DD_OPENCODE_PROVIDER,model:process.env.DD_OPENCODE_MODEL,variant:process.env.DD_OPENCODE_VARIANT,agent:process.env.DD_OPENCODE_AGENT},...extra}; }; return {"tool.execute.before":async(input,output)=>{ const command=output.args?.command; if(!candidate(command)) return; const result=await invoke(await envelope("before",input,{input:output.args})); if(result?.observed) participating.add(input.callID); const updated=result?.hookSpecificOutput?.updatedInput; if(updated) Object.assign(output.args,updated); },"tool.execute.after":async(input,output)=>{ if(!participating.delete(input.callID)) return; await invoke(await envelope("after",input,{input:input.args,outcome:{status:"completed",title:output.title}})); }}; };\n`;
|
|
38
39
|
const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
|
|
39
40
|
}
|
|
40
41
|
async function prepare(paths, config, authSource) {
|
|
@@ -84,7 +85,23 @@ export class Runtime {
|
|
|
84
85
|
}
|
|
85
86
|
async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
|
|
86
87
|
async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
|
|
87
|
-
async productive(name, task) {
|
|
88
|
+
async productive(name, task) {
|
|
89
|
+
if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true);
|
|
90
|
+
this.active = name; let result, completed = false;
|
|
91
|
+
try {
|
|
92
|
+
await this.persist({ active_tree: true, active_operation: name });
|
|
93
|
+
result = await task(); completed = true;
|
|
94
|
+
await this.persist({ active_tree: !result?.settled, active_operation: null });
|
|
95
|
+
await this.journal(name, result); return result;
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
const error = completed ? new DaemonError("operation_observation_lost", "OpenCode returned but its durable observation could not be saved; do not resend", false, { result, cause: errorPayload(cause) }) : cause;
|
|
98
|
+
const failures = [];
|
|
99
|
+
try { await this.persist({ active_tree: true, active_operation: null }); } catch (secondary) { failures.push(errorPayload(secondary)); }
|
|
100
|
+
try { await this.journal(`${name}.failed`, null); } catch (secondary) { failures.push(errorPayload(secondary)); }
|
|
101
|
+
if (failures.length) error.details = { ...error.details, observation_errors: failures };
|
|
102
|
+
throw error;
|
|
103
|
+
} finally { this.active = null; }
|
|
104
|
+
}
|
|
88
105
|
async cancelTree(id, seen = new Set()) {
|
|
89
106
|
if (seen.has(id)) return [];
|
|
90
107
|
seen.add(id);
|
|
@@ -170,6 +187,8 @@ export class Runtime {
|
|
|
170
187
|
},
|
|
171
188
|
...(params.cancelTree ? { cancel: id => this.cancelTree(id) } : {})
|
|
172
189
|
});
|
|
190
|
+
// The durable clean receipt must follow the owned physical stop barrier.
|
|
191
|
+
await stopProcessGroup(this.child);
|
|
173
192
|
await this.persist({ active_tree: false, shutdown_state: "stopping" });
|
|
174
193
|
return { stopped: true, clean: true, _shutdown: true };
|
|
175
194
|
}
|
|
@@ -21,7 +21,7 @@ export class OpenCodeClient {
|
|
|
21
21
|
} catch (error) {
|
|
22
22
|
if (error instanceof OpenCodeError) throw error;
|
|
23
23
|
// Aborting HTTP only loses the observer; it does not cancel the provider.
|
|
24
|
-
throw new OpenCodeError(error.name === "AbortError" ? "operation_observation_lost" : "opencode_unavailable", error.message, true, { method, pathname });
|
|
24
|
+
throw new OpenCodeError(error.name === "AbortError" || method === "POST" ? "operation_observation_lost" : "opencode_unavailable", error.message, true, { method, pathname });
|
|
25
25
|
} finally { clearTimeout(timer); }
|
|
26
26
|
}
|
|
27
27
|
health() { return this.request("GET", "/global/health", undefined, { directory: null, timeoutMs: 5_000 }); }
|
|
@@ -857,25 +857,34 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
857
857
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
858
858
|
const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
|
|
859
859
|
if (completionEvidence === "inferred" || completionEvidence === "failed") {
|
|
860
|
-
|
|
860
|
+
let observationError = null;
|
|
861
|
+
try { await bridge.flush(); } catch (error) { observationError = { code: error.code ?? "zcode_post_turn_observation_failed", message: error.message ?? String(error) }; }
|
|
861
862
|
const error = new Error(`ZCode completion is ${completionEvidence}, not a confirmed successful native turn`);
|
|
862
863
|
error.code = completionEvidence === "inferred" ? "native_outcome_unknown" : "zcode_turn_not_completed";
|
|
863
864
|
error.retryable = false;
|
|
864
|
-
error.details = { completion_evidence: completionEvidence, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId };
|
|
865
|
+
error.details = { completion_evidence: completionEvidence, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
|
|
865
866
|
throw error;
|
|
866
867
|
}
|
|
867
868
|
if (stopReason && stopReason !== "end_turn") {
|
|
868
869
|
// A close can settle the pending prompt before its terminal event. Do not
|
|
869
870
|
// collect live evidence first: ordinary inspection is allowed to recover
|
|
870
871
|
// an evicted Session, which would undo the just-confirmed close.
|
|
871
|
-
|
|
872
|
+
let observationError = null;
|
|
873
|
+
try { await bridge.flush(); } catch (error) { observationError = { code: error.code ?? "zcode_post_turn_observation_failed", message: error.message ?? String(error) }; }
|
|
872
874
|
const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
|
|
873
875
|
error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
|
|
874
|
-
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId };
|
|
876
|
+
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
|
|
877
|
+
throw error;
|
|
878
|
+
}
|
|
879
|
+
let evidence;
|
|
880
|
+
try { await bridge.flush(); evidence = await inspect(bridge, identity.adapterSessionId); }
|
|
881
|
+
catch (cause) {
|
|
882
|
+
const error = new Error("ZCode completed the native turn, but post-turn observation failed");
|
|
883
|
+
error.code = "native_outcome_observation_failed";
|
|
884
|
+
error.retryable = false;
|
|
885
|
+
error.details = { native_outcome: { stop_reason: stopReason ?? "end_turn", completion_evidence: completionEvidence ?? "native" }, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, observation_error: { code: cause.code ?? "zcode_post_turn_observation_failed", message: cause.message ?? String(cause) } };
|
|
875
886
|
throw error;
|
|
876
887
|
}
|
|
877
|
-
await bridge.flush();
|
|
878
|
-
const evidence = await inspect(bridge, identity.adapterSessionId);
|
|
879
888
|
evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
|
|
880
889
|
const usage_ingest_error = await forwardUsageBestEffort(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
|
|
881
890
|
if (initial_usage_error) evidence.initial_usage_error = initial_usage_error;
|
|
@@ -64,6 +64,13 @@ export async function assertDaemonOwnership(stateDir, dispatch = {}, assertCurre
|
|
|
64
64
|
assertCurrent();
|
|
65
65
|
if (result?.ok === true && result.admitted === true && result.process_id === record.id) return;
|
|
66
66
|
if (result?.ok !== true || result.reason !== "provider_capacity" || result.process_id !== record.id) throw Object.assign(new Error("productive runtime admission was not confirmed"), { code: "process_ownership_unknown" });
|
|
67
|
+
const predecessor = result.blocking_turns?.find(turn => turn?.process_id === record.id
|
|
68
|
+
&& turn?.session_id === dispatch["native-session-id"]
|
|
69
|
+
&& turn?.operation_id !== dispatch["operation-id"]);
|
|
70
|
+
if (predecessor) throw Object.assign(new Error("the same managed Session has an unsettled productive predecessor; reconcile it before dispatching another Turn"), {
|
|
71
|
+
code: "settlement_reconciliation_required",
|
|
72
|
+
details: { session_id: dispatch["native-session-id"], predecessor_operation_id: predecessor.operation_id, predecessor_operation: predecessor.operation }
|
|
73
|
+
});
|
|
67
74
|
// The daemon, not its disposable socket client, owns this queue. Recheck
|
|
68
75
|
// the RUN fence and process lease on every admission attempt.
|
|
69
76
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
@@ -12,7 +12,7 @@ export function assertNoCorrelatedRejection(context, scope) {
|
|
|
12
12
|
const latest = new Map();
|
|
13
13
|
for (const row of rows) {
|
|
14
14
|
const value = JSON.parse(row.outcome_json);
|
|
15
|
-
latest.set(value.operation, value);
|
|
15
|
+
latest.set(JSON.stringify([value.operation, value.session_id ?? null, value.work_id ?? null, value.attempt ?? null]), value);
|
|
16
16
|
}
|
|
17
17
|
for (const value of latest.values())
|
|
18
18
|
if (value.error && !isRegisteredRepair(context, value.error.details.continuation, scope))
|
package/dist/services/hooks.js
CHANGED
|
@@ -619,7 +619,8 @@ export function handleGrokEvent(context, input) {
|
|
|
619
619
|
const daemonId = stringValue(ddGrok.daemonId);
|
|
620
620
|
const agentId = stringValue(hook.agent_id) ?? stringValue(hook.agentId);
|
|
621
621
|
const eventKey = stringValue(hook.event_id) ?? stringValue(hook.eventId) ?? crypto.createHash("sha256").update(JSON.stringify({
|
|
622
|
-
harness: "grok-acp", providerSessionId,
|
|
622
|
+
harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
|
|
623
|
+
promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
|
|
623
624
|
})).digest("hex");
|
|
624
625
|
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: expectedRoot });
|
|
625
626
|
const payload = {
|
|
@@ -740,6 +741,12 @@ export function handleAgyEvent(context, input) {
|
|
|
740
741
|
const toolName = stringValue(event.tool);
|
|
741
742
|
if (!directory || !providerSessionId || !daemonId || !eventId || !toolName)
|
|
742
743
|
throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
|
|
744
|
+
const rawInput = objectRecord(event.input);
|
|
745
|
+
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
746
|
+
if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command))
|
|
747
|
+
&& (!Number.isInteger(event.execution_num) || !Number.isInteger(event.step_index))) {
|
|
748
|
+
throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require native execution and step identity", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
749
|
+
}
|
|
743
750
|
const translated = {
|
|
744
751
|
schema_id: "dd-flow/opencode-tool-event@1", source_harness: "antigravity-cli", phase: event.phase, event_id: eventId,
|
|
745
752
|
daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
|
|
@@ -874,7 +874,11 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
|
|
|
874
874
|
return null;
|
|
875
875
|
const parsed = parseLifecycleCommand(command);
|
|
876
876
|
const eventKey = parsed.kind === "standalone" ? commandOption(parsed.invocation, "hook-event-id") : undefined;
|
|
877
|
-
const matches = context.db.all(
|
|
877
|
+
const matches = context.db.all(daemonId
|
|
878
|
+
? "SELECT project_id, id, event_key, sanitized_summary FROM hook_events WHERE daemon_id = ? AND status = 'observed' AND outcome_json IS NULL"
|
|
879
|
+
: eventKey
|
|
880
|
+
? "SELECT project_id, id, event_key, sanitized_summary FROM hook_events WHERE event_key = ? AND status = 'observed' AND outcome_json IS NULL"
|
|
881
|
+
: "SELECT project_id, id, event_key, sanitized_summary FROM hook_events WHERE harness IN ('antigravity-cli','codex-desktop') AND status = 'observed' AND outcome_json IS NULL", daemonId || eventKey ? [daemonId ?? eventKey] : []).filter(row => {
|
|
878
882
|
if (eventKey && row.event_key !== eventKey)
|
|
879
883
|
return false;
|
|
880
884
|
try {
|
|
@@ -527,30 +527,40 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
527
527
|
if (failure)
|
|
528
528
|
throw failure.reason;
|
|
529
529
|
};
|
|
530
|
+
const reconcilePromptPredecessor = async (session) => {
|
|
531
|
+
const prior = state.last_receipt?.settlement;
|
|
532
|
+
if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
|
|
533
|
+
return;
|
|
534
|
+
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
535
|
+
state.last_receipt = receipt;
|
|
536
|
+
saveState(context, row, state);
|
|
537
|
+
const current = receipt.settlement;
|
|
538
|
+
if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
|
|
539
|
+
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
540
|
+
}
|
|
541
|
+
};
|
|
530
542
|
const prompt = async (session, text, operationId) => {
|
|
543
|
+
await reconcilePromptPredecessor(session);
|
|
531
544
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
532
545
|
fs.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
|
|
533
546
|
state.turns += 1;
|
|
534
547
|
saveState(context, row, state);
|
|
535
548
|
let receipt;
|
|
549
|
+
const adapter = controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, operationId, cancellation.signal);
|
|
536
550
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
await adapter.catch(() => { });
|
|
550
|
-
throw fatal ?? error;
|
|
551
|
-
}
|
|
551
|
+
receipt = await adapter;
|
|
552
|
+
checkOutcomes();
|
|
553
|
+
if (fatal)
|
|
554
|
+
throw fatal;
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
558
|
+
throw error;
|
|
559
|
+
await stopAfterFailure(error);
|
|
560
|
+
cancellation.abort();
|
|
561
|
+
await adapter.catch(() => { });
|
|
562
|
+
throw fatal ?? error;
|
|
552
563
|
}
|
|
553
|
-
finally { /* Adapter settlement is joined before returning. */ }
|
|
554
564
|
state.last_receipt = receipt;
|
|
555
565
|
saveState(context, row, state);
|
|
556
566
|
return receipt;
|
|
@@ -328,10 +328,15 @@ export function reserveProviderTurn(context, record, input) {
|
|
|
328
328
|
db.exec("COMMIT");
|
|
329
329
|
return { admitted: true, budget_scope: budget.scope_id, reused: true };
|
|
330
330
|
}
|
|
331
|
-
const
|
|
331
|
+
const activeTurns = db.all("SELECT process_id, metadata_json FROM managed_resources WHERE resource_kind = 'provider-turn' AND json_extract(metadata_json, '$.scope_id') = ? AND json_extract(metadata_json, '$.harness') = ? ORDER BY created_at, resource_key", [budget.scope_id, harness]);
|
|
332
|
+
const active = activeTurns.length;
|
|
332
333
|
if (active >= limit) {
|
|
334
|
+
const blocking_turns = activeTurns.map(turn => {
|
|
335
|
+
const claim = JSON.parse(turn.metadata_json);
|
|
336
|
+
return { process_id: turn.process_id, operation_id: claim.operation_id ?? null, operation: claim.operation ?? null, session_id: claim.session_id ?? null };
|
|
337
|
+
});
|
|
333
338
|
db.exec("COMMIT");
|
|
334
|
-
return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active };
|
|
339
|
+
return { admitted: false, reason: "provider_capacity", budget_scope: budget.scope_id, harness, limit, active, blocking_turns };
|
|
335
340
|
}
|
|
336
341
|
db.run("INSERT INTO managed_resources (resource_kind, resource_key, owner_id, lease_token, lease_expires_at, process_id, metadata_json, created_at, updated_at) VALUES ('provider-turn', ?, ?, ?, ?, ?, ?, ?, ?)", [key, record.owner_id, record.lease_token, record.lease_expires_at, record.id, JSON.stringify({ scope_id: budget.scope_id, harness, operation_id: input.operationId, operation: input.operation, session_id: input.sessionId ?? null, state_dir: metadata.state_dir }), now, now]);
|
|
337
342
|
db.exec("COMMIT");
|
|
@@ -527,8 +527,9 @@ function coordinatorPrompt(context, input) {
|
|
|
527
527
|
"",
|
|
528
528
|
"<verification_contract>",
|
|
529
529
|
"```json",
|
|
530
|
-
JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed
|
|
530
|
+
JSON.stringify({ schema_id: "dd-flow/code-verification@2", verdict: "passed", summary: "Evidence-backed conclusion.", unresolved: [], deviations: [] }, null, 2),
|
|
531
531
|
"```",
|
|
532
|
+
"Allowed verdict values are passed, needs_repair, and blocked. The example is valid JSON using passed; choose exactly one value.",
|
|
532
533
|
"The CLI verifies graph coverage and executes all deterministic checks. Do not duplicate its report; record only your semantic conclusion and evidence.",
|
|
533
534
|
"</verification_contract>",
|
|
534
535
|
"",
|
|
@@ -481,7 +481,7 @@ function resultTemplate(obligations = []) {
|
|
|
481
481
|
}
|
|
482
482
|
function renderPrompt(input) {
|
|
483
483
|
const obligationList = input.obligations.map((obligation) => `- ${obligation.id}: ${obligation.statement}`).join("\n");
|
|
484
|
-
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(), null, 2)}\n\`\`\``, "
|
|
484
|
+
return ["<work_context>", `- RUN: ${input.runId}`, `- Work: ${input.workId}`, `- Accepted SPECIFY JSON: ${input.specifyPath}`, `- Handoff: ${input.handoff.stage_handoff.effective}`, "</work_context>", "", "<accepted_obligations>", obligationList, "</accepted_obligations>", "", "<frozen_git_policy>", `- route: ${input.policy.route}`, `- integration branch: ${input.policy.integration_branch ?? "not applicable"}`, `- base ref: ${input.policy.base_ref ?? "not applicable"}`, `- feature branch: ${input.policy.feature_branch ?? "not applicable"}`, `- service worktree: ${input.policy.worktree_path ?? "not applicable"}`, `- bootstrap: ${input.policy.bootstrap_status}`, `- decision: ${input.policy.reason}`, "The CLI already created and bootstrapped this workspace at PROTOCOLIZE start. Do not create branches, worktrees, protocol files or Git commits yourself. This transition Work may run from the stable session; edit only the supplied RUN result file.", "</frozen_git_policy>", "", "<catalog>", input.catalog.length ? input.catalog.map((x) => `- ${x}`).join("\n") : "- inactive", "</catalog>", "", "<stage_instructions>", input.template.trim(), "</stage_instructions>", "", "<output_contract>", `Edit only ${input.resultPath}. Keep this exact JSON shape; CLI allocates ids and writes PRT/PSET/feature documents in the already provisioned workspace:\n\n\`\`\`json\n${JSON.stringify(resultTemplate(input.obligations), null, 2)}\n\`\`\``, "obligation_id values and their one-per-obligation entries are prefilled by the CLI. Preserve those IDs exactly; change only member_keys when semantic ownership requires it. The CLI rejects unknown, duplicate or missing obligations, and every member must own at least one AC-*.", "If PROTOCOLIZE itself exposes a material user decision with no reasonable default, do not finish and do not return to SPECIFY. Write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message and stop. Resume this same PROTOCOLIZE Work using the exact command returned by pause.", `When the answer is incorporated and the result is protocolized, finish:\n\n${input.finishCommand}`, "</output_contract>", ""].join("\n");
|
|
485
485
|
}
|
|
486
486
|
function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, stageRoot) {
|
|
487
487
|
const receiptPath = path.join(stageRoot, "workspace-route.json");
|