@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74
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/help.js +2 -0
- package/dist/cli/run-cli.js +15 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
- package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
- package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
- package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
- package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
- package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
- package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/services/code-checks.js +120 -19
- package/dist/services/controller-fanout.js +11 -0
- package/dist/services/engines.js +40 -9
- package/dist/services/eval-snapshots.js +30 -47
- package/dist/services/harness-adapter.js +12 -6
- package/dist/services/lifecycle-invocations.js +12 -8
- package/dist/services/native-session-control.js +6 -0
- package/dist/services/projects.js +6 -2
- package/dist/services/recovery-observation-budget.js +9 -1
- package/dist/services/run-control-worker.js +19 -0
- package/dist/services/run-control.js +87 -13
- package/dist/services/run-controller-adapter.js +134 -7
- package/dist/services/run-controller.js +11 -3
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/runs.js +1 -11
- package/dist/services/schema-validation.js +1 -1
- package/dist/services/stage-lifecycle.js +14 -2
- package/dist/services/usage.js +20 -87
- package/dist/services/vnext-code-review.js +35 -27
- package/dist/services/vnext-code.js +62 -26
- package/dist/services/vnext-merge.js +8 -8
- package/dist/services/vnext-plan-review.js +16 -16
- package/dist/services/vnext-plan.js +51 -11
- package/dist/services/work-registry.js +48 -27
- package/dist/shared/errors.js +12 -0
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
|
|
|
7
7
|
import { validateSchema } from "./schema-validation.js";
|
|
8
8
|
import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
|
|
9
9
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
10
|
-
import { readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
10
|
+
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
11
11
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
12
12
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
13
13
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
@@ -188,23 +188,6 @@ export function listWorks(context, input) {
|
|
|
188
188
|
}) };
|
|
189
189
|
}
|
|
190
190
|
export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id 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", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
|
|
191
|
-
/** Inspect materializations without changing either the accepted receipt or conflicting bytes. */
|
|
192
|
-
export function runWorkResultArtifactIssues(context, projectId, runId, materializedPath = (file) => file) {
|
|
193
|
-
const works = context.db.all(`SELECT w.work_id, w.result, (SELECT ws.result_path FROM work_sessions ws WHERE ws.work_id = w.work_id ORDER BY ws.created_at DESC LIMIT 1) AS result_path FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.result_schema IS NOT NULL AND w.result IS NOT NULL AND w.status = 'completed'`, [projectId, runId]);
|
|
194
|
-
const issues = [];
|
|
195
|
-
for (const work of works) {
|
|
196
|
-
const file = work.result_path ? materializedPath(work.result_path) : null;
|
|
197
|
-
const actual = file && fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
|
198
|
-
if (actual !== work.result)
|
|
199
|
-
issues.push({ code: actual === null ? "work_result_artifact_missing" : "work_result_artifact_drift", work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: actual === null ? null : crypto.createHash("sha256").update(actual).digest("hex") });
|
|
200
|
-
}
|
|
201
|
-
return issues;
|
|
202
|
-
}
|
|
203
|
-
export function assertRunWorkResultArtifacts(context, projectId, runId) {
|
|
204
|
-
const issue = runWorkResultArtifactIssues(context, projectId, runId)[0];
|
|
205
|
-
if (issue)
|
|
206
|
-
throw new AppError(issue.code, issue.code === "work_result_artifact_missing" ? "Completed structured Work has no retained result artifact" : "Completed Work result artifact no longer matches its authoritative receipt", 1, issue);
|
|
207
|
-
}
|
|
208
191
|
function receiptIntentPath(resultPath, sessionId) {
|
|
209
192
|
return `${resultPath}.pending-${crypto.createHash("sha256").update(sessionId).digest("hex")}`;
|
|
210
193
|
}
|
|
@@ -495,9 +478,9 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
495
478
|
// Give each new-generation start its own immutable publication destination.
|
|
496
479
|
const packetDirectory = generation > 0 ? path.join(directory, "starts", linkId) : directory;
|
|
497
480
|
const promptPath = path.join(packetDirectory, "prompt.md"), resultPath = path.join(directory, "result.json");
|
|
498
|
-
// The
|
|
481
|
+
// The first reviewer freezes shared inputs in the same transaction as its claim.
|
|
499
482
|
const readOnly = parsePayload(work)?.read_only === true;
|
|
500
|
-
|
|
483
|
+
let fingerprint;
|
|
501
484
|
let binding;
|
|
502
485
|
try {
|
|
503
486
|
binding = context.db.writeTransaction(() => {
|
|
@@ -518,6 +501,8 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
518
501
|
let prompt = renderWorkerPrompt(context, current, run, dependencyResults);
|
|
519
502
|
if (recovery)
|
|
520
503
|
prompt = `${recoveryPrompt(recovery)}\n\n${prompt}`;
|
|
504
|
+
if (readOnly)
|
|
505
|
+
fingerprint = sharedReviewInput(work, run, true) ?? workspaceFingerprint(run.workspace_root);
|
|
521
506
|
const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: run.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint } : {}) };
|
|
522
507
|
const contextBytes = `${JSON.stringify(workContext, null, 2)}\n`;
|
|
523
508
|
const response = { ok: true, work_id: current.work_id, work_session_id: linkId, worker_prompt_markdown: prompt, prompt_path: promptPath, ...(recovery ? { recovery } : {}), session_binding: { source: "PreToolUse", session: { harness_id: identity.harness, session_id: identity.nativeSessionId } } };
|
|
@@ -671,6 +656,27 @@ export function retryWork(context, id, reason) {
|
|
|
671
656
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
672
657
|
return { ok: true, work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason };
|
|
673
658
|
}
|
|
659
|
+
function sharedReviewInput(work, run, initialize = false) {
|
|
660
|
+
const kind = parsePayload(work)?.kind;
|
|
661
|
+
const relative = kind === "code-review" ? "06-code-review/review-context.json" : kind === "plan-review" ? "04-plan-review/work-context.json" : null;
|
|
662
|
+
if (!relative || !work.parent_work_id)
|
|
663
|
+
return null;
|
|
664
|
+
const file = path.join(requireRunHome(run), relative);
|
|
665
|
+
if (!fs.existsSync(file))
|
|
666
|
+
return null;
|
|
667
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
668
|
+
if (value.read_only_input?.parent_work_id !== work.parent_work_id)
|
|
669
|
+
return null;
|
|
670
|
+
if (!value.read_only_input.fingerprint && initialize) {
|
|
671
|
+
// Called under the Work-start writer reservation, after stage setup has
|
|
672
|
+
// finished. The first reviewer accepts the input shared by this group.
|
|
673
|
+
value.read_only_input.fingerprint = workspaceFingerprint(run.workspace_root);
|
|
674
|
+
const temporary = `${file}.tmp-${crypto.randomUUID()}`;
|
|
675
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
676
|
+
fs.renameSync(temporary, file);
|
|
677
|
+
}
|
|
678
|
+
return typeof value.read_only_input.fingerprint === "string" ? value.read_only_input.fingerprint : null;
|
|
679
|
+
}
|
|
674
680
|
async function settle(context, id, status, result, progress, retry) {
|
|
675
681
|
const work = requireWork(context, id);
|
|
676
682
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
@@ -685,14 +691,21 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
685
691
|
throw new AppError("runtime_missing", "Running Work has no open Work/Session link", 1, { work_id: id });
|
|
686
692
|
let receipts = [];
|
|
687
693
|
let coordinationDrift = [];
|
|
688
|
-
|
|
689
|
-
if (parsePayload(work)?.read_only === true) {
|
|
694
|
+
const assertReadOnlyInput = () => {
|
|
695
|
+
if (status === "completed" && parsePayload(work)?.read_only === true) {
|
|
696
|
+
const shared = sharedReviewInput(work, run);
|
|
697
|
+
// The final reviewer commits the shared input boundary under the writer
|
|
698
|
+
// reservation. Concurrent finishes cannot both skip this verification.
|
|
699
|
+
if (shared && context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND work_id <> ? AND status IN ('created','running','paused') AND json_extract(payload_json, '$.read_only') = 1 AND json_extract(payload_json, '$.kind') = ? LIMIT 1", [work.project_id, work.run_id, work.parent_work_id, work.work_id, parsePayload(work)?.kind]))
|
|
700
|
+
return;
|
|
690
701
|
const contextFile = link ? path.join(path.dirname(link.prompt_path), "context.json") : "";
|
|
691
|
-
const baseline = contextFile && fs.existsSync(contextFile) ? JSON.parse(fs.readFileSync(contextFile, "utf8")).workspace_fingerprint : null;
|
|
702
|
+
const baseline = shared ?? (contextFile && fs.existsSync(contextFile) ? JSON.parse(fs.readFileSync(contextFile, "utf8")).workspace_fingerprint : null);
|
|
692
703
|
const current = workspaceFingerprint(run.workspace_root);
|
|
693
704
|
if (typeof baseline !== "string" || baseline !== current)
|
|
694
|
-
throw new AppError("
|
|
705
|
+
throw new AppError("read_only_input_changed", "The workspace changed during read-only Work; its review input is stale (the writer is unknown)", 2, { work_id: work.work_id, baseline, current });
|
|
695
706
|
}
|
|
707
|
+
};
|
|
708
|
+
if (status === "completed") {
|
|
696
709
|
validateWorkResult(work, result, run.project_root, run.workspace_root, requireRunHome(run), run.id, link?.result_path ?? null);
|
|
697
710
|
const packet = codePacket(work);
|
|
698
711
|
if (packet) {
|
|
@@ -716,6 +729,7 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
716
729
|
const now = context.now();
|
|
717
730
|
context.db.beginWriteTransaction();
|
|
718
731
|
try {
|
|
732
|
+
assertReadOnlyInput();
|
|
719
733
|
assertWorkSettlementAllowed(context, work.project_id, work.run_id, work.work_id);
|
|
720
734
|
if (link && !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND status = 'running'", [link.id]))
|
|
721
735
|
throw new AppError("work_generation_stale", "Work Session was interrupted or superseded while checks ran", 1, { work_id: id });
|
|
@@ -749,7 +763,10 @@ function plannedAreaDrift(packet, result) {
|
|
|
749
763
|
const changed = JSON.parse(result).changed_paths;
|
|
750
764
|
if (!Array.isArray(changed) || packet.planned_write_areas.length === 0)
|
|
751
765
|
return [];
|
|
752
|
-
return changed.filter((item) => typeof item === "string" && !packet.planned_write_areas.some((area) =>
|
|
766
|
+
return changed.filter((item) => typeof item === "string" && !packet.planned_write_areas.some((area) => {
|
|
767
|
+
const normalized = area.replace(/[\\/]+$/, "");
|
|
768
|
+
return item === normalized || item.startsWith(`${normalized}/`);
|
|
769
|
+
}));
|
|
753
770
|
}
|
|
754
771
|
function bindSession(context, work, run, identity, now) {
|
|
755
772
|
const priorWorkSession = context.db.get("SELECT session_id FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC LIMIT 1", [work.work_id])?.session_id ?? null;
|
|
@@ -811,6 +828,10 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
|
|
|
811
828
|
}
|
|
812
829
|
function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
813
830
|
const result = value;
|
|
831
|
+
const changedPaths = (result.changed_paths ?? []).map((item) => normalizeWorkspacePath(projectRoot, item));
|
|
832
|
+
const ignored = ignoredWorkspacePaths(projectRoot, changedPaths);
|
|
833
|
+
if (ignored.length)
|
|
834
|
+
throw new AppError("environment_repair_not_product_change", "CODE Work must record ignored environment repair separately, not as a product changed_path", 2, { work_id: work.work_id, paths: ignored });
|
|
814
835
|
if ((result.deviations?.length ?? 0) > 0 || (result.blockers?.length ?? 0) > 0)
|
|
815
836
|
throw new AppError("work_contract_incomplete", "CODE Work cannot complete with unresolved deviations or blockers; fail the Work and report the contract mismatch", 2, { work_id: work.work_id, deviations: result.deviations ?? [], blockers: result.blockers ?? [] });
|
|
816
837
|
const packet = codePacket(work);
|
|
@@ -829,7 +850,7 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
|
829
850
|
assertPortableArtifactRef(ref, { workspaceRoot: projectRoot, runHome, runId });
|
|
830
851
|
}
|
|
831
852
|
const documentUpdates = (packet.document_updates ?? []);
|
|
832
|
-
const changed = new Set(
|
|
853
|
+
const changed = new Set(changedPaths);
|
|
833
854
|
const missingDocuments = documentUpdates.map((entry) => entry.path).filter((entry) => !changed.has(entry));
|
|
834
855
|
if (missingDocuments.length)
|
|
835
856
|
throw new AppError("document_update_missing", "CODE Work did not materialize every document update assigned by PLAN", 2, { work_id: work.work_id, missing_paths: missingDocuments });
|
|
@@ -840,7 +861,7 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
|
840
861
|
const assigned = packet.repair?.review_findings?.map((finding) => finding.finding_ref) ?? [];
|
|
841
862
|
validateReviewResolutions(work.work_id, assigned, result.resolutions ?? []);
|
|
842
863
|
if (assigned.length) {
|
|
843
|
-
if (
|
|
864
|
+
if (changedPaths.length === 0)
|
|
844
865
|
throw new AppError("review_repair_no_change", "Review repair must materialize a project change; a no-op cannot resolve a finding", 2, { work_id: work.work_id, findings: assigned });
|
|
845
866
|
for (const resolution of result.resolutions ?? [])
|
|
846
867
|
for (const ref of resolution.evidence_refs ?? [])
|
package/dist/shared/errors.js
CHANGED
|
@@ -9,6 +9,18 @@ export class AppError extends Error {
|
|
|
9
9
|
this.details = details;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/** Error fields must cross JSON boundaries as data: Error.message is not enumerable. */
|
|
13
|
+
export function errorRecord(error, depth = 0) {
|
|
14
|
+
const value = error;
|
|
15
|
+
const details = value?.details && typeof value.details === "object" && !Array.isArray(value.details) ? value.details : {};
|
|
16
|
+
return {
|
|
17
|
+
code: typeof value?.code === "string" ? value.code : "unexpected",
|
|
18
|
+
message: typeof value?.message === "string" ? value.message : String(error),
|
|
19
|
+
exitCode: typeof value?.exitCode === "number" ? value.exitCode : 1,
|
|
20
|
+
details,
|
|
21
|
+
...(value?.cause && depth < 3 ? { cause: errorRecord(value.cause, depth + 1) } : {})
|
|
22
|
+
};
|
|
23
|
+
}
|
|
12
24
|
export function isAppError(error) {
|
|
13
25
|
return error instanceof AppError;
|
|
14
26
|
}
|