@deftai/directive-core 0.78.0 → 0.79.1

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 (101) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.js +13 -2
  8. package/dist/doctor/payload-staleness.js +7 -1
  9. package/dist/hooks/dispatcher.d.ts +14 -2
  10. package/dist/hooks/dispatcher.js +77 -20
  11. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  12. package/dist/init-deposit/agent-hooks.js +10 -0
  13. package/dist/init-deposit/hygiene.js +1 -0
  14. package/dist/init-deposit/init-deposit.js +2 -0
  15. package/dist/init-deposit/prettierignore.d.ts +21 -0
  16. package/dist/init-deposit/prettierignore.js +83 -0
  17. package/dist/init-deposit/refresh.d.ts +3 -1
  18. package/dist/init-deposit/refresh.js +29 -40
  19. package/dist/init-deposit/scaffold.js +6 -12
  20. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  21. package/dist/init-deposit/xbrief-projections.js +117 -0
  22. package/dist/intake/reconcile-issues.d.ts +10 -0
  23. package/dist/intake/reconcile-issues.js +121 -3
  24. package/dist/issue-sync/index.d.ts +2 -0
  25. package/dist/issue-sync/index.js +2 -0
  26. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  27. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  28. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  29. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  30. package/dist/platform/index.d.ts +1 -0
  31. package/dist/platform/index.js +1 -0
  32. package/dist/platform/platform-capabilities.d.ts +5 -0
  33. package/dist/platform/platform-capabilities.js +9 -0
  34. package/dist/platform/shell-context.d.ts +30 -0
  35. package/dist/platform/shell-context.js +111 -0
  36. package/dist/pr-monitor/cadence.d.ts +6 -0
  37. package/dist/pr-monitor/cadence.js +13 -0
  38. package/dist/pr-monitor/index.d.ts +1 -1
  39. package/dist/pr-monitor/index.js +1 -1
  40. package/dist/pr-monitor/monitor.d.ts +7 -0
  41. package/dist/pr-monitor/monitor.js +59 -13
  42. package/dist/pr-wait-mergeable/wrappers.d.ts +9 -0
  43. package/dist/pr-wait-mergeable/wrappers.js +31 -2
  44. package/dist/preflight-cache/evaluate.d.ts +3 -8
  45. package/dist/preflight-cache/evaluate.js +19 -19
  46. package/dist/release/constants.d.ts +2 -0
  47. package/dist/release/constants.js +10 -1
  48. package/dist/release/flags.js +26 -0
  49. package/dist/release/issue-state-fetch.d.ts +27 -0
  50. package/dist/release/issue-state-fetch.js +146 -0
  51. package/dist/release/main.js +1 -0
  52. package/dist/release/native-steps.js +6 -4
  53. package/dist/release/pipeline.js +15 -5
  54. package/dist/release/preflight.d.ts +6 -2
  55. package/dist/release/preflight.js +19 -5
  56. package/dist/release/types.d.ts +3 -1
  57. package/dist/release-e2e/constants.js +6 -6
  58. package/dist/release-e2e/flags.d.ts +2 -0
  59. package/dist/release-e2e/flags.js +8 -4
  60. package/dist/release-e2e/main.js +15 -9
  61. package/dist/release-e2e/types.d.ts +4 -2
  62. package/dist/resolution/classify.d.ts +1 -1
  63. package/dist/resolution/classify.js +36 -11
  64. package/dist/scope/project-definition-sync.js +2 -150
  65. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  66. package/dist/scope/registry-artifact-sync.js +159 -0
  67. package/dist/scope/specification-sync.d.ts +3 -0
  68. package/dist/scope/specification-sync.js +16 -0
  69. package/dist/scope/transition.js +40 -12
  70. package/dist/session/cache-recovery.d.ts +19 -0
  71. package/dist/session/cache-recovery.js +26 -0
  72. package/dist/session/session-start.d.ts +2 -0
  73. package/dist/session/session-start.js +14 -3
  74. package/dist/session/verify-session-ritual.d.ts +1 -0
  75. package/dist/session/verify-session-ritual.js +1 -0
  76. package/dist/triage/welcome/default-mode.js +2 -0
  77. package/dist/triage/welcome/onboard.js +2 -0
  78. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  79. package/dist/ts-check-lane/run-lane.js +9 -0
  80. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  81. package/dist/vbrief-reconcile/labels.js +14 -0
  82. package/dist/vbrief-reconcile/main.d.ts +1 -0
  83. package/dist/vbrief-reconcile/main.js +13 -3
  84. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  85. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  86. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  87. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  88. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  89. package/dist/verify-env/agent-hooks.js +4 -3
  90. package/dist/verify-env/verify-hooks-installed.js +5 -5
  91. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  92. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  93. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  94. package/dist/vitest-runner/coverage-debt.js +155 -0
  95. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  96. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  97. package/dist/xbrief-migrate/detect.js +46 -1
  98. package/dist/xbrief-migrate/migrate-project.js +19 -4
  99. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  100. package/dist/xbrief-migrate/migration-containment.js +37 -0
  101. package/package.json +7 -3
