@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 { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
2
2
  import { evaluateAgentMerge, } from "../policy/require-human-merge.js";
3
+ import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
3
4
  import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
4
5
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
5
6
  import { makeResult } from "./result.js";
@@ -12,6 +13,12 @@ function isProtectedCheckConfigFailure(stderr) {
12
13
  tail.includes("Cannot find module") ||
13
14
  tail.includes("protected-check script not found:"));
14
15
  }
16
+ function defaultPostMergeUmbrellaReconcile(projectRoot, repo) {
17
+ const [, umOutcome] = reconcileUmbrellas(projectRoot, { repo });
18
+ if (umOutcome.changed.length > 0 || umOutcome.errors.length > 0) {
19
+ process.stderr.write(`${renderUmbrellasReport(umOutcome)}\n`);
20
+ }
21
+ }
15
22
  /** Run protected-check -> wait -> merge cascade (#1369). */
16
23
  export function waitMergeableAndMerge(prNumber, repo, options) {
17
24
  const protectedFn = options.protectedFn ?? runProtectedCheck;
@@ -93,7 +100,9 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
93
100
  error: semanticGreen.error,
94
101
  });
95
102
  }
96
- const [monRc, monStdout, monStderr] = monitorFn(prNumber, repo, options.capMinutes);
103
+ const [monRc, monStdout, monStderr] = monitorFn(prNumber, repo, options.capMinutes, {
104
+ projectRoot,
105
+ });
97
106
  const monitorPayload = parseMonitorPayload(monStdout);
98
107
  const [outcome, monitorExit] = classifyMonitorOutcome(monRc, monitorPayload);
99
108
  if (outcome !== "clean") {
@@ -114,6 +123,24 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
114
123
  }
115
124
  const [mergeRc, mergeStdout, mergeStderr] = mergeFn(prNumber, repo);
