@deksden-com/dd-flow-cli 0.9.0-beta.140 → 0.9.0-beta.142
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 +14 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/command-inputs.js +1 -1
- package/dist/cli/command-option-descriptions.js +1 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/run-cli.js +9 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +7 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +3 -3
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +13 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +64 -12
- package/dist/harness-runtime/lib/dd-opencode.mjs +12 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +3 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +36 -7
- package/dist/harness-runtime/lib/operation-errors.d.mts +2 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +11 -0
- package/dist/services/check-runner-evidence.js +102 -20
- package/dist/services/code-checks.js +69 -33
- package/dist/services/runtime-budget.js +16 -0
- package/dist/services/stage-lifecycle.js +57 -13
- package/dist/services/tooling.js +14 -12
- package/dist/services/verification-matrix.js +3 -2
- package/dist/services/vnext-plan.js +1 -2
- package/dist/services/vnext-protocolize.js +121 -10
- package/dist/services/worktrees.js +8 -5
- package/dist/shared/errors.js +11 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { parse as parseYaml } from "yaml";
|
|
8
|
-
import { AppError } from "../shared/errors.js";
|
|
8
|
+
import { AppError, errorRecord, redactDiagnostic, diagnosticJson } from "../shared/errors.js";
|
|
9
9
|
import { projectFeatureWorktreePath, resolveProjectRoot } from "../storage/paths.js";
|
|
10
10
|
import { previewNextEntityId } from "./ids.js";
|
|
11
11
|
import { registerProtocol } from "./protocols.js";
|
|
@@ -291,6 +291,7 @@ async function prepareProtocolizeIntent(context, run, resultFile, result, obliga
|
|
|
291
291
|
const protocolIds = allocateProtocolIds(context, run.project_id, projectRoot, result.delivery.members.map(member => member.slug));
|
|
292
292
|
const psetId = result.delivery.kind === "protocol_set" ? allocatePsetId(context, run.project_id, projectRoot, result.delivery.slug) : null;
|
|
293
293
|
const draft = fs.mkdtempSync(path.join(os.tmpdir(), "dd-flow-protocolize-"));
|
|
294
|
+
let draftFailure;
|
|
294
295
|
try {
|
|
295
296
|
// A draft must never follow a workspace symlink back into the live tree.
|
|
296
297
|
fs.cpSync(path.join(projectRoot, ".memory-bank"), path.join(draft, ".memory-bank"), { recursive: true, filter: source => !fs.lstatSync(source).isSymbolicLink() });
|
|
@@ -321,9 +322,24 @@ async function prepareProtocolizeIntent(context, run, resultFile, result, obliga
|
|
|
321
322
|
return candidate;
|
|
322
323
|
});
|
|
323
324
|
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
draftFailure = error;
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
324
329
|
finally {
|
|
330
|
+
cleanupProtocolizeDraft(draft, draftFailure);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function cleanupProtocolizeDraft(draft, failure) {
|
|
334
|
+
try {
|
|
325
335
|
fs.rmSync(draft, { recursive: true, force: true });
|
|
326
336
|
}
|
|
337
|
+
catch (cleanup) {
|
|
338
|
+
if (!failure)
|
|
339
|
+
throw new AppError("protocolize_draft_cleanup_failed", "PROTOCOLIZE draft cleanup failed", 1, { cause: errorRecord(cleanup), draft_path: draft, recoverable: false, effect: "unknown" });
|
|
340
|
+
const primary = failure;
|
|
341
|
+
primary.details = { ...primary.details, draft_cleanup_error: errorRecord(cleanup), draft_path: draft, recoverable: false };
|
|
342
|
+
}
|
|
327
343
|
}
|
|
328
344
|
function readProtocolizeIntent(context, projectId, runId, attempt) {
|
|
329
345
|
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.protocolize_intent' ORDER BY id DESC", [projectId]);
|
|
@@ -465,20 +481,115 @@ async function materialize(projectRoot, runId, runHome, result, obligations, ids
|
|
|
465
481
|
const absolute = path.resolve(projectRoot, file);
|
|
466
482
|
return created.some((directory) => absolute.startsWith(`${directory}${path.sep}`)) || absolute === psetPath;
|
|
467
483
|
});
|
|
468
|
-
await runTargetedMemoryBankLint(projectRoot, materializedFiles);
|
|
484
|
+
await runTargetedMemoryBankLint(projectRoot, materializedFiles, path.join(runHome, "diagnostics"));
|
|
469
485
|
return { files, before: Object.fromEntries([...originals].map(([file, bytes]) => [path.relative(projectRoot, file), bytes === null ? null : sha256(bytes)])) };
|
|
470
486
|
}
|
|
471
487
|
catch (error) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
fs.writeFileSync(file, content);
|
|
488
|
+
const primary = error instanceof Error ? error : new AppError("protocolize_materialization_failed", String(error));
|
|
489
|
+
const facts = {};
|
|
490
|
+
try {
|
|
491
|
+
facts.diagnostic_path = captureMaterializationFailure(projectRoot, runHome, [...changed].sort(), primary);
|
|
477
492
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
493
|
+
catch (capture) {
|
|
494
|
+
facts.capture_error = errorRecord(capture);
|
|
495
|
+
}
|
|
496
|
+
const rollback = rollbackMaterialization(projectRoot, originals, created);
|
|
497
|
+
const target = primary;
|
|
498
|
+
target.details = { ...target.details, ...facts, rollback,
|
|
499
|
+
...(rollback.unrestored.length ? { effect: "unknown", recoverable: false, disposition: "tooling_blocker" } : {}) };
|
|
500
|
+
throw primary;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
export function rollbackMaterialization(projectRoot, originals, created) {
|
|
504
|
+
const restored = [], unrestored = [];
|
|
505
|
+
const restore = (file, action) => {
|
|
506
|
+
try {
|
|
507
|
+
action();
|
|
508
|
+
restored.push(path.relative(projectRoot, file));
|
|
509
|
+
}
|
|
510
|
+
catch (cause) {
|
|
511
|
+
unrestored.push({ path: path.relative(projectRoot, file), error: errorRecord(cause) });
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
for (const [file, content] of originals)
|
|
515
|
+
restore(file, () => { if (content === null)
|
|
516
|
+
fs.rmSync(file, { force: true });
|
|
517
|
+
else
|
|
518
|
+
fs.writeFileSync(file, content); });
|
|
519
|
+
for (const dir of [...created].reverse())
|
|
520
|
+
restore(dir, () => fs.rmSync(dir, { recursive: true, force: true }));
|
|
521
|
+
return { restored, unrestored };
|
|
522
|
+
}
|
|
523
|
+
export function captureMaterializationFailure(projectRoot, runHome, files, error) {
|
|
524
|
+
const directory = path.join(runHome, "diagnostics", `protocolize-${crypto.randomUUID()}`);
|
|
525
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
526
|
+
let remaining = 1024 * 1024;
|
|
527
|
+
const documents = files.slice(0, 64).map(relative => {
|
|
528
|
+
const absolute = path.resolve(projectRoot, relative);
|
|
529
|
+
if (!absolute.startsWith(`${path.resolve(projectRoot)}${path.sep}`))
|
|
530
|
+
return { path: relative, unavailable: "outside_workspace" };
|
|
531
|
+
try {
|
|
532
|
+
assertNoSymlinkPath(path.resolve(projectRoot), absolute);
|
|
533
|
+
inside(fs.realpathSync(projectRoot), fs.realpathSync(absolute));
|
|
534
|
+
const info = fs.lstatSync(absolute);
|
|
535
|
+
if (!info.isFile())
|
|
536
|
+
return { path: relative, unavailable: "not_regular_file" };
|
|
537
|
+
// ponytail: bounded diagnostic hash, never chase a growing live file.
|
|
538
|
+
// Larger inputs retain a prefix hash explicitly marked incomplete.
|
|
539
|
+
const hash = crypto.createHash("sha256"), fd = fs.openSync(absolute, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
540
|
+
const chunk = Buffer.alloc(64 * 1024), parts = [];
|
|
541
|
+
let kept = 0, offset = 0, complete = false;
|
|
542
|
+
try {
|
|
543
|
+
const opened = fs.fstatSync(fd);
|
|
544
|
+
if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino)
|
|
545
|
+
throw new AppError("diagnostic_input_changed", "Diagnostic input changed before capture");
|
|
546
|
+
const limit = Math.min(opened.size, 1024 * 1024);
|
|
547
|
+
while (offset < limit) {
|
|
548
|
+
const read = fs.readSync(fd, chunk, 0, Math.min(chunk.length, limit - offset), offset);
|
|
549
|
+
if (!read)
|
|
550
|
+
break;
|
|
551
|
+
offset += read;
|
|
552
|
+
hash.update(chunk.subarray(0, read));
|
|
553
|
+
if (kept < Math.min(remaining, 65536)) {
|
|
554
|
+
const part = Buffer.from(chunk.subarray(0, Math.min(read, Math.min(remaining, 65536) - kept)));
|
|
555
|
+
parts.push(part);
|
|
556
|
+
kept += part.length;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const after = fs.fstatSync(fd), current = fs.lstatSync(absolute);
|
|
560
|
+
complete = offset === opened.size && after.size === opened.size && after.mtimeMs === opened.mtimeMs && after.ctimeMs === opened.ctimeMs
|
|
561
|
+
&& current.dev === opened.dev && current.ino === opened.ino;
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
fs.closeSync(fd);
|
|
565
|
+
}
|
|
566
|
+
const content = Buffer.from(redactDiagnostic(Buffer.concat(parts).toString("utf8"))).subarray(0, Math.min(remaining, 65536)).toString("utf8");
|
|
567
|
+
remaining -= Buffer.byteLength(content);
|
|
568
|
+
const digest = hash.digest("hex");
|
|
569
|
+
return { path: relative, sha256: complete ? digest : null, ...(!complete ? { prefix_sha256: digest, hashed_bytes: offset } : {}), hash_complete: complete, size: info.size, truncated: kept < info.size, content };
|
|
570
|
+
}
|
|
571
|
+
catch (cause) {
|
|
572
|
+
return { path: relative, unavailable: errorRecord(cause) };
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
const file = path.join(directory, "failure.json");
|
|
576
|
+
const capture = { schema_id: "dd-flow/protocolize-failure@1", error: errorRecord(error), documents, omitted_documents: Math.max(0, files.length - documents.length) };
|
|
577
|
+
let bytes = diagnosticJson(capture);
|
|
578
|
+
while (Buffer.byteLength(bytes) > 1024 * 1024) {
|
|
579
|
+
const contentLength = (document) => "content" in document ? String(document.content ?? "").length : 0;
|
|
580
|
+
const largest = documents.filter(document => contentLength(document) > 0).sort((a, b) => contentLength(b) - contentLength(a))[0];
|
|
581
|
+
if (largest && "content" in largest) {
|
|
582
|
+
largest.content = String(largest.content).slice(0, Math.floor(String(largest.content).length / 2));
|
|
583
|
+
largest.truncated = true;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
documents.pop();
|
|
587
|
+
capture.omitted_documents++;
|
|
588
|
+
}
|
|
589
|
+
bytes = diagnosticJson(capture);
|
|
481
590
|
}
|
|
591
|
+
fs.writeFileSync(file, bytes, { flag: "wx", mode: 0o600 });
|
|
592
|
+
return file;
|
|
482
593
|
}
|
|
483
594
|
function protocolMarkdown(id, member, blockedBy, runId, psetPath, featurePath, result, obligations, rawIntakePath) {
|
|
484
595
|
const a = member.primary_acceptance;
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { flowContractForState } from "../domain/flow-contract.js";
|
|
5
5
|
import { requireStage } from "../domain/validation.js";
|
|
6
|
-
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { AppError, processEvidence, redactDiagnostic } from "../shared/errors.js";
|
|
7
7
|
import { canonicalPath, projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
|
|
8
8
|
import { appendAudit } from "./audit.js";
|
|
9
9
|
import { persistProtocolState, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
@@ -259,20 +259,23 @@ function activeWorktreeRecord(context, projectId, protocolId) {
|
|
|
259
259
|
async function runExternal(command) {
|
|
260
260
|
const [bin, ...args] = command;
|
|
261
261
|
if (!bin) {
|
|
262
|
-
return { exit_code: null, stdout: "", stderr: "Missing executable." };
|
|
262
|
+
return { exit_code: null, signal: null, stdout: "", stderr: "Missing executable." };
|
|
263
263
|
}
|
|
264
264
|
const result = await commandResult(bin, args, { startupMs: SETUP_STARTUP_MS });
|
|
265
|
+
const evidence = processEvidence(result);
|
|
265
266
|
return {
|
|
266
267
|
exit_code: result.exit_code,
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
signal: result.signal,
|
|
269
|
+
stdout: result.exit_code === 0 && !result.signal ? sanitizeOutput(result.stdout) : evidence.stdout.text,
|
|
270
|
+
stderr: evidence.stderr.text,
|
|
271
|
+
output_truncated: { stdout: result.exit_code !== 0 && evidence.stdout.truncated, stderr: evidence.stderr.truncated }
|
|
269
272
|
};
|
|
270
273
|
}
|
|
271
274
|
function sanitizeCommand(command) {
|
|
272
275
|
return command.map((part) => (part.includes("=") ? part.replace(/=.*/, "=<redacted>") : part));
|
|
273
276
|
}
|
|
274
277
|
function sanitizeOutput(value) {
|
|
275
|
-
return value
|
|
278
|
+
return redactDiagnostic(value);
|
|
276
279
|
}
|
|
277
280
|
function parseWorktrunkResponse(stdout, command) {
|
|
278
281
|
let parsed;
|
package/dist/shared/errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { errorRecord as nativeErrorRecord } from "../harness-runtime/lib/operation-errors.mjs";
|
|
1
|
+
import { errorRecord as nativeErrorRecord, redactDiagnostic } from "../harness-runtime/lib/operation-errors.mjs";
|
|
2
|
+
export { redactDiagnostic, diagnosticJson } from "../harness-runtime/lib/operation-errors.mjs";
|
|
2
3
|
export class AppError extends Error {
|
|
3
4
|
code;
|
|
4
5
|
exitCode;
|
|
@@ -29,3 +30,12 @@ export function errorRecord(error, depth = 0) {
|
|
|
29
30
|
export function isAppError(error) {
|
|
30
31
|
return error instanceof AppError;
|
|
31
32
|
}
|
|
33
|
+
/** Bounded process diagnostics are observations, never command/retry authority. */
|
|
34
|
+
export function processEvidence(result) {
|
|
35
|
+
const excerpt = (value, limit) => {
|
|
36
|
+
const safe = redactDiagnostic(value), bytes = Buffer.from(safe);
|
|
37
|
+
return { text: new TextDecoder().decode(bytes.subarray(0, limit), { stream: true }), truncated: bytes.length > limit, bytes: bytes.length };
|
|
38
|
+
};
|
|
39
|
+
return { exit_code: result.exit_code, signal: result.signal ?? null,
|
|
40
|
+
stdout: excerpt(result.stdout, 8192), stderr: excerpt(result.stderr, 16384) };
|
|
41
|
+
}
|