@deftai/directive-core 0.93.0 → 0.95.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 (118) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/fetch.d.ts +29 -0
  3. package/dist/cache/fetch.js +131 -4
  4. package/dist/cache/operations.d.ts +8 -0
  5. package/dist/cache/operations.js +67 -4
  6. package/dist/check/cached-orchestrator.js +11 -0
  7. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  8. package/dist/check/consumer-gate-integrity.js +265 -0
  9. package/dist/check/index.d.ts +1 -0
  10. package/dist/check/index.js +1 -0
  11. package/dist/check/orchestrator.js +10 -0
  12. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  13. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  14. package/dist/doctor/main.js +98 -9
  15. package/dist/doctor/types.d.ts +2 -2
  16. package/dist/finish-loop/pr-finish-loop.js +1 -0
  17. package/dist/freshness/bind.d.ts +67 -0
  18. package/dist/freshness/bind.js +201 -0
  19. package/dist/freshness/cli.d.ts +30 -0
  20. package/dist/freshness/cli.js +180 -0
  21. package/dist/freshness/compare.d.ts +14 -0
  22. package/dist/freshness/compare.js +134 -0
  23. package/dist/freshness/generation.d.ts +44 -0
  24. package/dist/freshness/generation.js +172 -0
  25. package/dist/freshness/index.d.ts +13 -0
  26. package/dist/freshness/index.js +13 -0
  27. package/dist/freshness/report.d.ts +43 -0
  28. package/dist/freshness/report.js +139 -0
  29. package/dist/freshness/types.d.ts +70 -0
  30. package/dist/freshness/types.js +30 -0
  31. package/dist/handoff-evidence/index.d.ts +8 -0
  32. package/dist/handoff-evidence/index.js +7 -0
  33. package/dist/handoff-evidence/validate.d.ts +105 -0
  34. package/dist/handoff-evidence/validate.js +423 -0
  35. package/dist/hooks/dispatcher.d.ts +27 -2
  36. package/dist/hooks/dispatcher.js +134 -14
  37. package/dist/hooks/readonly.d.ts +14 -0
  38. package/dist/hooks/readonly.js +126 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +2 -0
  41. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  42. package/dist/init-deposit/agent-hooks.js +5 -2
  43. package/dist/init-deposit/init-deposit.d.ts +7 -1
  44. package/dist/init-deposit/init-deposit.js +24 -4
  45. package/dist/init-deposit/refresh.d.ts +9 -1
  46. package/dist/init-deposit/refresh.js +48 -5
  47. package/dist/intake/issue-ingest.d.ts +20 -0
  48. package/dist/intake/issue-ingest.js +58 -0
  49. package/dist/policy/index.d.ts +4 -0
  50. package/dist/policy/index.js +50 -1
  51. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  52. package/dist/policy/min-greptile-confidence.js +123 -0
  53. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  54. package/dist/pr-merge-readiness/compute.js +17 -4
  55. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  56. package/dist/pr-merge-readiness/evaluate.js +7 -4
  57. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  58. package/dist/pr-merge-readiness/mergeability.js +14 -2
  59. package/dist/pr-monitor/main.d.ts +2 -0
  60. package/dist/pr-monitor/main.js +31 -1
  61. package/dist/pr-monitor/monitor.js +5 -1
  62. package/dist/pr-monitor/readiness.js +3 -1
  63. package/dist/pr-monitor/types.d.ts +8 -0
  64. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  65. package/dist/pr-wait-mergeable/cascade.js +28 -1
  66. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  67. package/dist/pr-wait-mergeable/main.js +16 -0
  68. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  69. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  70. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  71. package/dist/pr-watch/constants.d.ts +4 -1
  72. package/dist/pr-watch/constants.js +5 -2
  73. package/dist/pr-watch/main.js +1 -0
  74. package/dist/pr-watch/probe.d.ts +1 -1
  75. package/dist/pr-watch/probe.js +4 -1
  76. package/dist/pr-watch/types.d.ts +7 -1
  77. package/dist/pr-watch/watch.js +3 -2
  78. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  79. package/dist/review-monitor/index.d.ts +1 -0
  80. package/dist/review-monitor/index.js +1 -0
  81. package/dist/review-monitor/l4-owner.d.ts +46 -0
  82. package/dist/review-monitor/l4-owner.js +262 -0
  83. package/dist/session/ritual-entrypoint.d.ts +6 -2
  84. package/dist/session/ritual-entrypoint.js +15 -2
  85. package/dist/session/session-ready.js +31 -14
  86. package/dist/session/session-start.d.ts +2 -1
  87. package/dist/session/session-start.js +66 -3
  88. package/dist/session/verify-session-ritual.d.ts +4 -2
  89. package/dist/session/verify-session-ritual.js +11 -11
  90. package/dist/slash/product-set.js +4 -1
  91. package/dist/swarm/verify-review-clean.d.ts +4 -3
  92. package/dist/swarm/verify-review-clean.js +28 -73
  93. package/dist/tool-events/classify.js +1 -0
  94. package/dist/triage/classify/index.d.ts +9 -0
  95. package/dist/triage/classify/index.js +34 -2
  96. package/dist/triage/classify/label-mirror.d.ts +143 -0
  97. package/dist/triage/classify/label-mirror.js +684 -0
  98. package/dist/triage/help/registry-data.d.ts +7 -7
  99. package/dist/triage/help/registry-data.js +27 -7
  100. package/dist/triage/queue/show.d.ts +1 -1
  101. package/dist/triage/queue/show.js +4 -2
  102. package/dist/triage/welcome/writers.js +16 -7
  103. package/dist/umbrella-current-shape/index.d.ts +45 -0
  104. package/dist/umbrella-current-shape/index.js +162 -8
  105. package/dist/vbrief-reconcile/index.d.ts +2 -2
  106. package/dist/vbrief-reconcile/index.js +1 -1
  107. package/dist/vbrief-reconcile/types.d.ts +14 -0
  108. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  109. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  110. package/dist/vbrief-validate/project-definition.js +1 -1
  111. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  112. package/dist/verify-env/agent-hook-readiness.js +150 -0
  113. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  114. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  115. package/dist/verify-env/agent-hooks.js +11 -3
  116. package/dist/verify-env/index.d.ts +1 -0
  117. package/dist/verify-env/index.js +1 -0
  118. package/package.json +7 -3