@@ -0,0 +1,26 @@
1
+ import { formatFrameworkCommand } from "../render/framework-commands.js";
2
+ const CACHE_FETCH_SOURCE = "github-issue";
3
+ /**
4
+ * Format a consumer-runnable `deft cache fetch-all` recovery command (#2574).
5
+ *
6
+ * Uses the space-separated CLI surface (not the colon task alias) and always
7
+ * includes required `--source` / `--repo` flags so following the hint literally
8
+ * reaches the handler instead of `unknown verb 'cache:fetch-all'`.
9
+ */
10
+ export function formatCacheFetchAllRecoveryCommand(repo, options = {}) {
11
+ const repoSlug = repo ?? "OWNER/NAME";
12
+ const args = ["cache", "fetch-all", "--source", CACHE_FETCH_SOURCE, "--repo", repoSlug];
13
+ if (options.force) {
14
+ args.push("--force");
15
+ }
16
+ return formatFrameworkCommand(args);
17
+ }
18
+ /**
19
+ * Branch-aware recovery hint (#1953 Option 3, #2574 argv fix).
20
+ * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
21
+ */
22
+ export function recoveryHintForStaleFailure(causes, repo = null) {
23
+ const command = formatCacheFetchAllRecoveryCommand(repo, { force: causes.ageStale });
24
+ return ` Recovery: run \`${command}\` to refresh and reconcile upstream state.`;
25
+ }
26
+ //# sourceMappingURL=cache-recovery.js.map
@@ -1,3 +1,4 @@
1
+ import { type EnvironmentContext } from "../platform/shell-context.js";
1
2
  import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
2
3
  import type { GitRunner } from "./git.js";
3
4
  import { ritualStatePath } from "./ritual-sentinel.js";
@@ -40,6 +41,7 @@ export interface SessionStartOptions {
40
41
  exitCode: number;
41
42
  };
42
43
  readonly resolveUserMd?: (projectRoot: string) => ResolveUserMdResult;
44
+ readonly probeEnvironment?: () => EnvironmentContext;
43
45
  }
44
46
  export declare function parseDeferrals(rawValues: readonly string[]): {
45
47
  deferrals: Record<string, string>;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { runningInsideDeftRepo } from "../doctor/paths.js";
3
3
  import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
+ import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
5
6
  import { disclosureLine } from "../policy/disclosure.js";
6
7
  import { resolvePolicy } from "../policy/resolve.js";
7
8
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
@@ -187,7 +188,7 @@ export function defaultBranchSync(projectRoot, runGit = defaultGitRunner) {
187
188
  }
188
189
  return { branch, upstream, ahead, behind, warning };
189
190
  }
