@deksden-com/dd-flow-cli 0.9.0-beta.127 → 0.9.0-beta.133
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 +58 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +2 -2
- package/dist/cli/run-cli.js +6 -6
- package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
- package/dist/cli/runtime-maintenance-ingress.js +5 -4
- package/dist/cli/runtime-process.js +27 -2
- package/dist/cli.js +7 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
- package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
- package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +41 -9
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +113 -31
- package/dist/services/compatibility-preflight.js +9 -2
- package/dist/services/dashboard.js +33 -26
- package/dist/services/engines.js +88 -57
- package/dist/services/lifecycle-invocations.js +4 -2
- package/dist/services/managed-lease-monitor.js +9 -4
- package/dist/services/prompts.js +3 -1
- package/dist/services/protocols.js +3 -1
- package/dist/services/repair-intents.js +4 -2
- package/dist/services/run-control-worker.js +2 -1
- package/dist/services/run-controller.js +20 -17
- package/dist/services/run-fork.js +2 -2
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-lifecycle.js +13 -7
- package/dist/services/tooling.js +32 -23
- package/dist/services/vnext-code-review.js +2 -0
- package/dist/services/vnext-code.js +5 -1
- package/dist/services/vnext-merge.js +4 -1
- package/dist/services/vnext-protocolize.js +17 -16
- package/dist/services/work-registry.js +3 -1
- package/dist/services/worktrees.js +16 -14
- package/package.json +1 -1
package/dist/services/tooling.js
CHANGED
|
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { commandResult, commandText } from '../harness-runtime/lib/process-json.mjs';
|
|
6
|
+
import { publishSetup, SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
|
|
6
7
|
import { AppError } from "../shared/errors.js";
|
|
7
8
|
import { ensureDir } from "../storage/paths.js";
|
|
8
9
|
const manifestRelativePath = path.join("tools", "worktrunk-manifest.json");
|
|
@@ -71,7 +72,7 @@ export function inspectManagedWorktrunk(context) {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
export function ensureManagedWorktrunk(context) {
|
|
75
|
+
export async function ensureManagedWorktrunk(context) {
|
|
75
76
|
const manifest = worktrunkManifest();
|
|
76
77
|
const platform = worktrunkPlatform();
|
|
77
78
|
const cachedRoot = worktrunkCacheRoot(context, platform);
|
|
@@ -92,14 +93,14 @@ export function ensureManagedWorktrunk(context) {
|
|
|
92
93
|
next_action: "Place the verified official artifact in the managed tool cache and retry."
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
|
-
installWorktrunkArtifact(manifest, platform, cachedRoot);
|
|
96
|
+
await installWorktrunkArtifact(manifest, platform, cachedRoot);
|
|
96
97
|
return verifyCachedWorktrunk(context, manifest, platform);
|
|
97
98
|
}
|
|
98
99
|
finally {
|
|
99
100
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
|
-
function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
103
|
+
async function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
103
104
|
const artifact = artifactFor(manifest, platform);
|
|
104
105
|
const parent = path.dirname(targetRoot);
|
|
105
106
|
ensureDir(parent);
|
|
@@ -107,8 +108,17 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
|
107
108
|
const archivePath = path.join(tempRoot, `worktrunk.${artifact.archive}`);
|
|
108
109
|
const extractedRoot = path.join(tempRoot, "extracted");
|
|
109
110
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
let observedBytes = 0;
|
|
112
|
+
publishSetup('probe_started', 'worktrunk-download');
|
|
113
|
+
const download = await commandResult("curl", ["--fail", "--location", "--silent", "--show-error", "-o", archivePath, artifact.url], { startupMs: SETUP_STARTUP_MS, progress: () => {
|
|
114
|
+
const bytes = fs.existsSync(archivePath) ? fs.statSync(archivePath).size : 0;
|
|
115
|
+
if (bytes > observedBytes) {
|
|
116
|
+
observedBytes = bytes;
|
|
117
|
+
publishSetup('transfer_progress', `worktrunk-transfer:${bytes}`);
|
|
118
|
+
}
|
|
119
|
+
return observedBytes;
|
|
120
|
+
} });
|
|
121
|
+
if (download.exit_code !== 0) {
|
|
112
122
|
throw new AppError("tool_unavailable", "Unable to download the pinned Worktrunk artifact", 1, {
|
|
113
123
|
tool: manifest.tool,
|
|
114
124
|
version: manifest.version,
|
|
@@ -118,6 +128,7 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
|
118
128
|
stderr: sanitize(download.stderr)
|
|
119
129
|
});
|
|
120
130
|
}
|
|
131
|
+
publishSetup('probe_completed', 'worktrunk-download');
|
|
121
132
|
const archiveChecksum = sha256File(archivePath);
|
|
122
133
|
if (archiveChecksum !== artifact.sha256) {
|
|
123
134
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk artifact checksum mismatch", 1, {
|
|
@@ -129,12 +140,12 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
|
|
|
129
140
|
});
|
|
130
141
|
}
|
|
131
142
|
ensureDir(extractedRoot);
|
|
132
|
-
extractArchive(archivePath, extractedRoot, artifact.archive);
|
|
143
|
+
await extractArchive(archivePath, extractedRoot, artifact.archive);
|
|
133
144
|
const extractedBinary = findBinary(extractedRoot);
|
|
134
145
|
if (!extractedBinary)
|
|
135
146
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive does not contain wt", 1, { platform });
|
|
136
147
|
fs.chmodSync(extractedBinary, 0o755);
|
|
137
|
-
verifyWorktrunkVersion(extractedBinary, manifest.version);
|
|
148
|
+
await verifyWorktrunkVersion(extractedBinary, manifest.version);
|
|
138
149
|
const binaryChecksum = sha256File(extractedBinary);
|
|
139
150
|
const stagedRoot = path.join(tempRoot, "managed");
|
|
140
151
|
ensureDir(stagedRoot);
|
|
@@ -186,7 +197,7 @@ function verifyCachedWorktrunk(context, manifest, platform) {
|
|
|
186
197
|
actual_binary_sha256: actualBinaryChecksum
|
|
187
198
|
});
|
|
188
199
|
}
|
|
189
|
-
|
|
200
|
+
// The admitted binary checksum binds the version probe performed at install.
|
|
190
201
|
return {
|
|
191
202
|
bin,
|
|
192
203
|
version: manifest.version,
|
|
@@ -195,23 +206,23 @@ function verifyCachedWorktrunk(context, manifest, platform) {
|
|
|
195
206
|
binary_sha256: actualBinaryChecksum
|
|
196
207
|
};
|
|
197
208
|
}
|
|
198
|
-
function extractArchive(archive, destination, format) {
|
|
209
|
+
async function extractArchive(archive, destination, format) {
|
|
199
210
|
if (format === "tar.xz") {
|
|
200
|
-
const listing =
|
|
201
|
-
if (listing.
|
|
211
|
+
const listing = await commandResult("tar", ["-tJf", archive], { startupMs: SETUP_STARTUP_MS });
|
|
212
|
+
if (listing.exit_code !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
|
|
202
213
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe or unreadable file list", 1);
|
|
203
214
|
}
|
|
204
|
-
const extracted =
|
|
205
|
-
if (extracted.
|
|
215
|
+
const extracted = await commandResult("tar", ["-xJvf", archive, "-C", destination], { startupMs: SETUP_STARTUP_MS });
|
|
216
|
+
if (extracted.exit_code !== 0)
|
|
206
217
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { stderr: sanitize(extracted.stderr) });
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
209
|
-
const listing =
|
|
210
|
-
if (listing.
|
|
220
|
+
const listing = await commandResult("unzip", ["-Z1", archive], { startupMs: SETUP_STARTUP_MS });
|
|
221
|
+
if (listing.exit_code !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
|
|
211
222
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe or unreadable file list", 1);
|
|
212
223
|
}
|
|
213
|
-
const extracted =
|
|
214
|
-
if (extracted.
|
|
224
|
+
const extracted = await commandResult("unzip", [archive, "-d", destination], { startupMs: SETUP_STARTUP_MS });
|
|
225
|
+
if (extracted.exit_code !== 0)
|
|
215
226
|
throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { stderr: sanitize(extracted.stderr) });
|
|
216
227
|
}
|
|
217
228
|
function findBinary(root) {
|
|
@@ -229,13 +240,11 @@ function findBinary(root) {
|
|
|
229
240
|
}
|
|
230
241
|
return null;
|
|
231
242
|
}
|
|
232
|
-
function verifyWorktrunkVersion(bin, expected) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
if (result.status !== 0 || !output.includes(expected)) {
|
|
243
|
+
async function verifyWorktrunkVersion(bin, expected) {
|
|
244
|
+
const output = await commandText(bin, ['--version'], { startupMs: SETUP_STARTUP_MS, phase: 'native-version' });
|
|
245
|
+
if (!output.includes(expected)) {
|
|
236
246
|
throw new AppError("tool_integrity_failed", "Managed Worktrunk version verification failed", 1, {
|
|
237
247
|
expected_version: expected,
|
|
238
|
-
status: result.status,
|
|
239
248
|
output: sanitize(output)
|
|
240
249
|
});
|
|
241
250
|
}
|
|
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { assertSourceRepairEligible } from "./check-runner-evidence.js";
|
|
7
8
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
10
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, prepareFlowRunStageCompletion, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
|
|
@@ -243,6 +244,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
243
244
|
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: decisionFile });
|
|
244
245
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
245
246
|
if (failed.length) {
|
|
247
|
+
assertSourceRepairEligible(failed);
|
|
246
248
|
context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
|
|
247
249
|
publishVerificationMatrix(context, matrix);
|
|
248
250
|
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
@@ -7,8 +7,9 @@ import fs from "node:fs";
|
|
|
7
7
|
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { AppError } from "../shared/errors.js";
|
|
10
|
+
import { assertSourceRepairEligible } from "./check-runner-evidence.js";
|
|
10
11
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
11
|
-
import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
12
|
+
import { aggregateCheckDeclarations, checkReceipts, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
12
13
|
import { requireProjectByRoot } from "./projects.js";
|
|
13
14
|
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, prepareFlowRunStageCompletion, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
14
15
|
import { validateSchema } from "./schema-validation.js";
|
|
@@ -224,6 +225,7 @@ export async function finishVnextCode(context, input) {
|
|
|
224
225
|
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: input.verificationFile });
|
|
225
226
|
const matrixInstruction = matrix ? verificationMatrixInstruction(matrix.metadata, home, "semantic verification in code-verification.json") : "";
|
|
226
227
|
if (failed.length) {
|
|
228
|
+
assertSourceRepairEligible(failed);
|
|
227
229
|
context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
|
|
228
230
|
publishVerificationMatrix(context, matrix);
|
|
229
231
|
const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
|
|
@@ -406,6 +408,8 @@ export function prepareVnextCodeRepair(context, input) {
|
|
|
406
408
|
check_receipt_id: input.checkReceiptId
|
|
407
409
|
});
|
|
408
410
|
}
|
|
411
|
+
if (input.checkReceiptId)
|
|
412
|
+
assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: run.id }).filter(item => item.id === input.checkReceiptId));
|
|
409
413
|
const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
|
|
410
414
|
const packets = origins.map((work) => packet(work));
|
|
411
415
|
const invariantPackets = receipt || input.semanticUnresolved?.length
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
8
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
9
9
|
import { AppError } from "../shared/errors.js";
|
|
10
|
+
import { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
|
|
10
11
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
11
12
|
import { requireProjectByRoot } from "./projects.js";
|
|
12
13
|
import { nextMergeRequestId } from "./ids.js";
|
|
@@ -314,7 +315,8 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
314
315
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
315
316
|
if (failed.length) {
|
|
316
317
|
context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
|
|
317
|
-
|
|
318
|
+
const sourceRepair = failed.every(receipt => checkRepairDisposition(receipt) === "source_repair");
|
|
319
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Only assertion-backed failures permit source repair; tooling or unknown evidence requires diagnosis/settlement first.", 2, { failures: failed, ...(sourceRepair ? { repair_command: repairCommand(context, request, projectRoot) } : { outcome: "blocked", recoverable: false }), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
|
|
318
320
|
}
|
|
319
321
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
320
322
|
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
@@ -476,6 +478,7 @@ export async function repairVnextMerge(context, input) {
|
|
|
476
478
|
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
477
479
|
throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
|
|
478
480
|
}
|
|
481
|
+
assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: request.run_id }).filter(receipt => failedReceiptIds(request).includes(receipt.id)));
|
|
479
482
|
abortIntegration(request);
|
|
480
483
|
const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
|
|
481
484
|
if (!originWorkIds.length)
|
|
@@ -71,10 +71,10 @@ export function prepareVnextProtocolizeArtifacts(context, input) {
|
|
|
71
71
|
return { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog, workspace };
|
|
72
72
|
}
|
|
73
73
|
/** Materialize the next-stage packet; this is execution, not read-only CLI preparation. */
|
|
74
|
-
export function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
|
|
74
|
+
export async function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
|
|
75
75
|
const { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog } = prepared;
|
|
76
76
|
fs.mkdirSync(stageRoot, { recursive: true });
|
|
77
|
-
const policy = provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
|
|
77
|
+
const policy = await provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
|
|
78
78
|
const routedRun = requireRun(context, projectRoot, run.id);
|
|
79
79
|
writeJson(path.join(stageRoot, "work-context.json"), { schema_id: "dd-flow/vnext-protocolize-context@2", system: { run_id: run.id, work_id: work.work_id, stage, handoff: input.handoff }, input: { accepted_specify_json: specifyPath, accepted_specify_markdown: specifyMarkdownPath }, catalog: projectCatalog, workspace: { project_root: projectRoot, workspace_root: routedRun.workspace_root, stage_root: stageRoot, git_policy: policy } });
|
|
80
80
|
if (!fs.existsSync(resultPath))
|
|
@@ -138,7 +138,7 @@ function readProtocolizeContext(file) {
|
|
|
138
138
|
catch { /* retained context is not a caller argument */ }
|
|
139
139
|
throw new AppError("runtime_context_invalid", "PROTOCOLIZE context is missing a valid frozen handoff", 1, { file });
|
|
140
140
|
}
|
|
141
|
-
export function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
|
|
141
|
+
export async function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
|
|
142
142
|
const { projectRoot, project, run, work, protocolizeRoot, contextPath, packetInput, packet, stageAttached, mode, promptPath } = prepared;
|
|
143
143
|
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
144
144
|
const previous = context.db.get("SELECT ws.session_id, s.stop_reason FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, work.work_id]);
|
|
@@ -153,7 +153,7 @@ export function startVnextProtocolize(context, input, prepared = prepareVnextPro
|
|
|
153
153
|
if (mode === "new_session" && previous?.session_id === sessionId)
|
|
154
154
|
throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to start in a fresh session", 1);
|
|
155
155
|
if (packet)
|
|
156
|
-
prepareVnextProtocolize(context, packetInput, packet);
|
|
156
|
+
await prepareVnextProtocolize(context, packetInput, packet);
|
|
157
157
|
const frozenContext = prepared.frozenContext ?? readProtocolizeContext(contextPath);
|
|
158
158
|
const externalContext = applyExternalStageContext({ context, stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
159
159
|
const binding = bindStageCoordinatorWork(context, { workId: work.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(requireRunHome(run), "02-protocolize", "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
@@ -178,7 +178,7 @@ function specifyHandoff(file, workId) {
|
|
|
178
178
|
catch { /* reported below */ }
|
|
179
179
|
throw new AppError("runtime_context_invalid", "SPECIFY Work is missing its immutable handoff policy", 1, { work_id: workId });
|
|
180
180
|
}
|
|
181
|
-
export function finishVnextProtocolize(context, input) {
|
|
181
|
+
export async function finishVnextProtocolize(context, input) {
|
|
182
182
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
183
183
|
const project = requireProjectByRoot(context, projectRoot);
|
|
184
184
|
const run = requireRun(context, projectRoot, input.runId);
|
|
@@ -205,7 +205,7 @@ export function finishVnextProtocolize(context, input) {
|
|
|
205
205
|
const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
|
|
206
206
|
const receipt = readWorkspaceRouteReceipt(root);
|
|
207
207
|
const workspaceRoot = run.workspace_root;
|
|
208
|
-
const intent = prepareProtocolizeIntent(context, run, resultFile, result, obligations);
|
|
208
|
+
const intent = await prepareProtocolizeIntent(context, run, resultFile, result, obligations);
|
|
209
209
|
publishProtocolizeIntent(context, project.id, intent);
|
|
210
210
|
const protocolIds = intent.protocol_ids;
|
|
211
211
|
const changedFiles = intent.changed_files;
|
|
@@ -246,7 +246,7 @@ export function finishVnextProtocolize(context, input) {
|
|
|
246
246
|
const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode } });
|
|
247
247
|
return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", ...(next.command ? { next_command: next.command } : {}), next, artifacts: { result: resultFile, report_json: reportJson, report_markdown: reportMarkdown, report_html: reportHtml, protocols: protocolIds.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "summary.md")) } };
|
|
248
248
|
}
|
|
249
|
-
function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
|
|
249
|
+
async function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
|
|
250
250
|
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
|
|
251
251
|
const index = row ? JSON.parse(row.index_json) : null;
|
|
252
252
|
const attempt = index?.stage_runs?.find(item => item.stage === stage)?.attempt;
|
|
@@ -270,11 +270,12 @@ function prepareProtocolizeIntent(context, run, resultFile, result, obligations)
|
|
|
270
270
|
? { ...result.feature, epic_path: path.join(draft, path.relative(projectRoot, result.feature.epic_path)) }
|
|
271
271
|
: result.feature;
|
|
272
272
|
const draftResult = { ...result, feature };
|
|
273
|
-
const
|
|
273
|
+
const materialized = await materialize(draft, run.id, requireRunHome(run), draftResult, obligations, protocolIds, psetId);
|
|
274
|
+
const changedFiles = materialized.files;
|
|
274
275
|
const files = changedFiles.map(relative => {
|
|
275
|
-
const
|
|
276
|
+
const target = path.join(draft, relative);
|
|
276
277
|
const bytes = fs.readFileSync(target, "utf8");
|
|
277
|
-
return { relative, before_sha256:
|
|
278
|
+
return { relative, before_sha256: materialized.before[path.relative(draft, target)] ?? null, after_sha256: sha256(bytes), bytes };
|
|
278
279
|
});
|
|
279
280
|
const candidate = { run_id: run.id, attempt, workspace_root: projectRoot, result_sha256: resultSha, protocol_ids: protocolIds, pset_id: psetId, changed_files: changedFiles, files };
|
|
280
281
|
return context.db.writeTransaction(() => {
|
|
@@ -366,7 +367,7 @@ function ensureManagedWorktreeRecorded(context, input) {
|
|
|
366
367
|
recordManagedWorktree(context, input);
|
|
367
368
|
}
|
|
368
369
|
function sha256(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
|
|
369
|
-
function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
370
|
+
async function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
|
|
370
371
|
const psetPath = psetId ? path.join(projectRoot, ".memory-bank", "protocol", "_set", `${psetId}.md`) : null;
|
|
371
372
|
const created = [];
|
|
372
373
|
const changed = new Set();
|
|
@@ -404,8 +405,8 @@ function materialize(projectRoot, runId, runHome, result, obligations, ids, pset
|
|
|
404
405
|
const absolute = path.resolve(projectRoot, file);
|
|
405
406
|
return created.some((directory) => absolute.startsWith(`${directory}${path.sep}`)) || absolute === psetPath;
|
|
406
407
|
});
|
|
407
|
-
runTargetedMemoryBankLint(projectRoot, materializedFiles);
|
|
408
|
-
return files;
|
|
408
|
+
await runTargetedMemoryBankLint(projectRoot, materializedFiles);
|
|
409
|
+
return { files, before: Object.fromEntries([...originals].map(([file, bytes]) => [path.relative(projectRoot, file), bytes === null ? null : sha256(bytes)])) };
|
|
409
410
|
}
|
|
410
411
|
catch (error) {
|
|
411
412
|
for (const [file, content] of originals) {
|
|
@@ -670,7 +671,7 @@ function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, st
|
|
|
670
671
|
prepareManagedWorktreeBootstrapSources(projectRoot);
|
|
671
672
|
return { kind: "feature", receiptPath, config, stableFacts, sessionMode, branch, create };
|
|
672
673
|
}
|
|
673
|
-
function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
|
|
674
|
+
async function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
|
|
674
675
|
if (prepared.kind === "retained")
|
|
675
676
|
return prepared.policy;
|
|
676
677
|
const { receiptPath } = prepared;
|
|
@@ -679,8 +680,8 @@ function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
|
|
|
679
680
|
return prepared.policy;
|
|
680
681
|
}
|
|
681
682
|
const { config, stableFacts, sessionMode, branch, create } = prepared;
|
|
682
|
-
const created = createManagedWorktree(context, create, create);
|
|
683
|
-
const bootstrap = bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
|
|
683
|
+
const created = await createManagedWorktree(context, create, create);
|
|
684
|
+
const bootstrap = await bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
|
|
684
685
|
rebindFlowRunWorkspace(context, { projectRoot, runId: run.id, workspaceRoot: created.worktreePath, reason: "PROTOCOLIZE start provisioned the configured feature-worktree route" });
|
|
685
686
|
const policy = { route: "feature_worktree", integration_branch: config.workspace.integration_branch, base_ref: stableFacts.head, feature_branch: branch, worktree_path: created.worktreePath, reason: "Project workspace configuration requires a managed feature worktree before durable protocol documents exist.", provision_stage: "protocolize_start", stage_session_mode: sessionMode, provisioned: true, bootstrap_status: bootstrap.status };
|
|
686
687
|
writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy, created, bootstrap });
|
|
@@ -12,7 +12,7 @@ import { validateSchema } from "./schema-validation.js";
|
|
|
12
12
|
import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
|
|
13
13
|
import { prepareAgentOutputFile } from "../storage/agent-output.js";
|
|
14
14
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
|
-
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
|
+
import { assertSourceRepairEligible, ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
16
16
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
17
17
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
18
18
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
@@ -1104,6 +1104,8 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1104
1104
|
validateFailedCheckRetry(context, { projectId: work.project_id, runId: work.run_id, receiptId: retry.receiptId, declarations: requiredChecks, reason: retry.reason });
|
|
1105
1105
|
receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(retry ? { retryReceiptId: retry.receiptId, retryReason: retry.reason } : {}), ...(progress ? { progress } : {}) });
|
|
1106
1106
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
1107
|
+
if (failed.length)
|
|
1108
|
+
assertSourceRepairEligible(failed);
|
|
1107
1109
|
if (failed.length)
|
|
1108
1110
|
throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
|
|
1109
1111
|
}
|
|
@@ -10,6 +10,8 @@ import { persistProtocolState, readProtocolRuntimeState, requireProtocol } from
|
|
|
10
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
11
|
import { ensureManagedWorktrunk, inspectManagedWorktrunk } from "./tooling.js";
|
|
12
12
|
import { propagateOwnedWorkspaceHooks } from "./workspace-files.js";
|
|
13
|
+
import { commandResult } from '../harness-runtime/lib/process-json.mjs';
|
|
14
|
+
import { SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
|
|
13
15
|
export function planWorktree(context, input) {
|
|
14
16
|
const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
|
|
15
17
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
@@ -40,11 +42,11 @@ export function prepareWorktreeCreate(context, input) {
|
|
|
40
42
|
}
|
|
41
43
|
return prepareManagedWorktree({ ...input, projectRoot: protocol.project_root });
|
|
42
44
|
}
|
|
43
|
-
export function createWorktreeRecord(context, input, prepared = prepareWorktreeCreate(context, input)) {
|
|
45
|
+
export async function createWorktreeRecord(context, input, prepared = prepareWorktreeCreate(context, input)) {
|
|
44
46
|
const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
|
|
45
47
|
if (activeWorktreeRecord(context, protocol.project_id, protocol.id))
|
|
46
48
|
throw new AppError("worktree_already_exists", "Protocol already has an active worktree record", 1);
|
|
47
|
-
const created = createManagedWorktree(context, prepared, prepared);
|
|
49
|
+
const created = await createManagedWorktree(context, prepared, prepared);
|
|
48
50
|
recordManagedWorktree(context, { protocolId: protocol.id, projectRoot: protocol.project_root, branch: prepared.branch, base: prepared.base, created });
|
|
49
51
|
return { ok: true, worktree: activeWorktreeRecord(context, protocol.project_id, protocol.id) };
|
|
50
52
|
}
|
|
@@ -82,13 +84,13 @@ export function prepareManagedWorktree(input) {
|
|
|
82
84
|
return { projectRoot, branch: input.branch, base: base.stdout.trim(), path: destination };
|
|
83
85
|
}
|
|
84
86
|
/** Creates the checkout before a PRT exists. PROTOCOLIZE materializes the PRT only inside this checkout. */
|
|
85
|
-
export function createManagedWorktree(context, input, prepared = prepareManagedWorktree(input)) {
|
|
87
|
+
export async function createManagedWorktree(context, input, prepared = prepareManagedWorktree(input)) {
|
|
86
88
|
const { projectRoot } = prepared;
|
|
87
|
-
const worktrunk = ensureManagedWorktrunk(context);
|
|
89
|
+
const worktrunk = await ensureManagedWorktrunk(context);
|
|
88
90
|
// Worktrunk owns creation; the CLI supplies its project-scoped service path
|
|
89
91
|
// as a one-shot config override rather than relying on a developer's user config.
|
|
90
92
|
const command = [worktrunk.bin, "-C", projectRoot, "--config-set", `worktree-path = ${JSON.stringify(prepared.path)}`, "switch", "--create", prepared.branch, "--base", prepared.base, "--no-cd", "--format=json", "-y"];
|
|
91
|
-
const result = runExternal(command);
|
|
93
|
+
const result = await runExternal(command);
|
|
92
94
|
if (result.exit_code !== 0)
|
|
93
95
|
throw new AppError("worktrunk_command_unavailable", "Worktrunk switch failed", 1, { command: sanitizeCommand(command), result });
|
|
94
96
|
const response = parseWorktrunkResponse(result.stdout, command);
|
|
@@ -137,16 +139,16 @@ export function prepareManagedWorktreeBootstrap(input) {
|
|
|
137
139
|
throw new AppError("bootstrap_unavailable", "Bootstrap worktree must be a directory", 1, { worktree_path: worktreePath });
|
|
138
140
|
return { projectRoot, worktreePath };
|
|
139
141
|
}
|
|
140
|
-
export function bootstrapManagedWorktree(context, input, prepared = prepareManagedWorktreeBootstrap(input)) {
|
|
142
|
+
export async function bootstrapManagedWorktree(context, input, prepared = prepareManagedWorktreeBootstrap(input)) {
|
|
141
143
|
const { worktreePath } = prepared;
|
|
142
|
-
const worktrunk = ensureManagedWorktrunk(context);
|
|
144
|
+
const worktrunk = await ensureManagedWorktrunk(context);
|
|
143
145
|
const commands = [
|
|
144
146
|
[worktrunk.bin, "-C", worktreePath, "step", "copy-ignored", "--require-include"],
|
|
145
147
|
[worktrunk.bin, "-C", worktreePath, "hook", "pre-start", "--yes"]
|
|
146
148
|
];
|
|
147
149
|
const results = [];
|
|
148
150
|
for (const command of commands) {
|
|
149
|
-
const result = runExternal(command);
|
|
151
|
+
const result = await runExternal(command);
|
|
150
152
|
results.push({ command: sanitizeCommand(command), ...result });
|
|
151
153
|
if (result.exit_code !== 0) {
|
|
152
154
|
throw new AppError("bootstrap_failed", "Project-owned Worktrunk bootstrap failed", 1, { command: sanitizeCommand(command), result, results });
|
|
@@ -167,14 +169,14 @@ export function prepareWorktreeBootstrap(context, input) {
|
|
|
167
169
|
}
|
|
168
170
|
return { protocol, record, managed: prepareManagedWorktreeBootstrap({ projectRoot: protocol.project_root, worktreePath: record.worktree_path }) };
|
|
169
171
|
}
|
|
170
|
-
export function bootstrapWorktree(context, input, prepared = prepareWorktreeBootstrap(context, input)) {
|
|
172
|
+
export async function bootstrapWorktree(context, input, prepared = prepareWorktreeBootstrap(context, input)) {
|
|
171
173
|
const { protocol, record } = prepared;
|
|
172
174
|
const active = activeWorktreeRecord(context, protocol.project_id, protocol.id);
|
|
173
175
|
if (!active || active.worktree_path !== record.worktree_path)
|
|
174
176
|
throw new AppError("worktree_active", "Worktree changed after bootstrap preparation", 1);
|
|
175
177
|
let result;
|
|
176
178
|
try {
|
|
177
|
-
result = bootstrapManagedWorktree(context, prepared.managed, prepared.managed);
|
|
179
|
+
result = await bootstrapManagedWorktree(context, prepared.managed, prepared.managed);
|
|
178
180
|
}
|
|
179
181
|
catch (error) {
|
|
180
182
|
if (error instanceof AppError && error.code === "bootstrap_failed")
|
|
@@ -254,16 +256,16 @@ export function worktreeRecord(context, projectId, protocolId) {
|
|
|
254
256
|
function activeWorktreeRecord(context, projectId, protocolId) {
|
|
255
257
|
return context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ? AND status = 'active'", [projectId, protocolId]);
|
|
256
258
|
}
|
|
257
|
-
function runExternal(command) {
|
|
259
|
+
async function runExternal(command) {
|
|
258
260
|
const [bin, ...args] = command;
|
|
259
261
|
if (!bin) {
|
|
260
262
|
return { exit_code: null, stdout: "", stderr: "Missing executable." };
|
|
261
263
|
}
|
|
262
|
-
const result =
|
|
264
|
+
const result = await commandResult(bin, args, { startupMs: SETUP_STARTUP_MS });
|
|
263
265
|
return {
|
|
264
|
-
exit_code:
|
|
266
|
+
exit_code: result.exit_code,
|
|
265
267
|
stdout: sanitizeOutput(result.stdout),
|
|
266
|
-
stderr: sanitizeOutput(result.stderr
|
|
268
|
+
stderr: sanitizeOutput(result.stderr)
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
271
|
function sanitizeCommand(command) {
|