@deftai/directive-core 0.66.2 → 0.67.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 (68) hide show
  1. package/dist/agents-md-advisory/evaluate.d.ts +44 -0
  2. package/dist/agents-md-advisory/evaluate.js +115 -0
  3. package/dist/agents-md-advisory/index.d.ts +2 -0
  4. package/dist/agents-md-advisory/index.js +2 -0
  5. package/dist/agents-md-budget/evaluate.d.ts +38 -0
  6. package/dist/agents-md-budget/evaluate.js +152 -0
  7. package/dist/agents-md-budget/index.d.ts +2 -0
  8. package/dist/agents-md-budget/index.js +2 -0
  9. package/dist/codebase/map.d.ts +1 -0
  10. package/dist/codebase/map.js +5 -2
  11. package/dist/doctor/constants.d.ts +5 -2
  12. package/dist/doctor/constants.js +15 -1
  13. package/dist/doctor/flags.js +5 -1
  14. package/dist/doctor/main.js +99 -8
  15. package/dist/doctor/types.d.ts +7 -0
  16. package/dist/intake/github-auth-modes.d.ts +1 -0
  17. package/dist/intake/github-auth-modes.js +1 -0
  18. package/dist/intake/issue-ingest.d.ts +6 -2
  19. package/dist/intake/issue-ingest.js +65 -9
  20. package/dist/platform/agents-consumer-header.d.ts +13 -0
  21. package/dist/platform/agents-consumer-header.js +57 -0
  22. package/dist/platform/agents-md.js +4 -1
  23. package/dist/platform/index.d.ts +1 -0
  24. package/dist/platform/index.js +1 -0
  25. package/dist/policy/agents-md-advisory.d.ts +52 -0
  26. package/dist/policy/agents-md-advisory.js +63 -0
  27. package/dist/policy/agents-md-budget.d.ts +24 -0
  28. package/dist/policy/agents-md-budget.js +89 -0
  29. package/dist/policy/index.d.ts +1 -0
  30. package/dist/policy/index.js +1 -0
  31. package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
  32. package/dist/pr-merge-readiness/ci-gate.js +79 -0
  33. package/dist/pr-merge-readiness/compute.d.ts +5 -1
  34. package/dist/pr-merge-readiness/compute.js +90 -6
  35. package/dist/pr-merge-readiness/gh.d.ts +8 -0
  36. package/dist/pr-merge-readiness/gh.js +30 -5
  37. package/dist/pr-merge-readiness/index.d.ts +3 -0
  38. package/dist/pr-merge-readiness/index.js +2 -0
  39. package/dist/pr-merge-readiness/main.d.ts +3 -0
  40. package/dist/pr-merge-readiness/main.js +73 -6
  41. package/dist/pr-merge-readiness/output.js +20 -0
  42. package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
  43. package/dist/pr-merge-readiness/slizard-gate.js +119 -0
  44. package/dist/preflight-cache/evaluate.d.ts +1 -0
  45. package/dist/preflight-cache/evaluate.js +17 -10
  46. package/dist/render/project-render.d.ts +16 -2
  47. package/dist/render/project-render.js +55 -22
  48. package/dist/swarm/launch.d.ts +6 -0
  49. package/dist/swarm/launch.js +2 -2
  50. package/dist/swarm/routing-verify.d.ts +1 -1
  51. package/dist/swarm/routing-verify.js +11 -10
  52. package/dist/swarm/routing.d.ts +9 -0
  53. package/dist/swarm/routing.js +46 -0
  54. package/dist/swarm/verify-review-clean-cli.js +25 -1
  55. package/dist/swarm/verify-review-clean.d.ts +9 -1
  56. package/dist/swarm/verify-review-clean.js +76 -3
  57. package/dist/verify-source/biome-config.d.ts +41 -0
  58. package/dist/verify-source/biome-config.js +97 -0
  59. package/dist/verify-source/index.d.ts +1 -0
  60. package/dist/verify-source/index.js +1 -0
  61. package/dist/xbrief-migrate/agents-header.d.ts +69 -0
  62. package/dist/xbrief-migrate/agents-header.js +179 -0
  63. package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
  64. package/dist/xbrief-migrate/fs-helpers.js +11 -0
  65. package/dist/xbrief-migrate/index.d.ts +1 -0
  66. package/dist/xbrief-migrate/index.js +1 -0
  67. package/dist/xbrief-migrate/migrate-project.js +29 -10
  68. package/package.json +11 -3
