@deksden-com/dd-flow-cli 0.9.0-beta.17 → 0.9.0-beta.19
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 +12 -0
- package/README.md +21 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +16 -2
- package/dist/cli/run-cli.js +77 -14
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/services/cli-operation-classifier.js +2 -0
- package/dist/services/eval-snapshots.js +14 -6
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +121 -30
- package/dist/services/lifecycle-command.js +8 -0
- package/dist/services/run-recovery.js +104 -0
- package/dist/services/stage-lifecycle.js +3 -0
- package/dist/services/usage.js +13 -3
- package/dist/services/vnext-fanout.js +40 -0
- package/dist/services/work-registry.js +80 -7
- package/dist/storage/database.js +27 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.19
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Require a fresh trusted hook receipt from the physical Work owner before terminal stage or Work lifecycle commands can mutate a vNext RUN. Record terminal native-child observations at their originating Stage so unbound delegation is retained as a diagnostic instead of contaminating a later fan-out wave.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.18
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Add Factory Droid harness routing, native lifecycle receipts and physical Session usage ingestion. Preserve child identity and reject conflicting hook replays or inclusive Droid usage to prevent incorrect Work binding and double counting.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.17
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -17,6 +17,27 @@ Never use a broad `pkill`/`killall`. A failed owned stop retains resources.
|
|
|
17
17
|
|
|
18
18
|
It does not replace Memory Bank prompts and does not make product, design, merge, or verification judgments. Prompts own intent, route selection, planning depth, evidence meaning, and semantic readiness. The CLI owns explicit local state: projects, protocols, Codex session bindings, transitions, worktree records, lanes, locks, merge queue jobs, hook records, and audit events.
|
|
19
19
|
|
|
20
|
+
## Factory Droid harness
|
|
21
|
+
|
|
22
|
+
Configure `droid-cli` in `~/.dd-flow/harnesses.json` with absolute
|
|
23
|
+
`adapter_command` (`dd-droid`) and `runtime_command` (`droid`) paths. Agent
|
|
24
|
+
profiles use the `droid` alias; the adapter receives `--droid-bin`.
|
|
25
|
+
|
|
26
|
+
The controlled adapter sends `dd-flow/droid-tool-event@1` JSON to
|
|
27
|
+
`dd-flow droid event handle --project-root <root> --json`. Events include a
|
|
28
|
+
stable `event_id`, `daemon_id`, `phase` (`before` or `after`), native Session
|
|
29
|
+
identity and parent, absolute Session directory, `Execute` input, observed
|
|
30
|
+
profile and transcript path. Before a lifecycle invocation, the returned
|
|
31
|
+
`hookSpecificOutput.updatedInput` adds its one-use `--hook-event-id` receipt.
|
|
32
|
+
The event ID belongs to the native hook invocation and is reused only for
|
|
33
|
+
transport retries. Session success is established by the Work contract.
|
|
34
|
+
|
|
35
|
+
`dd-flow droid usage ingest --project-root <root> --json` accepts cumulative
|
|
36
|
+
per-Session counters with explicit `usage_scope: physical_session` and
|
|
37
|
+
`completeness: complete|partial`. Inclusive root counters are rejected;
|
|
38
|
+
partial windows remain unavailable in RUN totals until complete evidence
|
|
39
|
+
arrives. Keep inclusive provider totals in adapter evidence for reconciliation.
|
|
40
|
+
|
|
20
41
|
## Runtime Model
|
|
21
42
|
|
|
22
43
|
Root local database:
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.19",
|
|
4
|
+
"cli_commit": "02648aa2db37e9332089ca941b06f5735b563496",
|
|
5
|
+
"built_at": "2026-09-06T13:33:22.164Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "4.0.
|
|
8
|
-
"commit": "
|
|
7
|
+
"version": "4.0.6",
|
|
8
|
+
"commit": "2aafb30d23e3f646b736b7ead25438cab97b16bf",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
package/dist/cli/help.js
CHANGED
|
@@ -38,6 +38,7 @@ Core commands:
|
|
|
38
38
|
codex home/hooks/hook Manage Codex homes, hooks, and hook events.
|
|
39
39
|
zcode event/usage Ingest trusted zcode-acp lifecycle and usage evidence.
|
|
40
40
|
grok event/usage Ingest trusted Grok Build lifecycle and usage evidence.
|
|
41
|
+
droid event/usage Ingest trusted Factory Droid lifecycle and physical usage evidence.
|
|
41
42
|
opencode event/usage Ingest trusted OpenCode lifecycle and usage evidence.
|
|
42
43
|
agy event/usage Ingest trusted Antigravity CLI lifecycle and usage evidence.
|
|
43
44
|
worktree plan/create/status Record Worktrunk-backed feature worktrees.
|
|
@@ -154,7 +155,8 @@ Usage:
|
|
|
154
155
|
dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
155
156
|
dd-flow run list --project-root <root> --json
|
|
156
157
|
dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> --json
|
|
157
|
-
dd-flow run
|
|
158
|
+
dd-flow run recovery inspect|begin|seal|capture|resume <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
159
|
+
dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate|--incomplete|--recovery-id <RCV-ID>) --project-root <root> --output <directory> --json
|
|
158
160
|
dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> --json
|
|
159
161
|
dd-flow run snapshot bootstrap create --project-root <root> --output <directory> --json
|
|
160
162
|
dd-flow run snapshot bootstrap restore --snapshot <directory> --project-root <prepared-root> --json
|
|
@@ -169,7 +171,7 @@ Usage:
|
|
|
169
171
|
|
|
170
172
|
RUN-* is the execution envelope for one concrete flow launch. Semantic truth remains in protocol, experiment, DEF, scenario, evidence, and Memory Bank documents. New run artifacts are stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runs/<RUN-ID-slug>/ with one run.json projection and an append-only timeline.jsonl. Timeline is the compact full run report: summary is always present, while --hide may suppress bulky sections. Use \`dd-flow stat usage --run …\` to reread registered local Codex transcript counters and tool-call facts and refresh the current usage projection; unavailable data is explicit rather than zero.
|
|
171
173
|
|
|
172
|
-
run prepare-vnext-specify allocates an otherwise unstarted vNext RUN for the canonical SPECIFY entry checkpoint. run snapshot is the eval-only checkpoint boundary: it snapshots one quiescent RUN, its dedicated DD_FLOW_HOME, and the exact project tree including its Git repository, then restores them into a fresh dedicated runtime. It is not a general runtime backup command and fails closed for a shared runtime.
|
|
174
|
+
run prepare-vnext-specify allocates an otherwise unstarted vNext RUN for the canonical SPECIFY entry checkpoint. run recovery makes an interrupted RUN read-only, requires an explicit settled-tree receipt, captures immutable recovery evidence, then explicitly returns delegated Work to the ready graph. run snapshot is the eval-only checkpoint boundary: it snapshots one quiescent RUN, its dedicated DD_FLOW_HOME, and the exact project tree including its Git repository, then restores them into a fresh dedicated runtime. It is not a general runtime backup command and fails closed for a shared runtime.
|
|
173
175
|
|
|
174
176
|
Examples:
|
|
175
177
|
dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
|
|
@@ -191,6 +193,7 @@ Usage:
|
|
|
191
193
|
dd-flow stage fanout status <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
|
|
192
194
|
dd-flow stage fanout dispatch <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
|
|
193
195
|
dd-flow stage fanout reconcile <RUN-ID> --project-root <root> --stage <name> --observations-file <native-observations.json> --json
|
|
196
|
+
dd-flow stage native observe <RUN-ID> --project-root <root> --stage <name> --observations-file <native-observations.json> --json
|
|
194
197
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--semantic-file <RUN-local-json>] --json
|
|
195
198
|
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage specify (--result-file <RUN-local-specify.json>|--result-stdin) --outcome <specified|failed|cancelled> --json
|
|
196
199
|
|
|
@@ -549,6 +552,17 @@ Usage:
|
|
|
549
552
|
|
|
550
553
|
Both payloads are read from stdin. The event adapter retains lifecycle identity;
|
|
551
554
|
the usage adapter retains cumulative provider counters for RUN accounting.`
|
|
555
|
+
],
|
|
556
|
+
[
|
|
557
|
+
"droid event",
|
|
558
|
+
`dd-flow droid event - ingest a controlled Factory Droid Execute hook
|
|
559
|
+
|
|
560
|
+
Usage:
|
|
561
|
+
dd-flow droid event handle --project-root <root> --json
|
|
562
|
+
dd-flow droid usage ingest --project-root <root> --json
|
|
563
|
+
|
|
564
|
+
Reads trusted adapter JSON from stdin. Usage requires physical_session scope and explicit completeness.
|
|
565
|
+
`
|
|
552
566
|
],
|
|
553
567
|
[
|
|
554
568
|
"opencode event",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlan
|
|
|
10
10
|
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
11
11
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
12
12
|
import { cleanupApply, cleanupScan } from "../services/cleanup.js";
|
|
13
|
-
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
13
|
+
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
14
14
|
import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
|
|
15
15
|
import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
|
|
16
16
|
import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
|
|
@@ -26,7 +26,7 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunSe
|
|
|
26
26
|
import { previewNextEntityId } from "../services/ids.js";
|
|
27
27
|
import { validateSchema } from "../services/schema-validation.js";
|
|
28
28
|
import { preflightMemoryPermissions } from "../services/memory-permissions.js";
|
|
29
|
-
import { ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
|
|
29
|
+
import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
|
|
30
30
|
import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
|
|
31
31
|
import { publishProjectSummary } from "../services/project-summary.js";
|
|
32
32
|
import { requireProjectByRoot } from "../services/projects.js";
|
|
@@ -41,14 +41,15 @@ import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services
|
|
|
41
41
|
import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
|
|
42
42
|
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
43
43
|
import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
|
|
44
|
-
import { getVnextFanoutStatus, reconcileVnextFanout } from "../services/vnext-fanout.js";
|
|
44
|
+
import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout } from "../services/vnext-fanout.js";
|
|
45
45
|
import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
|
|
46
46
|
import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
|
|
47
47
|
import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
|
|
48
48
|
import { renderWorkerPrompt } from "../services/prompts.js";
|
|
49
49
|
import { finishStage, startStage } from "../services/stage-lifecycle.js";
|
|
50
|
-
import { addWorkBatch, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
|
|
50
|
+
import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
|
|
51
51
|
import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
|
|
52
|
+
import { beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
|
|
52
53
|
import { loadExternalStageContext } from "../services/stage-context.js";
|
|
53
54
|
import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
|
|
54
55
|
import { startRuntimeService } from "../services/runtime-service.js";
|
|
@@ -670,6 +671,11 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
670
671
|
}
|
|
671
672
|
throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch|reconcile> <RUN> --stage <stage> --project-root <path>; reconcile requires --observations-file <native-observations.json>", 2);
|
|
672
673
|
}
|
|
674
|
+
if (family === "stage" && command === "native") {
|
|
675
|
+
if (requiredPosition(parsed, 0, "native action") !== "observe")
|
|
676
|
+
throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
|
|
677
|
+
return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
|
|
678
|
+
}
|
|
673
679
|
if (family === "run" && command === "capacity") {
|
|
674
680
|
if (requiredPosition(parsed, 0, "capacity action") !== "record")
|
|
675
681
|
throw new AppError("usage", "Use: dd-flow run capacity record <RUN> --available-slots <0..15> --project-root <path>", 2);
|
|
@@ -765,11 +771,18 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
765
771
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
766
772
|
if (Boolean(resultFile) === resultStdin)
|
|
767
773
|
throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
|
|
774
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
775
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
|
|
776
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
768
777
|
const result = resultFile ? fs.readFileSync(path.resolve(resultFile), "utf8") : await readStdin(io.stdin);
|
|
769
|
-
return await finishWork(context,
|
|
778
|
+
return await finishWork(context, workId, result, commandProgress(io, output));
|
|
779
|
+
}
|
|
780
|
+
if (family === "work" && command === "fail") {
|
|
781
|
+
const workId = requiredPosition(parsed, 0, "work-id");
|
|
782
|
+
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
|
|
783
|
+
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
784
|
+
return failWork(context, workId, requiredOption(parsed, "reason"));
|
|
770
785
|
}
|
|
771
|
-
if (family === "work" && command === "fail")
|
|
772
|
-
return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
773
786
|
if (family === "work" && command === "cancel")
|
|
774
787
|
return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
|
|
775
788
|
if (family === "work" && command === "retry")
|
|
@@ -882,8 +895,17 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
882
895
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
883
896
|
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
884
897
|
const retryReason = optionalOption(parsed, "reason");
|
|
898
|
+
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
885
899
|
if (retryCheckId && !retryReason?.trim())
|
|
886
900
|
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
901
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
902
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
903
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
904
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
905
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
906
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
907
|
+
if (protectedLifecycle)
|
|
908
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
|
|
887
909
|
if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
|
|
888
910
|
const outcome = optionalOption(parsed, "outcome");
|
|
889
911
|
if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
|
|
@@ -949,11 +971,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
949
971
|
if (family === "stage" && command === "pause") {
|
|
950
972
|
if (!hasOption(parsed, "question-stdin"))
|
|
951
973
|
throw new AppError("usage", "stage pause requires --question-stdin", 2);
|
|
974
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
975
|
+
const runId = requiredPosition(parsed, 0, "run-id");
|
|
976
|
+
const stage = requiredOption(parsed, "stage");
|
|
977
|
+
const workId = requiredOption(parsed, "work");
|
|
978
|
+
const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
|
|
979
|
+
|| (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
|
|
980
|
+
|| (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
981
|
+
|| (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
|
|
982
|
+
|| (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
|
|
983
|
+
|| (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
|
|
984
|
+
if (protectedLifecycle)
|
|
985
|
+
assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
952
986
|
return pauseStageForUser(context, {
|
|
953
|
-
projectRoot
|
|
954
|
-
runId
|
|
955
|
-
stage
|
|
956
|
-
workId
|
|
987
|
+
projectRoot,
|
|
988
|
+
runId,
|
|
989
|
+
stage,
|
|
990
|
+
workId,
|
|
957
991
|
question: await readStdin(io.stdin)
|
|
958
992
|
});
|
|
959
993
|
}
|
|
@@ -1070,6 +1104,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1070
1104
|
throw new AppError("usage", "Unknown opencode usage action", 2);
|
|
1071
1105
|
return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1072
1106
|
}
|
|
1107
|
+
if (family === "droid" && command === "event") {
|
|
1108
|
+
if (requiredPosition(parsed, 0, "droid event action") !== "handle")
|
|
1109
|
+
throw new AppError("usage", "Unknown droid event action", 2);
|
|
1110
|
+
return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1111
|
+
}
|
|
1112
|
+
if (family === "droid" && command === "usage") {
|
|
1113
|
+
if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
|
|
1114
|
+
throw new AppError("usage", "Unknown droid usage action", 2);
|
|
1115
|
+
return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
|
|
1116
|
+
}
|
|
1073
1117
|
if (family === "agy" && command === "event")
|
|
1074
1118
|
return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
|
|
1075
1119
|
if (family === "agy" && command === "usage") {
|
|
@@ -1419,6 +1463,22 @@ function dispatchSession(context, command, parsed) {
|
|
|
1419
1463
|
throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
|
|
1420
1464
|
}
|
|
1421
1465
|
function dispatchRun(context, command, parsed) {
|
|
1466
|
+
if (command === "recovery") {
|
|
1467
|
+
const action = requiredPosition(parsed, 0, "recovery action");
|
|
1468
|
+
const projectRoot = requiredOption(parsed, "project-root");
|
|
1469
|
+
const runId = requiredPosition(parsed, 1, "run-id");
|
|
1470
|
+
if (action === "inspect")
|
|
1471
|
+
return inspectRunRecovery(context, { projectRoot, runId });
|
|
1472
|
+
if (action === "begin")
|
|
1473
|
+
return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") });
|
|
1474
|
+
if (action === "seal")
|
|
1475
|
+
return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") });
|
|
1476
|
+
if (action === "capture")
|
|
1477
|
+
return recordRecoveryCapture(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") });
|
|
1478
|
+
if (action === "resume")
|
|
1479
|
+
return resumeRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id") });
|
|
1480
|
+
throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume", 2);
|
|
1481
|
+
}
|
|
1422
1482
|
if (command === "snapshot") {
|
|
1423
1483
|
const action = requiredPosition(parsed, 0, "snapshot action");
|
|
1424
1484
|
if (action === "bootstrap") {
|
|
@@ -1438,9 +1498,10 @@ function dispatchRun(context, command, parsed) {
|
|
|
1438
1498
|
if (action === "create") {
|
|
1439
1499
|
const candidate = parsed.options.has("candidate");
|
|
1440
1500
|
const incomplete = parsed.options.has("incomplete");
|
|
1501
|
+
const recoveryId = optionalOption(parsed, "recovery-id");
|
|
1441
1502
|
const stageEntry = optionalOption(parsed, "stage-entry");
|
|
1442
|
-
if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) !== 1)
|
|
1443
|
-
throw new AppError("usage", "Use exactly one of --candidate, --incomplete or --stage-entry", 2);
|
|
1503
|
+
if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) + Number(recoveryId !== undefined) !== 1)
|
|
1504
|
+
throw new AppError("usage", "Use exactly one of --candidate, --incomplete, --recovery-id or --stage-entry", 2);
|
|
1444
1505
|
const input = {
|
|
1445
1506
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
1446
1507
|
runId: requiredPosition(parsed, 1, "run-id"),
|
|
@@ -1448,6 +1509,8 @@ function dispatchRun(context, command, parsed) {
|
|
|
1448
1509
|
};
|
|
1449
1510
|
if (incomplete)
|
|
1450
1511
|
return createEvalRunSnapshot(context, { ...input, incomplete: true });
|
|
1512
|
+
if (recoveryId)
|
|
1513
|
+
return createEvalRunSnapshot(context, { ...input, recoveryId });
|
|
1451
1514
|
return candidate
|
|
1452
1515
|
? createEvalRunSnapshot(context, { ...input, candidate: true })
|
|
1453
1516
|
: createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
|
|
@@ -2372,7 +2435,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2372
2435
|
if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
|
|
2373
2436
|
return requiredOption(parsed, "project-root");
|
|
2374
2437
|
}
|
|
2375
|
-
if (family
|
|
2438
|
+
if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
|
|
2376
2439
|
return requiredOption(parsed, "project-root");
|
|
2377
2440
|
if (family === "codex" && command === "hooks") {
|
|
2378
2441
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"schema_id": {"const": "dd-flow/agent-profile@1"},
|
|
9
9
|
"id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"},
|
|
10
|
-
"harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok"]},
|
|
10
|
+
"harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok", "droid"]},
|
|
11
11
|
"provider": {"type": "string", "minLength": 1},
|
|
12
12
|
"model": {"type": "string", "minLength": 1},
|
|
13
13
|
"reasoning": {"type": "string", "minLength": 1},
|
|
@@ -106,6 +106,8 @@ function isRouterNative(args) {
|
|
|
106
106
|
|| (first === "zcode" && args[1] === "usage" && args[2] === "ingest")
|
|
107
107
|
|| (first === "grok" && args[1] === "event" && args[2] === "handle")
|
|
108
108
|
|| (first === "grok" && args[1] === "usage" && args[2] === "ingest")
|
|
109
|
+
|| (first === "droid" && args[1] === "event" && args[2] === "handle")
|
|
110
|
+
|| (first === "droid" && args[1] === "usage" && args[2] === "ingest")
|
|
109
111
|
|| (first === "opencode" && args[1] === "event" && args[2] === "handle")
|
|
110
112
|
|| (first === "opencode" && args[1] === "usage" && args[2] === "ingest")
|
|
111
113
|
|| (first === "agy" && args[1] === "event" && args[2] === "handle")
|
|
@@ -55,7 +55,13 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
55
55
|
const project = requireProjectByRoot(context, projectRoot);
|
|
56
56
|
const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
|
|
57
57
|
assertDedicatedHome(context, project.id, input.runId);
|
|
58
|
-
|
|
58
|
+
const recovery = typeof input.recoveryId === "string";
|
|
59
|
+
if (recovery) {
|
|
60
|
+
const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
|
|
61
|
+
if (!guard || guard.status !== "sealed")
|
|
62
|
+
throw new AppError("recovery_invalid_state", "Recovery snapshots require the matching sealed recovery guard", 1, { run_id: input.runId, recovery_id: input.recoveryId ?? null, status: guard?.status ?? null });
|
|
63
|
+
}
|
|
64
|
+
if (input.incomplete || recovery) { /* Recovery captures only after the runner seals all writers. */ }
|
|
59
65
|
else if (input.candidate)
|
|
60
66
|
assertTerminalCandidate(status.index.stage_runs ?? []);
|
|
61
67
|
else if (input.stageEntry)
|
|
@@ -66,7 +72,7 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
66
72
|
const activeChildren = input.candidate
|
|
67
73
|
? context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND status IN ('created', 'running', 'paused')", [project.id, input.runId])?.count ?? 0
|
|
68
74
|
: context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND (status IN ('running', 'paused') OR (status = 'created' AND (? IS NULL OR COALESCE(result_schema, '') NOT LIKE ?)))", [project.id, input.runId, allowedCreatedResultSchema, allowedCreatedResultSchema ? `${allowedCreatedResultSchema}@%` : ""])?.count ?? 0;
|
|
69
|
-
if (!input.incomplete && activeChildren > 0)
|
|
75
|
+
if (!input.incomplete && !recovery && activeChildren > 0)
|
|
70
76
|
throw new AppError("snapshot_not_quiescent", `${input.candidate ? "Candidate" : "Stage-entry"} snapshot requires no active child Work`, 1, { run_id: input.runId, active_children: activeChildren });
|
|
71
77
|
const output = path.resolve(input.output);
|
|
72
78
|
if (fs.existsSync(output))
|
|
@@ -103,9 +109,9 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
103
109
|
project_id: project.id,
|
|
104
110
|
project_root: projectRoot,
|
|
105
111
|
dd_flow_home: context.ddFlowHome,
|
|
106
|
-
purpose: input.incomplete ? "incomplete" : input.candidate ? "candidate" : "stage_entry",
|
|
107
|
-
stage_entry: input.incomplete || input.candidate ? null : input.stageEntry,
|
|
108
|
-
...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : {}),
|
|
112
|
+
purpose: input.incomplete ? "incomplete" : recovery ? "recovery" : input.candidate ? "candidate" : "stage_entry",
|
|
113
|
+
stage_entry: input.incomplete || recovery || input.candidate ? null : input.stageEntry,
|
|
114
|
+
...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : recovery ? { recovery_id: input.recoveryId, consistency: "sealed_writer_barrier_required", active_child_work_count: activeChildren, restorable: "same-runtime-or-explicit-restore" } : {}),
|
|
109
115
|
created_at: context.now(),
|
|
110
116
|
runtime_sha256: treeHash(path.join(output, "runtime")),
|
|
111
117
|
project_sha256: treeHash(path.join(output, "project")),
|
|
@@ -129,6 +135,8 @@ export function restoreEvalRunSnapshot(context, input) {
|
|
|
129
135
|
const manifest = readManifest(snapshot);
|
|
130
136
|
if (manifest.purpose === "incomplete")
|
|
131
137
|
throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
|
|
138
|
+
if (manifest.purpose === "recovery")
|
|
139
|
+
throw new AppError("snapshot_recovery_requires_explicit_restore", "Recovery snapshots require the recovery runner; they are not a generic stage-entry fixture", 1, { snapshot });
|
|
132
140
|
if (manifest.purpose === "candidate")
|
|
133
141
|
throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
|
|
134
142
|
if (!manifest.stage_entry)
|
|
@@ -383,7 +391,7 @@ function readManifest(snapshot) {
|
|
|
383
391
|
if (!fs.existsSync(file))
|
|
384
392
|
throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
|
|
385
393
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
386
|
-
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"].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)
|
|
394
|
+
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)
|
|
387
395
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
388
396
|
if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
389
397
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
|
@@ -10,6 +10,7 @@ export function harnessKey(value) {
|
|
|
10
10
|
zcode: "zcode-acp", "zcode-acp": "zcode-acp",
|
|
11
11
|
grok: "grok-acp", "grok-acp": "grok-acp",
|
|
12
12
|
agy: "antigravity-cli", "antigravity-cli": "antigravity-cli",
|
|
13
|
+
droid: "droid-cli", "droid-cli": "droid-cli",
|
|
13
14
|
opencode: "opencode-server", "opencode-server": "opencode-server"
|
|
14
15
|
};
|
|
15
16
|
const resolved = keys[value];
|
|
@@ -60,6 +61,7 @@ export function harnessRuntimeArguments(ddFlowHome, harness) {
|
|
|
60
61
|
"zcode-acp": "--zcode-acp-bin",
|
|
61
62
|
"grok-acp": "--grok-bin",
|
|
62
63
|
"antigravity-cli": "--agy-bin",
|
|
64
|
+
"droid-cli": "--droid-bin",
|
|
63
65
|
"opencode-server": "--opencode-bin"
|
|
64
66
|
};
|
|
65
67
|
return [option[key], runtime];
|