190
- function runReadOnlySessionStart(projectRoot, options, instant) {
191
+ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
191
192
  const lines = [];
192
193
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
193
194
  const userMd = resolveUserMd(projectRoot);
@@ -198,6 +199,7 @@ function runReadOnlySessionStart(projectRoot, options, instant) {
198
199
  : safeDiagnostic;
199
200
  lines.push(READ_ONLY_ALIGNMENT_MESSAGE);
200
201
  lines.push(userMdLine);
202
+ lines.push(formatEnvironmentContext(environment));
201
203
  const resultPayload = {
202
204
  ready: true,
203
205
  exit_code: 0,
@@ -217,6 +219,7 @@ function runReadOnlySessionStart(projectRoot, options, instant) {
217
219
  found: userMd.found,
218
220
  diagnostic: userMd.diagnostic,
219
221
  },
222
+ environment: environmentContextToDict(environment),
220
223
  message: READ_ONLY_RESULT_MESSAGE,
221
224
  };
222
225
  return { code: 0, payload: resultPayload, lines };
@@ -226,16 +229,22 @@ export function runSessionStart(projectRoot, options = {}) {
226
229
  const instant = options.now ?? new Date();
227
230
  const deferrals = options.deferrals ?? {};
228
231
  const runGit = options.runGit ?? defaultGitRunner;
232
+ const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
229
233
  if (posture === READ_ONLY_POSTURE) {
230
- return runReadOnlySessionStart(projectRoot, options, instant);
234
+ return runReadOnlySessionStart(projectRoot, options, instant, environment);
231
235
  }
232
236
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
233
237
  if (gitHeadValue === null) {
234
238
  const payload = {
235
239
  ready: false,
240
+ environment: environmentContextToDict(environment),
236
241
  message: gitError ?? "could not resolve git HEAD",
237
242
  };
238
- return { code: 2, payload, lines: [payload.message] };
243
+ return {
244
+ code: 2,
245
+ payload,
246
+ lines: [formatEnvironmentContext(environment), payload.message],
247
+ };
239
248
  }
240
249
  const quickSteps = recordDeferredSteps(QUICK_STEPS, deferrals, instant);
241
250
  const gatedSteps = recordDeferredSteps(GATED_STEPS, deferrals, instant);
@@ -265,6 +274,7 @@ export function runSessionStart(projectRoot, options = {}) {
265
274
  lines.push(message);
266
275
  lines.push(userMdLine);
267
276
  }
277
+ lines.push(formatEnvironmentContext(environment));
268
278
  if (!quickSteps.branch_policy) {
269
279
  const result = resolvePolicy(projectRoot);
270
280
  const message = disclosureLine(result);
@@ -377,6 +387,7 @@ export function runSessionStart(projectRoot, options = {}) {
377
387
  found: userMd.found,
378
388
  diagnostic: userMd.diagnostic,
379
389
  },
390
+ environment: environmentContextToDict(environment),
380
391
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
381
392
  };
382
393
  return { code, payload: resultPayload, lines };
@@ -1,5 +1,6 @@
1
1
  import { type GitRunner } from "./git.js";
2
2
  import { type DirectivePosture } from "./posture.js";
3
+ export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
3
4
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
4
5
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
5
6
  export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<string, readonly string[]>>;
@@ -7,6 +7,7 @@ import { defaultRitualRunner } from "./ritual-entrypoint.js";
7
7
  import { readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
8
8
  import { GATED_STEPS, QUICK_STEPS } from "./session-start.js";
9
9
  import { resolveSessionRitualStalenessHours } from "./staleness.js";
10
+ export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
10
11
  export const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
11
12
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
12
13
  export const GATED_ENTRYPOINT_COMMANDS = {
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { ensureTriageCacheHydrated } from "../../cache/empty-populate.js";
2
3
  import { maybeSelfHealCache } from "../../cache/fetch.js";
3
4
  import { FIRST_TIME_NUDGE, INCOMPLETE_NUDGE_TEMPLATE } from "./constants.js";
4
5
  import { classifyOnboarding, detectPriorState } from "./prior-state.js";
@@ -32,6 +33,7 @@ export function runDefaultMode(projectRoot, options = {}) {
32
33
  };
33
34
  const heal = options.selfHealFn ??
34
35
  ((root) => {
36
+ ensureTriageCacheHydrated(resolve(root));
35
37
  maybeSelfHealCache(resolve(root));
36
38
  });
37
39
  heal(projectRoot);
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { ensureTriageCacheHydrated } from "../../cache/empty-populate.js";
3
4
  import { maybeSelfHealCache } from "../../cache/fetch.js";
4
5
  import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
5
6
  import { DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, TRIAGE_SKILL_PATH } from "./constants.js";
@@ -57,6 +58,7 @@ export function runOnboardMode(projectRoot, options = {}) {
57
58
  }
58
59
  const heal = options.selfHealFn ??
59
60
  ((root) => {
61
+ ensureTriageCacheHydrated(resolve(root));
60
62
  maybeSelfHealCache(resolve(root));
61
63
  });
62
64
  heal(projectRoot);
@@ -41,6 +41,11 @@ export interface RunTsLaneOptions {
41
41
  readonly runner?: LaneRunner;
42
42
  /** Injected sink for human-facing notices (defaults to stdout). */
43
43
  readonly out?: (message: string) => void;
44
+ /**
45
+ * Env used to resolve release coverage-debt before sanitizeTsLaneEnv strips
46
+ * DEFT_RELEASE_PREFLIGHT (#2618). Defaults to process.env.
47
+ */
48
+ readonly env?: NodeJS.ProcessEnv;
44
49
  }
45
50
  /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
46
51
  export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
@@ -20,6 +20,7 @@ import { spawnSync } from "node:child_process";
20
20
  import { existsSync } from "node:fs";
21
21
  import { posix, win32 } from "node:path";
22
22
  import { BRANCH_GATE_BYPASS_ENV, RELEASE_PREFLIGHT_ENV } from "../release/constants.js";
23
+ import { resolveCoverageDebtIssue } from "../vitest-runner/coverage-debt.js";
23
24
  /** Release Step-5 vars that must not leak into vitest via inherited pnpm env (#2434). */
24
25
  const TS_LANE_POISON_ENV_KEYS = [BRANCH_GATE_BYPASS_ENV, RELEASE_PREFLIGHT_ENV];
25
26
  /**
@@ -96,12 +97,20 @@ export function runTsLane(projectRoot, options) {
96
97
  const { pnpm } = options;
97
98
  const runner = options.runner ?? defaultRunner;
98
99
  const out = options.out ?? ((message) => process.stdout.write(`${message}\n`));
100
+ // Resolve debt from the pre-sanitize env: sanitizeTsLaneEnv strips
101
+ // DEFT_RELEASE_PREFLIGHT (required for the env-based debt path), so forward
102
+ // --allow-coverage-debt=#N on the vitest argv instead (#2618 / #2573).
103
+ const debt = resolveCoverageDebtIssue([], options.env ?? process.env);
104
+ const debtIssue = debt.kind === "valid" ? debt.issue : null;
99
105
  if (!pnpm) {
100
106
  out(SKIP_NOTICE);
101
107
  return 0;
102
108
  }
103
109
  for (const command of LANE_COMMANDS) {
104
110
  const argv = [pnpm, ...command];
111
+ if (debtIssue !== null && command[1] === "test") {
112
+ argv.push("--", `--allow-coverage-debt=${debtIssue}`);
113
+ }
105
114
  const result = runner(argv, projectRoot);
106
115
  const code = result.status;
107
116
  // A null status means the child was terminated by a signal (SIGKILL / OOM /
@@ -14,6 +14,9 @@ export interface ReconcileLabelsOptions {
14
14
  readonly repo?: string | null;
15
15
  readonly dryRun?: boolean;
16
16
  readonly client?: LabelClient;
17
+ /** Opt in to mutating issues outside the resolved project repo (#2601). */
18
+ readonly allowCrossRepo?: boolean;
19
+ readonly repoAllowlist?: readonly string[];
17
20
  }
18
21
  export declare function reconcileLabels(projectRoot: string, options?: ReconcileLabelsOptions): [number, ReconcileLabelsOutcome];
19
22
  export declare function renderLabelsReport(outcome: ReconcileLabelsOutcome): string;
@@ -4,6 +4,7 @@ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "..
4
4
  import { call } from "../scm/call.js";
5
5
  import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
6
6
  import { depResolved, RESOLVED_FOLDERS } from "./graph.js";
7
+ import { isRepoMutationAllowed } from "./repo-guard.js";
7
8
  import { allScopeIds, asStrList } from "./swarm-deps.js";
8
9
  export const SCAN_FOLDERS = ["proposed", "pending", "active"];
9
10
  export const MANAGED_LABELS = ["status:blocked", "epic", "status:tracker", "rfc"];
@@ -163,6 +164,19 @@ export function reconcileLabels(projectRoot, options = {}) {
163
164
  ? metadata.swarm
164
165
  : {};
165
166
  const desired = computeDesiredLabels(plan, hasUnresolvedDeps(swarm, knownIds));
167
+ // Guard before any SCM read so cross-repo refusal is explicit (#2601 / Greptile P1).
168
+ const mutateGate = isRepoMutationAllowed(effectiveRepo, root, {
169
+ allowCrossRepo: options.allowCrossRepo,
170
+ allowlist: options.repoAllowlist,
171
+ explicitRepo: options.repo ?? null,
172
+ });
173
+ if (!mutateGate.allowed) {
174
+ outcome.errors.push({
175
+ story_id: storyId,
176
+ message: mutateGate.reason ?? `refusing cross-repo mutation on ${effectiveRepo}`,
177
+ });
178
+ continue;
179
+ }
166
180
  let current;
167
181
  try {
168
182
  current = client.fetchLabels(effectiveRepo, number);
@@ -7,6 +7,7 @@ export interface CliOptions {
7
7
  scenario?: string | null;
8
8
  all?: boolean;
9
9
  fixtureRoot?: string | null;
10
+ allowCrossRepo?: boolean;
10
11
  }
11
12
  export declare function usage(): void;
12
13
  export declare function runGraph(opts: CliOptions): number;
@@ -28,6 +28,8 @@ function parseOptions(rest) {
28
28
  }
29
29
  else if (arg === "--force")
30
30
  opts.force = true;
31
+ else if (arg === "--allow-cross-repo")
32
+ opts.allowCrossRepo = true;
31
33
  else if (arg === "--dry-run")
32
34
  opts.dryRun = true;
33
35
  else if (arg === "--json")
@@ -46,7 +48,7 @@ function parseOptions(rest) {
46
48
  return opts;
47
49
  }
48
50
  export function usage() {
49
- process.stderr.write("usage: vbrief-reconcile <graph|labels|umbrellas> [--project-root PATH] [--dry-run] [--json] [--force] [--repo OWNER/NAME]\n" +
51
+ process.stderr.write("usage: vbrief-reconcile <graph|labels|umbrellas> [--project-root PATH] [--dry-run] [--json] [--force] [--repo OWNER/NAME] [--allow-cross-repo]\n" +
50
52
  " vbrief-reconcile parity --scenario NAME [--fixture-root PATH]\n" +
51
53
  " vbrief-reconcile parity --all [--fixture-root PATH]\n");
52
54
  }
@@ -70,7 +72,11 @@ export function runGraph(opts) {
70
72
  }
71
73
  export function runLabels(opts) {
72
74
  const root = resolve(opts.projectRoot ?? ".");
73
- const [code, outcome] = reconcileLabels(root, { repo: opts.repo, dryRun: opts.dryRun });
75
+ const [code, outcome] = reconcileLabels(root, {
76
+ repo: opts.repo,
77
+ dryRun: opts.dryRun,
78
+ allowCrossRepo: opts.allowCrossRepo,
79
+ });
74
80
  if (code === 2) {
75
81
  if (opts.json) {
76
82
  process.stdout.write(`${JSON.stringify({ error: "no vbrief/ directory found" })}\n`);
@@ -88,7 +94,11 @@ export function runLabels(opts) {
88
94
  }
89
95
  export function runUmbrellas(opts) {
90
96
  const root = resolve(opts.projectRoot ?? ".");
91
- const [code, outcome] = reconcileUmbrellas(root, { repo: opts.repo, dryRun: opts.dryRun });
97
+ const [code, outcome] = reconcileUmbrellas(root, {
98
+ repo: opts.repo,
99
+ dryRun: opts.dryRun,
100
+ allowCrossRepo: opts.allowCrossRepo,
101
+ });
92
102
  if (code === 2) {
93
103
  if (opts.json) {
94
104
  process.stdout.write(`${JSON.stringify({ error: "no vbrief/ directory found" })}\n`);
@@ -291,7 +291,11 @@ export function runParityScenario(name, options) {
291
291
  ],
292
292
  });
293
293
  const client = options.labelClient ?? new MemoryLabelClient();
294
- const [code, outcome] = reconcileLabels(root, { client, dryRun: true });
294
+ const [code, outcome] = reconcileLabels(root, {
295
+ client,
296
+ dryRun: true,
297
+ repo: "deftai/directive",
298
+ });
295
299
  return {
296
300
  scenario: name,
297
301
  ok: true,
@@ -314,7 +318,11 @@ export function runParityScenario(name, options) {
314
318
  ],
315
319
  });
316
320
  const client = options.labelClient ?? new MemoryLabelClient();
317
- const [code, outcome] = reconcileLabels(root, { client, dryRun: true });
321
+ const [code, outcome] = reconcileLabels(root, {
322
+ client,
323
+ dryRun: true,
324
+ repo: "deftai/directive",
325
+ });
318
326
  return {
319
327
  scenario: name,
320
328
  ok: true,
@@ -342,6 +350,7 @@ export function runParityScenario(name, options) {
342
350
  client,
343
351
  dryRun: true,
344
352
  now: FIXED_NOW.toISOString().replace(/\.\d{3}Z$/, "Z"),
353
+ repo: "deftai/directive",
345
354
  });
346
355
  return {
347
356
  scenario: name,
@@ -367,8 +376,13 @@ export function runParityScenario(name, options) {
367
376
  });
368
377
  const client = options.umbrellaClient ?? SHARED_UMBRELLA_CLIENT;
369
378
  const now = FIXED_NOW.toISOString().replace(/\.\d{3}Z$/, "Z");
370
- reconcileUmbrellas(root, { client, dryRun: false, now });
371
- const [code, outcome] = reconcileUmbrellas(root, { client, dryRun: true, now });
379
+ reconcileUmbrellas(root, { client, dryRun: false, now, repo: "deftai/directive" });
380
+ const [code, outcome] = reconcileUmbrellas(root, {
381
+ client,
382
+ dryRun: true,
383
+ now,
384
+ repo: "deftai/directive",
385
+ });
372
386
  return {
373
387
  scenario: name,
374
388
  ok: true,
@@ -0,0 +1,17 @@
1
+ export interface RepoMutateGuardOptions {
2
+ readonly allowCrossRepo?: boolean;
3
+ readonly allowlist?: readonly string[];
4
+ readonly explicitRepo?: string | null;
5
+ }
6
+ /** Normalize owner/name slugs for case-insensitive comparison. */
7
+ export declare function normalizeRepoSlug(repo: string): string;
8
+ /**
9
+ * Fail-closed gate before SCM label/comment mutations (#2601).
10
+ * Cross-repo targets are refused unless explicitly opted in or allowlisted.
11
+ */
12
+ export declare function isRepoMutationAllowed(targetRepo: string, projectRoot: string, options?: RepoMutateGuardOptions): {
13
+ readonly allowed: boolean;
14
+ readonly projectRepo: string | null;
15
+ readonly reason?: string;
16
+ };
17
+ //# sourceMappingURL=repo-guard.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { resolveRepo } from "../triage/queue/repo.js";
2
+ /** Normalize owner/name slugs for case-insensitive comparison. */
3
+ export function normalizeRepoSlug(repo) {
4
+ return repo.trim().toLowerCase();
5
+ }
6
+ /**
7
+ * Fail-closed gate before SCM label/comment mutations (#2601).
8
+ * Cross-repo targets are refused unless explicitly opted in or allowlisted.
9
+ */
10
+ export function isRepoMutationAllowed(targetRepo, projectRoot, options = {}) {
11
+ const projectRepo = resolveRepo(options.explicitRepo ?? null, projectRoot);
12
+ if (options.allowCrossRepo) {
13
+ return { allowed: true, projectRepo };
14
+ }
15
+ const normalizedTarget = normalizeRepoSlug(targetRepo);
16
+ for (const entry of options.allowlist ?? []) {
17
+ if (normalizeRepoSlug(entry) === normalizedTarget) {
18
+ return { allowed: true, projectRepo };
19
+ }
20
+ }
21
+ if (projectRepo !== null && normalizeRepoSlug(projectRepo) === normalizedTarget) {
22
+ return { allowed: true, projectRepo };
23
+ }
24
+ const hint = projectRepo !== null
25
+ ? `(project repo: ${projectRepo})`
26
+ : "(project repo unknown — set --repo or git remote origin)";
27
+ return {
28
+ allowed: false,
29
+ projectRepo,
30
+ reason: `refusing cross-repo mutation on ${targetRepo} ${hint}`,
31
+ };
32
+ }
33
+ //# sourceMappingURL=repo-guard.js.map
@@ -41,6 +41,9 @@ export interface ReconcileUmbrellasOptions {
41
41
  readonly dryRun?: boolean;
42
42
  readonly client?: UmbrellaClient;
43
43
  readonly now?: string;
44
+ /** Opt in to mutating comments on issues outside the resolved project repo (#2601). */
45
+ readonly allowCrossRepo?: boolean;
46
+ readonly repoAllowlist?: readonly string[];
44
47
  }
45
48
  export declare function reconcileUmbrellas(projectRoot: string, options?: ReconcileUmbrellasOptions): [number, ReconcileUmbrellasOutcome];
46
49
  export declare function renderUmbrellasReport(outcome: ReconcileUmbrellasOutcome): string;
@@ -5,6 +5,7 @@ import { createIssueComment, editIssueCommentBody, GitHubBodyError, } from "../i
5
5
  import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
6
6
  import { call } from "../scm/call.js";
7
7
  import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
8
+ import { isRepoMutationAllowed } from "./repo-guard.js";
8
9
  export const OPEN_FOLDERS = ["proposed", "pending", "active"];
9
10
  export const CLOSED_FOLDERS = ["completed", "cancelled"];
10
11
  export const LIFECYCLE_FOLDERS = [...OPEN_FOLDERS, ...CLOSED_FOLDERS];
@@ -261,6 +262,14 @@ function planShape(epicData, index) {
261
262
  return [openChildren, closedChildren, waves];
262
263
  }
263
264
  function reconcileOneEpic(epicData, index, options) {
265
+ const mutateGate = isRepoMutationAllowed(options.repo, options.projectRoot, {
266
+ allowCrossRepo: options.allowCrossRepo,
267
+ allowlist: options.repoAllowlist,
268
+ explicitRepo: options.explicitRepo ?? null,
269
+ });
270
+ if (!mutateGate.allowed) {
271
+ throw new Error(mutateGate.reason ?? `refusing cross-repo mutation on ${options.repo}`);
272
+ }
264
273
  const [openChildren, closedChildren, waves] = planShape(epicData, index);
265
274
  const total = openChildren.length + closedChildren.length;
266
275
  const comments = options.client.fetchComments(options.repo, options.number);
@@ -412,6 +421,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
412
421
  client,
413
422
  dryRun: options.dryRun ?? false,
414
423
  now,
424
+ projectRoot: root,
425
+ allowCrossRepo: options.allowCrossRepo ?? false,
426
+ repoAllowlist: options.repoAllowlist,
427
+ explicitRepo: options.repo ?? null,
415
428
  });
416
429
  if (change.action === "unchanged")
417
430
  outcome.unchanged.push(change);
@@ -419,7 +432,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
419
432
  outcome.changed.push(change);
420
433
  }
421
434
  catch (exc) {
422
- outcome.errors.push({ story_id: storyId, message: String(exc) });
435
+ outcome.errors.push({
436
+ story_id: storyId,
437
+ message: exc instanceof Error ? exc.message : String(exc),
438
+ });
423
439
  }
424
440
  }
425
441
  }
@@ -25,7 +25,7 @@ export function evaluateAgentHooks(projectRoot) {
25
25
  if (unhealthy.length > 0) {
26
26
  return {
27
27
  code: 1,
28
- message: "❌ deft agent hooks NON-FUNCTIONAL:\n" +
28
+ message: "❌ deft agent hook registration INCOMPLETE:\n" +
29
29
  unhealthy
30
30
  .map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
31
31
  .join("\n") +
@@ -36,8 +36,9 @@ export function evaluateAgentHooks(projectRoot) {
36
36
  }
37
37
  return {
38
38
  code: 0,
39
- message: "✓ deft agent hooks installed and functional for Claude, Grok, Cursor " +
40
- "(SessionStart + PreToolUse direct-write tools only; shell/MCP policy is deferred).",
39
+ message: "✓ deft agent hooks registered and structurally valid for Claude, Grok, Cursor, Codex " +
40
+ "(SessionStart + PreToolUse direct-write tools only; Codex runtime trust is " +
41
+ "user-controlled and must be reviewed with `/hooks`; shell/MCP policy is deferred).",
41
42
  stream: "stdout",
42
43
  registrations,
43
44
  };
@@ -134,7 +134,7 @@ export function evaluate(projectRoot, options = {}) {
134
134
  return {
135
135
  code: 1,
136
136
  message: "❌ deft hooks not installed: core.hooksPath is unset.\n" +
137
- " Recovery: run `task setup` (or re-run the deft installer).",
137
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
138
138
  stream: "stderr",
139
139
  };
140
140
  }
@@ -147,7 +147,7 @@ export function evaluate(projectRoot, options = {}) {
147
147
  code: 1,
148
148
  message: `❌ deft hooks wired but NON-FUNCTIONAL: core.hooksPath=${hooksPath} ` +
149
149
  `but the directory ${hooksDir} does not exist (#1463 false-green).\n` +
150
- " Recovery: re-run the deft installer / `task setup` to deposit the hooks.",
150
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
151
151
  stream: "stderr",
152
152
  };
153
153
  }
@@ -157,7 +157,7 @@ export function evaluate(projectRoot, options = {}) {
157
157
  code: 1,
158
158
  message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} is missing ` +
159
159
  `${missingHooks.join(", ")} (#1463 false-green).\n` +
160
- " Recovery: re-run the deft installer / `task setup`.",
160
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
161
161
  stream: "stderr",
162
162
  };
163
163
  }
@@ -169,7 +169,7 @@ export function evaluate(projectRoot, options = {}) {
169
169
  message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} hook(s) ` +
170
170
  `${nonExec.join(", ")} are not executable (git mode is not ` +
171
171
  "100755); git silently skips non-executable hooks on Unix (#1477).\n" +
172
- " Recovery: re-run the deft installer / `task setup`, or " +
172
+ " Recovery: run `deft update` to refresh `.githooks/`, or " +
173
173
  "`chmod +x .githooks/pre-commit .githooks/pre-push`.",
174
174
  stream: "stderr",
175
175
  };
@@ -198,7 +198,7 @@ export function evaluate(projectRoot, options = {}) {
198
198
  message: "❌ deft hooks wired but NON-FUNCTIONAL:\n" +
199
199
  contentIssues.map((issue) => ` - ${issue.replace(/\r?\n/g, " ")}`).join("\n") +
200
200
  "\n" +
201
- " Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
201
+ " Recovery: run `deft update` to refresh project-root `.githooks/`.",
202
202
  stream: "stderr",
203
203
  };
204
204
  }
@@ -0,0 +1,3 @@
1
+ /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
2
+ export default function coverageDebtTeardown(): Promise<void>;
3
+ //# sourceMappingURL=coverage-debt-teardown.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { COVERAGE_GOAL, countRecentCoverageDebtMentions, formatCoverageAttribution, formatOveruseWarning, metricsBelowGoal, readCoverageTotalsFromReport, resolveCoverageDebtIssue, } from "./coverage-debt.js";
4
+ /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
5
+ export default async function coverageDebtTeardown() {
6
+ const resolution = resolveCoverageDebtIssue(process.argv, process.env);
7
+ if (resolution.kind === "none")
8
+ return;
9
+ if (resolution.kind === "invalid") {
10
+ throw new Error(`coverage-debt: ${resolution.reason}`);
11
+ }
12
+ const repoRoot = join(import.meta.dirname, "..", "..", "..", "..");
13
+ const totals = readCoverageTotalsFromReport(join(repoRoot, "coverage"));
14
+ if (totals === null) {
15
+ throw new Error("coverage-debt: could not read coverage/coverage-final.json");
16
+ }
17
+ const missed = metricsBelowGoal(totals);
18
+ if (missed.length === 0) {
19
+ process.stderr.write(`coverage-debt: note — --allow-coverage-debt=#${resolution.issue} set but all metrics meet the ${COVERAGE_GOAL.branches}% goal\n`);
20
+ return;
21
+ }
22
+ process.stderr.write(`${formatCoverageAttribution(resolution.issue, totals)}\n`);
23
+ try {
24
+ const changelog = readFileSync(join(repoRoot, "CHANGELOG.md"), "utf8");
25
+ const overuse = formatOveruseWarning(countRecentCoverageDebtMentions(changelog));
26
+ if (overuse)
27
+ process.stderr.write(`${overuse}\n`);
28
+ }
29
+ catch {
30
+ // CHANGELOG unreadable — debt attribution still stands.
31
+ }
32
+ }
33
+ //# sourceMappingURL=coverage-debt-teardown.js.map
@@ -0,0 +1,50 @@
1
+ /** Shared coverage goal (vitest thresholds). */
2
+ export declare const COVERAGE_GOAL: {
3
+ readonly lines: 85;
4
+ readonly functions: 85;
5
+ readonly branches: 85;
6
+ readonly statements: 85;
7
+ };
8
+ export type CoverageMetric = keyof typeof COVERAGE_GOAL;
9
+ export interface CoverageTotals {
10
+ readonly lines: number;
11
+ readonly functions: number;
12
+ readonly branches: number;
13
+ readonly statements: number;
14
+ }
15
+ export type CoverageDebtResolution = {
16
+ readonly kind: "none";
17
+ } | {
18
+ readonly kind: "valid";
19
+ readonly issue: number;
20
+ } | {
21
+ readonly kind: "invalid";
22
+ readonly reason: string;
23
+ };
24
+ /** Parse `#2573`, `2573`, or bare numeric strings. */
25
+ export declare function parseCoverageDebtIssueNumber(raw: string): number | null;
26
+ /** Parse `--allow-coverage-debt=#N` tokens from argv. */
27
+ export declare function parseCoverageDebtArgv(argv: readonly string[]): CoverageDebtResolution;
28
+ /**
29
+ * Resolve coverage-debt issue from argv or release-scoped env (#2573 / #1553).
30
+ * Raw env bypass is accepted only during release Step-5 preflight.
31
+ */
32
+ export declare function resolveCoverageDebtIssue(argv: readonly string[], env?: NodeJS.ProcessEnv): CoverageDebtResolution;
33
+ interface IstanbulBranchHits {
34
+ readonly [branchId: string]: readonly number[];
35
+ }
36
+ interface IstanbulFileCoverage {
37
+ readonly s?: Readonly<Record<string, number>>;
38
+ readonly f?: Readonly<Record<string, number>>;
39
+ readonly b?: IstanbulBranchHits;
40
+ }
41
+ /** Aggregate global coverage percentages from istanbul coverage-final.json. */
42
+ export declare function summarizeCoverageFinal(coverageFinal: Readonly<Record<string, IstanbulFileCoverage>>): CoverageTotals;
43
+ export declare function readCoverageTotalsFromReport(coverageDir: string): CoverageTotals | null;
44
+ export declare function metricsBelowGoal(totals: CoverageTotals): CoverageMetric[];
45
+ export declare function formatCoverageAttribution(issue: number, totals: CoverageTotals): string;
46
+ /** Count recent release sections citing coverage-debt (CHANGELOG scan). */
47
+ export declare function countRecentCoverageDebtMentions(changelog: string, maxSections?: number): number;
48
+ export declare function formatOveruseWarning(recentCount: number): string | null;
49
+ export {};
50
+ //# sourceMappingURL=coverage-debt.d.ts.map