@@ -1,4 +1,11 @@
1
+ import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
1
2
  type JsonObject = Record<string, unknown>;
3
+ /** PROJECT-DEFINITION artifact shape (filename + envelope key + emitted version). */
4
+ interface ProjectDefinitionLayout {
5
+ readonly filename: string;
6
+ readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY | typeof LEGACY_INFO_ROOT_KEY;
7
+ readonly infoVersion: string;
8
+ }
2
9
  /** Durable review state for PROJECT-DEFINITION narrative staleness (#640). */
3
10
  export interface StalenessReviewMetadata {
4
11
  /** ISO-8601 UTC when narratives were last reviewed/acknowledged. */
@@ -24,12 +31,19 @@ export declare function buildStalenessAcknowledgement(completedItems: readonly L
24
31
  existing?: StalenessReviewMetadata | null;
25
32
  }): StalenessReviewMetadata;
26
33
  export declare function computeStalenessFlags(narratives: Record<string, string>, completedItems: readonly LifecycleItem[], review?: StalenessReviewMetadata | null): string[];
27
- export declare function createSkeleton(items: LifecycleItem[], now: string): JsonObject;
34
+ export declare function createSkeleton(items: LifecycleItem[], now: string, layout: ProjectDefinitionLayout): JsonObject;
28
35
  export interface RenderProjectOptions {
29
36
  readonly now?: Date;
30
37
  }
31
38
  export type RenderProjectResult = readonly [boolean, string];
32
- /** Regenerate PROJECT-DEFINITION.vbrief.json (mirrors ``scripts/project_render.render_project_definition``). */
39
+ /**
40
+ * Regenerate the PROJECT-DEFINITION artifact for `vbriefDir`.
41
+ *
42
+ * Layout-aware (#2149): on a migrated `xbrief/` root it targets
43
+ * `PROJECT-DEFINITION.xbrief.json` with an `xBRIEFInfo` envelope; on a legacy `vbrief/`
44
+ * root it keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
45
+ * (Mirrors ``scripts/project_render.render_project_definition``.)
46
+ */
33
47
  export declare function renderProjectDefinition(vbriefDir: string, options?: RenderProjectOptions): RenderProjectResult;
