@deftai/directive-core 0.98.1 → 0.99.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.
Files changed (55) hide show
  1. package/dist/authz/classify.js +265 -73
  2. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  3. package/dist/consumer-check-contract/evaluate.js +188 -3
  4. package/dist/consumer-check-contract/index.d.ts +1 -1
  5. package/dist/consumer-check-contract/index.js +1 -1
  6. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  7. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  8. package/dist/decision/index.d.ts +17 -0
  9. package/dist/decision/index.js +35 -0
  10. package/dist/decision/list.d.ts +47 -0
  11. package/dist/decision/list.js +250 -0
  12. package/dist/decision/schema.d.ts +88 -0
  13. package/dist/decision/schema.js +293 -0
  14. package/dist/decision/write.d.ts +82 -0
  15. package/dist/decision/write.js +427 -0
  16. package/dist/eval/report.d.ts +29 -0
  17. package/dist/eval/report.js +69 -0
  18. package/dist/eval/run.d.ts +9 -0
  19. package/dist/eval/run.js +40 -4
  20. package/dist/eval/version-pin.d.ts +99 -0
  21. package/dist/eval/version-pin.js +181 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/platform/host-content-surface.d.ts +74 -0
  25. package/dist/platform/host-content-surface.js +214 -0
  26. package/dist/platform/index.d.ts +1 -0
  27. package/dist/platform/index.js +1 -0
  28. package/dist/policy/ceremony-dial.d.ts +233 -0
  29. package/dist/policy/ceremony-dial.js +829 -0
  30. package/dist/policy/deft-directive-disable.js +12 -2
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-merge-readiness/evaluate.js +10 -0
  34. package/dist/pr-merge-readiness/mergeability.js +5 -0
  35. package/dist/pr-merge-readiness/output.js +2 -0
  36. package/dist/pr-merge-readiness/parse.js +4 -0
  37. package/dist/pr-merge-readiness/types.d.ts +6 -0
  38. package/dist/scope/effort-activate-gate.d.ts +28 -0
  39. package/dist/scope/effort-activate-gate.js +64 -0
  40. package/dist/scope/index.d.ts +1 -0
  41. package/dist/scope/index.js +1 -0
  42. package/dist/scope/transition.js +8 -0
  43. package/dist/session/session-start.d.ts +24 -1
  44. package/dist/session/session-start.js +183 -26
  45. package/dist/swarm/index.d.ts +2 -0
  46. package/dist/swarm/index.js +2 -0
  47. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  48. package/dist/swarm/pre-dispatch-cli.js +143 -0
  49. package/dist/swarm/pre-dispatch.d.ts +87 -0
  50. package/dist/swarm/pre-dispatch.js +373 -0
  51. package/dist/vbrief-activate/activate.js +6 -0
  52. package/dist/vbrief-validate/constants.d.ts +2 -0
  53. package/dist/vbrief-validate/constants.js +2 -0
  54. package/dist/vbrief-validate/schema.js +4 -1
  55. package/package.json +15 -3
@@ -4,7 +4,9 @@ import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { bindSessionGeneration } from "../freshness/bind.js";
5
5
  import { readLiveGeneration } from "../freshness/generation.js";
6
6
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
7
+ import { hostContentSurfaceToDict, maybeFormatHostContentSurfaceLines, } from "../platform/host-content-surface.js";
7
8
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
9
+ import { ceremonyDialToDict, formatCeremonyDialStatusLine, mergeCeremonyDialDeferrals, resolveCeremonyDial, resolveSessionCeremonyDialInputs, } from "../policy/ceremony-dial.js";
8
10
  import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
9
11
  import { disclosureLine } from "../policy/disclosure.js";
10
12
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
@@ -33,7 +35,14 @@ export const READ_ONLY_RESULT_MESSAGE = "read-only session posture (alignment on
33
35
  export const SESSION_CEREMONY_TIERS = ["cold", "rearm"];
34
36
  export const COLD_CEREMONY_TIER = "cold";
35
37
  export const REARM_CEREMONY_TIER = "rearm";
36
- export const QUICK_STEPS = ["alignment", "branch_policy", "triage_welcome"];
38
+ // verify_tools is mutation readiness recorded on cold path (#3214 / #3156) —
39
+ // included so re-arm refuses after a tools-failed cold start.
40
+ export const QUICK_STEPS = [
41
+ "alignment",
42
+ "branch_policy",
43
+ "triage_welcome",
44
+ "verify_tools",
45
+ ];
37
46
  export const GATED_STEPS = ["agent_hooks", "doctor", "cache_fresh"];
38
47
  /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
39
48
  export const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
@@ -105,7 +114,13 @@ export function assessRearmEligibility(projectRoot, options = {}) {
105
114
  }
