@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.106
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/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +23 -8
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +3 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -1
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +12 -4
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/external-work-launch.js +2 -1
- package/dist/services/hooks.js +153 -43
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +5 -12
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +83 -88
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +25 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
|
@@ -3,7 +3,7 @@ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout,
|
|
|
3
3
|
import { getFlowRunVariables } from "./runs.js";
|
|
4
4
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
5
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
|
-
import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
6
|
+
import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
7
|
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
8
8
|
function semanticCompletionPrompt(stage, completed) {
|
|
9
9
|
const task = stage === "plan-review"
|
|
@@ -15,7 +15,7 @@ function semanticCompletionPrompt(stage, completed) {
|
|
|
15
15
|
`All declared ${stage} child Work has settled (${completed} completed).`,
|
|
16
16
|
task,
|
|
17
17
|
"Then invoke the exact Stage finish command from the original authoritative packet.",
|
|
18
|
-
"
|
|
18
|
+
"If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
|
|
19
19
|
"Do not create speculative repair Work or start a successor Stage."
|
|
20
20
|
].join(" ");
|
|
21
21
|
}
|
|
@@ -27,7 +27,15 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
27
27
|
"Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
|
|
28
28
|
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
29
29
|
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
30
|
-
|
|
30
|
+
stageLifecycleInstruction
|
|
31
|
+
].join("\n\n");
|
|
32
|
+
}
|
|
33
|
+
export function controllerStageResumePrompt(pauseId, command) {
|
|
34
|
+
return [
|
|
35
|
+
`The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
|
|
36
|
+
command,
|
|
37
|
+
"Do not alter the answer. Follow the returned packet, then stop at this Stage boundary.",
|
|
38
|
+
stageLifecycleInstruction
|
|
31
39
|
].join("\n\n");
|
|
32
40
|
}
|
|
33
41
|
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
@@ -62,7 +70,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
62
70
|
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
63
71
|
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
64
72
|
}
|
|
65
|
-
const prompt = (action, text) => ({ kind: "prompt", action, prompt: text
|
|
73
|
+
const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
|
|
66
74
|
if (fanout.state === "coordinator_required")
|
|
67
75
|
return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
68
76
|
if (fanout.works.failed || fanout.works.cancelled)
|
|
@@ -4,6 +4,8 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
6
|
import { registerStoreWriter } from "../storage/writer-contract.js";
|
|
7
|
+
import { beginSnapshotImport, completeSnapshotImport } from "../storage/import-guard.js";
|
|
8
|
+
import { foreignKeyTransaction } from "../storage/foreign-key-transaction.js";
|
|
7
9
|
import { createContext } from "../runtime/context.js";
|
|
8
10
|
import { AppError } from "../shared/errors.js";
|
|
9
11
|
import { resolveProjectRoot, runtimeProtocolDir, runtimeRunDir } from "../storage/paths.js";
|
|
@@ -16,6 +18,7 @@ import { resourceHome } from "./managed-processes.js";
|
|
|
16
18
|
import { codexHomeSharedEntries } from "./hooks.js";
|
|
17
19
|
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
18
20
|
import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
|
|
21
|
+
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
19
22
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
20
23
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
21
24
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -387,9 +390,12 @@ function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest
|
|
|
387
390
|
if (context.db.get("SELECT 1 FROM projects LIMIT 1") || context.db.get("SELECT 1 FROM runs LIMIT 1"))
|
|
388
391
|
throw new AppError("restore_home_not_empty", "Snapshot restore cannot replace an initialized runtime", 1);
|
|
389
392
|
}
|
|
390
|
-
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input)) {
|
|
393
|
+
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input), prearmedImportId) {
|
|
391
394
|
const { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile } = prepared;
|
|
392
395
|
assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
|
|
396
|
+
const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
|
|
397
|
+
const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
|
|
398
|
+
workspaceRoot: restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome), sourceManifestSha256 });
|
|
393
399
|
clearProjectTree(projectRoot, false);
|
|
394
400
|
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
395
401
|
replaceProjectTree(projectRoot, sourceProject);
|
|
@@ -399,7 +405,8 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
399
405
|
const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
|
|
400
406
|
// The dispatch context was intentionally closed before replacing db.sqlite.
|
|
401
407
|
// A fresh process owns all subsequent normal commands.
|
|
402
|
-
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome });
|
|
408
|
+
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
|
|
409
|
+
let result, primary, failed = false;
|
|
403
410
|
try {
|
|
404
411
|
const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
|
|
405
412
|
rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
|
|
@@ -433,7 +440,11 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
433
440
|
fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
|
|
434
441
|
writeJson(relocationFile, relocation);
|
|
435
442
|
}
|
|
436
|
-
|
|
443
|
+
const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
444
|
+
if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
|
|
445
|
+
throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
|
|
446
|
+
completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
|
|
447
|
+
result = {
|
|
437
448
|
ok: true,
|
|
438
449
|
schema_id: "dd-flow/eval-run-restore@1",
|
|
439
450
|
snapshot,
|
|
@@ -448,9 +459,19 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
448
459
|
next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
|
|
449
460
|
};
|
|
450
461
|
}
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
catch (error) {
|
|
463
|
+
primary = error;
|
|
464
|
+
failed = true;
|
|
453
465
|
}
|
|
466
|
+
const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
|
|
467
|
+
if (failed) {
|
|
468
|
+
if (primary instanceof AppError && cleanup.length)
|
|
469
|
+
primary.details.import_cleanup_errors = cleanup;
|
|
470
|
+
throw primary;
|
|
471
|
+
}
|
|
472
|
+
if (cleanup.length)
|
|
473
|
+
throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
|
|
474
|
+
return result;
|
|
454
475
|
}
|
|
455
476
|
export function prepareVnextSpecifyRun(context, input) {
|
|
456
477
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -498,10 +519,11 @@ function assertSnapshotRuntimeDetachAllowed(get) {
|
|
|
498
519
|
function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
499
520
|
const hasTable = (table) => Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?", [table]));
|
|
500
521
|
assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
|
|
501
|
-
const detachedTables = ["lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
522
|
+
const detachedTables = ["lifecycle_packet_assignments", "lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
502
523
|
const runTables = Object.fromEntries([...detachedTables, "runs", "merge_requests", "sessions", "work_sessions"].map(table => [table, context.db.all(`SELECT * FROM ${table}`)]));
|
|
503
524
|
const resourceFile = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
504
525
|
const resources = fs.existsSync(resourceFile) ? new DatabaseSync(resourceFile) : null;
|
|
526
|
+
let resourceHeld = false, resourceCommitted = false, result, primary, failed = false;
|
|
505
527
|
try {
|
|
506
528
|
if (resources)
|
|
507
529
|
registerStoreWriter(resources);
|
|
@@ -512,42 +534,56 @@ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
|
512
534
|
fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/snapshot-runtime-lineage@1", source_snapshot: snapshot, source_manifest_sha256: fileHash(path.join(snapshot, "snapshot.json")), source_runtime: manifest.dd_flow_home, restored_runtime: context.ddFlowHome, run_id: manifest.run_id, run_tables: runTables, resource_tables: resourceTables }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
513
535
|
syncPath(file);
|
|
514
536
|
syncPath(directory);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
context.db.
|
|
518
|
-
for (const table of
|
|
519
|
-
context.db.
|
|
537
|
+
const lineage = JSON.stringify({ lineage_file: file, lineage_sha256: fileHash(file) });
|
|
538
|
+
context.db.writeTransaction(() => {
|
|
539
|
+
assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
|
|
540
|
+
for (const [table, rows] of Object.entries(runTables))
|
|
541
|
+
if (JSON.stringify(context.db.all(`SELECT * FROM ${table}`)) !== JSON.stringify(rows))
|
|
542
|
+
throw new AppError("snapshot_runtime_changed", "Imported runtime changed during lineage preparation", 1, { table });
|
|
543
|
+
context.db.run("UPDATE sessions SET metadata_json = json_set(metadata_json, '$.snapshot_import', json(?))", [lineage]);
|
|
544
|
+
for (const table of detachedTables) {
|
|
545
|
+
try {
|
|
546
|
+
context.db.exec(`DELETE FROM ${table}`);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
throw new AppError("snapshot_runtime_detach_failed", `Could not detach imported ${table} authority`, 1, { table, cause: String(error) });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
520
552
|
context.db.run("UPDATE merge_requests SET dispatch_owner = NULL, dispatch_lease_token = NULL, dispatch_lease_expires_at = NULL, lock_acquired_at = NULL, adapter_receipt_json = NULL");
|
|
521
553
|
// Only the copied dedicated registry is touched. Source/external homes
|
|
522
554
|
// are historical references in the archive and are never opened here.
|
|
523
555
|
if (resources) {
|
|
524
556
|
resources.exec("BEGIN IMMEDIATE");
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
catch (error) {
|
|
531
|
-
resources.exec("ROLLBACK");
|
|
532
|
-
throw error;
|
|
557
|
+
resourceHeld = true;
|
|
558
|
+
for (const [table, rows] of Object.entries(resourceTables)) {
|
|
559
|
+
if (JSON.stringify(resources.prepare(`SELECT * FROM ${table}`).all()) !== JSON.stringify(rows))
|
|
560
|
+
throw new AppError("snapshot_runtime_changed", "Imported resource evidence changed during lineage preparation", 1, { table });
|
|
561
|
+
resources.exec(`DELETE FROM ${table}`);
|
|
533
562
|
}
|
|
563
|
+
resources.exec("COMMIT");
|
|
564
|
+
resourceHeld = false;
|
|
565
|
+
resourceCommitted = true;
|
|
534
566
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
catch (error) {
|
|
538
|
-
context.db.exec("ROLLBACK");
|
|
539
|
-
throw error;
|
|
540
|
-
}
|
|
541
|
-
return file;
|
|
567
|
+
});
|
|
568
|
+
result = file;
|
|
542
569
|
}
|
|
543
|
-
|
|
544
|
-
|
|
570
|
+
catch (error) {
|
|
571
|
+
primary = error;
|
|
572
|
+
failed = true;
|
|
545
573
|
}
|
|
574
|
+
const cleanup = resources ? cleanupScopeWriters([{ name: resourceFile, db: resources, rollback: resourceHeld, close: "always" }]) : [];
|
|
575
|
+
if (failed || cleanup.length) {
|
|
576
|
+
const details = { effect: resourceCommitted ? "partial" : "unknown", ...(cleanup.length ? { import_cleanup_errors: cleanup } : {}) };
|
|
577
|
+
if (primary instanceof AppError) {
|
|
578
|
+
Object.assign(primary.details, details);
|
|
579
|
+
throw primary;
|
|
580
|
+
}
|
|
581
|
+
throw new AppError("snapshot_runtime_detach_failed", primary instanceof Error ? primary.message : "Imported runtime authority could not be detached", 1, { ...details, ...(failed ? { cause: String(primary) } : {}) });
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
546
584
|
}
|
|
547
585
|
function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome, recovery = false) {
|
|
548
|
-
context.db
|
|
549
|
-
context.db.beginWriteTransaction();
|
|
550
|
-
try {
|
|
586
|
+
foreignKeyTransaction(context.db, () => {
|
|
551
587
|
context.db.run("UPDATE projects SET root = ?", [newRoot]);
|
|
552
588
|
context.db.run("UPDATE runs SET project_root = ?, workspace_root = ?", [newRoot, newWorkspace]);
|
|
553
589
|
for (const column of ["runtime_path", "run_dir", "run_index_path", "run_home_path", "run_root", "index_json"])
|
|
@@ -575,15 +611,7 @@ function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, ol
|
|
|
575
611
|
if (!recovery)
|
|
576
612
|
context.db.run("UPDATE work_sessions SET status = CASE WHEN status = 'running' THEN 'completed' ELSE status END, completed_at = COALESCE(completed_at, ?), updated_at = ?", [context.now(), context.now()]);
|
|
577
613
|
context.db.run("DELETE FROM codex_session_bindings");
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
catch (error) {
|
|
581
|
-
context.db.exec("ROLLBACK");
|
|
582
|
-
throw error;
|
|
583
|
-
}
|
|
584
|
-
finally {
|
|
585
|
-
context.db.exec("PRAGMA foreign_keys = ON");
|
|
586
|
-
}
|
|
614
|
+
});
|
|
587
615
|
// Only operational database paths move. Imported packets, results and
|
|
588
616
|
// journals retain their historical bytes, including interrupted JSONL tails.
|
|
589
617
|
// The frozen workspace route is an operational receipt consumed by later
|
|
@@ -660,7 +688,7 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
660
688
|
restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
|
|
661
689
|
return fs.realpathSync(workspaceRoot);
|
|
662
690
|
}
|
|
663
|
-
function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
|
|
691
|
+
export function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
|
|
664
692
|
if (manifest.workspace.kind === "project")
|
|
665
693
|
return projectRoot;
|
|
666
694
|
const managedRoot = physicalPath(manifest.dd_flow_home), sourceRoot = physicalPath(manifest.workspace.source_root);
|
|
@@ -132,16 +132,27 @@ export function assertSameSessionMerge(input) {
|
|
|
132
132
|
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot switch execution profiles; select a compatible profile or server MERGE", 2, { previous_stage: stage, previous_profile_id: previous.id, merge_profile_id: merge.id });
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
/** The RUN identity root and a native Session's fixed launch directory are distinct. */
|
|
136
|
+
export function stageExecutionCwd(input) {
|
|
137
|
+
return path.resolve(input.stage === "specify" || input.stage === "protocolize" ? input.projectRoot
|
|
138
|
+
: input.stage === "merge" ? input.mergeTargetWorkspace ?? input.projectRoot
|
|
139
|
+
: input.workspaceRoot);
|
|
140
|
+
}
|
|
135
141
|
export function resolveStageTransition(input) {
|
|
136
142
|
const target = resolveExecutionPolicy(input);
|
|
137
143
|
const previous = input.fromStage ? resolveExecutionPolicy({ ...input, stage: input.fromStage }).coordinator : null;
|
|
138
144
|
const compatible = !previous || sameSessionProfile(previous, target.coordinator);
|
|
145
|
+
const previousCwd = input.fromStage ? stageExecutionCwd({ ...input, stage: input.fromStage }) : null;
|
|
146
|
+
const targetCwd = stageExecutionCwd(input);
|
|
147
|
+
const sameWorkspace = previousCwd === null || previousCwd === targetCwd;
|
|
139
148
|
if (input.stage === "merge" && input.mergeMode === "same_session") {
|
|
140
149
|
if (!compatible)
|
|
141
150
|
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot replace its current coordinator", 2);
|
|
151
|
+
if (!sameWorkspace)
|
|
152
|
+
throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native coordinator workspace", 2, { source_workspace: previousCwd, target_workspace: targetCwd });
|
|
142
153
|
return { ...target, session_mode: "same_session" };
|
|
143
154
|
}
|
|
144
|
-
return { ...target, session_mode: !compatible || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
155
|
+
return { ...target, session_mode: !compatible || !sameWorkspace || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
145
156
|
}
|
|
146
157
|
function validateDelegation(value) {
|
|
147
158
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["mode", "agent_profile_id", "max_parallel"].includes(key)))
|
|
@@ -15,6 +15,7 @@ import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
|
15
15
|
import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
16
16
|
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
17
|
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
18
|
+
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
18
19
|
/**
|
|
19
20
|
* Creates an external provider root for one already-created Work. The launch
|
|
20
21
|
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
@@ -106,7 +107,7 @@ export async function launchExternalWork(context, input) {
|
|
|
106
107
|
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
107
108
|
throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
|
|
108
109
|
const promptFile = path.join(root, "launch.md");
|
|
109
|
-
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once
|
|
110
|
+
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
|
|
110
111
|
const prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
|
|
111
112
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
112
113
|
context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
|
package/dist/services/hooks.js
CHANGED
|
@@ -7,11 +7,12 @@ import { AppError } from "../shared/errors.js";
|
|
|
7
7
|
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
8
8
|
import { parseJsonObject } from "../shared/json.js";
|
|
9
9
|
import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
10
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
10
11
|
import { appendAudit } from "./audit.js";
|
|
11
12
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
12
13
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
13
14
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
14
|
-
import { observeLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
15
|
+
import { observeLifecycleInvocation, observedLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
15
16
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
16
17
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
17
18
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
|
|
@@ -549,21 +550,22 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
549
550
|
const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
|
|
550
551
|
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
|
|
551
552
|
const observedProfile = objectRecord(ddZcode.observedProfile);
|
|
553
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
552
554
|
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
553
555
|
const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
|
|
554
556
|
const persist = (scope) => {
|
|
555
557
|
if (scope)
|
|
556
558
|
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
557
|
-
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd:
|
|
558
|
-
const payload = { ...hook, session_id: providerSessionId, cwd:
|
|
559
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
560
|
+
const payload = { ...hook, session_id: providerSessionId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
559
561
|
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
|
|
560
562
|
const inserted = recordHookEvent(context, {
|
|
561
563
|
projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
|
|
562
564
|
daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? stringValue(hook.agentId) ?? null, turnId: null,
|
|
563
565
|
eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey,
|
|
564
|
-
transcriptPath: stringValue(hook.transcript_path) ?? null, cwd:
|
|
566
|
+
transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd
|
|
565
567
|
});
|
|
566
|
-
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd:
|
|
568
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
567
569
|
const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
|
|
568
570
|
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
|
|
569
571
|
};
|
|
@@ -602,13 +604,14 @@ export function handleGrokEvent(context, input) {
|
|
|
602
604
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
603
605
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
604
606
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
605
|
-
|
|
607
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
606
608
|
if (bootstrapStageStart)
|
|
607
609
|
registerProject(context, { root: expectedRoot });
|
|
608
610
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
609
611
|
const meta = objectRecord(hook._meta);
|
|
610
612
|
const ddGrok = objectRecord(meta.ddGrok);
|
|
611
613
|
const observedProfile = objectRecord(ddGrok.observedProfile);
|
|
614
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
612
615
|
const rootProviderSessionId = stringValue(ddGrok.rootProviderSessionId);
|
|
613
616
|
const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
|
|
614
617
|
if (!rootProviderSessionId || !providerSessionId)
|
|
@@ -622,23 +625,35 @@ export function handleGrokEvent(context, input) {
|
|
|
622
625
|
harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
|
|
623
626
|
promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
|
|
624
627
|
})).digest("hex");
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
628
|
+
const persist = (scope) => {
|
|
629
|
+
if (scope)
|
|
630
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
631
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
632
|
+
const payload = {
|
|
633
|
+
...hook, session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
634
|
+
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
|
|
635
|
+
};
|
|
636
|
+
const inserted = recordHookEvent(context, {
|
|
637
|
+
projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
|
|
638
|
+
daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
|
|
639
|
+
eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: observedCwd
|
|
640
|
+
});
|
|
641
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
642
|
+
return inserted;
|
|
629
643
|
};
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
projectId: project.id,
|
|
637
|
-
|
|
644
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
|
|
645
|
+
...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
|
|
646
|
+
if (!lifecycle)
|
|
647
|
+
return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
648
|
+
let inserted = false;
|
|
649
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
650
|
+
command, harness: "grok-acp", projectId: project.id,
|
|
651
|
+
identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
|
|
652
|
+
recordReceipt: scope => { inserted = persist(scope); return eventKey; }
|
|
638
653
|
});
|
|
639
|
-
|
|
640
|
-
...(
|
|
641
|
-
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
654
|
+
if (receipt.invocationId)
|
|
655
|
+
return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
656
|
+
return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
642
657
|
}
|
|
643
658
|
/** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
|
|
644
659
|
export function handleOpenCodeEvent(context, input) {
|
|
@@ -654,6 +669,65 @@ export function handleDroidEvent(context, input) {
|
|
|
654
669
|
throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
|
|
655
670
|
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
656
671
|
}
|
|
672
|
+
/** Stable project identity is not the native Session's execution workspace. */
|
|
673
|
+
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId) {
|
|
674
|
+
if (facts?.bootstrapStageStart)
|
|
675
|
+
return projectRoot;
|
|
676
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
677
|
+
if (!project)
|
|
678
|
+
return projectRoot;
|
|
679
|
+
let work;
|
|
680
|
+
if (facts?.workId) {
|
|
681
|
+
try {
|
|
682
|
+
work = resolveWorkReference(context, facts.workId, { projectId: project.id });
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2] ?? issuedRunId;
|
|
690
|
+
if (!reference)
|
|
691
|
+
return projectRoot;
|
|
692
|
+
const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
693
|
+
if (!run)
|
|
694
|
+
return projectRoot; // Hook ingress does not validate a mistyped CLI target.
|
|
695
|
+
if (work) {
|
|
696
|
+
// PROTOCOLIZE provisions the feature checkout after its coordinator
|
|
697
|
+
// Session has launched in the stable project root. Its transition Work
|
|
698
|
+
// must keep that physical route until the Stage is complete.
|
|
699
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'stage_work_memberships'")
|
|
700
|
+
&& context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage IN ('specify','protocolize') LIMIT 1", [project.id, run.id, work.work_id]))
|
|
701
|
+
return projectRoot;
|
|
702
|
+
const merge = context.db.get("SELECT target_workspace FROM merge_requests WHERE executor_work_id = ? AND run_id = ?", [work.work_id, run.id]);
|
|
703
|
+
if (merge)
|
|
704
|
+
return resolveProjectRoot(merge.target_workspace);
|
|
705
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'work_launches'")) {
|
|
706
|
+
const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching', 'start_requested')", [work.work_id]);
|
|
707
|
+
if (launch?.provider_session_id === providerSessionId && run.run_root) {
|
|
708
|
+
const copy = JSON.parse(launch.profile_json).review_copy?.root;
|
|
709
|
+
if (copy) {
|
|
710
|
+
const target = path.resolve(run.run_root, copy);
|
|
711
|
+
const relative = path.relative(run.run_root, target);
|
|
712
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`))
|
|
713
|
+
throw new AppError("read_only_workspace_unbound", "Review copy escapes its owning RUN", 1);
|
|
714
|
+
return resolveProjectRoot(target);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (facts?.stageName === "specify" || facts?.stageName === "protocolize")
|
|
720
|
+
return projectRoot;
|
|
721
|
+
if (facts?.stageName === "merge") {
|
|
722
|
+
const pending = context.db.all("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND run_id = ? AND status IN ('dispatching','active','action_required') ORDER BY updated_at DESC", [project.id, run.id]);
|
|
723
|
+
const targets = [...new Set(pending.map(item => resolveProjectRoot(item.target_workspace)))];
|
|
724
|
+
if (targets.length > 1)
|
|
725
|
+
throw new AppError("merge_workspace_ambiguous", "Current MERGE requests disagree on the native execution workspace", 1, { run_id: run.id, target_workspaces: targets });
|
|
726
|
+
if (targets.length === 1)
|
|
727
|
+
return targets[0];
|
|
728
|
+
}
|
|
729
|
+
return resolveProjectRoot(run.workspace_root);
|
|
730
|
+
}
|
|
657
731
|
function handleControlledToolEvent(context, input, event, harness) {
|
|
658
732
|
const agy = harness === "antigravity-cli";
|
|
659
733
|
const droid = harness === "droid-cli";
|
|
@@ -676,9 +750,7 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
676
750
|
throw new AppError(`${errorPrefix}_session_identity_invalid`, `${label} event is missing trusted physical identity`, 1);
|
|
677
751
|
}
|
|
678
752
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
679
|
-
|
|
680
|
-
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} Session directory does not match the controlled workspace`, 1, { expected: expectedRoot, actual: directory });
|
|
681
|
-
}
|
|
753
|
+
const observedCwd = resolveProjectRoot(directory);
|
|
682
754
|
if (droid && nativeParentId === providerSessionId)
|
|
683
755
|
throw new AppError("droid_session_identity_invalid", "A Droid Session cannot parent itself", 1);
|
|
684
756
|
const sessionId = storageSessionId(nativeSessionIdentity(harness, providerSessionId));
|
|
@@ -690,8 +762,8 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
690
762
|
const inserted = recordHookEvent(context, {
|
|
691
763
|
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
692
764
|
daemonId, sessionId, agentId: stringValue(session.agent_id) ?? null, turnId: stringValue(event.message_id) ?? null,
|
|
693
|
-
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd:
|
|
694
|
-
eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd:
|
|
765
|
+
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: observedCwd, tool_name: toolName, status: objectRecord(event.outcome).status },
|
|
766
|
+
eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
695
767
|
});
|
|
696
768
|
return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
|
|
697
769
|
}
|
|
@@ -699,10 +771,17 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
699
771
|
return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
|
|
700
772
|
}
|
|
701
773
|
const lifecycle = lifecycleFacts(command);
|
|
702
|
-
|
|
774
|
+
const auxiliary = continuationCommand(command);
|
|
775
|
+
if (!lifecycle && !auxiliary)
|
|
703
776
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
777
|
+
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
778
|
+
? observedLifecycleInvocation(context, command, daemonId)?.scope.runId : null;
|
|
779
|
+
const expectedCwd = controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId);
|
|
780
|
+
if (observedCwd !== expectedCwd) {
|
|
781
|
+
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
782
|
+
}
|
|
704
783
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
705
|
-
|
|
784
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
706
785
|
if (bootstrapStageStart)
|
|
707
786
|
registerProject(context, { root: expectedRoot });
|
|
708
787
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
@@ -713,28 +792,55 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
713
792
|
}
|
|
714
793
|
}
|
|
715
794
|
const agentId = stringValue(session.agent_id);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
795
|
+
const persist = (scope) => {
|
|
796
|
+
if (scope)
|
|
797
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
798
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: observedCwd });
|
|
799
|
+
const payload = {
|
|
800
|
+
session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName, tool_input: rawInput, command,
|
|
801
|
+
...(agy ? { cwd_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}),
|
|
802
|
+
provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
|
|
803
|
+
mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
|
|
804
|
+
};
|
|
805
|
+
const inserted = recordHookEvent(context, {
|
|
806
|
+
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
807
|
+
daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
|
|
808
|
+
status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
809
|
+
});
|
|
810
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName, eventKey: baseKey });
|
|
811
|
+
return inserted;
|
|
721
812
|
};
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
813
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
|
|
814
|
+
if (agy && lifecycle) {
|
|
815
|
+
let inserted = false;
|
|
816
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
817
|
+
command, harness, projectId: project.id, identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
818
|
+
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
819
|
+
});
|
|
820
|
+
return { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
821
|
+
}
|
|
822
|
+
const inserted = context.db.writeTransaction(() => persist());
|
|
823
|
+
return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
730
824
|
}
|
|
731
825
|
/** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
|
|
732
826
|
export function handleAgyEvent(context, input) {
|
|
733
827
|
const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
|
|
734
828
|
if (event.schema_id !== "dd-flow/agy-tool-event@1")
|
|
735
829
|
throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
|
|
736
|
-
|
|
737
|
-
|
|
830
|
+
// workspacePaths lists available workspaces; its first element is not a
|
|
831
|
+
// native selected-cwd observation. The daemon's spawn cwd is a separate,
|
|
832
|
+
// launch-bound route, and the list only corroborates its membership.
|
|
833
|
+
const workspacePaths = Array.isArray(event.workspace_paths)
|
|
834
|
+
? event.workspace_paths.filter((value) => typeof value === "string" && path.isAbsolute(value)) : [];
|
|
835
|
+
const launchCwd = stringValue(event.launch_cwd);
|
|
836
|
+
const qualifiedLaunchCwd = launchCwd && path.isAbsolute(launchCwd) ? launchCwd : undefined;
|
|
837
|
+
const directory = qualifiedLaunchCwd ?? (workspacePaths.length === 1 ? workspacePaths[0] : undefined);
|
|
838
|
+
if (!directory && workspacePaths.length > 1)
|
|
839
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity did not identify the active route among multiple workspaces", 1, { workspace_paths: workspacePaths });
|
|
840
|
+
if (directory && qualifiedLaunchCwd && workspacePaths.length
|
|
841
|
+
&& !workspacePaths.some(value => resolveProjectRoot(value) === resolveProjectRoot(directory))) {
|
|
842
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity launch cwd is absent from the native workspace list", 1, { launch_cwd: directory, workspace_paths: workspacePaths });
|
|
843
|
+
}
|
|
738
844
|
const providerSessionId = stringValue(event.conversation_id);
|
|
739
845
|
const daemonId = stringValue(event.daemon_id);
|
|
740
846
|
const eventId = stringValue(event.event_id);
|
|
@@ -743,6 +849,9 @@ export function handleAgyEvent(context, input) {
|
|
|
743
849
|
throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
|
|
744
850
|
const rawInput = objectRecord(event.input);
|
|
745
851
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
852
|
+
if (command && (lifecycleFacts(command) || continuationCommand(command)) && !qualifiedLaunchCwd) {
|
|
853
|
+
throw new AppError("agy_workspace_route_unproven", "Managed Antigravity call has no launch-bound execution cwd", 1, { phase: "prepare", effect: "no_effect", recoverable: false, workspace_paths: workspacePaths });
|
|
854
|
+
}
|
|
746
855
|
const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
|
|
747
856
|
const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
|
|
748
857
|
if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
|
|
@@ -753,6 +862,7 @@ export function handleAgyEvent(context, input) {
|
|
|
753
862
|
daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
|
|
754
863
|
message_id: event.step_index === undefined ? null : String(event.step_index),
|
|
755
864
|
session: { provider_session_id: providerSessionId, parent_provider_session_id: event.parent_conversation_id, directory },
|
|
865
|
+
cwd_provenance: qualifiedLaunchCwd ? "launch_bound" : "workspace_paths_singleton",
|
|
756
866
|
profile: { provider: objectRecord(event.profile).provider, model: objectRecord(event.profile).model, variant: objectRecord(event.profile).reasoning, agent: objectRecord(event.profile).mode },
|
|
757
867
|
outcome: { status: stringValue(event.error) ? "error" : "completed" }
|
|
758
868
|
};
|