34
48
  /**
35
49
  * Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
@@ -1,10 +1,34 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
4
4
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
5
5
  import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
6
+ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
6
7
  import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
7
8
  import { splitCamel, splitWords } from "./text-utils.js";
9
+ /**
10
+ * Resolve the PROJECT-DEFINITION artifact shape for a lifecycle root directory.
11
+ *
12
+ * The decision is STRUCTURAL and keyed on the lifecycle root directory name -- the
13
+ * same signal `resolveLifecycleLayout` / `resolveLifecycleRoot` produce, so ingest and
14
+ * render never diverge (#2149). A migrated `xbrief/` root gets `PROJECT-DEFINITION.xbrief.json`
15
+ * + `xBRIEFInfo`; a legacy `vbrief/` root keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
16
+ * This prevents render from writing a legacy-named/enveloped artifact into a migrated tree.
17
+ */
18
+ function resolveProjectDefinitionLayout(vbriefDir) {
19
+ const migrated = basename(vbriefDir) === MIGRATED_ARTIFACT_DIR;
20
+ return migrated
21
+ ? {
22
+ filename: `PROJECT-DEFINITION${MIGRATED_ARTIFACT_SUFFIX}`,
23
+ infoRootKey: MIGRATED_INFO_ROOT_KEY,
24
+ infoVersion: VBRIEF_VERSION,
25
+ }
26
+ : {
27
+ filename: `PROJECT-DEFINITION${LEGACY_ARTIFACT_SUFFIX}`,
28
+ infoRootKey: LEGACY_INFO_ROOT_KEY,
29
+ infoVersion: EMITTED_VBRIEF_VERSION,
30
+ };
31
+ }
8
32
  export function scanLifecycleFolders(vbriefDir) {
9
33
  const items = [];
10
34
  for (const folderName of PROJECT_LIFECYCLE_FOLDERS) {
@@ -25,11 +49,11 @@ export function scanLifecycleFolders(vbriefDir) {
25
49
  try {
26
50
  const data = JSON.parse(readFileSync(full, "utf8"));
27
51
  const plan = (data.plan ?? {});
28
- const title = String(plan.title ?? vbriefFile.replace(/\.vbrief\.json$/, ""));
52
+ const title = String(plan.title ?? stripArtifactSuffix(vbriefFile));
29
53
  const status = deriveRegistryItemStatus(plan.status, folderName);
30
54
  const references = registryMetadataReferencesFromScope(plan.references);
31
55
  const item = {
32
- id: vbriefFile.replace(/\.vbrief\.json$/, "").replace(/\.vbrief$/, ""),
56
+ id: stripArtifactSuffix(vbriefFile),
33
57
  title,
34
58
  status,
35
59
  metadata: {
@@ -44,7 +68,7 @@ export function scanLifecycleFolders(vbriefDir) {
44
68
  }
45
69
  catch {
46
70
  items.push({
47
- id: vbriefFile.replace(/\.vbrief\.json$/, "").replace(/\.vbrief$/, ""),
71
+ id: stripArtifactSuffix(vbriefFile),
48
72
  title: `[unreadable] ${vbriefFile}`,
49
73
  status: "draft",
50
74
  metadata: {
@@ -136,12 +160,12 @@ export function computeStalenessFlags(narratives, completedItems, review = null)
136
160
  const pending = unacknowledgedCompletedItems(completedItems, review);
137
161
  return flagStaleNarratives(narratives, pending);
138
162
  }
139
- export function createSkeleton(items, now) {
163
+ export function createSkeleton(items, now, layout) {
140
164
  const completedItems = items.filter((i) => i.status === "completed");
141
165
  const stalenessFlags = computeStalenessFlags({ ...SKELETON_NARRATIVES }, completedItems);
142
166
  return {
143
- vBRIEFInfo: {
144
- version: EMITTED_VBRIEF_VERSION,
167
+ [layout.infoRootKey]: {
168
+ version: layout.infoVersion,
145
169
  description: "Project definition -- synthesized gestalt of the project",
146
170
  created: now,
147
171
  updated: now,
@@ -155,11 +179,19 @@ export function createSkeleton(items, now) {
155
179
  },
156
180
  };
157
181
  }
158
- /** Regenerate PROJECT-DEFINITION.vbrief.json (mirrors ``scripts/project_render.render_project_definition``). */
182
+ /**
183
+ * Regenerate the PROJECT-DEFINITION artifact for `vbriefDir`.
184
+ *
185
+ * Layout-aware (#2149): on a migrated `xbrief/` root it targets
186
+ * `PROJECT-DEFINITION.xbrief.json` with an `xBRIEFInfo` envelope; on a legacy `vbrief/`
187
+ * root it keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
188
+ * (Mirrors ``scripts/project_render.render_project_definition``.)
189
+ */
159
190
  export function renderProjectDefinition(vbriefDir, options = {}) {
160
191
  const nowDate = options.now ?? new Date();
161
192
  const now = nowDate.toISOString().replace(/\.\d{3}Z$/, "Z");
162
- const projectDefPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
193
+ const layout = resolveProjectDefinitionLayout(vbriefDir);
194
+ const projectDefPath = join(vbriefDir, layout.filename);
163
195
  const items = scanLifecycleFolders(vbriefDir);
164
196
  const createdNew = !existsSync(projectDefPath);
165
197
  let projectDef;
@@ -172,10 +204,11 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
172
204
  }
173
205
  const plan = (projectDef.plan ?? {});
174
206
  plan.items = items;
175
- if (typeof projectDef.vBRIEFInfo !== "object" || projectDef.vBRIEFInfo === null) {
176
- projectDef.vBRIEFInfo = {};
207
+ if (typeof projectDef[layout.infoRootKey] !== "object" ||
208
+ projectDef[layout.infoRootKey] === null) {
209
+ projectDef[layout.infoRootKey] = {};
177
210
  }
178
- projectDef.vBRIEFInfo.updated = now;
211
+ projectDef[layout.infoRootKey].updated = now;
179
212
  const narratives = typeof plan.narratives === "object" &&
180
213
  plan.narratives !== null &&
181
214
  !Array.isArray(plan.narratives)
@@ -194,7 +227,7 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
194
227
  projectDef.plan = plan;
195
228
  }
196
229
  else {
197
- projectDef = createSkeleton(items, now);
230
+ projectDef = createSkeleton(items, now, layout);
198
231
  }
199
232
  mkdirSync(vbriefDir, { recursive: true });
200
233
  writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
@@ -202,7 +235,7 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
202
235
  const planMeta = (projectDef.plan?.metadata ?? {});
203
236
  const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
204
237
  const action = createdNew ? "created" : "updated";
205
- const parts = [`✓ PROJECT-DEFINITION.vbrief.json ${action} (${itemCount} scope items)`];
238
+ const parts = [`✓ ${layout.filename} ${action} (${itemCount} scope items)`];
206
239
  if (flagCount > 0)
207
240
  parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
208
241
  return [true, parts.join("\n")];
@@ -216,7 +249,8 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
216
249
  export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
217
250
  const nowDate = options.now ?? new Date();
218
251
  const now = isoTimestamp(nowDate);
219
- const projectDefPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
252
+ const layout = resolveProjectDefinitionLayout(vbriefDir);
253
+ const projectDefPath = join(vbriefDir, layout.filename);
220
254
  if (!existsSync(projectDefPath)) {
221
255
  return [false, `✗ ${projectDefPath} not found — run project:render first`];
222
256
  }
@@ -245,10 +279,11 @@ export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
245
279
  ? plan.narratives
246
280
  : {};
247
281
  planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
248
- if (typeof projectDef.vBRIEFInfo !== "object" || projectDef.vBRIEFInfo === null) {
249
- projectDef.vBRIEFInfo = {};
282
+ if (typeof projectDef[layout.infoRootKey] !== "object" ||
283
+ projectDef[layout.infoRootKey] === null) {
284
+ projectDef[layout.infoRootKey] = {};
250
285
  }
251
- projectDef.vBRIEFInfo.updated = now;
286
+ projectDef[layout.infoRootKey].updated = now;
252
287
  projectDef.plan = plan;
253
288
  writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
254
289
  const ackCount = completedItems.length;
@@ -280,9 +315,7 @@ export function main(argv) {
280
315
  process.stderr.write("Usage: project-render [--acknowledge-staleness] [--project-root <dir>] [vbrief_dir]\n");
281
316
  return 2;
282
317
  }
283
- const vbriefDir = projectRoot !== undefined
284
- ? resolveLifecycleRoot(resolve(projectRoot))
285
- : (positional[0] ?? "vbrief");
318
+ const vbriefDir = positional[0] ?? resolveLifecycleRoot(resolve(projectRoot !== undefined ? projectRoot : "."));
286
319
  const [ok, message] = acknowledge
287
320
  ? acknowledgeProjectDefinitionStaleness(vbriefDir)
288
321
  : renderProjectDefinition(vbriefDir);
@@ -68,6 +68,12 @@ export interface LaunchArgs {
68
68
  readinessGate?: ReadinessGateFn;
69
69
  worktreeResolver?: WorktreeResolverFn;
70
70
  runtimeAuthProbe?: RuntimeAuthProbeFn;
71
+ /**
72
+ * Injection seam for the routing-provider environment lookup, mirroring
73
+ * `resolveRoutingPath`'s `environ` parameter (#1877 Greptile follow-up).
74
+ * Defaults to `process.env` when unset.
75
+ */
76
+ environ?: NodeJS.ProcessEnv;
71
77
  }
72
78
  export declare function swarmLaunch(args: LaunchArgs): {
73
79
  exitCode: number;
@@ -8,7 +8,7 @@ import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-w
8
8
  import { selectionOrderingKey } from "../triage/queue/selection.js";
9
9
  import { DEFAULT_BASE_BRANCH, EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK, GATE_ADVISE, GATE_ENFORCE, LEAF_CODING_WORKER_ROLE, } from "./constants.js";
10
10
  import { readinessReport } from "./readiness.js";
11
- import { dispatchProviderFromRuntime, loadRoutingFile, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
11
+ import { loadRoutingFile, resolveDispatchProvider, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
12
12
  import { dispatchProviderFor, enforceSubagentBackendPolicy } from "./subagent-backend.js";
13
13
  import { resolveWorktreeMap } from "./worktrees.js";
14
14
  export const defaultPreflightGate = (vbriefPath) => {
@@ -525,7 +525,7 @@ export function swarmLaunch(args) {
525
525
  let modelSource = null;
526
526
  let routingProvider = null;
527
527
  if (routingFile !== null) {
528
- routingProvider = dispatchProviderFromRuntime(runtimeMode);
528
+ routingProvider = resolveDispatchProvider(args.environ ?? process.env);
529
529
  const route = resolveModelRoute(routingFile, routingProvider, LEAF_CODING_WORKER_ROLE);
530
530
  // A malformed decision object must fail loud here: the legacy backend gate
531
531
  // was already bypassed above (routingFile !== null), so silently continuing
@@ -9,7 +9,7 @@ export interface VerifyRoutingOptions {
9
9
  advise?: boolean;
10
10
  /** Override the resolved provider (else derived from the runtime). */
11
11
  provider?: string | null;
12
- /** Inject the runtime descriptor (else getPlatformCapabilities().runtimeMode). */
12
+ /** Inject the runtime descriptor (legacy test seam; else resolveDispatchProvider). */
13
13
  runtimeProbe?: () => string;
14
14
  }
15
15
  export interface VerifyRoutingResult {
@@ -8,9 +8,8 @@
8
8
  * 1 = at least one in-scope role is undecided / not dispatchable
9
9
  * 2 = config error (unreadable / malformed route file)
10
10
  */
11
- import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
12
11
  import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
13
- import { dispatchProviderFromRuntime, HARNESS_BOUND_PROVIDERS, loadRoutingFile, ROUTING_MODE_HARNESS_DEFAULT, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
12
+ import { dispatchProviderFromRuntime, HARNESS_BOUND_PROVIDERS, loadRoutingFile, ROUTING_MODE_HARNESS_DEFAULT, resolveDispatchProvider, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
14
13
  export const ROUTING_SET_CMD = "task swarm:routing-set -- --role <role> --model <slug> (or --harness-default)";
15
14
  /** Roles the pre-dispatch gate checks by default: the actual model lever. */
16
15
  export const DEFAULT_GATED_ROLES = ["leaf-implementation"];
@@ -18,15 +17,17 @@ function resolveProvider(options) {
18
17
  if (options.provider !== undefined && options.provider !== null && options.provider.length > 0) {
19
18
  return options.provider;
20
19
  }
21
- const probe = options.runtimeProbe ?? (() => getPlatformCapabilities().runtimeMode);
22
- let runtimeMode = "";
23
- try {
24
- runtimeMode = probe();
25
- }
26
- catch {
27
- runtimeMode = "";
20
+ if (options.runtimeProbe !== undefined) {
21
+ let runtimeMode = "";
22
+ try {
23
+ runtimeMode = options.runtimeProbe();
24
+ }
25
+ catch {
26
+ runtimeMode = "";
27
+ }
28
+ return dispatchProviderFromRuntime(runtimeMode);
28
29
  }
29
- return dispatchProviderFromRuntime(runtimeMode);
30
+ return resolveDispatchProvider(options.environ ?? process.env);
30
31
  }
31
32
  export function verifyRouting(options) {
32
33
  const roles = options.roles && options.roles.length > 0 ? options.roles : DEFAULT_GATED_ROLES;
@@ -9,6 +9,8 @@ export declare const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
9
9
  export declare const ROUTING_FILENAME = "routing.local.json";
10
10
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
11
11
  export declare const HARNESS_BOUND_PROVIDERS: Set<string>;
12
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
13
+ export declare const ROUTING_GATED_DISPATCH_PROVIDERS: Set<string>;
12
14
  export interface RouteDecision {
13
15
  model: string | null;
14
16
  mode?: string;
@@ -48,6 +50,13 @@ export declare function loadRoutingFile(path: string): {
48
50
  export declare function resolveModelRoute(file: RoutingFile | null, provider: string, role: string): RouteResolution;
49
51
  /** Map a runtime descriptor (platform-capabilities.runtimeMode) to a route key. */
50
52
  export declare function dispatchProviderFromRuntime(runtimeMode: string): string;
53
+ /**
54
+ * Resolve the `dispatch_provider` routing key from the active runtime envelope.
55
+ * Separate from `runtime_mode` (#1557): Cursor sessions may carry
56
+ * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
57
+ * `cursor` for model selection (#1877).
58
+ */
59
+ export declare function resolveDispatchProvider(environ?: NodeJS.ProcessEnv): string;
51
60
  /**
52
61
  * Write a decision back to the route file (create-if-missing). Stamps
53
62
  * `decidedAt` when the caller did not supply one. Used by the interactive
@@ -31,6 +31,12 @@ export const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
31
31
  export const ROUTING_FILENAME = "routing.local.json";
32
32
  /** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
33
33
  export const HARNESS_BOUND_PROVIDERS = new Set(["grok"]);
34
+ /** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
35
+ export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set(["cursor", "grok"]);
36
+ const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
37
+ function envTruthy(environ, name) {
38
+ return TRUTHY_ENV.has((environ[name] ?? "").trim().toLowerCase());
39
+ }
34
40
  /**
35
41
  * Resolve the route-file path. Honors the `DEFT_ROUTING_PATH` override first
36
42
  * (keeps both maintainer and consumer testable), then reads from the MAIN
@@ -150,12 +156,52 @@ export function dispatchProviderFromRuntime(runtimeMode) {
150
156
  }
151
157
  return normalized;
152
158
  }
159
+ /**
160
+ * Resolve the `dispatch_provider` routing key from the active runtime envelope.
161
+ * Separate from `runtime_mode` (#1557): Cursor sessions may carry
162
+ * `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
163
+ * `cursor` for model selection (#1877).
164
+ */
165
+ export function resolveDispatchProvider(environ = process.env) {
166
+ if (envTruthy(environ, "CURSOR_COMPOSER") || envTruthy(environ, "CURSOR_AGENT")) {
167
+ return "cursor";
168
+ }
169
+ const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
170
+ if (envTruthy(environ, "GROK_BUILD") || runtime === "grok-build") {
171
+ return "grok";
172
+ }
173
+ if (runtime === "cloud" || runtime === "headless") {
174
+ return "cloud-headless";
175
+ }
176
+ if (envTruthy(environ, "GITHUB_ACTIONS") ||
177
+ envTruthy(environ, "BUILDKITE") ||
178
+ (envTruthy(environ, "CI") &&
179
+ !envTruthy(environ, "CURSOR_COMPOSER") &&
180
+ !envTruthy(environ, "CURSOR_AGENT"))) {
181
+ return "cloud-headless";
182
+ }
183
+ return "unknown";
184
+ }
185
+ /**
186
+ * Keys that would mutate the prototype chain rather than set an own property
187
+ * if used as a computed object key. Rejected for provider/role names so a
188
+ * malicious routing input cannot pollute `Object.prototype` (CodeQL
189
+ * js/prototype-polluting-assignment).
190
+ */
191
+ const FORBIDDEN_ROUTING_KEYS = new Set(["__proto__", "constructor", "prototype"]);
192
+ function assertSafeRoutingKey(kind, key) {
193
+ if (FORBIDDEN_ROUTING_KEYS.has(key)) {
194
+ throw new Error(`routing ${kind} name "${key}" is not allowed`);
195
+ }
196
+ }
153
197
  /**
154
198
  * Write a decision back to the route file (create-if-missing). Stamps
155
199
  * `decidedAt` when the caller did not supply one. Used by the interactive
156
200
  * resolver path (resolver step 5) and the `swarm:routing-set` task.
157
201
  */
158
202
  export function writeModelDecision(path, provider, role, decision) {
203
+ assertSafeRoutingKey("provider", provider);
204
+ assertSafeRoutingKey("role", role);
159
205
  const { data } = loadRoutingFile(path);
160
206
  const file = data ?? {};
161
207
  const existing = providerBlockOf(file, provider);
@@ -4,8 +4,11 @@ import { verifyReviewClean } from "./verify-review-clean.js";
4
4
  export function verifyReviewCleanMain(argv = process.argv.slice(2)) {
5
5
  const prNumbers = [];
6
6
  const cohortGlobs = [];
7
+ const ciIgnoreChecks = [];
7
8
  let repo = null;
8
9
  let emitJson = false;
10
+ let skipCi = false;
11
+ let skipSlizard = false;
9
12
  for (let i = 0; i < argv.length; i += 1) {
10
13
  const arg = argv[i];
11
14
  if (arg === "--cohort" && argv[i + 1] !== undefined) {
@@ -19,11 +22,32 @@ export function verifyReviewCleanMain(argv = process.argv.slice(2)) {
19
22
  else if (arg === "--json") {
20
23
  emitJson = true;
21
24
  }
25
+ else if (arg === "--skip-ci") {
26
+ skipCi = true;
27
+ }
28
+ else if (arg === "--skip-slizard") {
29
+ skipSlizard = true;
30
+ }
31
+ else if (arg === "--ci-ignore-check" && argv[i + 1] !== undefined) {
32
+ ciIgnoreChecks.push(argv[i + 1] ?? "");
33
+ i += 1;
34
+ }
35
+ else if (arg?.startsWith("--ci-ignore-check=")) {
36
+ ciIgnoreChecks.push(arg.slice("--ci-ignore-check=".length));
37
+ }
22
38
  else if (arg !== undefined && /^\d+$/.test(arg)) {
23
39
  prNumbers.push(Number.parseInt(arg, 10));
24
40
  }
25
41
  }
26
- const result = verifyReviewClean({ prNumbers, cohortGlobs, repo, emitJson });
42
+ const result = verifyReviewClean({
43
+ prNumbers,
44
+ cohortGlobs,
45
+ repo,
46
+ emitJson,
47
+ skipCi,
48
+ skipSlizard,
49
+ ciIgnoreChecks,
50
+ });
27
51
  if (result.stdout.length > 0) {
28
52
  process.stdout.write(result.stdout);
29
53
  }
@@ -1,4 +1,7 @@
1
+ import type { CiGateOptions } from "../pr-merge-readiness/ci-gate.js";
2
+ import type { SlizardGateOptions } from "../pr-merge-readiness/slizard-gate.js";
1
3
  import type { RunGhFn } from "../pr-merge-readiness/types.js";
4
+ type ReviewGateOptions = CiGateOptions & SlizardGateOptions;
2
5
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
3
6
  export interface CohortResolutionError {
4
7
  vbrief_path: string;
@@ -10,6 +13,8 @@ export interface CohortPRResult {
10
13
  verdict: Record<string, unknown>;
11
14
  failures: string[];
12
15
  clean: boolean;
16
+ ci_summary: Record<string, unknown> | null;
17
+ slizard_summary: Record<string, unknown> | null;
13
18
  }
14
19
  export interface CohortResult {
15
20
  repo: string | null;
@@ -21,7 +26,7 @@ export declare function resolveCohortFromVbriefs(vbriefGlobs: readonly string[])
21
26
  prNumbers: number[];
22
27
  failures: CohortResolutionError[];
23
28
  };
24
- export declare function evaluatePr(prNumber: number, repo: string | null, runGh?: RunGhFn): CohortPRResult | null;
29
+ export declare function evaluatePr(prNumber: number, repo: string | null, runGh?: RunGhFn, options?: ReviewGateOptions): CohortPRResult | null;
25
30
  export declare function cohortResultToDict(cohort: CohortResult): Record<string, unknown>;
26
31
  export declare function renderReviewCleanText(cohort: CohortResult): string;
27
32
  export interface VerifyReviewCleanArgs {
@@ -29,6 +34,9 @@ export interface VerifyReviewCleanArgs {
29
34
  cohortGlobs?: readonly string[];
30
35
  repo?: string | null;
31
36
  emitJson?: boolean;
37
+ skipCi?: boolean;
38
+ skipSlizard?: boolean;
39
+ ciIgnoreChecks?: readonly string[];
32
40
  runGh?: RunGhFn;
33
41
  }
34
42
  export declare function verifyReviewClean(args: VerifyReviewCleanArgs): {
@@ -1,8 +1,10 @@
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 { defaultRunGh, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
4
+ import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
5
+ import { defaultRunGh, fetchCheckRunsRest, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
5
6
  import { evaluateGates, parseGreptileBody } from "../pr-merge-readiness/index.js";
7
+ import { evaluateSlizardGate, isSlizardCheck } from "../pr-merge-readiness/slizard-gate.js";
6
8
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
7
9
  function extractPrFromUri(uri) {
8
10
  const pullIdx = uri.indexOf("/pull/");
@@ -101,7 +103,7 @@ export function resolveCohortFromVbriefs(vbriefGlobs) {
101
103
  }
102
104
  return { prNumbers: seenPrs, failures };
103
105
  }
104
- export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
106
+ export function evaluatePr(prNumber, repo, runGh = defaultRunGh, options = {}) {
105
107
  const headSha = fetchPrHeadSha(prNumber, repo, runGh);
106
108
  if (headSha === null) {
107
109
  return null;
@@ -112,12 +114,71 @@ export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
112
114
  }
113
115
  const verdict = parseGreptileBody(body);
114
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
+ }
115
174
  return {
116
175
  pr_number: prNumber,
117
176
  head_sha: headSha,
118
177
  verdict: { ...verdict },
119
178
  failures: [...failures],
120
179
  clean: failures.length === 0,
180
+ ci_summary: ciSummary,
181
+ slizard_summary: slizardSummary,
121
182
  };
122
183
  }
123
184
  export function cohortResultToDict(cohort) {
@@ -131,6 +192,8 @@ export function cohortResultToDict(cohort) {
131
192
  clean: r.clean,
132
193
  verdict: r.verdict,
133
194
  failures: r.failures,
195
+ ci_summary: r.ci_summary,
196
+ slizard_summary: r.slizard_summary,
134
197
  })),
135
198
  resolution_errors: cohort.resolution_errors,
136
199
  };
@@ -159,6 +222,12 @@ export function renderReviewCleanText(cohort) {
159
222
  lines.push(` Findings: P0=${String(v.p0Count ?? 0)} ` +
160
223
  `P1=${String(v.p1Count ?? 0)} P2=${String(v.p2Count ?? 0)}`);
161
224
  lines.push(` Errored sentinel: ${v.errored === true ? "True" : "False"}`);
225
+ if (r.ci_summary && typeof r.ci_summary.summary_line === "string") {
226
+ lines.push(` ${r.ci_summary.summary_line}`);
227
+ }
228
+ if (r.slizard_summary && typeof r.slizard_summary.summary_line === "string") {
229
+ lines.push(` ${r.slizard_summary.summary_line}`);
230
+ }
162
231
  r.failures.forEach((fail, index) => {
163
232
  lines.push(` [${index + 1}] ${fail}`);
164
233
  });
@@ -212,7 +281,11 @@ export function verifyReviewClean(args) {
212
281
  const runGh = args.runGh ?? defaultRunGh;
213
282
  const prResults = [];
214
283
  for (const prNum of prNumbers) {
215
- const perPr = evaluatePr(prNum, args.repo ?? null, runGh);
284
+ const perPr = evaluatePr(prNum, args.repo ?? null, runGh, {
285
+ skipCi: args.skipCi,
286
+ skipSlizard: args.skipSlizard,
287
+ ignoreCheckNames: args.ciIgnoreChecks,
288
+ });
216
289
  if (perPr === null) {
217
290
  return { exitCode: EXIT_EXTERNAL_ERROR, stdout: "", stderr: "" };
218
291
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * biome-config.ts -- guard against #2190 (biome check . reports
3
+ * recommended-preset diagnostics as errors vs warnings non-deterministically
4
+ * on a pinned biome version).
5
+ *
6
+ * A preset-inherited rule severity can silently change tier across a biome
7
+ * version bump (or, per #2190, apparently even across runs on the identical
8
+ * pinned version). Pinning `noUnusedVariables` / `noNonNullAssertion` to an
9
+ * explicit non-"error" severity in biome.json makes the tier config-owned so
10
+ * it cannot flip a required CI check to failing on pre-existing diagnostics.
11
+ */
12
+ /** The two rules #2190 observed flipping severity in CI (see docs/analysis/2026-07-02-2190-biome-determinism.md). */
13
+ export declare const GUARDED_RULES: ReadonlyArray<{
14
+ readonly group: string;
15
+ readonly rule: string;
16
+ }>;
17
+ export interface RuleSeverityFinding {
18
+ readonly group: string;
19
+ readonly rule: string;
20
+ /** The literal severity value found, or null when the rule has no explicit entry. */
21
+ readonly severity: string | null;
22
+ }
23
+ export interface BiomeConfigGuardResult {
24
+ readonly code: 0 | 1 | 2;
25
+ readonly findings: readonly RuleSeverityFinding[];
26
+ readonly message: string;
27
+ }
28
+ /**
29
+ * Parse a biome.json document (already JSON.parse'd) and report the explicit
30
+ * severity declared for each guarded rule.
31
+ */
32
+ export declare function findRuleSeverities(biomeConfig: unknown, rules?: ReadonlyArray<{
33
+ readonly group: string;
34
+ readonly rule: string;
35
+ }>): RuleSeverityFinding[];
36
+ /**
37
+ * Evaluate biome.json at `projectRoot` and fail closed when a guarded rule
38
+ * has no explicit severity, or an explicit severity of "error".
39
+ */
40
+ export declare function evaluateBiomeConfigGuard(projectRoot?: string): BiomeConfigGuardResult;
41
+ //# sourceMappingURL=biome-config.d.ts.map