@@ -1,5 +1,6 @@
1
1
  import { cmdDoctor } from "../doctor/main.js";
2
2
  import { evaluate } from "../preflight-cache/evaluate.js";
3
+ import { evaluateAgentHookReadinessSafely, } from "../verify-env/agent-hook-readiness.js";
3
4
  export const ENTRYPOINT_TIMEOUT_SECONDS = 300;
4
5
  export const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
5
6
  /** Run an in-process ritual entrypoint with stdout/stderr capture. */
@@ -66,9 +67,21 @@ export function runCacheFreshMain(argv) {
66
67
  }
67
68
  return result.code;
68
69
  }
69
- /** Default gated-tier runner: in-process doctor + cache-fresh (mirrors verify_session_ritual.py). */
70
- export function defaultRitualRunner(command, projectRoot) {
70
+ /** Default gated-tier runner: in-process hook readiness, doctor, and cache freshness. */
71
+ export function defaultRitualRunner(command, projectRoot, seams = {}) {
71
72
  const [verb, ...rest] = command;
73
+ if (verb === "verify:hooks-installed") {
74
+ const evaluateReadiness = seams.evaluateAgentHookReadiness
75
+ ? (root) => evaluateAgentHookReadinessSafely(root, seams.evaluateAgentHookReadiness)
76
+ : evaluateAgentHookReadinessSafely;
77
+ const result = evaluateReadiness(projectRoot);
78
+ const output = `${result.message}\n`;
79
+ return {
80
+ code: result.code,
81
+ stdout: result.stream === "stdout" ? output : "",
82
+ stderr: result.stream === "stderr" ? output : "",
83
+ };
84
+ }
72
85
  if (verb === "doctor") {
73
86
  return callMain((argv) => cmdDoctor(argv), ["--project-root", projectRoot, ...rest]);
74
87
  }
@@ -90,12 +90,20 @@ export function runSessionReady(projectRoot, options = {}) {
90
90
  const env = options.env ?? process.env;
91
91
  const lines = [];
92
92
  const steps = [];
93
- const inspect = options.inspectRitual ?? ((root, opts) => inspectSessionRitual(root, opts));
94
- const verify = options.verifyRitual ?? ((root, opts) => verifySessionRitual(root, opts));
95
- const start = options.runStart ?? ((root, opts) => runSessionStart(root, opts));
93
+ const inspect = options.inspectRitual ?? inspectSessionRitual;
94
+ const verify = options.verifyRitual ?? verifySessionRitual;
95
+ const start = options.runStart ?? runSessionStart;
96
96
  const fetchAll = options.fetchAll ?? cacheFetchAll;
97
97
  const inferRepo = options.inferRepo ?? ((root) => inferSessionReadyRepo(root, env));
98
- // --- Fast path: already green for PreToolUse gated inspect ---
98
+ const verifyOpts = {
99
+ tier: "gated",
100
+ posture: "mutation",
101
+ now,
102
+ runGit: options.runGit,
103
+ runner: options.runner,
104
+ forceGatedSteps: ["agent_hooks"],
105
+ };
106
+ // --- Fast path: inspect is green, but refresh functional hook readiness before returning. ---
99
107
  const gatedInspect = inspect(projectRoot, {
100
108
  tier: "gated",
101
109
  posture: "mutation",
@@ -103,6 +111,22 @@ export function runSessionReady(projectRoot, options = {}) {
103
111
  runGit: options.runGit,
104
112
  });
105
113
  if (gatedInspect.code === 0) {
114
+ steps.push("verify:session-ritual:gated");
115
+ const refreshed = verify(projectRoot, verifyOpts);
116
+ if (!isGatedVerifyActuallyReady(refreshed)) {
117
+ const message = refreshed.code === 0 && refreshed.bypassed
118
+ ? bypassedReadyFailure(refreshed)
119
+ : `${refreshed.message}\n Remaining blocker after session:ready. Fix the step above, then re-run \`${readyCommand()}\`.`;
120
+ lines.push(message);
121
+ return {
122
+ code: refreshed.code === 0 ? 1 : refreshed.code,
123
+ message,
124
+ path: SESSION_READY_FAILED,
125
+ lines,
126
+ steps,
127
+ duration_ms: elapsedMs(started),
128
+ };
129
+ }
106
130
  const message = "OK session ready (gated ritual already fresh).";
107
131
  lines.push(message);
108
132
  return {
@@ -138,7 +162,7 @@ export function runSessionReady(projectRoot, options = {}) {
138
162
  `session:start failed (exit ${startResult.code}). Recovery: run \`${readyCommand()}\` again after fixing the blocker.`;
139
163
  lines.push(message);
140
164
  return {
141
- code: startResult.code === 0 ? 1 : startResult.code,
165
+ code: startResult.code,
142
166
  message,
143
167
  path: SESSION_READY_FAILED,
144
168
  lines,
@@ -149,13 +173,6 @@ export function runSessionReady(projectRoot, options = {}) {
149
173
  }
150
174
  // --- Gated verify (lazy doctor + cache_fresh) ---
151
175
  steps.push("verify:session-ritual:gated");
152
- const verifyOpts = {
153
- tier: "gated",
154
- posture: "mutation",
155
- now,
156
- runGit: options.runGit,
157
- runner: options.runner,
158
- };
159
176
  let verifyResult = verify(projectRoot, verifyOpts);
160
177
  if (isGatedVerifyActuallyReady(verifyResult)) {
161
178
  const message = "OK session ready (gated ritual verified).";
@@ -222,7 +239,7 @@ export function runSessionReady(projectRoot, options = {}) {
222
239
  };
223
240
  }
224
241
  steps.push("verify:session-ritual:gated:retry");
225
- verifyResult = verify(projectRoot, verifyOpts);
242
+ verifyResult = verify(projectRoot, { ...verifyOpts, forceGatedSteps: [] });
226
243
  if (isGatedVerifyActuallyReady(verifyResult)) {
227
244
  const message = "OK session ready (recovered via cache refresh).";
228
245
  lines.push(message);
@@ -253,7 +270,7 @@ export function runSessionReady(projectRoot, options = {}) {
253
270
  ` Remaining blocker after session:ready. Fix the step above, then re-run \`${readyCommand()}\`.`;
254
271
  lines.push(message);
255
272
  return {
256
- code: verifyResult.code === 0 ? 1 : verifyResult.code,
273
+ code: verifyResult.code,
257
274
  message,
258
275
  path: SESSION_READY_FAILED,
259
276
  lines,
@@ -16,7 +16,8 @@ export type SessionCeremonyTier = (typeof SESSION_CEREMONY_TIERS)[number];
16
16
  export declare const COLD_CEREMONY_TIER: SessionCeremonyTier;
17
17
  export declare const REARM_CEREMONY_TIER: SessionCeremonyTier;
18
18
  export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome"];
19
- export declare const GATED_STEPS: readonly ["doctor", "cache_fresh"];
19
+ export declare const GATED_STEPS: readonly ["agent_hooks", "doctor", "cache_fresh"];
20
+ export type GatedStepName = (typeof GATED_STEPS)[number];
20
21
  /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
21
22
  export declare const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
22
23
  /** Human-readable skip notice when optional network is off the hot path (#2991). */
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { runningInsideDeftRepo } from "../doctor/paths.js";
3
3
  import { emitSessionEvalReadback } from "../eval/readback.js";
4
+ import { bindSessionGeneration } from "../freshness/bind.js";
5
+ import { readLiveGeneration } from "../freshness/generation.js";
4
6
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
7
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
8
  import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
@@ -20,6 +22,7 @@ import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js
20
22
  import { emitSessionStartProcessCost } from "./process-cost.js";
21
23
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
22
24
  import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
25
+ import { timestampIso } from "./time.js";
23
26
  export const SESSION_POSTURES = ["read-only", "mutation"];
24
27
  export const READ_ONLY_POSTURE = "read-only";
25
28
  export const MUTATION_POSTURE = "mutation";
@@ -30,7 +33,7 @@ export const SESSION_CEREMONY_TIERS = ["cold", "rearm"];
30
33
  export const COLD_CEREMONY_TIER = "cold";
31
34
  export const REARM_CEREMONY_TIER = "rearm";
32
35
  export const QUICK_STEPS = ["alignment", "branch_policy", "triage_welcome"];
33
- export const GATED_STEPS = ["doctor", "cache_fresh"];
36
+ export const GATED_STEPS = ["agent_hooks", "doctor", "cache_fresh"];
34
37
  /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
35
38
  export const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
36
39
  /** Human-readable skip notice when optional network is off the hot path (#2991). */
@@ -148,6 +151,10 @@ export function parseDeferrals(rawValues) {
148
151
  const name = raw.slice(0, eq);
149
152
  const reason = raw.slice(eq + 1);
150
153
  const stepName = normaliseStepName(name.trim());
154
+ if (stepName === "agent_hooks") {
155
+ errors.push('ritual step "agent_hooks" is not deferrable');
156
+ continue;
157
+ }
151
158
  if (!allowed.has(stepName)) {
152
159
  errors.push(`unknown ritual step ${JSON.stringify(name)}; expected one of ${JSON.stringify([...allowed].sort())}`);
153
160
  continue;
@@ -454,10 +461,11 @@ function runSessionRearm(projectRoot, options, instant, environment) {
454
461
  };
455
462
  const gatedSteps = restampSteps(eligibility.state.gatedSteps, instant);
456
463
  const writeStarted = performance.now();
464
+ const rearmSessionId = (options.newSessionId ?? randomUUID)();
457
465
  // Fresh payload (no rearm_needed / compact_resume_at) clears compact markers (#2992).
458
466
  const writePayload = {
459
467
  ...newRitualStatePayload({
460
- sessionId: (options.newSessionId ?? randomUUID)(),
468
+ sessionId: rearmSessionId,
461
469
  gitHead: eligibility.currentHead,
462
470
  worktreePath: eligibility.currentWorktree,
463
471
  startedAt: instant,
@@ -502,6 +510,32 @@ function runSessionRearm(projectRoot, options, instant, environment) {
502
510
  .filter(([, step]) => !step.ok && !step.deferred_reason)
503
511
  .map(([name]) => name);
504
512
  const code = failed.length > 0 ? 1 : 0;
513
+ // #3117: bind live deposit generation into session context on re-arm (host-agnostic).
514
+ let freshnessBind = null;
515
+ try {
516
+ const bound = bindSessionGeneration(projectRoot, {
517
+ sessionId: rearmSessionId,
518
+ nowIso: timestampIso(instant),
519
+ payloadLoaded: true,
520
+ });
521
+ freshnessBind = {
522
+ bound_generation: bound.bound.boundGeneration,
523
+ live_generation: bound.live.generation,
524
+ content_version: bound.live.contentVersion,
525
+ path: bound.path,
526
+ };
527
+ lines.push(`[deft freshness] bound generation ${bound.bound.boundGeneration} ` +
528
+ `(live deposit v${bound.live.contentVersion})`);
529
+ lines.push(`[deft freshness] set DEFT_SESSION_ID=${rearmSessionId} for bare freshness:report/bind ` +
530
+ `(or pass --session-id; required for multi-agent trusted readiness)`);
531
+ }
532
+ catch {
533
+ const live = readLiveGeneration(projectRoot);
534
+ if (live !== null) {
535
+ lines.push(`[deft freshness] live generation ${live.generation} present; session bind deferred ` +
536
+ `(run \`deft freshness:bind\` after loading payload surfaces)`);
537
+ }
538
+ }
505
539
  // #2994: local process-cost event (best-effort; never blocks ceremony).
506
540
  emitSessionStartProcessCost({
507
541
  ceremonyTier: REARM_CEREMONY_TIER,
@@ -519,6 +553,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
519
553
  ceremony_tier: REARM_CEREMONY_TIER,
520
554
  rearm_eligible: true,
521
555
  state_path: statePath,
556
+ ...(freshnessBind ? { freshness: freshnessBind } : {}),
522
557
  quick_steps: quickSteps,
523
558
  gated_steps: gatedSteps,
524
559
  steps: stepTimings,
@@ -829,8 +864,9 @@ export function runSessionStart(projectRoot, options = {}) {
829
864
  lines.push(consentPrompt.trimEnd());
830
865
  }
831
866
  const writeStarted = performance.now();
867
+ const coldSessionId = (options.newSessionId ?? randomUUID)();
832
868
  const payload = newRitualStatePayload({
833
- sessionId: (options.newSessionId ?? randomUUID)(),
869
+ sessionId: coldSessionId,
834
870
  gitHead: gitHeadValue,
835
871
  worktreePath: worktreePath(projectRoot, runGit),
836
872
  startedAt: instant,
@@ -860,11 +896,38 @@ export function runSessionStart(projectRoot, options = {}) {
860
896
  .map(([name]) => name);
861
897
  const code = failed.length > 0 ? 1 : 0;
862
898
  const totalMs = elapsedMs(overallStarted);
899
+ // #3117: bind live deposit generation when payload surfaces load (cold path).
900
+ let freshnessBind = null;
901
+ try {
902
+ const bound = bindSessionGeneration(projectRoot, {
903
+ sessionId: coldSessionId,
904
+ nowIso: timestampIso(instant),
905
+ payloadLoaded: true,
906
+ });
907
+ freshnessBind = {
908
+ bound_generation: bound.bound.boundGeneration,
909
+ live_generation: bound.live.generation,
910
+ content_version: bound.live.contentVersion,
911
+ path: bound.path,
912
+ };
913
+ lines.push(`[deft freshness] bound generation ${bound.bound.boundGeneration} ` +
914
+ `(live deposit v${bound.live.contentVersion})`);
915
+ lines.push(`[deft freshness] set DEFT_SESSION_ID=${coldSessionId} for bare freshness:report/bind ` +
916
+ `(or pass --session-id; required for multi-agent trusted readiness)`);
917
+ }
918
+ catch {
919
+ const live = readLiveGeneration(projectRoot);
920
+ if (live !== null) {
921
+ lines.push(`[deft freshness] live generation ${live.generation} present; session bind deferred ` +
922
+ `(run \`deft freshness:bind\` after loading payload surfaces)`);
923
+ }
924
+ }
863
925
  const resultPayload = {
864
926
  ready: code === 0,
865
927
  exit_code: code,
866
928
  ceremony_tier: COLD_CEREMONY_TIER,
867
929
  state_path: statePath,
930
+ ...(freshnessBind ? { freshness: freshnessBind } : {}),
868
931
  quick_steps: quickSteps,
869
932
  gated_steps: gatedSteps,
870
933
  steps: stepTimings,
@@ -1,10 +1,10 @@
1
1
  import { type GitRunner } from "./git.js";
2
2
  import { type DirectivePosture } from "./posture.js";
3
- import { type SessionCeremonyTier } from "./session-start.js";
3
+ import { type GatedStepName, type SessionCeremonyTier } from "./session-start.js";
4
4
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
5
5
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
6
6
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
7
- export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<string, readonly string[]>>;
7
+ export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<GatedStepName, readonly string[]>>;
8
8
  export interface VerifyResult {
9
9
  readonly code: number;
10
10
  readonly message: string;
@@ -37,6 +37,8 @@ export interface VerifySessionRitualOptions {
37
37
  readonly posture?: DirectivePosture;
38
38
  readonly envPosture?: string | undefined;
39
39
  readonly handoffText?: string | null;
40
+ /** Re-run selected gated prerequisites even when their recorded step is green. */
41
+ readonly forceGatedSteps?: readonly GatedStepName[];
40
42
  }
41
43
  export interface InspectSessionRitualOptions {
42
44
  readonly tier?: "quick" | "gated";
@@ -11,6 +11,7 @@ export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from
11
11
  export const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
12
12
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
13
13
  export const GATED_ENTRYPOINT_COMMANDS = {
14
+ agent_hooks: ["verify:hooks-installed", "--scope=agent", "--live"],
14
15
  doctor: ["doctor"],
15
16
  cache_fresh: ["verify:cache-fresh"],
16
17
  };
@@ -26,16 +27,11 @@ function stepPasses(step) {
26
27
  }
27
28
  function failedStepMessage(tierName, stepName, step) {
28
29
  const coldCmd = formatSessionStartRecoveryCommand("cold");
29
- if (step === null || step === undefined) {
30
+ if (step === undefined) {
30
31
  return (`session ritual ${tierName} step '${stepName}' is missing. ` +
31
32
  `Run \`${coldCmd}\` before implementation dispatch.`);
32
33
  }
33
- if (typeof step === "object" &&
34
- step !== null &&
35
- step.deferred_reason) {
36
- return "";
37
- }
38
- const message = typeof step === "object" && step !== null ? step.message : null;
34
+ const message = step.message;
39
35
  const suffix = typeof message === "string" && message.length > 0 ? `: ${message}` : "";
40
36
  return `session ritual ${tierName} step '${stepName}' failed${suffix}`;
41
37
  }
@@ -53,9 +49,9 @@ export function formatRitualRecoveryInstruction(tier = "cold") {
53
49
  `(one-shot: session:start + gated ritual + cache recovery as needed).`);
54
50
  }
55
51
  function runGatedStep(projectRoot, payload, stepName, runner, now) {
56
- const command = [...(GATED_ENTRYPOINT_COMMANDS[stepName] ?? [])];
52
+ const command = [...GATED_ENTRYPOINT_COMMANDS[stepName]];
57
53
  const { code, stdout, stderr } = runner(command, projectRoot);
58
- const message = stdout.trim() || stderr.trim() || `${command[0] ?? stepName} exited ${code}`;
54
+ const message = stdout.trim() || stderr.trim() || `${command[0]} exited ${code}`;
59
55
  const gated = payload.gated_steps ?? {};
60
56
  gated[stepName] = ritualStep({
61
57
  ok: code === 0,
@@ -354,11 +350,15 @@ export function verifySessionRitual(projectRoot, options = {}) {
354
350
  const gated = { ...payload.gated_steps };
355
351
  payload.gated_steps = gated;
356
352
  const runCmd = options.runner ?? defaultRitualRunner;
353
+ const forced = new Set(options.forceGatedSteps ?? []);
357
354
  for (const stepName of GATED_STEPS) {
358
355
  const step = gated[stepName];
359
- if (step?.deferred_reason)
356
+ if (step?.deferred_reason && stepName !== "agent_hooks")
360
357
  continue;
361
- if (stepPasses(step))
358
+ // Hook readiness is a live mutation prerequisite, not a cacheable doctor
359
+ // result. Re-run it at every gated boundary so registration/shim drift
360
+ // cannot remain hidden behind an earlier green ritual record.
361
+ if (stepName !== "agent_hooks" && stepPasses(step) && !forced.has(stepName))
362
362
  continue;
363
363
  const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant);
364
364
  if (writeError !== null) {
@@ -109,9 +109,12 @@ export const PRODUCT_COMMANDS = Object.freeze([
109
109
  {
110
110
  logicalId: "/deft:checkpoint",
111
111
  filenameStem: "deft-checkpoint",
112
+ // description documents the save path; wrappers load the strategy doc (same as continue).
113
+ // ⊗ Point dispatchPath at xbrief/continue.xbrief.json — that file is the checkpoint *output*,
114
+ // not the instruction doc; first invoke fails when it does not exist yet (#3105).
112
115
  description: "Save session state to xbrief/continue.xbrief.json",
113
116
  dispatchKind: "session",
114
- dispatchPath: "xbrief/continue.xbrief.json",
117
+ dispatchPath: "resilience/continue-here.md",
115
118
  },
116
119
  ]);
117
120
  /**
@@ -1,7 +1,6 @@
1
- import type { CiGateOptions } from "../pr-merge-readiness/ci-gate.js";
2
- import type { SlizardGateOptions } from "../pr-merge-readiness/slizard-gate.js";
1
+ import { type ComputeGateOptions } from "../pr-merge-readiness/compute.js";
3
2
  import type { RunGhFn } from "../pr-merge-readiness/types.js";
4
- type ReviewGateOptions = CiGateOptions & SlizardGateOptions;
3
+ type ReviewGateOptions = ComputeGateOptions;
5
4
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
6
5
  export interface CohortResolutionError {
7
6
  vbrief_path: string;
@@ -15,6 +14,8 @@ export interface CohortPRResult {
15
14
  clean: boolean;
16
15
  ci_summary: Record<string, unknown> | null;
17
16
  slizard_summary: Record<string, unknown> | null;
17
+ via?: string;
18
+ partial_data?: Record<string, unknown>;
18
19
  }
19
20
  export interface CohortResult {
20
21
  repo: string | null;
@@ -1,10 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { hasArtifactSuffix } from "../layout/resolve.js";
4
- import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
5
- import { defaultRunGh, fetchCheckRunsRest, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
6
- import { evaluateGates, parseGreptileBody } from "../pr-merge-readiness/index.js";
7
- import { evaluateSlizardGate, isSlizardCheck } from "../pr-merge-readiness/slizard-gate.js";
4
+ import { computeGateResult, isMergeReady, } from "../pr-merge-readiness/compute.js";
5
+ import { VIA_ERROR, VIA_FALLBACK2 } from "../pr-merge-readiness/constants.js";
6
+ import { defaultRunGh } from "../pr-merge-readiness/gh.js";
8
7
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
9
8
  function extractPrFromUri(uri) {
10
9
  const pullIdx = uri.indexOf("/pull/");
@@ -104,83 +103,29 @@ export function resolveCohortFromVbriefs(vbriefGlobs) {
104
103
  return { prNumbers: seenPrs, failures };
105
104
  }
106
105
  export function evaluatePr(prNumber, repo, runGh = defaultRunGh, options = {}) {
107
- const headSha = fetchPrHeadSha(prNumber, repo, runGh);
108
- if (headSha === null) {
106
+ const result = computeGateResult(prNumber, repo, runGh, options);
107
+ if (result.via === VIA_ERROR) {
109
108
  return null;
110
109
  }
111
- const body = fetchGreptileCommentBody(prNumber, repo, runGh);
112
- if (body === null) {
113
- return null;
114
- }
115
- const verdict = parseGreptileBody(body);
116
- const failures = evaluateGates(prNumber, headSha, verdict);
117
- let ciSummary = null;
118
- let slizardSummary = null;
119
- if (failures.length === 0) {
120
- if (repo === null) {
121
- failures.push("Could not resolve repo for required CI check-run gate. " +
122
- "Use --repo OWNER/REPO or run from a checked-out repository.");
123
- ciSummary = {
124
- ready_state: "blocked",
125
- error: "repo unresolved for check-runs lookup",
126
- };
127
- slizardSummary = { ready_state: "skipped", present: false };
128
- }
129
- else if (options.skipCi === true) {
130
- const ci = evaluateCiGate([], { skipCi: true });
131
- ciSummary = {
132
- ...ci.summary,
133
- summary_line: "CI check-runs: skipped (--skip-ci)",
134
- };
135
- const slizard = evaluateSlizardGate([], options);
136
- failures.push(...slizard.failures);
137
- slizardSummary = { ...slizard.summary };
138
- }
139
- else {
140
- const checks = fetchCheckRunsRest(headSha, repo, runGh);
141
- if (checks.summary === null) {
142
- failures.push("Required CI check-runs could not be fetched; fail closed by default (#2169). " +
143
- `Root cause: ${checks.error}`);
144
- ciSummary = {
145
- ready_state: "blocked",
146
- error: checks.error,
147
- checked_count: 0,
148
- ignored_checks: [...(options.ignoreCheckNames ?? [])],
149
- failed_required: [],
150
- pending_required: [],
151
- conclusions: [],
152
- };
153
- slizardSummary = { ready_state: "skipped", present: false };
154
- }
155
- else {
156
- const slizard = evaluateSlizardGate(checks.checkRuns, options);
157
- // SLizard is scored by the structured gate; exclude it from the generic CI set.
158
- const slizardNames = checks.checkRuns
159
- .filter((r) => isSlizardCheck(r.name))
160
- .map((r) => r.name);
161
- const ci = evaluateCiGate(checks.checkRuns, {
162
- skipCi: options.skipCi,
163
- ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
164
- });
165
- failures.push(...ci.failures, ...slizard.failures);
166
- ciSummary = {
167
- ...ci.summary,
168
- summary_line: buildCiSummaryLine(ci.summary),
169
- };
170
- slizardSummary = { ...slizard.summary };
171
- }
172
- }
173
- }
110
+ const ciSummary = asRecord(result.partialData.ci);
111
+ const slizardSummary = asRecord(result.partialData.slizard);
174
112
  return {
175
113
  pr_number: prNumber,
176
- head_sha: headSha,
177
- verdict: { ...verdict },
178
- failures: [...failures],
179
- clean: failures.length === 0,
114
+ head_sha: result.headSha,
115
+ verdict: { ...result.verdict },
116
+ failures: [...result.failures],
117
+ clean: result.via !== VIA_FALLBACK2 && isMergeReady(result.failures),
180
118
  ci_summary: ciSummary,
181
119
  slizard_summary: slizardSummary,
120
+ via: result.via,
121
+ partial_data: { ...result.partialData },
182
122
  };
183
123
  }
124
+ function asRecord(value) {
125
+ return value !== null && typeof value === "object" && !Array.isArray(value)
126
+ ? { ...value }
127
+ : null;
128
+ }
184
129
  export function cohortResultToDict(cohort) {
185
130
  return {
186
131
  repo: cohort.repo,
@@ -194,6 +139,8 @@ export function cohortResultToDict(cohort) {
194
139
  failures: r.failures,
195
140
  ci_summary: r.ci_summary,
196
141
  slizard_summary: r.slizard_summary,
142
+ via: r.via ?? null,
143
+ partial_data: r.partial_data ?? {},
197
144
  })),
198
145
  resolution_errors: cohort.resolution_errors,
199
146
  };
@@ -216,6 +163,7 @@ export function renderReviewCleanText(cohort) {
216
163
  lines.push("");
217
164
  lines.push(` PR #${r.pr_number} -- ${status}`);
218
165
  lines.push(` HEAD SHA: ${r.head_sha ?? "<unknown>"}`);
166
+ lines.push(` Gate via: ${r.via ?? "<unknown>"}`);
219
167
  lines.push(` Greptile reviewed: ${String(v.lastReviewedSha ?? "<not parsed>")}`);
220
168
  const conf = v.confidence;
221
169
  lines.push(` Confidence: ${conf === null || conf === undefined ? "<not parsed>" : String(conf)}/5`);
@@ -228,6 +176,13 @@ export function renderReviewCleanText(cohort) {
228
176
  if (r.slizard_summary && typeof r.slizard_summary.summary_line === "string") {
229
177
  lines.push(` ${r.slizard_summary.summary_line}`);
230
178
  }
179
+ const override = asRecord(r.partial_data?.verdict_override);
180
+ if (override !== null) {
181
+ lines.push(` Verdict override: ${String(override.reason ?? "<unknown>")}`);
182
+ if (typeof override.basis === "string") {
183
+ lines.push(` Override basis: ${override.basis}`);
184
+ }
185
+ }
231
186
  r.failures.forEach((fail, index) => {
232
187
  lines.push(` [${index + 1}] ${fail}`);
233
188
  });
@@ -212,6 +212,7 @@ const VERIFY_TASK_VERBS = new Set([
212
212
  "verify:forward-coverage",
213
213
  "verify:story-ready",
214
214
  "verify:review-monitor",
215
+ "verify:l4-owner",
215
216
  "pr:watch",
216
217
  "pr:merge-ready",
217
218
  "coverage:hotspots",
@@ -85,4 +85,13 @@ export declare function validateProject(projectRoot: string): {
85
85
  };
86
86
  /** Render --list output for a project root. */
87
87
  export declare function listProject(projectRoot: string): string;
88
+ import { type ClassifyAction, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorEngine, type LabelMirrorItem, type LabelMirrorOptions as LabelMirrorOptionsCore, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule } from "./label-mirror.js";
89
+ export { type ClassifyAction, DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, type LabelMirrorEngine, type LabelMirrorItem, type LabelMirrorOutcome, type LabelMirrorPolicy, type LabelMirrorStatus, labelMirrorOutcomeToJson, type ResolvedLabelMirrorPolicy, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
90
+ /** Public options: engine is optional (defaults to this module's classify API). */
91
+ export type LabelMirrorOptions = Omit<LabelMirrorOptionsCore, "engine"> & {
92
+ readonly engine?: LabelMirrorEngine;
93
+ };
94
+ export { extractReferencedRepoIssueKeys } from "./label-mirror.js";
95
+ /** Tier-1 label mirror with the #1129 classify engine bound in. */
96
+ export declare function mirrorLabels(projectRoot: string, options?: LabelMirrorOptions): [number, LabelMirrorOutcome];
88
97
  //# sourceMappingURL=index.d.ts.map
@@ -721,7 +721,9 @@ export function validateProject(projectRoot) {
721
721
  const rel = projectDefinitionPath(root);
722
722
  const classifyErrs = validateTriageAutoClassifyOnPlan(plan, rel);
723
723
  const holderErrs = validateTriageHoldMarkersOnPlan(plan, rel);
724
- const errors = [...classifyErrs, ...holderErrs];
724
+ // label-mirror validates plan.policy.triageLabelMirror (#1423); imported via re-export graph.
725
+ const mirrorErrs = validateTriageLabelMirrorOnPlanFromModule(plan, rel);
726
+ const errors = [...classifyErrs, ...holderErrs, ...mirrorErrs];
725
727
  if (errors.length > 0) {
726
728
  const lines = errors.map((err) => `FAIL: ${err}`);
727
729
  lines.push("");
@@ -732,7 +734,7 @@ export function validateProject(projectRoot) {
732
734
  const markers = resolveHoldMarkers({ projectRoot: root });
733
735
  return {
734
736
  code: 0,
735
- stdout: "OK: triageAutoClassify[] + triageHoldMarkers[] valid " +
737
+ stdout: "OK: triageAutoClassify[] + triageHoldMarkers[] + triageLabelMirror valid " +
736
738
  `(${rules.length} rules, ${markers.length} hold markers).\n`,
737
739
  stderr: "",
738
740
  };
@@ -744,4 +746,34 @@ export function listProject(projectRoot) {
744
746
  const markers = resolveHoldMarkers({ projectRoot: root });
745
747
  return `${renderList(rules, { holdMarkers: markers })}\n`;
746
748
  }
749
+ // ---------------------------------------------------------------------------
750
+ // Tier-1 label mirror (#1423 Wave 1)
751
+ // label-mirror.ts does not import this module (avoids ESM/SLizard cycle).
752
+ // Public mirrorLabels injects the classify engine into the pure implementation.
753
+ // ---------------------------------------------------------------------------
754
+ import { DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, mirrorLabels as mirrorLabelsCore, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlan as validateTriageLabelMirrorOnPlanFromModule, } from "./label-mirror.js";
755
+ export { DEFAULT_IDEMPOTENCY_LABEL, defaultLabelMirrorPolicy, desiredLabelsForClassification, labelMirrorOutcomeToJson, renderLabelMirrorReport, resolveLabelMirrorPolicy, validateLabelMirrorPolicy, validateTriageLabelMirrorOnPlanFromModule as validateTriageLabelMirrorOnPlan, };
756
+ function defaultLabelMirrorEngine() {
757
+ return {
758
+ classifyIssue: (issue, options) => classifyIssue(issue, {
759
+ rules: options?.rules,
760
+ holdMarkers: options?.holdMarkers,
761
+ vbriefReferenced: options?.vbriefReferenced,
762
+ hasTriageDecision: options?.hasTriageDecision,
763
+ now: options?.now,
764
+ }),
765
+ resolveClassifyRules,
766
+ resolveHoldMarkers,
767
+ extractReferencedIssues,
768
+ };
769
+ }
770
+ export { extractReferencedRepoIssueKeys } from "./label-mirror.js";
771
+ /** Tier-1 label mirror with the #1129 classify engine bound in. */
772
+ export function mirrorLabels(projectRoot, options = {}) {
773
+ const { engine, ...rest } = options;
774
+ return mirrorLabelsCore(projectRoot, {
775
+ ...rest,
776
+ engine: engine ?? defaultLabelMirrorEngine(),
777
+ });
778
+ }
747
779
  //# sourceMappingURL=index.js.map