116
125
  if (mergeRc === 0) {
126
+ // Best-effort umbrella checklist + current-shape refresh after child merge (#1649).
127
+ // Failures must not flip a successful merge into an error exit.
128
+ // null = skip; undefined = live default (skipped under VITEST unless injected).
129
+ const umbrellaFn = options.umbrellaReconcileFn === null
130
+ ? null
131
+ : options.umbrellaReconcileFn !== undefined
132
+ ? options.umbrellaReconcileFn
133
+ : process.env.VITEST === "true"
134
+ ? null
135
+ : defaultPostMergeUmbrellaReconcile;
136
+ if (umbrellaFn !== null) {
137
+ try {
138
+ umbrellaFn(projectRoot, repo);
139
+ }
140
+ catch {
141
+ /* best-effort; merge remains authoritative */
142
+ }
143
+ }
117
144
  return makeResult({
118
145
  prNumber,
119
146
  repo,
@@ -8,6 +8,8 @@ export interface ParsedWaitMergeableArgs {
8
8
  readonly cascadeMode: boolean;
9
9
  readonly requireMasterCiGreen: boolean;
10
10
  readonly baseBranch: string | null;
11
+ /** Project root for minGreptileConfidence (#3095 / #3102). Null = cwd at run. */
12
+ readonly projectRoot: string | null;
11
13
  readonly error?: string;
12
14
  }
13
15
  export declare function parseWaitMergeableArgs(argv: readonly string[]): ParsedWaitMergeableArgs;
@@ -19,6 +19,7 @@ export function parseWaitMergeableArgs(argv) {
19
19
  let cascadeMode = false;
20
20
  let requireMasterCiGreen = false;
21
21
  let baseBranch = null;
22
+ let projectRoot = null;
22
23
  const baseReturn = () => ({
23
24
  prNumber,
24
25
  repo,
@@ -28,6 +29,7 @@ export function parseWaitMergeableArgs(argv) {
28
29
  cascadeMode,
29
30
  requireMasterCiGreen,
30
31
  baseBranch,
32
+ projectRoot,
31
33
  });
32
34
  for (let i = 0; i < argv.length; i += 1) {
33
35
  const arg = argv[i];
@@ -51,6 +53,17 @@ export function parseWaitMergeableArgs(argv) {
51
53
  else if (arg?.startsWith("--base-branch=")) {
52
54
  baseBranch = arg.slice("--base-branch=".length);
53
55
  }
56
+ else if (arg === "--project-root") {
57
+ const value = argv[i + 1];
58
+ if (value === undefined) {
59
+ return { ...baseReturn(), error: "argument --project-root: expected one argument" };
60
+ }
61
+ projectRoot = value;
62
+ i += 1;
63
+ }
64
+ else if (arg?.startsWith("--project-root=")) {
65
+ projectRoot = arg.slice("--project-root=".length);
66
+ }
54
67
  else if (arg === "--repo") {
55
68
  const value = argv[i + 1];
56
69
  if (value === undefined) {
@@ -154,6 +167,9 @@ export function runWaitMergeable(argv, options = {}) {
154
167
  cascadeMode: args.cascadeMode,
155
168
  requireMasterCiGreen: args.requireMasterCiGreen,
156
169
  baseBranch: args.baseBranch,
170
+ // Explicit CLI root (or cwd) so remote-target cascade does not resolve
171
+ // minGreptileConfidence from an unrelated directory (#3102).
172
+ projectRoot: args.projectRoot ?? process.cwd(),
157
173
  });
158
174
  const summaryLabel = summaryLabelForExit(result.exitCode);
159
175
  process.stderr.write(`[pr_wait_mergeable] PR #${result.prNumber} repo=${result.repo} ` +
@@ -12,6 +12,8 @@ export interface WaitMergeableResult {
12
12
  }
13
13
  export type SubprocessTriple = readonly [number, string, string];
14
14
  export type ProtectedCheckFn = (prNumber: number, repo: string | null, protectedIssues: readonly number[]) => SubprocessTriple;
15
- export type MonitorFn = (prNumber: number, repo: string, capMinutes: number) => SubprocessTriple;
15
+ export type MonitorFn = (prNumber: number, repo: string, capMinutes: number, options?: {
16
+ readonly projectRoot?: string;
17
+ }) => SubprocessTriple;
16
18
  export type MergeFn = (prNumber: number, repo: string | null) => SubprocessTriple;
17
19
  //# sourceMappingURL=types.d.ts.map
@@ -35,6 +35,12 @@ export declare function runProtectedCheck(prNumber: number, repo: string | null,
35
35
  export interface RunMonitorOptions {
36
36
  readonly nodeExecutable?: string;
37
37
  readonly timeout?: number;
38
+ /**
39
+ * Project root for plan.policy.review.minGreptileConfidence when the
40
+ * cascade cwd is not the monitored project (#3095 residual / #3102).
41
+ * Defaults to process.cwd() when omitted.
42
+ */
43
+ readonly projectRoot?: string;
38
44
  }
39
45
  /** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
40
46
  export declare function runMonitor(prNumber: number, repo: string, capMinutes: number, options?: RunMonitorOptions): SubprocessTriple;
@@ -105,6 +105,7 @@ export function runMonitor(prNumber, repo, capMinutes, options = {}) {
105
105
  }
106
106
  const node = options.nodeExecutable ?? process.execPath;
107
107
  const timeoutSec = options.timeout ?? capMinutes * 60 + 60;
108
+ const projectRoot = options.projectRoot ?? process.cwd();
108
109
  const cmd = [
109
110
  scriptPath,
110
111
  String(prNumber),
@@ -112,6 +113,8 @@ export function runMonitor(prNumber, repo, capMinutes, options = {}) {
112
113
  repo,
113
114
  "--cap-minutes",
114
115
  String(capMinutes),
116
+ "--project-root",
117
+ projectRoot,
115
118
  "--json",
116
119
  ];
117
120
  // Stream the monitor's per-poll heartbeat live (a buffered poll looks like a
@@ -6,7 +6,10 @@
6
6
  * cannot promise to poll and then silently forget (2026-05-11 three-strikes on
7
7
  * #1051 / #1054).
8
8
  */
9
- /** CLEAN: SHA-matched, non-errored, no P0/P1, confidence > 3, CI green. */
9
+ /**
10
+ * CLEAN: SHA-matched, non-errored, no P0/P1, confidence >= resolved min, CI green.
11
+ * Min defaults to consumer 4 (legacy > 3); dogfood/policy may raise to 5 (#3095).
12
+ */
10
13
  export declare const EXIT_CLEAN = 0;
11
14
  /** NEW_P0_P1: blocking findings on the CURRENT (SHA-matched) review. */
12
15
  export declare const EXIT_NEW_P0_P1 = 1;
@@ -6,7 +6,10 @@
6
6
  * cannot promise to poll and then silently forget (2026-05-11 three-strikes on
7
7
  * #1051 / #1054).
8
8
  */
9
- /** CLEAN: SHA-matched, non-errored, no P0/P1, confidence > 3, CI green. */
9
+ /**
10
+ * CLEAN: SHA-matched, non-errored, no P0/P1, confidence >= resolved min, CI green.
11
+ * Min defaults to consumer 4 (legacy > 3); dogfood/policy may raise to 5 (#3095).
12
+ */
10
13
  export const EXIT_CLEAN = 0;
11
14
  /** NEW_P0_P1: blocking findings on the CURRENT (SHA-matched) review. */
12
15
  export const EXIT_NEW_P0_P1 = 1;
@@ -57,7 +60,7 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
57
60
  " --project-root PATH Chdir before probing (optional)\n" +
58
61
  "\n" +
59
62
  "exit codes:\n" +
60
- " 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
63
+ " 0 CLEAN SHA-matched review, confidence >= policy min (default 4; dogfood 5), no P0/P1, CI green\n" +
61
64
  " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
62
65
  " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
63
66
  /**
@@ -223,6 +223,7 @@ export function runWatch(argv, options = {}) {
223
223
  clockFn: options.clockFn,
224
224
  probeFn: options.probeFn,
225
225
  stallThreshold: options.stallThreshold,
226
+ projectRoot: args.projectRoot ?? process.cwd(),
226
227
  });
227
228
  if (args.emitJson) {
228
229
  process.stdout.write(emitWatchJson(result));
@@ -9,5 +9,5 @@ import type { WatchProbe } from "./types.js";
9
9
  * detector (#1056 AC-2). All gh access routes through the injectable RunGhFn
10
10
  * seam, which defaults to the UTF-8-safe execFile capture (#1366).
11
11
  */
12
- export declare function probeOnce(prNumber: number, repoArg: string | null, runGh?: RunGhFn): WatchProbe;
12
+ export declare function probeOnce(prNumber: number, repoArg: string | null, runGh?: RunGhFn, projectRoot?: string | null): WatchProbe;
13
13
  //# sourceMappingURL=probe.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { detect, evaluateCleanGate, parseConfidence, parseLastReviewedShaMarkdownLink, parseLastReviewedShaNaiveInline, } from "../content-contracts/skills/greptile-detector.js";
2
+ import { resolveMinGreptileConfidence } from "../policy/min-greptile-confidence.js";
2
3
  import { evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
3
4
  import { GREPTILE_ERRORED_SENTINEL } from "../pr-merge-readiness/constants.js";
4
5
  import { defaultRunGh, fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "../pr-merge-readiness/gh.js";
@@ -32,9 +33,10 @@ function errorProbe(headSha, message) {
32
33
  * detector (#1056 AC-2). All gh access routes through the injectable RunGhFn
33
34
  * seam, which defaults to the UTF-8-safe execFile capture (#1366).
34
35
  */
35
- export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
36
+ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh, projectRoot = null) {
36
37
  const resolved = resolveRepo(repoArg, runGh);
37
38
  const repo = resolved.repo;
39
+ const minConfidence = resolveMinGreptileConfidence(projectRoot ?? process.cwd()).min;
38
40
  // 1. HEAD SHA -- primary `gh pr view`, then REST fallback when a repo resolved.
39
41
  let headSha = fetchPrHeadSha(prNumber, repo, runGh);
40
42
  if (headSha === null && repo !== null) {
@@ -88,6 +90,7 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
88
90
  ciFailures,
89
91
  errored,
90
92
  terminalCheckRun,
93
+ minConfidence,
91
94
  });
92
95
  return {
93
96
  found,
@@ -34,7 +34,7 @@ export type SleepFn = (seconds: number) => void;
34
34
  export interface MonotonicClock {
35
35
  now(): number;
36
36
  }
37
- export type ProbeFn = (prNumber: number, repo: string | null, runGh: RunGhFn) => WatchProbe;
37
+ export type ProbeFn = (prNumber: number, repo: string | null, runGh: RunGhFn, projectRoot?: string | null) => WatchProbe;
38
38
  export interface WatchOptions {
39
39
  readonly maxWaitMinutes?: number;
40
40
  readonly pollSeconds?: number;
@@ -44,6 +44,12 @@ export interface WatchOptions {
44
44
  readonly sleepFn?: SleepFn;
45
45
  readonly clockFn?: MonotonicClock;
46
46
  readonly probeFn?: ProbeFn;
47
+ /**
48
+ * Project root for resolving plan.policy.review.minGreptileConfidence (#3095).
49
+ * When omitted, probeOnce uses process.cwd(). Finish-loop / pr:watch MUST pass
50
+ * the caller project-root so a chdir elsewhere cannot apply the wrong floor.
51
+ */
52
+ readonly projectRoot?: string | null;
47
53
  }
48
54
  export interface WatchResult {
49
55
  readonly verdict: string;
@@ -44,7 +44,8 @@ export function watch(prNumber, repo, options = {}) {
44
44
  const runGh = options.runGh ?? defaultRunGh;
45
45
  const clockFn = options.clockFn ?? systemMonotonicClock;
46
46
  const sleepFn = options.sleepFn ?? defaultSleep;
47
- const probeFn = options.probeFn ?? probeOnce;
47
+ const projectRoot = options.projectRoot ?? null;
48
+ const probeFn = options.probeFn ?? ((n, r, gh, root) => probeOnce(n, r, gh, root ?? projectRoot));
48
49
  const capSeconds = Math.max(0, maxWaitMinutes * 60);
49
50
  // Bounded loop (time is still the authority via the cap check below) so no
50
51
  // unbounded `while (true)`; +1 covers the trailing poll after the final wait.
@@ -62,7 +63,7 @@ export function watch(prNumber, repo, options = {}) {
62
63
  pollCount: poll,
63
64
  });
64
65
  for (let poll = 1; poll <= maxPolls; poll += 1) {
65
- const probe = probeFn(prNumber, repo, runGh);
66
+ const probe = probeFn(prNumber, repo, runGh, projectRoot);
66
67
  lastProbe = probe;
67
68
  const elapsed = Math.round(clockFn.now() - startedAt);
68
69
  process.stderr.write(`${formatWatchStatus(poll, maxPolls, probe, elapsed)}\n`);
@@ -152,9 +152,14 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
152
152
  return [false, `greenfield smoke: expected global deft at ${deft}`];
153
153
  }
154
154
  const pyFree = pythonFreePathEnv(envBase);
155
+ const installedBin = join(npmPrefix, "bin");
156
+ const installedEnv = {
157
+ ...pyFree,
158
+ PATH: `${installedBin}:${pyFree.PATH ?? ""}`,
159
+ };
155
160
  [ok, reason] = runStep(spawn, "directive init", deft, ["init", "--yes", "--repo-root", projectDir], {
156
161
  cwd: work,
157
- env: pyFree,
162
+ env: installedEnv,
158
163
  timeoutMs: 120_000,
159
164
  }, onProgress);
160
165
  if (!ok)
@@ -173,8 +178,7 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
173
178
  return [false, "greenfield smoke: repo-root Python run shim present after init"];
174
179
  }
175
180
  const checkEnv = {
176
- ...pyFree,
177
- PATH: `${join(npmPrefix, "bin")}:${pyFree.PATH ?? ""}`,
181
+ ...installedEnv,
178
182
  DEFT_SESSION_RITUAL_SKIP: "1",
179
183
  };
180
184
  onProgress?.("greenfield smoke: running consumer task deft:check (engine-invoke path)");
@@ -1,5 +1,6 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./github-lease.js";
3
+ export * from "./l4-owner.js";
3
4
  export * from "./lease-comment.js";
4
5
  export * from "./record.js";
5
6
  export * from "./tier-detection.js";
@@ -1,5 +1,6 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./github-lease.js";
3
+ export * from "./l4-owner.js";
3
4
  export * from "./lease-comment.js";
4
5
  export * from "./record.js";
5
6
  export * from "./tier-detection.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Owner Continuity / L4 owner gate (#3090).
3
+ *
4
+ * Exit 0 only when a fresh sticky review-owner lease exists on the PR,
5
+ * or the caller asserts `review_cycle: done` after Step 6.
6
+ * Freeform started/pending/initiated values are rejected.
7
+ * skipped / n/a / parent-retained are process evidence only — they do NOT
8
+ * satisfy this machine gate when --pr is required (lease-or-done).
9
+ */
10
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY } from "./constants.js";
11
+ import type { ReviewOwnerGithubSeams } from "./github-lease.js";
12
+ import { type ReviewMonitorRecord } from "./record.js";
13
+ /** Allowed review_cycle evidence values (prefix form for in_progress/skipped). */
14
+ export type ReviewCycleEvidence = "done" | "n/a" | `in_progress:${string}` | `skipped:${string}`;
15
+ export declare const L4_OWNER_HELP: string;
16
+ export declare function parseReviewCycleEvidence(raw: string | null | undefined): {
17
+ ok: true;
18
+ value: ReviewCycleEvidence | null;
19
+ } | {
20
+ ok: false;
21
+ reason: string;
22
+ };
23
+ /** Parse in_progress:<pr>#<ref> into parts; null if not that form. */
24
+ export declare function parseInProgressEvidence(value: string): {
25
+ pr: number;
26
+ ref: string;
27
+ } | null;
28
+ export interface VerifyL4OwnerArgs {
29
+ readonly pr: number;
30
+ readonly projectRoot: string;
31
+ readonly repo?: string | null;
32
+ readonly headSha?: string | null;
33
+ readonly reviewCycle?: string | null;
34
+ readonly now?: Date;
35
+ readonly seams?: ReviewOwnerGithubSeams;
36
+ }
37
+ export interface VerifyL4OwnerResult {
38
+ readonly exitCode: typeof EXIT_READY | typeof EXIT_NOT_READY | typeof EXIT_CONFIG_ERROR;
39
+ readonly message: string;
40
+ readonly monitorRecord: ReviewMonitorRecord | null;
41
+ readonly reviewCycle: ReviewCycleEvidence | null;
42
+ readonly path: "lease" | "done" | "none" | "illegal" | "config";
43
+ }
44
+ export declare function evaluateL4OwnerGate(args: VerifyL4OwnerArgs): VerifyL4OwnerResult;
45
+ export declare function l4OwnerResultToJson(result: VerifyL4OwnerResult): Record<string, unknown>;
46
+ //# sourceMappingURL=l4-owner.d.ts.map
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Owner Continuity / L4 owner gate (#3090).
3
+ *
4
+ * Exit 0 only when a fresh sticky review-owner lease exists on the PR,
5
+ * or the caller asserts `review_cycle: done` after Step 6.
6
+ * Freeform started/pending/initiated values are rejected.
7
+ * skipped / n/a / parent-retained are process evidence only — they do NOT
8
+ * satisfy this machine gate when --pr is required (lease-or-done).
9
+ */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { resolveRepo } from "../triage/queue/repo.js";
13
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY } from "./constants.js";
14
+ import { fetchActiveMonitorFromGithub } from "./record.js";
15
+ export const L4_OWNER_HELP = "usage: task verify:l4-owner -- --pr <N> [options]\n" +
16
+ "\n" +
17
+ "Owner Continuity / L4 owner gate (#3090): after drive-to:merge-ready /\n" +
18
+ "babysit / shepherd claims, exit 0 only when a sticky GitHub review-owner\n" +
19
+ "lease is fresh on the PR, or the caller asserts --review-cycle done\n" +
20
+ "(Step 6 fail-closed all-of on HEAD). Freeform started/pending/initiated\n" +
21
+ "are rejected. skipped / n/a / parent-retained do NOT satisfy this machine\n" +
22
+ "gate (process A/B/C still applies). Silent hold exits 1.\n" +
23
+ "\n" +
24
+ "options:\n" +
25
+ " -h, --help Show this help and exit 0\n" +
26
+ " --pr N Pull request number (required unless --help)\n" +
27
+ " --repo OWNER/REPO Repository (optional; inferred from origin)\n" +
28
+ " --head-sha SHA Expected HEAD SHA (optional freshness check)\n" +
29
+ " --project-root PATH Project root (default: cwd)\n" +
30
+ " --review-cycle VALUE done | in_progress:<pr>#<ref> | skipped:<reason> | n/a\n" +
31
+ " --json Emit structured JSON on stdout\n" +
32
+ "\n" +
33
+ "exit codes:\n" +
34
+ " 0 READY Fresh sticky lease, or --review-cycle done\n" +
35
+ " 1 NOT READY Silent hold / illegal evidence / lease missing\n" +
36
+ " 2 CONFIG Usage / path / GitHub fetch error\n";
37
+ const FORBIDDEN_FREEFORM = new Set(["started", "pending", "initiated", "start", "in_progress"]);
38
+ export function parseReviewCycleEvidence(raw) {
39
+ if (raw === null || raw === undefined || raw.trim() === "") {
40
+ return { ok: true, value: null };
41
+ }
42
+ const value = raw.trim();
43
+ const lower = value.toLowerCase();
44
+ if (FORBIDDEN_FREEFORM.has(lower)) {
45
+ return {
46
+ ok: false,
47
+ reason: `illegal freeform review_cycle value '${value}' (#3090); ` +
48
+ "use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
49
+ };
50
+ }
51
+ if (value === "done" || value === "n/a") {
52
+ return { ok: true, value };
53
+ }
54
+ if (value.startsWith("in_progress:")) {
55
+ const rest = value.slice("in_progress:".length);
56
+ if (!rest.includes("#") || rest.endsWith("#") || rest.startsWith("#")) {
57
+ return {
58
+ ok: false,
59
+ reason: "in_progress requires form in_progress:<pr>#<monitor_or_lease_ref> (#3090)",
60
+ };
61
+ }
62
+ const prPart = rest.slice(0, rest.indexOf("#"));
63
+ if (!/^\d+$/.test(prPart) || Number(prPart) <= 0) {
64
+ return {
65
+ ok: false,
66
+ reason: "in_progress:<pr> must be a positive integer PR number (#3090)",
67
+ };
68
+ }
69
+ return { ok: true, value: value };
70
+ }
71
+ if (value.startsWith("skipped:")) {
72
+ const rest = value.slice("skipped:".length).trim();
73
+ if (rest.length === 0) {
74
+ return { ok: false, reason: "skipped requires a non-empty reason (#3090)" };
75
+ }
76
+ return { ok: true, value: value };
77
+ }
78
+ return {
79
+ ok: false,
80
+ reason: `unknown review_cycle value '${value}' (#3090); ` +
81
+ "use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
82
+ };
83
+ }
84
+ /** Parse in_progress:<pr>#<ref> into parts; null if not that form. */
85
+ export function parseInProgressEvidence(value) {
86
+ if (!value.startsWith("in_progress:")) {
87
+ return null;
88
+ }
89
+ const rest = value.slice("in_progress:".length);
90
+ const hash = rest.indexOf("#");
91
+ if (hash <= 0) {
92
+ return null;
93
+ }
94
+ const prPart = rest.slice(0, hash);
95
+ const ref = rest.slice(hash + 1);
96
+ if (!/^\d+$/.test(prPart) || ref.length === 0) {
97
+ return null;
98
+ }
99
+ return { pr: Number(prPart), ref };
100
+ }
101
+ function oneLine(value) {
102
+ return value.replace(/\r?\n/g, " ");
103
+ }
104
+ export function evaluateL4OwnerGate(args) {
105
+ const projectRoot = resolve(args.projectRoot);
106
+ let isDir = false;
107
+ try {
108
+ isDir = existsSync(projectRoot) && statSync(projectRoot).isDirectory();
109
+ }
110
+ catch {
111
+ isDir = false;
112
+ }
113
+ if (!isDir) {
114
+ return {
115
+ exitCode: EXIT_CONFIG_ERROR,
116
+ message: `verify_l4_owner: --project-root is not a directory: ${oneLine(projectRoot)}`,
117
+ monitorRecord: null,
118
+ reviewCycle: null,
119
+ path: "config",
120
+ };
121
+ }
122
+ const parsed = parseReviewCycleEvidence(args.reviewCycle);
123
+ if (!parsed.ok) {
124
+ return {
125
+ exitCode: EXIT_NOT_READY,
126
+ message: `verify_l4_owner: ${oneLine(parsed.reason)}`,
127
+ monitorRecord: null,
128
+ reviewCycle: null,
129
+ path: "illegal",
130
+ };
131
+ }
132
+ // Machine gate is lease-or-done only (#3090). skipped/n/a are handoff enum values
133
+ // but must not green-light ownership of an open PR without lease or Step 6 done.
134
+ if (parsed.value === "n/a" || parsed.value?.startsWith("skipped:")) {
135
+ return {
136
+ exitCode: EXIT_NOT_READY,
137
+ message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} does not satisfy the machine ` +
138
+ `gate for PR #${args.pr} (#3090). Exit 0 requires a fresh sticky lease or --review-cycle done.`,
139
+ monitorRecord: null,
140
+ reviewCycle: parsed.value,
141
+ path: "none",
142
+ };
143
+ }
144
+ if (parsed.value === "done") {
145
+ return {
146
+ exitCode: EXIT_READY,
147
+ message: `verify_l4_owner: review_cycle=done asserted for PR #${args.pr} (#3090). ` +
148
+ "Caller MUST have satisfied Step 6 fail-closed all-of on HEAD before this claim.",
149
+ monitorRecord: null,
150
+ reviewCycle: "done",
151
+ path: "done",
152
+ };
153
+ }
154
+ // Validate in_progress PR binding when evidence is present (before lease fetch).
155
+ if (parsed.value?.startsWith("in_progress:")) {
156
+ const parts = parseInProgressEvidence(parsed.value);
157
+ if (parts === null) {
158
+ return {
159
+ exitCode: EXIT_NOT_READY,
160
+ message: `verify_l4_owner: malformed in_progress evidence ${oneLine(parsed.value)} (#3090).`,
161
+ monitorRecord: null,
162
+ reviewCycle: parsed.value,
163
+ path: "illegal",
164
+ };
165
+ }
166
+ if (parts.pr !== args.pr) {
167
+ return {
168
+ exitCode: EXIT_NOT_READY,
169
+ message: `verify_l4_owner: review_cycle PR #${parts.pr} does not match --pr ${args.pr} (#3090). ` +
170
+ "Ownership evidence must bind to the target PR.",
171
+ monitorRecord: null,
172
+ reviewCycle: parsed.value,
173
+ path: "none",
174
+ };
175
+ }
176
+ }
177
+ const repo = resolveRepo(args.repo ?? null, projectRoot);
178
+ if (repo === null) {
179
+ return {
180
+ exitCode: EXIT_CONFIG_ERROR,
181
+ message: "verify_l4_owner: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
182
+ monitorRecord: null,
183
+ reviewCycle: parsed.value,
184
+ path: "config",
185
+ };
186
+ }
187
+ const now = args.now ?? new Date();
188
+ const githubMonitor = fetchActiveMonitorFromGithub(repo, args.pr, {
189
+ now,
190
+ headSha: args.headSha ?? null,
191
+ seams: args.seams,
192
+ });
193
+ if (githubMonitor !== null && typeof githubMonitor === "object" && "error" in githubMonitor) {
194
+ return {
195
+ exitCode: EXIT_CONFIG_ERROR,
196
+ message: `verify_l4_owner: ${oneLine(githubMonitor.error)}`,
197
+ monitorRecord: null,
198
+ reviewCycle: parsed.value,
199
+ path: "config",
200
+ };
201
+ }
202
+ if (githubMonitor !== null) {
203
+ const ref = parsed.value ??
204
+ `in_progress:${args.pr}#${githubMonitor.monitor_agent_id}`;
205
+ return {
206
+ exitCode: EXIT_READY,
207
+ message: `verify_l4_owner: active GitHub review-owner lease for PR #${args.pr} ` +
208
+ `(monitor_agent_id=${oneLine(githubMonitor.monitor_agent_id)}, owner=${oneLine(githubMonitor.owner)}) ` +
209
+ "— review_cycle path=lease (#3090).",
210
+ monitorRecord: githubMonitor,
211
+ reviewCycle: ref,
212
+ path: "lease",
213
+ };
214
+ }
215
+ // parent-retained is process path B — machine gate cannot verify it (lease-or-done only).
216
+ if (parsed.value?.startsWith("in_progress:")) {
217
+ const parts = parseInProgressEvidence(parsed.value);
218
+ if (parts?.ref === "parent-retained") {
219
+ return {
220
+ exitCode: EXIT_NOT_READY,
221
+ message: `verify_l4_owner: parent-retained for PR #${args.pr} is process path B only (#3090). ` +
222
+ "Machine gate requires a sticky <!-- deft:review-owner --> lease or --review-cycle done. " +
223
+ "Keep parent dual-source poll/fix; do not emit L4 status:pass as terminal.",
224
+ monitorRecord: null,
225
+ reviewCycle: parsed.value,
226
+ path: "none",
227
+ };
228
+ }
229
+ return {
230
+ exitCode: EXIT_NOT_READY,
231
+ message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} claims in_progress but no sticky ` +
232
+ `<!-- deft:review-owner --> lease found for PR #${args.pr} (#3090 / #2797).\n` +
233
+ " Register: deft review-monitor:register --pr <N> --monitor-agent-id <id> --platform-primitive …",
234
+ monitorRecord: null,
235
+ reviewCycle: parsed.value,
236
+ path: "none",
237
+ };
238
+ }
239
+ return {
240
+ exitCode: EXIT_NOT_READY,
241
+ message: `verify_l4_owner: silent hold on PR #${args.pr} — no fresh sticky lease and no ` +
242
+ "review_cycle=done (#3090 Owner Continuity Gate).\n" +
243
+ " Same turn MUST end in A (monitor+lease), B (parent-retained + next dual-source action),\n" +
244
+ " or C (explicit BLOCKED/FAILED finish). Check-run SUCCESS alone is not CLEAN.",
245
+ monitorRecord: null,
246
+ reviewCycle: null,
247
+ path: "none",
248
+ };
249
+ }
250
+ export function l4OwnerResultToJson(result) {
251
+ return {
252
+ exit_code: result.exitCode,
253
+ message: result.message,
254
+ monitor_agent_id: result.monitorRecord?.monitor_agent_id ?? null,
255
+ monitor_owner: result.monitorRecord?.owner ?? null,
256
+ monitor_record: result.monitorRecord,
257
+ path: result.path,
258
+ ready: result.exitCode === EXIT_READY,
259
+ review_cycle: result.reviewCycle,
260
+ };
261
+ }
262
+ //# sourceMappingURL=l4-owner.js.map
@@ -1,6 +1,10 @@
1
+ import { type AgentHookReadinessResult } from "../verify-env/agent-hook-readiness.js";
1
2
  export declare const ENTRYPOINT_TIMEOUT_SECONDS = 300;
2
3
  export declare const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
3
4
  export type RitualEntrypointFn = (argv: readonly string[]) => number;
5
+ export interface DefaultRitualRunnerSeams {
6
+ readonly evaluateAgentHookReadiness?: (projectRoot: string) => AgentHookReadinessResult;
7
+ }
4
8
  /** Run an in-process ritual entrypoint with stdout/stderr capture. */
5
9
  export declare function callMain(mainFn: RitualEntrypointFn, argv: readonly string[], _options?: {
6
10
  label?: string;
@@ -11,8 +15,8 @@ export declare function callMain(mainFn: RitualEntrypointFn, argv: readonly stri
11
15
  };
12
16
  /** CLI-shaped cache-fresh entrypoint (mirrors scripts/preflight_cache.py main). */
13
17
  export declare function runCacheFreshMain(argv: readonly string[]): number;
14
- /** Default gated-tier runner: in-process doctor + cache-fresh (mirrors verify_session_ritual.py). */
15
- export declare function defaultRitualRunner(command: readonly string[], projectRoot: string): {
18
+ /** Default gated-tier runner: in-process hook readiness, doctor, and cache freshness. */
19
+ export declare function defaultRitualRunner(command: readonly string[], projectRoot: string, seams?: DefaultRitualRunnerSeams): {
16
20
  code: number;
17
21
  stdout: string;
18
22
  stderr: string;