106
115
  }
107
116
  for (const stepName of QUICK_STEPS) {
108
- if (!stepPassesForRearm(state.quickSteps[stepName])) {
117
+ const step = state.quickSteps[stepName];
118
+ // Legacy ritual-state (pre-#3214 tools persistence) omits verify_tools —
119
+ // treat missing as pass; explicit failure still refuses re-arm.
120
+ if (stepName === "verify_tools" && (step === undefined || step === null)) {
121
+ continue;
122
+ }
123
+ if (!stepPassesForRearm(step)) {
109
124
  return {
110
125
  eligible: false,
111
126
  reason: `quick step '${stepName}' is missing or failed (full cold session:start required)`,
@@ -340,6 +355,34 @@ function resolveSessionScmReadiness(options, allowOptionalNetwork) {
340
355
  };
341
356
  }
342
357
  }
358
+ function resolveHostContentSurface(projectRoot, options, runtimeMode) {
359
+ try {
360
+ const seams = options.hostContentSurfaceSeams ?? {};
361
+ return maybeFormatHostContentSurfaceLines(projectRoot, {
362
+ ...seams,
363
+ environ: seams.environ ?? options.env,
364
+ runtimeMode: seams.runtimeMode ?? runtimeMode ?? null,
365
+ });
366
+ }
367
+ catch {
368
+ // best-effort — session start must not abort on host-surface probe failures (#3162)
369
+ return {
370
+ report: {
371
+ contentClass: "unknown",
372
+ classSource: "probe-error",
373
+ signals: [],
374
+ managedSection: {
375
+ state: "unknown",
376
+ embeddedSha: null,
377
+ bodyHash: null,
378
+ path: `${projectRoot}/AGENTS.md`,
379
+ },
380
+ runtimeMode: null,
381
+ },
382
+ lines: [],
383
+ };
384
+ }
385
+ }
343
386
  function runReadOnlySessionStart(projectRoot, options, instant, environment) {
344
387
  const lines = [];
345
388
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
@@ -351,10 +394,13 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
351
394
  : safeDiagnostic;
352
395
  // #2275: report SCM availability even on read-only alignment (shallow; no network).
353
396
  const scm = resolveSessionScmReadiness(options, false);
397
+ // #3162: host content-surface class + managed drift (advisory).
398
+ const hostSurface = resolveHostContentSurface(projectRoot, options, scm.runtimeMode);
354
399
  lines.push(READ_ONLY_ALIGNMENT_MESSAGE);
355
400
  lines.push(userMdLine);
356
401
  lines.push(formatEnvironmentContext(environment));
357
402
  lines.push(...formatScmReadinessLines(scm));
403
+ lines.push(...hostSurface.lines);
358
404
  const resultPayload = {
359
405
  ready: true,
360
406
  exit_code: 0,
@@ -376,6 +422,7 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
376
422
  },
377
423
  environment: environmentContextToDict(environment),
378
424
  scm: scmReadinessToDict(scm),
425
+ host_content_surface: hostContentSurfaceToDict(hostSurface.report),
379
426
  message: READ_ONLY_RESULT_MESSAGE,
380
427
  };
381
428
  return { code: 0, payload: resultPayload, lines };
@@ -418,11 +465,14 @@ function runSessionRearm(projectRoot, options, instant, environment) {
418
465
  const alignmentMessage = `${READ_ONLY_ALIGNMENT_MESSAGE} ${userMdLine}`;
419
466
  // #2275: re-arm still reports SCM state (shallow; no network).
420
467
  const scm = resolveSessionScmReadiness(options, false);
468
+ // #3162: host content-surface class + managed drift (advisory).
469
+ const hostSurface = resolveHostContentSurface(projectRoot, options, scm.runtimeMode);
421
470
  const lines = [
422
471
  READ_ONLY_ALIGNMENT_MESSAGE,
423
472
  userMdLine,
424
473
  formatEnvironmentContext(environment),
425
474
  ...formatScmReadinessLines(scm),
475
+ ...hostSurface.lines,
426
476
  REARM_SKIPPED_FAT_PATH_MESSAGE,
427
477
  ];
428
478
  // Light branch-policy disclosure (local only) so re-arm still surfaces policy state.
@@ -446,6 +496,41 @@ function runSessionRearm(projectRoot, options, instant, environment) {
446
496
  }
447
497
  const priorQuick = eligibility.state.quickSteps;
448
498
  const priorTriage = priorQuick.triage_welcome ?? ritualStep({ ok: true, ts: instant });
499
+ // Greptile P1: legacy ritual-state without verify_tools must re-run tools —
500
+ // never invent ok:true. When prior exists, preserve without re-run (#2992).
501
+ let toolsStep;
502
+ if (priorQuick.verify_tools && typeof priorQuick.verify_tools === "object") {
503
+ const priorTools = priorQuick.verify_tools;
504
+ toolsStep = {
505
+ ...priorTools,
506
+ ts: ritualStep({ ok: priorTools.ok === true, ts: instant }).ts,
507
+ message: typeof priorTools.message === "string"
508
+ ? priorTools.message
509
+ : "verify:tools preserved on re-arm",
510
+ };
511
+ }
512
+ else {
513
+ const verifyToolsFn = options.verifyTools ??
514
+ ((output) => {
515
+ const toolLines = [];
516
+ const result = verifyRequiredTools({ outputFn: (line) => toolLines.push(line) });
517
+ for (const line of toolLines) {
518
+ output(line);
519
+ }
520
+ return { exitCode: result.exitCode };
521
+ });
522
+ const toolsOutcome = verifyToolsFn((line) => lines.push(line));
523
+ const toolsOk = toolsOutcome.exitCode === 0;
524
+ toolsStep = ritualStep({
525
+ ok: toolsOk,
526
+ ts: instant,
527
+ message: toolsOk
528
+ ? "verify:tools re-run on re-arm (legacy ritual lacked tools step)"
529
+ : `verify:tools failed on re-arm (exit ${toolsOutcome.exitCode})`,
530
+ exitCode: toolsOutcome.exitCode,
531
+ durationMs: 0,
532
+ });
533
+ }
449
534
  const policyOk = policyResult.error === null || policyResult.source === "default-fail-closed";
450
535
  const quickSteps = {
451
536
  alignment: ritualStep({
@@ -461,6 +546,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
461
546
  exitCode: policyOk ? 0 : 2,
462
547
  durationMs: 0,
463
548
  }),
549
+ verify_tools: toolsStep,
464
550
  // Preserve prior triage outcome; do not re-run welcome / self-heal on re-arm.
465
551
  triage_welcome: {
466
552
  ...priorTriage,
@@ -578,6 +664,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
578
664
  },
579
665
  environment: environmentContextToDict(environment),
580
666
  scm: scmReadinessToDict(scm),
667
+ host_content_surface: hostContentSurfaceToDict(hostSurface.report),
581
668
  message: code === 0 ? "session ritual re-armed" : "session ritual re-arm failed",
582
669
  },
583
670
  lines,
@@ -659,12 +746,26 @@ export function runSessionStart(projectRoot, options = {}) {
659
746
  const overallStarted = performance.now();
660
747
  const stepTimings = [];
661
748
  const allowOptionalNetwork = resolveSessionStartOptionalNetwork(options);
749
+ // #3214 / #3156: select ritual (ceremony) depth before building deferral maps.
750
+ // Rapid/minimal auto-defer informational cold steps only; mutation readiness
751
+ // (doctor, cache_fresh, agent_hooks, verify_tools) stays constant.
752
+ // Two-stage + provisional intake (#3214 design note / #1581 ordering): fill
753
+ // missing size/tier/shape from env/verb/files/deposit BEFORE resolve — never
754
+ // block on plan-item effort (post-planning only). Cold incomplete → rapid.
755
+ const { inputs: resolvedDialInputs, provisional: provisionalDial } = resolveSessionCeremonyDialInputs(projectRoot, options.ceremonyDialInputs, {
756
+ ...options.ceremonyDialHints,
757
+ env: options.env,
758
+ });
759
+ const ceremonyDialSelection = options.ceremonyDial ?? resolveCeremonyDial(projectRoot, { inputs: resolvedDialInputs });
760
+ const effectiveDeferrals = mergeCeremonyDialDeferrals(deferrals, ceremonyDialSelection);
761
+ const skipFatPath = ceremonyDialSelection.profile.skipFatPath;
662
762
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
663
763
  if (gitHeadValue === null) {
664
764
  const payload = {
665
765
  ready: false,
666
766
  exit_code: 2,
667
767
  ceremony_tier: COLD_CEREMONY_TIER,
768
+ ceremony_dial: ceremonyDialToDict(ceremonyDialSelection),
668
769
  environment: environmentContextToDict(environment),
669
770
  message: gitError ?? "could not resolve git HEAD",
670
771
  };
@@ -682,9 +783,13 @@ export function runSessionStart(projectRoot, options = {}) {
682
783
  lines: [formatEnvironmentContext(environment), payload.message],
683
784
  };
684
785
  }
685
- const quickSteps = recordDeferredSteps(QUICK_STEPS, deferrals, instant);
686
- const gatedSteps = recordDeferredSteps(GATED_STEPS, deferrals, instant);
786
+ const quickSteps = recordDeferredSteps(QUICK_STEPS, effectiveDeferrals, instant);
787
+ const gatedSteps = recordDeferredSteps(GATED_STEPS, effectiveDeferrals, instant);
687
788
  const lines = [];
789
+ lines.push(formatCeremonyDialStatusLine(ceremonyDialSelection));
790
+ if (provisionalDial.reasons.length > 0 && options.ceremonyDial === undefined) {
791
+ lines.push(`[deft ceremony-dial] provisional: ${provisionalDial.reasons.join("; ")}`);
792
+ }
688
793
  // Resolve USER.md via the shared first-hit-wins resolver so the alignment
689
794
  // step finds preferences automatically in mismatched / headless sandboxes
690
795
  // with zero manual DEFT_USER_PATH (#2271 / #2124). Never throws: an absent
@@ -724,6 +829,14 @@ export function runSessionStart(projectRoot, options = {}) {
724
829
  const scm = resolveSessionScmReadiness(options, allowOptionalNetwork);
725
830
  lines.push(...formatScmReadinessLines(scm));
726
831
  stepTimings.push({ name: "scm_readiness", duration_ms: elapsedMs(scmStepStarted) });
832
+ // #3162: host content-surface class + managed AGENTS drift (advisory; never blocks).
833
+ const hostSurfaceStepStarted = performance.now();
834
+ const hostSurface = resolveHostContentSurface(projectRoot, options, scm.runtimeMode);
835
+ lines.push(...hostSurface.lines);
836
+ stepTimings.push({
837
+ name: "host_content_surface",
838
+ duration_ms: elapsedMs(hostSurfaceStepStarted),
839
+ });
727
840
  if (!quickSteps.branch_policy) {
728
841
  const stepStarted = performance.now();
729
842
  const result = resolvePolicy(projectRoot);
@@ -753,6 +866,11 @@ export function runSessionStart(projectRoot, options = {}) {
753
866
  else {
754
867
  stepTimings.push({ name: "branch_policy", duration_ms: 0, skipped: true });
755
868
  }
869
+ // #3214 / #3156: verify_tools is mutation readiness — always run, even under
870
+ // rapid/minimal. Dial skipFatPath only lightens *ceremony* (triage welcome,
871
+ // optional network, staleness tickler), never readiness gates.
872
+ // Persist outcome into quick_steps so ritual-state records failure (not only
873
+ // process exit) — re-arm / later readers must not see a green cold start.
756
874
  {
757
875
  const stepStarted = performance.now();
758
876
  const verifyToolsFn = options.verifyTools ??
@@ -764,10 +882,28 @@ export function runSessionStart(projectRoot, options = {}) {
764
882
  }
765
883
  return { exitCode: result.exitCode };
766
884
  });
767
- verifyToolsFn((line) => lines.push(line));
768
- stepTimings.push({ name: "verify_tools", duration_ms: elapsedMs(stepStarted) });
885
+ const toolsOutcome = verifyToolsFn((line) => lines.push(line));
886
+ const durationMs = elapsedMs(stepStarted);
887
+ const toolsOk = toolsOutcome.exitCode === 0;
888
+ const toolsMessage = toolsOk
889
+ ? "verify:tools ok"
890
+ : `verify:tools failed (exit ${toolsOutcome.exitCode}); session not ready.`;
891
+ if (!toolsOk) {
892
+ lines.push(`[deft session] ${toolsMessage}`);
893
+ }
894
+ // Record on quick_steps (durable ritual-state) in addition to step timings.
895
+ quickSteps.verify_tools = ritualStep({
896
+ ok: toolsOk,
897
+ ts: instant,
898
+ message: toolsMessage,
899
+ exitCode: toolsOutcome.exitCode,
900
+ command: ["verify:tools"],
901
+ durationMs,
902
+ });
903
+ stepTimings.push({ name: "verify_tools", duration_ms: durationMs });
769
904
  }
770
- if (!quickSteps.triage_welcome) {
905
+ // #3214: rapid/minimal skip informational cold-path ceremony only.
906
+ if (!quickSteps.triage_welcome && !skipFatPath) {
771
907
  const stepStarted = performance.now();
772
908
  const captured = [];
773
909
  const triageCommand = ["triage_welcome.run_default_mode", "--project-root", projectRoot];
@@ -825,7 +961,8 @@ export function runSessionStart(projectRoot, options = {}) {
825
961
  stepTimings.push({ name: "triage_welcome", duration_ms: 0, skipped: true });
826
962
  }
827
963
  // #2991: npm release probe is optional network — off by default so ritual write is not blocked.
828
- if (allowOptionalNetwork) {
964
+ // #3214: also skipped under rapid/minimal dial (lifecycleWrites light/minimal).
965
+ if (allowOptionalNetwork && !skipFatPath) {
829
966
  const stepStarted = performance.now();
830
967
  try {
831
968
  const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
@@ -838,16 +975,20 @@ export function runSessionStart(projectRoot, options = {}) {
838
975
  }
839
976
  else {
840
977
  stepTimings.push({ name: "release_probe", duration_ms: 0, skipped: true });
841
- lines.push(OPTIONAL_NETWORK_SKIPPED_MESSAGE);
842
- }
843
- try {
844
- const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
845
- now: instant,
846
- });
847
- lines.push(...tickler.lines);
978
+ if (!skipFatPath) {
979
+ lines.push(OPTIONAL_NETWORK_SKIPPED_MESSAGE);
980
+ }
848
981
  }
849
- catch {
850
- // Staleness tickler is best-effort and must never block session start.
982
+ if (!skipFatPath) {
983
+ try {
984
+ const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
985
+ now: instant,
986
+ });
987
+ lines.push(...tickler.lines);
988
+ }
989
+ catch {
990
+ // Staleness tickler is best-effort and must never block session start.
991
+ }
851
992
  }
852
993
  if (!runningInsideDeftRepo(projectRoot) && shouldEmitMigrateNudge(projectRoot)) {
853
994
  lines.push(MIGRATE_COMPLETION_NUDGE);
@@ -886,14 +1027,27 @@ export function runSessionStart(projectRoot, options = {}) {
886
1027
  }
887
1028
  const writeStarted = performance.now();
888
1029
  const coldSessionId = (options.newSessionId ?? randomUUID)();
889
- const payload = newRitualStatePayload({
890
- sessionId: coldSessionId,
891
- gitHead: gitHeadValue,
892
- worktreePath: worktreePath(projectRoot, runGit),
893
- startedAt: instant,
894
- quickSteps,
895
- gatedSteps,
896
- });
1030
+ const dialDict = {
1031
+ ...ceremonyDialToDict(ceremonyDialSelection),
1032
+ provisional: {
1033
+ taskSize: provisionalDial.taskSize,
1034
+ modelTier: provisionalDial.modelTier,
1035
+ projectShape: provisionalDial.projectShape,
1036
+ reasons: [...provisionalDial.reasons],
1037
+ },
1038
+ };
1039
+ const payload = {
1040
+ ...newRitualStatePayload({
1041
+ sessionId: coldSessionId,
1042
+ gitHead: gitHeadValue,
1043
+ worktreePath: worktreePath(projectRoot, runGit),
1044
+ startedAt: instant,
1045
+ quickSteps,
1046
+ gatedSteps,
1047
+ }),
1048
+ // #3214: record dial choice on ritual-state for audit / later re-arm context.
1049
+ ceremony_dial: dialDict,
1050
+ };
897
1051
  let statePath;
898
1052
  try {
899
1053
  statePath = writeRitualState(projectRoot, payload);
@@ -915,6 +1069,7 @@ export function runSessionStart(projectRoot, options = {}) {
915
1069
  const failed = Object.entries(quickSteps)
916
1070
  .filter(([, step]) => !step.ok && !step.deferred_reason)
917
1071
  .map(([name]) => name);
1072
+ // verify_tools is recorded on quick_steps; failure makes ready=false.
918
1073
  const code = failed.length > 0 ? 1 : 0;
919
1074
  const totalMs = elapsedMs(overallStarted);
920
1075
  // #3117: bind live deposit generation when payload surfaces load (cold path).
@@ -947,13 +1102,14 @@ export function runSessionStart(projectRoot, options = {}) {
947
1102
  ready: code === 0,
948
1103
  exit_code: code,
949
1104
  ceremony_tier: COLD_CEREMONY_TIER,
1105
+ ceremony_dial: dialDict,
950
1106
  state_path: statePath,
951
1107
  ...(freshnessBind ? { freshness: freshnessBind } : {}),
952
1108
  quick_steps: quickSteps,
953
1109
  gated_steps: gatedSteps,
954
1110
  steps: stepTimings,
955
1111
  duration_ms: totalMs,
956
- optional_network: allowOptionalNetwork,
1112
+ optional_network: allowOptionalNetwork && !skipFatPath,
957
1113
  user_md: {
958
1114
  path: userMd.path,
959
1115
  rung: userMd.rung,
@@ -962,6 +1118,7 @@ export function runSessionStart(projectRoot, options = {}) {
962
1118
  },
963
1119
  environment: environmentContextToDict(environment),
964
1120
  scm: scmReadinessToDict(scm),
1121
+ host_content_surface: hostContentSurfaceToDict(hostSurface.report),
965
1122
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
966
1123
  };
967
1124
  // #2994: local process-cost event (best-effort; never blocks ceremony).
@@ -5,6 +5,8 @@ export * from "./finalize-cohort.js";
5
5
  export { finalizeCohortMain, parseFinalizeCohortArgv } from "./finalize-cohort-cli.js";
6
6
  export * from "./launch.js";
7
7
  export { launchMain, parseLaunchArgv } from "./launch-cli.js";
8
+ export * from "./pre-dispatch.js";
9
+ export { parsePreDispatchArgv, preDispatchMain } from "./pre-dispatch-cli.js";
8
10
  export * from "./readiness.js";
9
11
  export { readinessMain } from "./readiness-cli.js";
10
12
  export * from "./routing.js";
@@ -5,6 +5,8 @@ export * from "./finalize-cohort.js";
5
5
  export { finalizeCohortMain, parseFinalizeCohortArgv } from "./finalize-cohort-cli.js";
6
6
  export * from "./launch.js";
7
7
  export { launchMain, parseLaunchArgv } from "./launch-cli.js";
8
+ export * from "./pre-dispatch.js";
9
+ export { parsePreDispatchArgv, preDispatchMain } from "./pre-dispatch-cli.js";
8
10
  export * from "./readiness.js";
9
11
  export { readinessMain } from "./readiness-cli.js";
10
12
  export * from "./routing.js";
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { type CompleteStatus, type PreDispatchAction } from "./pre-dispatch.js";
3
+ export interface ParsedPreDispatchArgv {
4
+ projectRoot: string;
5
+ scopeId: string | null;
6
+ targetId: string | null;
7
+ workflowId: string | null;
8
+ action: PreDispatchAction | null;
9
+ sourceRevision: string | null;
10
+ attemptId: string | null;
11
+ status: CompleteStatus | null;
12
+ workerId: string | null;
13
+ externalRunId: string | null;
14
+ json: boolean;
15
+ help: boolean;
16
+ }
17
+ export declare function parsePreDispatchArgv(argv: string[]): ParsedPreDispatchArgv;
18
+ export declare function preDispatchMain(argv?: string[]): number;
19
+ //# sourceMappingURL=pre-dispatch-cli.d.ts.map
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for task swarm:pre-dispatch (#3228).
4
+ *
5
+ * Exit codes:
6
+ * 0 — allow (begin succeeded / complete-cancel succeeded)
7
+ * 1 — gate deny (e.g. DENY_DUPLICATE_ACTIVE) or complete with no active attempt
8
+ * 2 — config / usage error
9
+ */
10
+ import { resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { EXIT_CONFIG_ERROR } from "./constants.js";
13
+ import { COMPLETE_STATUSES, formatPreDispatchReport, PRE_DISPATCH_ACTIONS, swarmPreDispatch, } from "./pre-dispatch.js";
14
+ export function parsePreDispatchArgv(argv) {
15
+ const out = {
16
+ projectRoot: ".",
17
+ scopeId: null,
18
+ targetId: null,
19
+ workflowId: null,
20
+ action: null,
21
+ sourceRevision: null,
22
+ attemptId: null,
23
+ status: null,
24
+ workerId: null,
25
+ externalRunId: null,
26
+ json: false,
27
+ help: false,
28
+ };
29
+ for (let i = 0; i < argv.length; i += 1) {
30
+ const arg = argv[i];
31
+ if (arg === "--help" || arg === "-h") {
32
+ out.help = true;
33
+ }
34
+ else if (arg === "--json") {
35
+ out.json = true;
36
+ }
37
+ else if (arg === "--project-root" && argv[i + 1] !== undefined) {
38
+ out.projectRoot = argv[i + 1] ?? ".";
39
+ i += 1;
40
+ }
41
+ else if ((arg === "--scope-id" || arg === "--scope") && argv[i + 1] !== undefined) {
42
+ out.scopeId = argv[i + 1] ?? null;
43
+ i += 1;
44
+ }
45
+ else if ((arg === "--target-id" || arg === "--target") && argv[i + 1] !== undefined) {
46
+ out.targetId = argv[i + 1] ?? null;
47
+ i += 1;
48
+ }
49
+ else if ((arg === "--workflow-id" || arg === "--workflow") && argv[i + 1] !== undefined) {
50
+ out.workflowId = argv[i + 1] ?? null;
51
+ i += 1;
52
+ }
53
+ else if (arg === "--action" && argv[i + 1] !== undefined) {
54
+ out.action = (argv[i + 1] ?? null);
55
+ i += 1;
56
+ }
57
+ else if ((arg === "--source-revision" || arg === "--revision") && argv[i + 1] !== undefined) {
58
+ out.sourceRevision = argv[i + 1] ?? null;
59
+ i += 1;
60
+ }
61
+ else if (arg === "--attempt-id" && argv[i + 1] !== undefined) {
62
+ out.attemptId = argv[i + 1] ?? null;
63
+ i += 1;
64
+ }
65
+ else if (arg === "--status" && argv[i + 1] !== undefined) {
66
+ out.status = (argv[i + 1] ?? null);
67
+ i += 1;
68
+ }
69
+ else if (arg === "--worker-id" && argv[i + 1] !== undefined) {
70
+ out.workerId = argv[i + 1] ?? null;
71
+ i += 1;
72
+ }
73
+ else if (arg === "--external-run-id" && argv[i + 1] !== undefined) {
74
+ out.externalRunId = argv[i + 1] ?? null;
75
+ i += 1;
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ const USAGE = `Usage: task swarm:pre-dispatch -- --scope-id <id> --target-id <worktree|branch> [options]
81
+
82
+ Pre-dispatch gate for implement leaves (#3228 / #3143 DENY_DUPLICATE_ACTIVE).
83
+ Before spawning a peer implement leaf: run with default --action begin.
84
+ exit 0 allow (attempt begun)
85
+ exit 1 active deny / gate block (do not spawn)
86
+ exit 2 config / usage error
87
+
88
+ Takeover: --action cancel, then pre-dispatch begin again (not concurrent dual active).
89
+ Terminal: --action complete [--status succeeded|failed|cancelled|blocked]
90
+
91
+ Options:
92
+ --scope-id, --scope <id> Unit scope (story/issue or xBRIEF plan id)
93
+ --target-id, --target <id> Unit target (worktree path or branch)
94
+ --workflow-id, --workflow <id> Default: drive-to:merge-ready
95
+ --action begin|complete|cancel Default: begin
96
+ --source-revision <sha> Default: git rev-parse HEAD
97
+ --attempt-id <id> For complete/cancel
98
+ --status <status> For complete: ${COMPLETE_STATUSES.join("|")}
99
+ --worker-id <id> Optional worker stamp on begin
100
+ --external-run-id <id> Optional external run id
101
+ --project-root <path> Project root (ledger under .deft/delivery-attempts/)
102
+ --json Machine-readable result on stdout
103
+ -h, --help Show this help
104
+ `;
105
+ export function preDispatchMain(argv = process.argv.slice(2)) {
106
+ const parsed = parsePreDispatchArgv(argv);
107
+ if (parsed.help) {
108
+ process.stdout.write(USAGE);
109
+ return EXIT_CONFIG_ERROR;
110
+ }
111
+ if (parsed.action !== null &&
112
+ !PRE_DISPATCH_ACTIONS.includes(parsed.action)) {
113
+ process.stderr.write(`Error: --action must be one of: ${PRE_DISPATCH_ACTIONS.join(", ")}.\n`);
114
+ return EXIT_CONFIG_ERROR;
115
+ }
116
+ if (parsed.status !== null && !COMPLETE_STATUSES.includes(parsed.status)) {
117
+ process.stderr.write(`Error: --status must be one of: ${COMPLETE_STATUSES.join(", ")}.\n`);
118
+ return EXIT_CONFIG_ERROR;
119
+ }
120
+ const result = swarmPreDispatch({
121
+ projectRoot: resolve(parsed.projectRoot),
122
+ scopeId: parsed.scopeId ?? "",
123
+ targetId: parsed.targetId ?? "",
124
+ workflowId: parsed.workflowId ?? undefined,
125
+ action: parsed.action ?? undefined,
126
+ sourceRevision: parsed.sourceRevision ?? undefined,
127
+ attemptId: parsed.attemptId ?? undefined,
128
+ status: parsed.status ?? undefined,
129
+ workerId: parsed.workerId,
130
+ externalRunId: parsed.externalRunId,
131
+ });
132
+ if (parsed.json) {
133
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
134
+ }
135
+ else {
136
+ process.stdout.write(`${formatPreDispatchReport(result)}\n`);
137
+ }
138
+ return result.exitCode;
139
+ }
140
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
141
+ process.exit(preDispatchMain());
142
+ }
143
+ //# sourceMappingURL=pre-dispatch-cli.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Swarm implement-leaf pre-dispatch gate (#3228).
3
+ *
4
+ * Wires #3143 delivery-attempt DENY_DUPLICATE_ACTIVE onto the swarm re-dispatch
5
+ * path: before starting a peer implement leaf on a unit, if a non-terminal
6
+ * attempt already exists → exit non-zero and do not spawn.
7
+ *
8
+ * Takeover is two steps: cancel (complete status=cancelled) the prior attempt,
9
+ * then pre-dispatch begin again — never concurrent dual active.
10
+ */
11
+ import { type AttemptTrigger, type DeliveryAttemptRecord, type PreDispatchDecision } from "../delivery-attempt/index.js";
12
+ import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
13
+ /** Default workflow id for drive-to:merge-ready implement leaves. */
14
+ export declare const IMPLEMENT_LEAF_WORKFLOW_ID = "drive-to:merge-ready";
15
+ export declare const PRE_DISPATCH_ACTIONS: readonly ["begin", "complete", "cancel"];
16
+ export type PreDispatchAction = (typeof PRE_DISPATCH_ACTIONS)[number];
17
+ export declare const COMPLETE_STATUSES: readonly ["succeeded", "failed", "cancelled", "blocked"];
18
+ export type CompleteStatus = (typeof COMPLETE_STATUSES)[number];
19
+ export interface SwarmPreDispatchInput {
20
+ readonly projectRoot: string;
21
+ readonly scopeId: string;
22
+ readonly targetId: string;
23
+ readonly workflowId?: string;
24
+ readonly action?: PreDispatchAction;
25
+ readonly sourceRevision?: string;
26
+ readonly attemptId?: string;
27
+ readonly status?: CompleteStatus;
28
+ readonly workerId?: string | null;
29
+ readonly externalRunId?: string | null;
30
+ readonly trigger?: AttemptTrigger;
31
+ readonly now?: string;
32
+ }
33
+ export interface SwarmPreDispatchResult {
34
+ readonly exitCode: typeof EXIT_OK | typeof EXIT_GATE_FAILED | typeof EXIT_CONFIG_ERROR;
35
+ readonly decision: PreDispatchDecision | null;
36
+ readonly reason: string;
37
+ readonly action: PreDispatchAction;
38
+ readonly scopeId: string;
39
+ readonly targetId: string;
40
+ readonly workflowId: string;
41
+ readonly attempt: DeliveryAttemptRecord | null;
42
+ readonly activeAttemptIds: readonly string[];
43
+ }
44
+ export declare function resolveSourceRevision(projectRoot: string, explicit?: string): string;
45
+ /**
46
+ * True when targetId should be treated as a filesystem path (worktree), not an
47
+ * opaque branch/ref id. Branch names with `/` (e.g. `feat/foo`) stay opaque.
48
+ */
49
+ export declare function looksLikeFilesystemTarget(targetId: string): boolean;
50
+ /**
51
+ * Canonical unit target for ledger keys so relative/absolute/separator/case
52
+ * variants of the same worktree do not split gate state (#3228 Greptile P1).
53
+ *
54
+ * Always resolve under projectRoot to a stable absolute lexical key (even
55
+ * before the path exists). Do **not** realpath: following a symlink that is
56
+ * created between dispatches would change the key and split ledgers.
57
+ * Case-fold prevents case-insensitive FS splits. Existence never changes
58
+ * the ledger key.
59
+ */
60
+ export declare function normalizeTargetId(projectRoot: string, targetId: string): string;
61
+ /**
62
+ * Best-effort physical identity for alias peer checks. Ledger keys stay lexical
63
+ * (stable); when a path exists, realpath groups symlink aliases that point at
64
+ * the same worktree so sequential begins still DENY.
65
+ */
66
+ export declare function physicalTargetKey(projectRoot: string, targetId: string): string;
67
+ /** True when another unit (same scope+workflow, different target key) is active on same physical path. */
68
+ export declare function hasActiveAliasPeer(projectRoot: string, scopeId: string, targetId: string, workflowId: string): boolean;
69
+ /**
70
+ * Parse beginAttemptOnDisk-style error messages without a ReDoS-prone regex
71
+ * (CodeQL: polynomial regex on uncontrolled data).
72
+ */
73
+ export declare function parseDecisionFromError(message: string): {
74
+ decision: PreDispatchDecision | null;
75
+ reason: string;
76
+ };
77
+ /**
78
+ * Swarm pre-dispatch gate for implement leaves.
79
+ *
80
+ * - begin (default): evaluate #3143 gate; on allow, beginAttempt; exit 0 / 1 / 2
81
+ * - complete: terminal success/fail/blocked
82
+ * - cancel: terminal cancel (takeover step 1)
83
+ */
84
+ export declare function swarmPreDispatch(input: SwarmPreDispatchInput): SwarmPreDispatchResult;
85
+ /** Human-readable one-line report for CLI stdout. */
86
+ export declare function formatPreDispatchReport(result: SwarmPreDispatchResult): string;
87
+ //# sourceMappingURL=pre-dispatch.d.ts.map