@deksden-com/dd-flow-cli 0.9.0-beta.100 → 0.9.0-beta.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/hook-ingress.js +5 -10
- package/dist/cli/run-cli.js +9 -3
- package/dist/harness-runtime/bin/dd-agy.mjs +20 -7
- package/dist/harness-runtime/bin/dd-droid.mjs +2 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -4
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +13 -2
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +89 -36
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +1 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +24 -17
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +107 -20
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +5 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +6 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +63 -26
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +6 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +38 -4
- package/dist/harness-runtime/lib/native-hook-command.mjs +87 -13
- package/dist/harness-runtime/lib/process-json.mjs +3 -2
- package/dist/services/code-checks.js +53 -16
- package/dist/services/controller-fanout.js +37 -6
- package/dist/services/eval-snapshots.js +148 -38
- package/dist/services/external-work-launch.js +60 -5
- package/dist/services/prompts.js +2 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-worker.js +16 -11
- package/dist/services/run-controller-adapter.js +9 -0
- package/dist/services/runs.js +3 -0
- package/dist/services/runtime-scope-stop.js +66 -6
- package/dist/services/vnext-code-review.js +6 -2
- package/dist/services/vnext-code.js +1 -0
- package/dist/services/vnext-fanout.js +9 -1
- package/dist/services/vnext-merge.js +8 -4
- package/dist/services/vnext-plan-review.js +4 -15
- package/dist/services/vnext-plan.js +4 -0
- package/dist/services/vnext-protocolize.js +4 -2
- package/dist/services/vnext-specify.js +7 -0
- package/dist/services/work-registry.js +208 -22
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +3 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { getFlowRunVariables } from "./runs.js";
|
|
|
4
4
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
5
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
6
|
import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
|
+
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
7
8
|
function semanticCompletionPrompt(stage, completed) {
|
|
8
9
|
const task = stage === "plan-review"
|
|
9
10
|
? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
|
|
@@ -85,7 +86,11 @@ export async function nextControllerFanout(context, input) {
|
|
|
85
86
|
for (const work of wave)
|
|
86
87
|
if (!work.work_id || !work.start_command)
|
|
87
88
|
throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
|
|
88
|
-
|
|
89
|
+
if (input.stage === "plan-review" || input.stage === "code-review")
|
|
90
|
+
for (const work of wave)
|
|
91
|
+
assertWorkLaunchReady(context, work.work_id);
|
|
92
|
+
const totalWorks = fanout.works.created + fanout.works.running + fanout.works.completed + fanout.works.failed + fanout.works.cancelled;
|
|
93
|
+
return prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length }));
|
|
89
94
|
}
|
|
90
95
|
if (fanout.works.running)
|
|
91
96
|
return { kind: "wait", reason: "work_receipts_pending" };
|
|
@@ -109,11 +114,21 @@ export function controllerNativeChildren(receipt, rootSessionId) {
|
|
|
109
114
|
const value = object(raw);
|
|
110
115
|
const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
|
|
111
116
|
const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
|
|
112
|
-
if (!id
|
|
117
|
+
if (!id)
|
|
113
118
|
return;
|
|
119
|
+
const outcomeError = object(value.outcome_error);
|
|
120
|
+
if (typeof outcomeError.code === "string")
|
|
121
|
+
throw new AppError(outcomeError.code, String(outcomeError.message ?? "Native child outcome is unconfirmed"), 1, { ...object(outcomeError.details), session_id: id, source });
|
|
114
122
|
const state = String(value.status ?? value.state ?? fallback).toLowerCase();
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
if (state === "lost")
|
|
124
|
+
throw new AppError("native_child_outcome_lost", "Native child ended without a recoverable outcome", 1, { session_id: id, parent_session_id: parent, source, native_status: state });
|
|
125
|
+
// Provider idle means no current activity, not a Work result. Only an
|
|
126
|
+
// explicit terminal result or a separately proven root settlement may be
|
|
127
|
+
// terminal here; unknown states stay unknown for reconciliation.
|
|
128
|
+
const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
|
|
129
|
+
const rootChild = parent === rootSessionId;
|
|
130
|
+
const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
|
|
131
|
+
candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }) });
|
|
117
132
|
};
|
|
118
133
|
for (const value of list(receipt.descendants))
|
|
119
134
|
add(value, "adapter.descendants");
|
|
@@ -122,12 +137,28 @@ export function controllerNativeChildren(receipt, rootSessionId) {
|
|
|
122
137
|
const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
|
|
123
138
|
for (const value of list(subagents.running))
|
|
124
139
|
add(value, "zcode/session/subagents", rootSessionId, "running");
|
|
125
|
-
for (const value of
|
|
140
|
+
for (const value of list(subagents.completed))
|
|
126
141
|
add(value, "zcode/session/subagents", rootSessionId, "completed");
|
|
142
|
+
for (const value of list(object(subagents.ended).items)) {
|
|
143
|
+
const status = String(object(value).status ?? "unknown");
|
|
144
|
+
if (!["success", "failed", "cancelled", "lost"].includes(status))
|
|
145
|
+
throw new AppError("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
|
|
146
|
+
add(value, "zcode/session/subagents", rootSessionId);
|
|
147
|
+
}
|
|
127
148
|
const unique = new Map();
|
|
149
|
+
const rank = { unknown: 0, running: 1, settled_by_root: 2, completed: 3, failed: 3, cancelled: 3 };
|
|
128
150
|
for (const child of candidates) {
|
|
129
151
|
const previous = unique.get(child.session_id);
|
|
130
|
-
if (
|
|
152
|
+
if (previous && previous.parent_session_id !== child.parent_session_id) {
|
|
153
|
+
// Keep the contradictory parent visible to the existing provenance gate,
|
|
154
|
+
// independently of input order or which source happened to say success.
|
|
155
|
+
const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
|
|
156
|
+
unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
|
|
160
|
+
throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
|
|
161
|
+
if (!previous || rank[child.status] > rank[previous.status])
|
|
131
162
|
unique.set(child.session_id, child);
|
|
132
163
|
}
|
|
133
164
|
return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
|
|
@@ -15,6 +15,7 @@ import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recover
|
|
|
15
15
|
import { resourceHome } from "./managed-processes.js";
|
|
16
16
|
import { codexHomeSharedEntries } from "./hooks.js";
|
|
17
17
|
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
18
|
+
import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
|
|
18
19
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
19
20
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
20
21
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -38,17 +39,42 @@ export function prepareEvalBootstrapSnapshot(context, input) {
|
|
|
38
39
|
}
|
|
39
40
|
export function createEvalBootstrapSnapshot(context, input) {
|
|
40
41
|
const { projectRoot, output } = prepareEvalBootstrapSnapshot(context, input);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
const temporary = `${output}.partial-${crypto.randomUUID()}`;
|
|
43
|
+
const before = bootstrapSourceVersion(projectRoot);
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(temporary, { recursive: true });
|
|
46
|
+
fs.cpSync(projectRoot, path.join(temporary, "project"), { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(projectRoot) });
|
|
47
|
+
const after = bootstrapSourceVersion(projectRoot);
|
|
48
|
+
if (before !== after)
|
|
49
|
+
throw new AppError("snapshot_source_changed", "Bootstrap source changed during snapshot capture", 1, { phase: "bootstrap", recoverable: true });
|
|
50
|
+
const manifest = {
|
|
51
|
+
schema_id: "dd-eval/bootstrap-snapshot@1",
|
|
52
|
+
stage: "specify",
|
|
53
|
+
project_root: projectRoot,
|
|
54
|
+
project_sha256: treeHash(path.join(temporary, "project")),
|
|
55
|
+
project_git: snapshotProjectGit(projectRoot, temporary)
|
|
56
|
+
};
|
|
57
|
+
writeJson(path.join(temporary, "bootstrap.json"), manifest);
|
|
58
|
+
if (after !== bootstrapSourceVersion(projectRoot))
|
|
59
|
+
throw new AppError("snapshot_source_changed", "Bootstrap source changed while preserving Git evidence", 1, { phase: "bootstrap", recoverable: true });
|
|
60
|
+
syncSnapshotTree(temporary);
|
|
61
|
+
fs.renameSync(temporary, output);
|
|
62
|
+
syncPath(path.dirname(output));
|
|
63
|
+
return { ok: true, snapshot: output, ...manifest };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function bootstrapSourceVersion(projectRoot) {
|
|
71
|
+
return JSON.stringify([
|
|
72
|
+
treeHash(projectRoot, true),
|
|
73
|
+
treeModesHash(projectRoot, true),
|
|
74
|
+
git(projectRoot, ["symbolic-ref", "--quiet", "HEAD"]),
|
|
75
|
+
gitBytes(projectRoot, ["show-ref", "--head"]).toString("base64"),
|
|
76
|
+
indexEntries(projectRoot).toString("base64")
|
|
77
|
+
]);
|
|
52
78
|
}
|
|
53
79
|
/** Restore a pre-RUN project state; `stage start --bootstrap` creates runtime state afterwards. */
|
|
54
80
|
export function prepareEvalBootstrapRestore(input) {
|
|
@@ -185,7 +211,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
else
|
|
188
|
-
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions]);
|
|
214
|
+
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome));
|
|
189
215
|
// VACUUM INTO is a consistent SQLite read snapshot, including committed WAL
|
|
190
216
|
// pages. Copying the live database and its WAL separately is not atomic.
|
|
191
217
|
const databaseCopy = path.join(output, "runtime", path.relative(context.ddFlowHome, context.db.path));
|
|
@@ -217,7 +243,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
217
243
|
fs.cpSync(projectRoot, path.join(output, "project"), {
|
|
218
244
|
recursive: true,
|
|
219
245
|
verbatimSymlinks: true,
|
|
220
|
-
filter:
|
|
246
|
+
filter: projectPayloadFilter(projectRoot)
|
|
221
247
|
});
|
|
222
248
|
const projectGit = snapshotProjectGit(projectRoot, output);
|
|
223
249
|
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
|
|
@@ -244,6 +270,8 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
244
270
|
created_at: context.now(),
|
|
245
271
|
runtime_sha256: treeHash(path.join(output, "runtime")),
|
|
246
272
|
project_sha256: treeHash(path.join(output, "project")),
|
|
273
|
+
project_payload_policy: "service-files@1",
|
|
274
|
+
...(!recovery ? { runtime_payload_policy: "flow-evidence@1" } : {}),
|
|
247
275
|
project_git: projectGit,
|
|
248
276
|
workspace,
|
|
249
277
|
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind === "git_worktree" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
|
|
@@ -608,7 +636,7 @@ function snapshotWorkspace(workspaceRoot, projectRoot, output) {
|
|
|
608
636
|
const { branch, baseRef } = prepared;
|
|
609
637
|
const workspace = path.join(output, "workspace");
|
|
610
638
|
const bundle = path.join(output, "workspace.bundle");
|
|
611
|
-
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter:
|
|
639
|
+
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
|
|
612
640
|
runGit(projectRoot, ["bundle", "create", bundle, branch]);
|
|
613
641
|
return { kind: "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
|
|
614
642
|
}
|
|
@@ -675,7 +703,7 @@ function restoreProjectGit(snapshot, source, projectRoot) {
|
|
|
675
703
|
const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
|
|
676
704
|
function indexEntries(root) {
|
|
677
705
|
const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
|
|
678
|
-
.toString("utf8").split("\0").filter(entry => entry &&
|
|
706
|
+
.toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
|
|
679
707
|
.map(entry => `${entry}\0`).join(""));
|
|
680
708
|
const merge = {};
|
|
681
709
|
for (const name of mergeStateFiles) {
|
|
@@ -764,10 +792,11 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
|
|
|
764
792
|
];
|
|
765
793
|
verifySnapshotPayload(snapshot, manifest);
|
|
766
794
|
for (const entry of entries) {
|
|
795
|
+
const allowed = manifest.project_payload_policy === "service-files@1" ? projectPayloadFilter(entry.root) : snapshotPayloadAllowed;
|
|
767
796
|
const expectedModes = manifest.payload_modes?.[entry.name];
|
|
768
|
-
if (checkWorkingState && expectedModes && treeModesHash(entry.root,
|
|
797
|
+
if (checkWorkingState && expectedModes && treeModesHash(entry.root, allowed) !== expectedModes)
|
|
769
798
|
throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
|
|
770
|
-
if (checkWorkingState && (treeHash(entry.root,
|
|
799
|
+
if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "HEAD"]) !== entry.head || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
|
|
771
800
|
throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
|
|
772
801
|
}
|
|
773
802
|
}
|
|
@@ -805,18 +834,48 @@ function runGit(root, args) {
|
|
|
805
834
|
throw new AppError("snapshot_workspace_restore_failed", "Could not restore feature worktree", 1, { stderr: result.stderr.trim(), args });
|
|
806
835
|
}
|
|
807
836
|
function samePath(left, right) { return path.resolve(left) === path.resolve(right); }
|
|
808
|
-
function snapshotEnvironmentAllowed(source) {
|
|
809
|
-
const name = path.basename(source);
|
|
810
|
-
// Public configuration template, not a local environment/credential file.
|
|
811
|
-
return name === ".env.example" || (name !== ".env" && !name.startsWith(".env."));
|
|
812
|
-
}
|
|
813
837
|
function snapshotPayloadAllowed(source) {
|
|
814
838
|
const name = path.basename(source);
|
|
815
839
|
return ![".git", "node_modules", ".cache", ".npm", ".pnpm-store"].includes(name)
|
|
816
|
-
&&
|
|
840
|
+
&& localEnvironmentAllowed(source);
|
|
841
|
+
}
|
|
842
|
+
function projectPayloadFilter(root) {
|
|
843
|
+
const serviceAllowed = projectServicePathFilter(root);
|
|
844
|
+
return file => snapshotPayloadAllowed(file) && serviceAllowed(file);
|
|
817
845
|
}
|
|
818
846
|
function runtimePayloadAllowed(source) {
|
|
819
|
-
return snapshotPayloadAllowed(source) && !["auth.json", "auth.bk", "credentials.json", ".credentials.json", "credentials", ".credentials", "id_rsa", "id_ed25519", "engines", "checkouts"].includes(path.basename(source));
|
|
847
|
+
return snapshotPayloadAllowed(source) && !["auth.json", "auth.bk", "auth.encrypted", "auth.v2.key", "auth.v2.file", "credentials.json", ".credentials.json", "credentials", ".credentials", "id_rsa", "id_ed25519", "engines", "checkouts"].includes(path.basename(source));
|
|
848
|
+
}
|
|
849
|
+
/** Only a Grok daemon's verified provider root is excluded from flow evidence.
|
|
850
|
+
* An unrelated directory named grok-home remains ordinary project data. */
|
|
851
|
+
function grokProviderHomes(runtimeRoot) {
|
|
852
|
+
const homes = [];
|
|
853
|
+
const visit = (directory) => {
|
|
854
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
855
|
+
const file = path.join(directory, entry.name);
|
|
856
|
+
if (entry.isDirectory()) {
|
|
857
|
+
if (![".git", "node_modules", "grok-home"].includes(entry.name))
|
|
858
|
+
visit(file);
|
|
859
|
+
}
|
|
860
|
+
else if (entry.isFile() && entry.name === "daemon.json") {
|
|
861
|
+
let state;
|
|
862
|
+
try {
|
|
863
|
+
state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (state.schema_id !== "dd-grok/daemon-state@1")
|
|
869
|
+
continue;
|
|
870
|
+
const expected = path.join(directory, "grok-home");
|
|
871
|
+
if (state.config?.grokHome !== expected || state.config?.ddFlowHome !== runtimeRoot)
|
|
872
|
+
throw new AppError("snapshot_native_scope_invalid", "Grok provider home differs from its retained daemon binding", 1, { state_dir: directory });
|
|
873
|
+
homes.push(expected);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
visit(runtimeRoot);
|
|
878
|
+
return homes;
|
|
820
879
|
}
|
|
821
880
|
function sqliteFile(file) {
|
|
822
881
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
@@ -830,9 +889,55 @@ function sqliteFile(file) {
|
|
|
830
889
|
fs.closeSync(descriptor);
|
|
831
890
|
}
|
|
832
891
|
}
|
|
892
|
+
function sqliteIdentifier(value) { return `"${value.replaceAll('"', '""')}"`; }
|
|
893
|
+
function sqliteValue(value) {
|
|
894
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
895
|
+
return value;
|
|
896
|
+
if (typeof value === "number")
|
|
897
|
+
return Number.isFinite(value) ? value : String(value);
|
|
898
|
+
if (typeof value === "bigint")
|
|
899
|
+
return `${value}n`;
|
|
900
|
+
if (Buffer.isBuffer(value))
|
|
901
|
+
return `blob:${value.toString("base64")}`;
|
|
902
|
+
if (value instanceof Uint8Array)
|
|
903
|
+
return `blob:${Buffer.from(value).toString("base64")}`;
|
|
904
|
+
return String(value);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* SQLite page layout is not semantic identity: VACUUM, checkpoint and a
|
|
908
|
+
* delete/insert rewrite can change serialized bytes without changing the
|
|
909
|
+
* logical database. Snapshot only the schema and ordered logical row values.
|
|
910
|
+
* This intentionally applies to the small set of managed SQLite files, not
|
|
911
|
+
* to arbitrary provider databases.
|
|
912
|
+
*/
|
|
913
|
+
function sqliteSemanticFingerprint(db, resourceRegistry = false) {
|
|
914
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
915
|
+
db.exec("BEGIN");
|
|
916
|
+
try {
|
|
917
|
+
const schema = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name, sql").all();
|
|
918
|
+
const tables = schema.filter(row => row.type === "table").map(row => {
|
|
919
|
+
const columns = db.prepare(`PRAGMA table_info(${sqliteIdentifier(row.name)})`).all()
|
|
920
|
+
.sort((left, right) => Number(left.cid ?? 0) - Number(right.cid ?? 0))
|
|
921
|
+
.map(column => ({ name: String(column.name ?? ""), type: String(column.type ?? ""), notnull: Number(column.notnull ?? 0), default: sqliteValue(column.dflt_value ?? null), pk: Number(column.pk ?? 0) }));
|
|
922
|
+
const names = columns.map(column => column.name);
|
|
923
|
+
const stableNames = resourceRegistry && row.name === "managed_processes" ? names.filter(name => name !== "lease_expires_at" && name !== "updated_at") : names;
|
|
924
|
+
const rows = db.prepare(`SELECT * FROM ${sqliteIdentifier(row.name)}`).all()
|
|
925
|
+
.map(record => stableNames.map(name => [name, sqliteValue(record[name])]))
|
|
926
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
927
|
+
return { name: row.name, columns, rows };
|
|
928
|
+
});
|
|
929
|
+
return crypto.createHash("sha256").update(JSON.stringify({ schema, tables })).digest("hex");
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
try {
|
|
933
|
+
db.exec("ROLLBACK");
|
|
934
|
+
}
|
|
935
|
+
catch { /* The read transaction may already have closed on provider error. */ }
|
|
936
|
+
}
|
|
937
|
+
}
|
|
833
938
|
/** A WAL is copied through its SQLite read snapshot, not as raw bytes. */
|
|
834
|
-
function runtimeSnapshotKind(file, excluded) {
|
|
835
|
-
if (!runtimePayloadAllowed(file) || excluded.includes(file))
|
|
939
|
+
function runtimeSnapshotKind(file, excluded, providerHomes = []) {
|
|
940
|
+
if (!runtimePayloadAllowed(file) || providerHomes.some(home => file === home || isInside(home, file)) || excluded.includes(file))
|
|
836
941
|
return "excluded";
|
|
837
942
|
if (fs.lstatSync(file).isFile()) {
|
|
838
943
|
if (/-(?:wal|shm|journal)$/.test(file) && sqliteFile(file.replace(/-(?:wal|shm|journal)$/, "")))
|
|
@@ -854,10 +959,11 @@ function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
|
854
959
|
throw new AppError("not_found", "Snapshot RUN is not registered", 1);
|
|
855
960
|
const entries = new Map();
|
|
856
961
|
const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
962
|
+
const providerHomes = input.recoveryId === undefined ? grokProviderHomes(context.ddFlowHome) : [];
|
|
857
963
|
const visit = (directory) => {
|
|
858
964
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
859
965
|
const file = path.join(directory, entry.name);
|
|
860
|
-
const kind = runtimeSnapshotKind(file, excluded);
|
|
966
|
+
const kind = runtimeSnapshotKind(file, excluded, providerHomes);
|
|
861
967
|
if (kind === "excluded" || kind === "sidecar")
|
|
862
968
|
continue;
|
|
863
969
|
const key = `runtime/${path.relative(context.ddFlowHome, file)}`;
|
|
@@ -869,18 +975,18 @@ function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
|
869
975
|
else if (stat.isSymbolicLink())
|
|
870
976
|
entries.set(key, `link:${fs.readlinkSync(file)}`);
|
|
871
977
|
else if (kind === "sqlite") {
|
|
872
|
-
// Resource heartbeats are copied consistently but are not productive
|
|
873
|
-
// RUN writers; owner identity is fenced by boundary.verify().
|
|
874
|
-
if (file === path.join(context.ddFlowHome, "runtime.sqlite")) {
|
|
875
|
-
entries.set(key, `resource-sqlite:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
978
|
let db = databases.get(file);
|
|
879
979
|
if (!db) {
|
|
880
980
|
db = new DatabaseSync(file, { readOnly: true });
|
|
881
981
|
databases.set(file, db);
|
|
882
982
|
}
|
|
883
|
-
|
|
983
|
+
// Resource heartbeats are copied consistently but are not productive
|
|
984
|
+
// RUN writers; owner identity is fenced by boundary.verify().
|
|
985
|
+
if (file === path.join(resourceHome(context), "runtime.sqlite")) {
|
|
986
|
+
entries.set(key, `resource-sqlite:${sqliteSemanticFingerprint(db, true)}:${stat.mode & 0o111}`);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
entries.set(key, `sqlite:${sqliteSemanticFingerprint(db)}:${stat.mode & 0o111}`);
|
|
884
990
|
}
|
|
885
991
|
else if (stat.isFile())
|
|
886
992
|
entries.set(key, `file:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}:${stat.mode & 0o111}`);
|
|
@@ -944,7 +1050,7 @@ function codexHomeSnapshotExclusions(context, projectId, runId) {
|
|
|
944
1050
|
return excluded;
|
|
945
1051
|
}
|
|
946
1052
|
/** Copy a dedicated home or scoped recovery tree, preserving SQLite WAL commits. */
|
|
947
|
-
function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
1053
|
+
function copyRuntimeTree(source, destination, scoped = true, excluded = [], providerHomes = []) {
|
|
948
1054
|
if (fs.lstatSync(source).isSymbolicLink())
|
|
949
1055
|
throw new AppError("snapshot_runtime_symlink_outside_scope", "Recovery artifact roots must not be symbolic links", 1, { path: source });
|
|
950
1056
|
const root = physicalPath(source);
|
|
@@ -955,7 +1061,7 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
|
955
1061
|
fs.cpSync(source, destination, {
|
|
956
1062
|
recursive: true, verbatimSymlinks: true,
|
|
957
1063
|
filter: file => {
|
|
958
|
-
const kind = runtimeSnapshotKind(file, excluded);
|
|
1064
|
+
const kind = runtimeSnapshotKind(file, excluded, providerHomes);
|
|
959
1065
|
if (kind === "excluded" || kind === "sidecar")
|
|
960
1066
|
return false;
|
|
961
1067
|
const stat = fs.lstatSync(file);
|
|
@@ -1057,11 +1163,12 @@ function captureRecoveryAuxiliary(context, captured, input) {
|
|
|
1057
1163
|
}
|
|
1058
1164
|
function treeHash(root, filtered = false) {
|
|
1059
1165
|
const hash = crypto.createHash("sha256");
|
|
1166
|
+
const allowed = typeof filtered === "function" ? filtered : filtered ? projectPayloadFilter(root) : () => true;
|
|
1060
1167
|
const visit = (dir, relative) => {
|
|
1061
1168
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1062
1169
|
const file = path.join(dir, entry.name);
|
|
1063
1170
|
const rel = path.join(relative, entry.name);
|
|
1064
|
-
if (
|
|
1171
|
+
if (!allowed(file))
|
|
1065
1172
|
continue;
|
|
1066
1173
|
if (entry.isDirectory())
|
|
1067
1174
|
visit(file, rel);
|
|
@@ -1080,10 +1187,11 @@ function treeHash(root, filtered = false) {
|
|
|
1080
1187
|
}
|
|
1081
1188
|
function treeModesHash(root, filtered = false) {
|
|
1082
1189
|
const entries = [];
|
|
1190
|
+
const allowed = typeof filtered === "function" ? filtered : filtered ? projectPayloadFilter(root) : () => true;
|
|
1083
1191
|
const visit = (directory) => {
|
|
1084
1192
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1085
1193
|
const file = path.join(directory, entry.name);
|
|
1086
|
-
if (
|
|
1194
|
+
if (!allowed(file))
|
|
1087
1195
|
continue;
|
|
1088
1196
|
if (entry.isDirectory())
|
|
1089
1197
|
visit(file);
|
|
@@ -1131,6 +1239,8 @@ function readManifest(snapshot) {
|
|
|
1131
1239
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1132
1240
|
if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
|
|
1133
1241
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
1242
|
+
if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
|
|
1243
|
+
throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
|
|
1134
1244
|
if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
1135
1245
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
|
1136
1246
|
if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
4
5
|
import { ensureWorkLaunchStorage } from "../storage/database.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
7
|
import { runtimeBudgetForRun } from "./runtime-budget.js";
|
|
@@ -8,9 +9,12 @@ import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } fro
|
|
|
8
9
|
import { harnessRuntimeArguments, resolveHarnessCommand } from "./harness-config.js";
|
|
9
10
|
import { resolveExecutionPolicy, runtimeHarness, validateExecutionOverride } from "./execution-policy.js";
|
|
10
11
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
11
|
-
import { assertWorkLaunchReady, workStartCommand } from "./work-registry.js";
|
|
12
|
+
import { assertWorkLaunchReady, reviewSourceInputs, workStartCommand } from "./work-registry.js";
|
|
12
13
|
import { initCodexHome } from "./hooks.js";
|
|
13
14
|
import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
15
|
+
import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
16
|
+
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
|
+
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
14
18
|
/**
|
|
15
19
|
* Creates an external provider root for one already-created Work. The launch
|
|
16
20
|
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
@@ -43,13 +47,34 @@ export async function launchExternalWork(context, input) {
|
|
|
43
47
|
const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
|
|
44
48
|
if (resolved.delegation.mode !== "external")
|
|
45
49
|
throw new AppError("external_launch_not_selected", "This Work's frozen policy selects native delegation", 2, { work_id: work.work_id });
|
|
50
|
+
const agyWorker = runtimeHarness(resolved.worker) === "antigravity-cli";
|
|
51
|
+
if (agyWorker)
|
|
52
|
+
await qualifyAgyHooks(run.workspace_root);
|
|
46
53
|
const operationId = `OPR-${crypto.randomUUID()}`;
|
|
47
54
|
const root = path.join(run.run_root, "external-launches", operationId);
|
|
48
55
|
const journal = path.join(root, "adapter.events.jsonl");
|
|
49
56
|
fs.mkdirSync(root, { recursive: true });
|
|
57
|
+
const isolatedWorkspace = isReadOnlyWork(work.payload_json) ? path.join(root, "read-only-workspace") : null;
|
|
58
|
+
const reviewInputs = isolatedWorkspace ? reviewSourceInputs(context, work.work_id) : [];
|
|
59
|
+
const reviewBase = snapshot.workspace?.base && snapshot.workspace.base !== "unknown" ? snapshot.workspace.base : "HEAD";
|
|
60
|
+
let reviewCopy = null;
|
|
61
|
+
if (isolatedWorkspace) {
|
|
62
|
+
const allowed = reviewSourceFilter(run.workspace_root, reviewInputs, reviewBase);
|
|
63
|
+
const before = reviewSourceHash(run.workspace_root, reviewInputs, reviewBase);
|
|
64
|
+
fs.cpSync(run.workspace_root, isolatedWorkspace, { recursive: true, verbatimSymlinks: true, filter: allowed });
|
|
65
|
+
if (agyWorker)
|
|
66
|
+
await qualifyAgyHooks(isolatedWorkspace);
|
|
67
|
+
const after = reviewSourceHash(run.workspace_root, reviewInputs, reviewBase), copy = reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, false, reviewInputs, reviewBase);
|
|
68
|
+
if (before !== after || after !== copy)
|
|
69
|
+
throw new AppError("read_only_input_changed", "Read-only execution copy differs from its authoritative source", 2, { work_id: work.work_id, before, after, copy });
|
|
70
|
+
writeReviewGitEvidence(run.workspace_root, isolatedWorkspace, snapshot.workspace?.base, reviewInputs);
|
|
71
|
+
if (reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== after)
|
|
72
|
+
throw new AppError("read_only_input_changed", "Authoritative review workspace changed while collecting Git evidence", 2, { work_id: work.work_id });
|
|
73
|
+
reviewCopy = { root: path.relative(run.run_root, isolatedWorkspace).split(path.sep).join("/"), sha256: reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, true, reviewInputs, reviewBase), source_sha256: after };
|
|
74
|
+
}
|
|
50
75
|
const now = context.now();
|
|
51
76
|
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
52
|
-
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined };
|
|
77
|
+
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined, ...(isolatedWorkspace ? { DD_FLOW_READ_ONLY_WORKSPACE: isolatedWorkspace } : {}) };
|
|
53
78
|
// An external Codex root is a fresh thread, so it does not inherit the
|
|
54
79
|
// coordinator's managed PreToolUse hook. Its first `work start` must emit
|
|
55
80
|
// that hook event to bind the Work to the physical session.
|
|
@@ -61,8 +86,8 @@ export async function launchExternalWork(context, input) {
|
|
|
61
86
|
let runtimeContext = { ...context, env };
|
|
62
87
|
const runtimeOwner = { schema_id: "dd-flow/runtime-owner@1", owner_id: `run:${work.run_id}:work:${work.work_id}`, role: "worker", project_id: work.project_id, run_id: work.run_id, work_id: work.work_id, operation_id: operationId, state_dir: root, dd_flow_home: context.ddFlowHome, dd_flow_bin: flowExecutable, resource_home: context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, budget: runtimeBudgetForRun(context, work.project_id, work.run_id) };
|
|
63
88
|
const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
|
|
64
|
-
context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner } }), resolved.profile_hash, root, journal, now, now]);
|
|
65
|
-
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
89
|
+
context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner }, ...(reviewCopy ? { review_copy: reviewCopy } : {}) }), resolved.profile_hash, root, journal, now, now]);
|
|
90
|
+
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
66
91
|
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
67
92
|
try {
|
|
68
93
|
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 });
|
|
@@ -78,8 +103,10 @@ export async function launchExternalWork(context, input) {
|
|
|
78
103
|
projectRoot: run.project_root, runId: work.run_id, generation });
|
|
79
104
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
80
105
|
assertWorkLaunchReady(context, work.work_id);
|
|
106
|
+
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
107
|
+
throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
|
|
81
108
|
const promptFile = path.join(root, "launch.md");
|
|
82
|
-
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n`);
|
|
109
|
+
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
|
|
83
110
|
const prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
|
|
84
111
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
85
112
|
context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
|
|
@@ -90,6 +117,24 @@ export async function launchExternalWork(context, input) {
|
|
|
90
117
|
throw error;
|
|
91
118
|
}
|
|
92
119
|
}
|
|
120
|
+
export function writeReviewGitEvidence(source, copy, base, explicitInputs = []) {
|
|
121
|
+
const directory = path.join(copy, ".dd-flow", "review-evidence");
|
|
122
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
123
|
+
if (workspaceGitFiles(source, ["--cached"]) === null) {
|
|
124
|
+
fs.writeFileSync(path.join(directory, "gitless.txt"), "Source workspace is not a Git repository. Review the copied files and accepted RUN evidence.\n");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!base || base === "unknown")
|
|
128
|
+
throw new AppError("review_git_evidence_missing", "Git review source has no frozen base commit", 1, { workspace_root: source });
|
|
129
|
+
const paths = (reviewSourcePaths(source, explicitInputs, base) ?? []).map(file => `:(literal)${file}`);
|
|
130
|
+
for (const [name, args] of [["status.txt", ["status", "--short", "--untracked-files=all"]], ["diff.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary", base]]]) {
|
|
131
|
+
const result = paths.length ? spawnSync("git", ["-C", source, ...args, "--", ...paths], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : { status: 0, stdout: "", stderr: "", error: undefined };
|
|
132
|
+
if (result.status !== 0 || result.error)
|
|
133
|
+
throw new AppError("review_git_evidence_failed", "Could not collect read-only Git evidence for reviewer", 1, { workspace_root: source, command: name, cause: result.error?.message ?? result.stderr });
|
|
134
|
+
fs.writeFileSync(path.join(directory, name), result.stdout);
|
|
135
|
+
}
|
|
136
|
+
fs.writeFileSync(path.join(directory, "base.txt"), `${base}\n`);
|
|
137
|
+
}
|
|
93
138
|
function workOverride(payload) {
|
|
94
139
|
if (!payload)
|
|
95
140
|
return undefined;
|
|
@@ -108,3 +153,13 @@ function workOverride(payload) {
|
|
|
108
153
|
validateExecutionOverride(execution);
|
|
109
154
|
return execution;
|
|
110
155
|
}
|
|
156
|
+
function isReadOnlyWork(payload) {
|
|
157
|
+
if (!payload)
|
|
158
|
+
return false;
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(payload).read_only === true;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/services/prompts.js
CHANGED
|
@@ -300,6 +300,8 @@ function renderPrompt(input) {
|
|
|
300
300
|
`- Acceptance contribution: ${item.semantic_spine?.acceptance_contribution ?? "not_applicable"}`,
|
|
301
301
|
"",
|
|
302
302
|
"## Hard Write Boundary",
|
|
303
|
+
"Before editing, change into the project workspace shown below (cd -- WORKSPACE) or use its absolute path when the tool cannot persist cwd.",
|
|
304
|
+
"Write only the project workspace and the named report directory; never write another RUN/native home or Git/worktree control data.",
|
|
303
305
|
`- All project writes must stay under: \`${input.workspaceRoot}\``,
|
|
304
306
|
"- Do not write outside this workspace, to another RUN, or into Git/worktree control data.",
|
|
305
307
|
"- Accepted requirements and non-goals remain binding; planned write areas do not.",
|