@deksden-com/dd-flow-cli 0.4.2 → 0.6.0
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 +33 -4
- package/README.md +2 -2
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +16 -4
- package/dist/cli/run-cli.js +42 -17
- package/dist/domain/flow-contract.js +82 -3
- package/dist/domain/session-coverage.js +88 -0
- package/dist/domain/validation.js +56 -28
- package/dist/protocol/local-files.js +1 -16
- package/dist/schemas/code-stage-report.schema.json +2 -2
- package/dist/schemas/flow-contract.schema.json +152 -94
- package/dist/schemas/flow-run.schema.json +129 -23
- package/dist/schemas/mb-upgrade-review-data.schema.json +2 -2
- package/dist/schemas/memorybank-permissions-preflight.schema.json +13 -73
- package/dist/schemas/merge-stage-report.schema.json +2 -2
- package/dist/schemas/plan-stage-report.schema.json +38 -335
- package/dist/schemas/project-flow-pack-manifest.schema.json +4 -4
- package/dist/schemas/protocol-plan.schema.json +197 -0
- package/dist/schemas/release-impact.schema.json +9 -5
- package/dist/schemas/session-usage.schema.json +16 -0
- package/dist/schemas/stage-finish-input.schema.json +20 -0
- package/dist/schemas/stage-prompt.schema.json +35 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +30 -0
- package/dist/schemas/timeline-event.schema.json +29 -0
- package/dist/schemas/worktrunk-workspace.schema.json +19 -0
- package/dist/services/branch-context.js +9 -4
- package/dist/services/cleanup.js +77 -0
- package/dist/services/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +101 -250
- package/dist/services/memory-permissions.js +77 -69
- package/dist/services/migrations.js +1 -1
- package/dist/services/plan-runtime.js +124 -0
- package/dist/services/plans.js +22 -84
- package/dist/services/projects.js +2 -1
- package/dist/services/prompts.js +26 -21
- package/dist/services/protocols.js +29 -25
- package/dist/services/run-projection.js +93 -13
- package/dist/services/runs.js +128 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +97 -73
- package/dist/services/stage-lifecycle.js +737 -0
- package/dist/services/tooling.js +285 -0
- package/dist/services/usage.js +183 -30
- package/dist/services/version-status.js +1 -1
- package/dist/services/worktrees.js +88 -39
- package/dist/storage/database.js +72 -30
- package/dist/storage/paths.js +0 -9
- package/package.json +14 -13
- package/tools/worktrunk-manifest.json +34 -0
- package/dist/schemas/flow-run-index-v3.schema.json +0 -203
- package/dist/schemas/flow-run-index.schema.json +0 -175
package/dist/services/hooks.js
CHANGED
|
@@ -8,10 +8,9 @@ import { parseJsonObject } from "../shared/json.js";
|
|
|
8
8
|
import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
9
9
|
import { appendAudit } from "./audit.js";
|
|
10
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
|
-
import { activeFlowSessionsForProject,
|
|
11
|
+
import { activeFlowSessionsForProject, bindObservedFlowSession, flowSessionPayloadFromRegisterCommand, recordFlowSessionObservation } from "./sessions.js";
|
|
12
12
|
const defaultProfileName = "default";
|
|
13
13
|
const maxSanitizedSummaryLength = 1600;
|
|
14
|
-
const stopLoopLimit = 3;
|
|
15
14
|
const sharedEntries = [
|
|
16
15
|
"auth.json",
|
|
17
16
|
"auth.bk",
|
|
@@ -197,7 +196,7 @@ export function getCodexHooksStatus(context, input) {
|
|
|
197
196
|
};
|
|
198
197
|
}
|
|
199
198
|
const status = hookFileStatus(location.hooksPath, project.root);
|
|
200
|
-
const recorded = hookInstallation(context, project.id, installationScope(target, location.profile));
|
|
199
|
+
const recorded = hookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath));
|
|
201
200
|
const driftStatus = status.installed ? "none" : recorded?.installed === 1 ? "drifted" : status.drift_status;
|
|
202
201
|
return {
|
|
203
202
|
ok: true,
|
|
@@ -225,7 +224,7 @@ export function installCodexHooks(context, input) {
|
|
|
225
224
|
const nextConfig = mergeHooksConfig(existing, project.root);
|
|
226
225
|
ensureDir(path.dirname(location.hooksPath));
|
|
227
226
|
writeJsonFile(location.hooksPath, nextConfig);
|
|
228
|
-
recordHookInstallation(context, project.id, installationScope(target, location.profile), location.hooksPath, true, "none");
|
|
227
|
+
recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, true, "none");
|
|
229
228
|
appendAudit(context, {
|
|
230
229
|
projectId: project.id,
|
|
231
230
|
eventType: "codex_hooks.installed",
|
|
@@ -257,7 +256,7 @@ export function removeCodexHooks(context, input) {
|
|
|
257
256
|
const nextConfig = removeManagedHooks(existing, project.root);
|
|
258
257
|
ensureDir(path.dirname(location.hooksPath));
|
|
259
258
|
writeJsonFile(location.hooksPath, nextConfig);
|
|
260
|
-
recordHookInstallation(context, project.id, installationScope(target, location.profile), location.hooksPath, false, "not_installed");
|
|
259
|
+
recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, false, "not_installed");
|
|
261
260
|
appendAudit(context, {
|
|
262
261
|
projectId: project.id,
|
|
263
262
|
eventType: "codex_hooks.removed",
|
|
@@ -266,49 +265,75 @@ export function removeCodexHooks(context, input) {
|
|
|
266
265
|
return { ok: true, installed: false, target, profile: location.profile, hooks_path: location.hooksPath, backup, had_managed: hadManaged };
|
|
267
266
|
}
|
|
268
267
|
export function handleCodexHook(context, input) {
|
|
269
|
-
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
270
268
|
const payload = parseJsonObject(input.stdin || "{}", "codex hook stdin");
|
|
271
269
|
const eventName = input.event || stringValue(payload.hook_event_name) || "Unknown";
|
|
270
|
+
if (eventName !== "PreToolUse") {
|
|
271
|
+
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
272
|
+
}
|
|
272
273
|
const sessionId = stringValue(payload.session_id);
|
|
273
274
|
const turnId = stringValue(payload.turn_id);
|
|
274
275
|
const toolName = stringValue(payload.tool_name) ?? toolNameFromPayload(payload);
|
|
275
276
|
const command = commandFromPayload(payload);
|
|
277
|
+
if (!command)
|
|
278
|
+
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
279
|
+
const flowPayload = flowSessionPayloadFromRegisterCommand(command);
|
|
280
|
+
const bootstrapStageStart = /\bdd-flow\s+stage\s+start\b/.test(command) && /(?:^|\s)--bootstrap(?:\s|$)/.test(command);
|
|
281
|
+
if (!flowPayload && !bootstrapStageStart)
|
|
282
|
+
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
283
|
+
const project = projectForHook(context, input.projectRoot, stringValue(payload.cwd));
|
|
284
|
+
if (!project)
|
|
285
|
+
return { ok: true, observed: false, reason: "unrelated_cwd" };
|
|
276
286
|
const binding = sessionId ? upsertSessionBindingFromPayload(context, project, sessionId, payload) : undefined;
|
|
277
|
-
const
|
|
278
|
-
?
|
|
279
|
-
: undefined;
|
|
280
|
-
if (eventName === "PreToolUse" && sessionId && command) {
|
|
281
|
-
recordPendingFlowSessionBinding(context, project, {
|
|
282
|
-
sessionId,
|
|
283
|
-
command,
|
|
284
|
-
cwd: stringValue(payload.cwd),
|
|
285
|
-
transcriptPath: stringValue(payload.transcript_path),
|
|
286
|
-
turnId: turnId ?? undefined
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
const confirmedFlowSession = eventName === "PostToolUse" && sessionId && toolSucceeded(payload)
|
|
290
|
-
? confirmPendingFlowSessionBinding(context, project, { sessionId })
|
|
287
|
+
const observedSession = flowPayload
|
|
288
|
+
? bindObservedFlowSession(context, project, flowPayload, sessionId ?? flowPayload.session_id ?? undefined)
|
|
291
289
|
: undefined;
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
recordHookEvent(context, {
|
|
290
|
+
const effectiveSessionId = observedSession?.session_id ?? sessionId ?? null;
|
|
291
|
+
const protocolId = observedSession?.protocol_id ?? binding?.protocol_id ?? null;
|
|
292
|
+
const eventKey = hookEventKey(payload, eventName, toolName, command);
|
|
293
|
+
const inserted = recordHookEvent(context, {
|
|
296
294
|
projectId: project.id,
|
|
297
295
|
protocolId,
|
|
298
|
-
sessionId:
|
|
296
|
+
sessionId: effectiveSessionId,
|
|
299
297
|
turnId: turnId ?? null,
|
|
300
298
|
eventName,
|
|
301
299
|
toolName: toolName ?? null,
|
|
302
300
|
status: "observed",
|
|
303
|
-
payload
|
|
301
|
+
payload,
|
|
302
|
+
eventKey
|
|
304
303
|
});
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
if (effectiveSessionId) {
|
|
305
|
+
recordFlowSessionObservation(context, {
|
|
306
|
+
projectId: project.id,
|
|
307
|
+
sessionId: effectiveSessionId,
|
|
308
|
+
runId: observedSession?.run_id ?? null,
|
|
309
|
+
protocolId,
|
|
310
|
+
cwd: stringValue(payload.cwd) ?? null,
|
|
311
|
+
toolName: toolName ?? null,
|
|
312
|
+
eventKey
|
|
313
|
+
});
|
|
310
314
|
}
|
|
311
|
-
return
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
observed: inserted,
|
|
318
|
+
duplicate: !inserted,
|
|
319
|
+
event_key: eventKey,
|
|
320
|
+
session_id: effectiveSessionId,
|
|
321
|
+
protocol_id: protocolId,
|
|
322
|
+
...(sessionId
|
|
323
|
+
? {
|
|
324
|
+
hookSpecificOutput: {
|
|
325
|
+
hookEventName: "PreToolUse",
|
|
326
|
+
permissionDecision: "allow",
|
|
327
|
+
updatedInput: { command: commandWithSessionId(command, sessionId) }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
: {})
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function commandWithSessionId(command, sessionId) {
|
|
334
|
+
if (/(?:^|\s)--session-id(?:=|\s)/.test(command))
|
|
335
|
+
return command;
|
|
336
|
+
return `${command} --session-id ${JSON.stringify(sessionId)}`;
|
|
312
337
|
}
|
|
313
338
|
export function hookStatusForProject(context, projectId) {
|
|
314
339
|
return context.db.all(`SELECT scope, config_path, content_hash, installed, drift_status, updated_at
|
|
@@ -438,13 +463,10 @@ function sharedEntryStatus(sourceHome, targetHome) {
|
|
|
438
463
|
};
|
|
439
464
|
});
|
|
440
465
|
}
|
|
441
|
-
function expectedHooksConfig(
|
|
466
|
+
function expectedHooksConfig(_projectRoot) {
|
|
442
467
|
return {
|
|
443
468
|
hooks: {
|
|
444
|
-
|
|
445
|
-
PreToolUse: [hookEntry("Bash", hookCommand("PreToolUse", projectRoot), "dd-flow command guard")],
|
|
446
|
-
PostToolUse: [hookEntry("Bash", hookCommand("PostToolUse", projectRoot), "dd-flow command observer")],
|
|
447
|
-
Stop: [{ hooks: [commandHook(hookCommand("Stop", projectRoot), "dd-flow continuation guard")] }]
|
|
469
|
+
PreToolUse: [hookEntry("Bash", hookCommand("PreToolUse"), "dd-flow runtime observation")]
|
|
448
470
|
}
|
|
449
471
|
};
|
|
450
472
|
}
|
|
@@ -454,8 +476,8 @@ function hookEntry(matcher, command, statusMessage) {
|
|
|
454
476
|
function commandHook(command, statusMessage) {
|
|
455
477
|
return { type: "command", command, timeout: 5, statusMessage };
|
|
456
478
|
}
|
|
457
|
-
function hookCommand(event
|
|
458
|
-
return `dd-flow codex hook handle --event ${event} --
|
|
479
|
+
function hookCommand(event) {
|
|
480
|
+
return `PATH="\${HOME}/Library/pnpm:/opt/homebrew/bin:/usr/local/bin:\${PATH}" dd-flow codex hook handle --event ${event} --json`;
|
|
459
481
|
}
|
|
460
482
|
function resolveHookTarget(target) {
|
|
461
483
|
if (!target || target === "isolated") {
|
|
@@ -493,7 +515,7 @@ function requireHookLocation(context, project, target, profile) {
|
|
|
493
515
|
}
|
|
494
516
|
function hookFileStatus(hooksPath, projectRoot) {
|
|
495
517
|
const existing = readJsonIfExists(hooksPath);
|
|
496
|
-
const installed = existing ? hasExpectedHooks(existing
|
|
518
|
+
const installed = existing ? hasExpectedHooks(existing) : false;
|
|
497
519
|
const managed = existing ? hasManagedHooks(existing, projectRoot) : false;
|
|
498
520
|
return {
|
|
499
521
|
installed,
|
|
@@ -537,20 +559,20 @@ function removeManagedHooks(existing, projectRoot) {
|
|
|
537
559
|
}
|
|
538
560
|
return { ...existing, hooks: nextHooks };
|
|
539
561
|
}
|
|
540
|
-
function hasExpectedHooks(existing
|
|
562
|
+
function hasExpectedHooks(existing) {
|
|
541
563
|
const commands = collectHookCommands(existing);
|
|
542
|
-
return expectedCommands(
|
|
564
|
+
return expectedCommands().every((command) => commands.includes(command));
|
|
543
565
|
}
|
|
544
566
|
function hasManagedHooks(existing, projectRoot) {
|
|
545
567
|
return collectHookCommands(existing).some((command) => command.includes("dd-flow codex hook handle") &&
|
|
546
|
-
(command.includes(`--project-root ${JSON.stringify(projectRoot)}`) || command.includes(projectRoot)));
|
|
568
|
+
(command.includes("--event PreToolUse") || command.includes(`--project-root ${JSON.stringify(projectRoot)}`) || command.includes(projectRoot)));
|
|
547
569
|
}
|
|
548
|
-
function expectedCommands(
|
|
549
|
-
return ["
|
|
570
|
+
function expectedCommands() {
|
|
571
|
+
return [hookCommand("PreToolUse")];
|
|
550
572
|
}
|
|
551
573
|
function entryHasManagedCommand(entry, projectRoot) {
|
|
552
574
|
return collectHookCommands(entry).some((command) => command.includes("dd-flow codex hook handle") &&
|
|
553
|
-
(command.includes(`--project-root ${JSON.stringify(projectRoot)}`) || command.includes(projectRoot)));
|
|
575
|
+
(command.includes("--event PreToolUse") || command.includes(`--project-root ${JSON.stringify(projectRoot)}`) || command.includes(projectRoot)));
|
|
554
576
|
}
|
|
555
577
|
function collectHookCommands(value) {
|
|
556
578
|
if (Array.isArray(value)) {
|
|
@@ -627,8 +649,8 @@ function recordHookInstallation(context, projectId, scope, configPath, installed
|
|
|
627
649
|
drift_status = excluded.drift_status,
|
|
628
650
|
updated_at = excluded.updated_at`, [projectId, scope, configPath, hashObject(readJsonIfExists(configPath) ?? {}), installed ? 1 : 0, driftStatus, now, now]);
|
|
629
651
|
}
|
|
630
|
-
function installationScope(target, profile) {
|
|
631
|
-
return target === "isolated" ? `isolated:${profile}` :
|
|
652
|
+
function installationScope(target, profile, hooksPath) {
|
|
653
|
+
return target === "isolated" ? `isolated:${profile}` : `default:${path.dirname(hooksPath)}`;
|
|
632
654
|
}
|
|
633
655
|
function upsertSessionBindingFromPayload(context, project, sessionId, payload) {
|
|
634
656
|
const existing = sessionBinding(context, project.id, sessionId);
|
|
@@ -645,213 +667,54 @@ function upsertSessionBindingFromPayload(context, project, sessionId, payload) {
|
|
|
645
667
|
updated_at = excluded.updated_at`, [sessionId, project.id, cwd, transcriptPath, now, now]);
|
|
646
668
|
return sessionBinding(context, project.id, sessionId);
|
|
647
669
|
}
|
|
648
|
-
function bindProtocolFromToolEvent(context, project, sessionId, payload) {
|
|
649
|
-
if (!toolSucceeded(payload)) {
|
|
650
|
-
return sessionBinding(context, project.id, sessionId);
|
|
651
|
-
}
|
|
652
|
-
const command = commandFromPayload(payload);
|
|
653
|
-
const handshakeId = command ? handshakeFromCommand(command) : null;
|
|
654
|
-
if (!handshakeId) {
|
|
655
|
-
return sessionBinding(context, project.id, sessionId);
|
|
656
|
-
}
|
|
657
|
-
const protocol = context.db.get("SELECT id, handshake_id FROM protocols WHERE project_id = ? AND (handshake_id = ? OR id = ?) ORDER BY updated_at DESC LIMIT 1", [project.id, handshakeId, handshakeId.startsWith("PRT-") ? handshakeId : `PRT-${handshakeId}`]);
|
|
658
|
-
if (!protocol) {
|
|
659
|
-
return sessionBinding(context, project.id, sessionId);
|
|
660
|
-
}
|
|
661
|
-
context.db.run(`UPDATE codex_session_bindings
|
|
662
|
-
SET protocol_id = ?, handshake_id = ?, status = 'active', updated_at = ?
|
|
663
|
-
WHERE project_id = ? AND session_id = ?`, [protocol.id, protocol.handshake_id, context.now(), project.id, sessionId]);
|
|
664
|
-
appendAudit(context, {
|
|
665
|
-
protocolId: protocol.id,
|
|
666
|
-
projectId: project.id,
|
|
667
|
-
eventType: "codex_session.bound",
|
|
668
|
-
payload: { project_id: project.id, protocol_id: protocol.id, handshake_id: protocol.handshake_id, session_id: sessionId }
|
|
669
|
-
});
|
|
670
|
-
return sessionBinding(context, project.id, sessionId);
|
|
671
|
-
}
|
|
672
670
|
function sessionBinding(context, projectId, sessionId) {
|
|
673
671
|
return context.db.get("SELECT * FROM codex_session_bindings WHERE project_id = ? AND session_id = ?", [projectId, sessionId]);
|
|
674
672
|
}
|
|
675
673
|
function recordHookEvent(context, input) {
|
|
674
|
+
const existing = context.db.get("SELECT id FROM codex_hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
675
|
+
if (existing)
|
|
676
|
+
return false;
|
|
676
677
|
context.db.run(`INSERT INTO codex_hook_events
|
|
677
|
-
(project_id, protocol_id, session_id, turn_id, event_name, tool_name, status, sanitized_summary, created_at)
|
|
678
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
678
|
+
(project_id, protocol_id, session_id, turn_id, event_key, event_name, tool_name, status, sanitized_summary, created_at)
|
|
679
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
679
680
|
input.projectId,
|
|
680
681
|
input.protocolId,
|
|
681
682
|
input.sessionId,
|
|
682
683
|
input.turnId,
|
|
684
|
+
input.eventKey,
|
|
683
685
|
input.eventName,
|
|
684
686
|
input.toolName,
|
|
685
687
|
input.status,
|
|
686
688
|
sanitizedSummary(input.payload),
|
|
687
689
|
context.now()
|
|
688
690
|
]);
|
|
691
|
+
return true;
|
|
689
692
|
}
|
|
690
|
-
function
|
|
691
|
-
if (
|
|
692
|
-
return
|
|
693
|
-
|
|
694
|
-
const session = flowSessionById(context, project.id, sessionId);
|
|
695
|
-
if (!session || ["waiting_user", "blocked", "stopping", "stopped", "closed"].includes(session.status)) {
|
|
696
|
-
return {};
|
|
697
|
-
}
|
|
698
|
-
if (session.protocol_id && protocolAllowsStop(context, project.id, session.protocol_id)) {
|
|
699
|
-
return {};
|
|
700
|
-
}
|
|
701
|
-
if (session.flow_kind === "merge_worker" && session.continuation_policy === "merge_queue") {
|
|
702
|
-
return stopContinuationDecision(context, project.id, sessionId, `merge-worker:${session.worker_id ?? session.session_id}:${session.next_action ?? "wait-next"}`, `Continue dd-flow merge worker ${session.worker_id ?? session.session_id}: run .memory-bank/dd-flow/merge-start.md and continue dd-flow merge-queue wait-next from the registered merge workspace. Use dd-flow session stop-worker to stop this worker.`, { limit: null });
|
|
703
|
-
}
|
|
704
|
-
if (session.flow_kind === "merge_job" && session.continuation_policy === "merge_job") {
|
|
705
|
-
return stopContinuationDecision(context, project.id, sessionId, `merge-job:${session.protocol_id ?? session.session_id}:${session.next_action ?? "none"}`, `Continue dd-flow merge job ${session.protocol_id ?? ""}: complete merge work, close the protocol, and report terminal state.`);
|
|
706
|
-
}
|
|
707
|
-
const nextAction = session.next_action ?? nextActionFromProtocol(context, project.id, session.protocol_id);
|
|
708
|
-
if (!nextAction || nextAction === "none" || session.continuation_policy === "none") {
|
|
709
|
-
return {};
|
|
710
|
-
}
|
|
711
|
-
return stopContinuationDecision(context, project.id, sessionId, `${session.flow_kind}:${session.protocol_id ?? session.session_id}:${nextAction}`, `Continue dd-flow ${session.flow_kind}${session.protocol_id ? ` ${session.protocol_id}` : ""}: ${nextAction}`);
|
|
712
|
-
}
|
|
713
|
-
function nextActionFromProtocol(context, projectId, protocolId) {
|
|
714
|
-
if (!protocolId) {
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
const protocol = context.db.get("SELECT id, status, stage, next_action FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
718
|
-
if (!protocol) {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
if (["waiting_for_user", "blocked", "closed", "cancelled"].includes(protocol.stage) || ["waiting_for_user", "blocked", "closed", "cancelled"].includes(protocol.status)) {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
return protocol.next_action;
|
|
725
|
-
}
|
|
726
|
-
function protocolAllowsStop(context, projectId, protocolId) {
|
|
727
|
-
const protocol = context.db.get("SELECT status, stage FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
728
|
-
return Boolean(protocol && (["closed", "cancelled"].includes(protocol.status) || ["closed", "cancelled"].includes(protocol.stage)));
|
|
729
|
-
}
|
|
730
|
-
function stopContinuationDecision(context, projectId, sessionId, actionKey, reason, options = {}) {
|
|
731
|
-
const nextCount = updateFlowSessionContinuation(context, projectId, sessionId, actionKey);
|
|
732
|
-
const limit = options.limit === undefined ? stopLoopLimit : options.limit;
|
|
733
|
-
if (limit !== null && nextCount > limit) {
|
|
734
|
-
return {};
|
|
735
|
-
}
|
|
736
|
-
return { decision: "block", reason };
|
|
737
|
-
}
|
|
738
|
-
function preToolUseGuard(context, project, sessionId, payload, command) {
|
|
739
|
-
const cwd = stringValue(payload.cwd);
|
|
740
|
-
const blockedTarget = cwd ? selfWorktreeRemovalTarget(command, cwd) : null;
|
|
741
|
-
if (!blockedTarget) {
|
|
742
|
-
const roleDecision = mergeRoleGuard(context, project, sessionId, command);
|
|
743
|
-
if (roleDecision) {
|
|
744
|
-
return roleDecision;
|
|
745
|
-
}
|
|
693
|
+
function projectForHook(context, explicitRoot, cwd) {
|
|
694
|
+
if (explicitRoot)
|
|
695
|
+
return requireProjectByRoot(context, resolveProjectRoot(explicitRoot));
|
|
696
|
+
if (!cwd)
|
|
746
697
|
return undefined;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (mergeQueueAction) {
|
|
768
|
-
if (mergeQueueAction === "next" || mergeQueueAction === "wait-next") {
|
|
769
|
-
return session?.flow_kind === "merge_worker" ? undefined : blockMergeRole("Only a registered merge_worker session may claim or wait for merge queue jobs.");
|
|
770
|
-
}
|
|
771
|
-
return session?.flow_kind === "merge_job" ? undefined : blockMergeRole("Only a registered merge_job session may complete or fail a claimed merge queue job.");
|
|
772
|
-
}
|
|
773
|
-
if (mergeLaneLockMutation(command)) {
|
|
774
|
-
return isMergeRole ? undefined : blockMergeRole("Only registered merge sessions may mutate the merge lane lock.");
|
|
775
|
-
}
|
|
776
|
-
if (/\bgit\s+merge\b/.test(command)) {
|
|
777
|
-
return session?.flow_kind === "merge_job" ? undefined : blockMergeRole("Only a registered merge_job session may run git merge.");
|
|
778
|
-
}
|
|
779
|
-
return undefined;
|
|
780
|
-
}
|
|
781
|
-
function blockMergeRole(reason) {
|
|
782
|
-
return { decision: "block", reason };
|
|
783
|
-
}
|
|
784
|
-
function safeFlowSessionPayloadFromCommand(command) {
|
|
785
|
-
try {
|
|
786
|
-
return flowSessionPayloadFromRegisterCommand(command);
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
return undefined;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function claimedMergeJobMatches(context, projectId, protocolId, workerId) {
|
|
793
|
-
if (!protocolId || !workerId) {
|
|
794
|
-
return false;
|
|
795
|
-
}
|
|
796
|
-
const job = context.db.get("SELECT status, claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
797
|
-
return job?.status === "claimed" && job.claimed_by_session_id === workerId;
|
|
798
|
-
}
|
|
799
|
-
function mergeQueueMutation(command) {
|
|
800
|
-
const match = command.match(/\bdd-flow\s+merge-queue\s+(next|wait-next|complete|fail)\b/);
|
|
801
|
-
return match?.[1] ?? null;
|
|
802
|
-
}
|
|
803
|
-
function mergeLaneLockMutation(command) {
|
|
804
|
-
return /\bdd-flow\s+lane\s+lock\s+(acquire|heartbeat|release|wait|wait-acquire)\b/.test(command) && /(?:--lane(?:\s+|=)(?:"merge"|'merge'|merge)\b)/.test(command);
|
|
805
|
-
}
|
|
806
|
-
function selfWorktreeRemovalTarget(command, cwd) {
|
|
807
|
-
if (!/\bgit\s+worktree\s+remove\b/.test(command)) {
|
|
808
|
-
return null;
|
|
809
|
-
}
|
|
810
|
-
const cwdPath = normalizePathForGuard(cwd, cwd);
|
|
811
|
-
for (const target of gitWorktreeRemoveTargets(command)) {
|
|
812
|
-
const targetPath = normalizePathForGuard(target, cwd);
|
|
813
|
-
if (cwdPath === targetPath || cwdPath.startsWith(`${targetPath}${path.sep}`)) {
|
|
814
|
-
return targetPath;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return null;
|
|
818
|
-
}
|
|
819
|
-
function gitWorktreeRemoveTargets(command) {
|
|
820
|
-
const targets = [];
|
|
821
|
-
const pattern = /\bgit\s+worktree\s+remove\b([^;&|]*)/g;
|
|
822
|
-
for (const match of command.matchAll(pattern)) {
|
|
823
|
-
const args = shellishTokens(match[1] ?? "");
|
|
824
|
-
for (const arg of args) {
|
|
825
|
-
if (arg.startsWith("-")) {
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
targets.push(arg);
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return targets;
|
|
833
|
-
}
|
|
834
|
-
function shellishTokens(input) {
|
|
835
|
-
const tokens = [];
|
|
836
|
-
const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
837
|
-
for (const match of input.matchAll(pattern)) {
|
|
838
|
-
tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
839
|
-
}
|
|
840
|
-
return tokens.filter((token) => token.length > 0);
|
|
841
|
-
}
|
|
842
|
-
function normalizePathForGuard(value, cwd) {
|
|
843
|
-
const absolute = path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
844
|
-
try {
|
|
845
|
-
return fs.realpathSync(absolute);
|
|
846
|
-
}
|
|
847
|
-
catch {
|
|
848
|
-
return path.resolve(absolute);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
function hookContinue(eventName, protocolId) {
|
|
852
|
-
void eventName;
|
|
853
|
-
void protocolId;
|
|
854
|
-
return {};
|
|
698
|
+
const candidate = fs.existsSync(cwd) ? fs.realpathSync(cwd) : path.resolve(cwd);
|
|
699
|
+
const projects = context.db.all("SELECT * FROM projects WHERE status = 'active'");
|
|
700
|
+
return projects
|
|
701
|
+
.filter((project) => {
|
|
702
|
+
const relative = path.relative(project.root, candidate);
|
|
703
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
704
|
+
})
|
|
705
|
+
.sort((left, right) => right.root.length - left.root.length)[0];
|
|
706
|
+
}
|
|
707
|
+
function hookEventKey(payload, eventName, toolName, command) {
|
|
708
|
+
const explicit = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
709
|
+
if (explicit)
|
|
710
|
+
return explicit;
|
|
711
|
+
return crypto.createHash("sha256").update(JSON.stringify({
|
|
712
|
+
event: eventName,
|
|
713
|
+
session: stringValue(payload.session_id),
|
|
714
|
+
turn: stringValue(payload.turn_id),
|
|
715
|
+
tool: toolName ?? null,
|
|
716
|
+
command
|
|
717
|
+
})).digest("hex");
|
|
855
718
|
}
|
|
856
719
|
function commandFromPayload(payload) {
|
|
857
720
|
const direct = stringValue(payload.command);
|
|
@@ -865,18 +728,6 @@ function toolNameFromPayload(payload) {
|
|
|
865
728
|
const toolInput = objectRecord(payload.tool_input);
|
|
866
729
|
return stringValue(toolInput.name);
|
|
867
730
|
}
|
|
868
|
-
function toolSucceeded(payload) {
|
|
869
|
-
const response = objectRecord(payload.tool_response);
|
|
870
|
-
const status = stringValue(payload.status) ?? stringValue(response.status);
|
|
871
|
-
if (!status) {
|
|
872
|
-
return true;
|
|
873
|
-
}
|
|
874
|
-
return ["success", "succeeded", "ok", "0"].includes(status.toLowerCase());
|
|
875
|
-
}
|
|
876
|
-
function handshakeFromCommand(command) {
|
|
877
|
-
const match = command.match(/\bdd-flow\s+protocol\s+register\s+([^\s]+)/);
|
|
878
|
-
return match?.[1] ?? null;
|
|
879
|
-
}
|
|
880
731
|
function sanitizedSummary(payload) {
|
|
881
732
|
const summary = JSON.stringify(sanitizeValue(payload));
|
|
882
733
|
return summary.length > maxSanitizedSummaryLength ? `${summary.slice(0, maxSanitizedSummaryLength)}...` : summary;
|