@deftai/directive-core 0.77.0 → 0.79.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 (126) 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.d.ts +1 -0
  8. package/dist/doctor/main.js +48 -0
  9. package/dist/doctor/payload-staleness.js +7 -1
  10. package/dist/doctor/types.d.ts +3 -0
  11. package/dist/eval/triggers.d.ts +76 -0
  12. package/dist/eval/triggers.js +259 -0
  13. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  14. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  15. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  16. package/dist/eval-triggers-relocation/index.js +2 -0
  17. package/dist/hooks/dispatcher.d.ts +43 -0
  18. package/dist/hooks/dispatcher.js +173 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.js +3 -0
  21. package/dist/hooks/scope.d.ts +12 -0
  22. package/dist/hooks/scope.js +46 -0
  23. package/dist/hooks/tools.d.ts +4 -0
  24. package/dist/hooks/tools.js +23 -0
  25. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  26. package/dist/init-deposit/agent-hooks.js +238 -0
  27. package/dist/init-deposit/hygiene.js +4 -0
  28. package/dist/init-deposit/index.d.ts +1 -0
  29. package/dist/init-deposit/index.js +1 -0
  30. package/dist/init-deposit/init-deposit.js +4 -0
  31. package/dist/init-deposit/prettierignore.d.ts +21 -0
  32. package/dist/init-deposit/prettierignore.js +83 -0
  33. package/dist/init-deposit/refresh.d.ts +3 -1
  34. package/dist/init-deposit/refresh.js +31 -40
  35. package/dist/init-deposit/scaffold.js +6 -12
  36. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  37. package/dist/init-deposit/xbrief-projections.js +117 -0
  38. package/dist/intake/reconcile-issues.d.ts +11 -0
  39. package/dist/intake/reconcile-issues.js +166 -51
  40. package/dist/issue-sync/index.d.ts +2 -0
  41. package/dist/issue-sync/index.js +2 -0
  42. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  43. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  44. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  45. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  46. package/dist/platform/index.d.ts +1 -0
  47. package/dist/platform/index.js +1 -0
  48. package/dist/platform/platform-capabilities.d.ts +5 -0
  49. package/dist/platform/platform-capabilities.js +9 -0
  50. package/dist/platform/shell-context.d.ts +30 -0
  51. package/dist/platform/shell-context.js +111 -0
  52. package/dist/pr-monitor/cadence.d.ts +6 -0
  53. package/dist/pr-monitor/cadence.js +13 -0
  54. package/dist/pr-monitor/index.d.ts +1 -1
  55. package/dist/pr-monitor/index.js +1 -1
  56. package/dist/pr-monitor/monitor.d.ts +7 -0
  57. package/dist/pr-monitor/monitor.js +59 -13
  58. package/dist/preflight-cache/evaluate.d.ts +3 -8
  59. package/dist/preflight-cache/evaluate.js +19 -19
  60. package/dist/release/build-dist.js +1 -0
  61. package/dist/release/constants.d.ts +2 -0
  62. package/dist/release/constants.js +8 -1
  63. package/dist/release/flags.js +26 -0
  64. package/dist/release/issue-state-fetch.d.ts +27 -0
  65. package/dist/release/issue-state-fetch.js +146 -0
  66. package/dist/release/main.js +1 -0
  67. package/dist/release/native-steps.js +6 -4
  68. package/dist/release/pipeline.js +15 -5
  69. package/dist/release/preflight.d.ts +6 -2
  70. package/dist/release/preflight.js +19 -5
  71. package/dist/release/types.d.ts +3 -1
  72. package/dist/release-e2e/constants.js +6 -6
  73. package/dist/release-e2e/flags.d.ts +2 -0
  74. package/dist/release-e2e/flags.js +8 -4
  75. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  76. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  77. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  78. package/dist/release-e2e/main.js +15 -9
  79. package/dist/release-e2e/types.d.ts +4 -2
  80. package/dist/resolution/classify.d.ts +1 -1
  81. package/dist/resolution/classify.js +36 -11
  82. package/dist/scope/project-definition-sync.js +2 -150
  83. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  84. package/dist/scope/registry-artifact-sync.js +159 -0
  85. package/dist/scope/specification-sync.d.ts +3 -0
  86. package/dist/scope/specification-sync.js +16 -0
  87. package/dist/scope/transition.js +40 -12
  88. package/dist/session/cache-recovery.d.ts +19 -0
  89. package/dist/session/cache-recovery.js +26 -0
  90. package/dist/session/session-start.d.ts +2 -0
  91. package/dist/session/session-start.js +14 -3
  92. package/dist/session/verify-session-ritual.d.ts +17 -0
  93. package/dist/session/verify-session-ritual.js +64 -0
  94. package/dist/triage/help/registry-data.d.ts +12 -1
  95. package/dist/triage/help/registry-data.js +22 -1
  96. package/dist/triage/welcome/default-mode.js +2 -0
  97. package/dist/triage/welcome/onboard.js +2 -0
  98. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  99. package/dist/ts-check-lane/run-lane.js +9 -0
  100. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  101. package/dist/vbrief-reconcile/labels.js +14 -0
  102. package/dist/vbrief-reconcile/main.d.ts +1 -0
  103. package/dist/vbrief-reconcile/main.js +13 -3
  104. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  105. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  106. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  107. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  108. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  109. package/dist/verify-env/agent-hooks.d.ts +11 -0
  110. package/dist/verify-env/agent-hooks.js +46 -0
  111. package/dist/verify-env/command-spawn.d.ts +5 -0
  112. package/dist/verify-env/command-spawn.js +28 -9
  113. package/dist/verify-env/index.d.ts +1 -0
  114. package/dist/verify-env/index.js +1 -0
  115. package/dist/verify-env/verify-hooks-installed.js +5 -5
  116. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  117. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  118. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  119. package/dist/vitest-runner/coverage-debt.js +155 -0
  120. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  121. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  122. package/dist/xbrief-migrate/detect.js +46 -1
  123. package/dist/xbrief-migrate/migrate-project.js +19 -4
  124. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  125. package/dist/xbrief-migrate/migration-containment.js +37 -0
  126. package/package.json +19 -3
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
4
4
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
@@ -8,6 +8,7 @@ import { stampCompletionMetadata } from "./capacity-stamp.js";
8
8
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
9
9
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
10
10
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
11
+ import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
11
12
  import { formatVbriefJson, utcNowIso } from "./vbrief-json.js";
12
13
  export function runTransition(action, filePath, now = new Date()) {
13
14
  if (!(action in TRANSITIONS)) {
@@ -101,30 +102,57 @@ export function runTransition(action, filePath, now = new Date()) {
101
102
  }
102
103
  const formatted = formatVbriefJson(data);
103
104
  const crud = new InstrumentedVbriefCrud({ now: () => now });
104
- const writeResult = crud.update(resolvedPath, formatted, { trustedWrite: true });
105
- if (!writeResult.ok) {
106
- return { ok: false, message: writeResult.error ?? `CRUD update failed for ${resolvedPath}` };
107
- }
108
- try {
109
- persistCrudMetrics(projectRoot, crud.getMetrics());
110
- }
111
- catch {
112
- /* best-effort telemetry persistence */
113
- }
114
105
  if (targetFolder !== null) {
115
106
  const destDir = join(vbriefRoot, targetFolder);
116
107
  mkdirSync(destDir, { recursive: true });
117
108
  const destPath = join(destDir, basename);
118
- renameSync(resolvedPath, destPath);
109
+ if (existsSync(destPath)) {
110
+ return { ok: false, message: `Target already exists: ${destPath}` };
111
+ }
112
+ // #2578: stamp terminal status at the destination path in the same write as
113
+ // folder placement — never leave a non-terminal status under completed/.
114
+ const writeResult = crud.update(destPath, formatted, { trustedWrite: true });
115
+ if (!writeResult.ok) {
116
+ return { ok: false, message: writeResult.error ?? `CRUD update failed for ${destPath}` };
117
+ }
118
+ try {
119
+ unlinkSync(resolvedPath);
120
+ }
121
+ catch (err) {
122
+ try {
123
+ unlinkSync(destPath);
124
+ }
125
+ catch {
126
+ /* best-effort rollback */
127
+ }
128
+ return { ok: false, message: `Failed to remove source after move: ${String(err)}` };
129
+ }
130
+ try {
131
+ persistCrudMetrics(projectRoot, crud.getMetrics());
132
+ }
133
+ catch {
134
+ /* best-effort telemetry persistence */
135
+ }
119
136
  updateDecomposedParentBackReferences(data, resolvedPath, destPath, vbriefRoot);
120
137
  updateDecomposedChildBackReferences(data, resolvedPath, destPath, vbriefRoot);
121
138
  syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
139
+ syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
122
140
  const actionLabel = MOVE_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
123
141
  return {
124
142
  ok: true,
125
143
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
126
144
  };
127
145
  }
146
+ const writeResult = crud.update(resolvedPath, formatted, { trustedWrite: true });
147
+ if (!writeResult.ok) {
148
+ return { ok: false, message: writeResult.error ?? `CRUD update failed for ${resolvedPath}` };
149
+ }
150
+ try {
151
+ persistCrudMetrics(projectRoot, crud.getMetrics());
152
+ }
153
+ catch {
154
+ /* best-effort telemetry persistence */
155
+ }
128
156
  const actionLabel = STAY_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
129
157
  return {
130
158
  ok: true,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Format a consumer-runnable `deft cache fetch-all` recovery command (#2574).
3
+ *
4
+ * Uses the space-separated CLI surface (not the colon task alias) and always
5
+ * includes required `--source` / `--repo` flags so following the hint literally
6
+ * reaches the handler instead of `unknown verb 'cache:fetch-all'`.
7
+ */
8
+ export declare function formatCacheFetchAllRecoveryCommand(repo: string | null, options?: {
9
+ force?: boolean;
10
+ }): string;
11
+ /**
12
+ * Branch-aware recovery hint (#1953 Option 3, #2574 argv fix).
13
+ * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
14
+ */
15
+ export declare function recoveryHintForStaleFailure(causes: Readonly<{
16
+ ageStale: boolean;
17
+ driftDetected: boolean;
18
+ }>, repo?: string | null): string;
19
+ //# sourceMappingURL=cache-recovery.d.ts.map
@@ -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[]>>;
@@ -29,6 +30,22 @@ export interface VerifySessionRitualOptions {
29
30
  readonly envPosture?: string | undefined;
30
31
  readonly handoffText?: string | null;
31
32
  }
33
+ export interface InspectSessionRitualOptions {
34
+ readonly tier?: "quick" | "gated";
35
+ readonly now?: Date;
36
+ readonly runGit?: GitRunner;
37
+ readonly posture?: DirectivePosture;
38
+ readonly envPosture?: string | undefined;
39
+ readonly handoffText?: string | null;
40
+ }
41
+ /**
42
+ * Read-only ritual-state inspection for host hooks.
43
+ *
44
+ * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
45
+ * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
46
+ * probe, not a hidden `doctor` / cache-refresh mutation boundary.
47
+ */
48
+ export declare function inspectSessionRitual(projectRoot: string, options?: InspectSessionRitualOptions): VerifyResult;
32
49
  export declare function verifySessionRitual(projectRoot: string, options?: VerifySessionRitualOptions): VerifyResult;
33
50
  export declare function emitVerifyJson(result: VerifyResult): string;
34
51
  export declare function emitBypassWarning(result: VerifyResult): 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 = {
@@ -105,6 +106,69 @@ function evaluateLoadedState(projectRoot, state, input) {
105
106
  }
106
107
  return [0, `OK session ritual ${input.tier} tier is fresh.`];
107
108
  }
109
+ /**
110
+ * Read-only ritual-state inspection for host hooks.
111
+ *
112
+ * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
113
+ * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
114
+ * probe, not a hidden `doctor` / cache-refresh mutation boundary.
115
+ */
116
+ export function inspectSessionRitual(projectRoot, options = {}) {
117
+ const tier = options.tier ?? "quick";
118
+ const posture = resolveSessionPosture({
119
+ explicitPosture: options.posture ?? null,
120
+ envPosture: options.envPosture ?? process.env.DEFT_SESSION_POSTURE,
121
+ handoffText: options.handoffText,
122
+ tier,
123
+ });
124
+ const ritualStateRequired = posture === "mutation" && !ritualStateIsPostureAuthority();
125
+ const statePath = ritualStatePath(projectRoot);
126
+ if (posture === "read-only") {
127
+ return {
128
+ code: 0,
129
+ message: readOnlyPostureMessage(tier),
130
+ tier,
131
+ statePath,
132
+ bypassed: false,
133
+ wouldFailCode: null,
134
+ posture,
135
+ ritualStateRequired: false,
136
+ };
137
+ }
138
+ const missingStateFile = !existsSync(statePath);
139
+ const [state, err] = readRitualState(projectRoot);
140
+ if (state === null) {
141
+ const code = missingStateFile ? 1 : 2;
142
+ const startCommand = formatFrameworkCommand(["session:start"]);
143
+ return {
144
+ code,
145
+ message: code === 1
146
+ ? `${err}. Run \`${startCommand}\` before implementation dispatch.`
147
+ : (err ?? "ritual state invalid"),
148
+ tier,
149
+ statePath,
150
+ bypassed: false,
151
+ wouldFailCode: null,
152
+ posture,
153
+ ritualStateRequired,
154
+ };
155
+ }
156
+ const [code, message] = evaluateLoadedState(projectRoot, state, {
157
+ tier,
158
+ now: options.now ?? new Date(),
159
+ runGit: options.runGit,
160
+ });
161
+ return {
162
+ code,
163
+ message,
164
+ tier,
165
+ statePath,
166
+ bypassed: false,
167
+ wouldFailCode: null,
168
+ posture,
169
+ ritualStateRequired,
170
+ };
171
+ }
108
172
  export function verifySessionRitual(projectRoot, options = {}) {
109
173
  const tier = options.tier ?? "quick";
110
174
  const posture = resolveSessionPosture({
@@ -364,6 +364,17 @@ export declare const registryData: {
364
364
  readonly see_also: readonly ["task eval:run", "task eval:report", "#1703"];
365
365
  readonly placeholder: false;
366
366
  };
367
+ readonly "task eval:triggers": {
368
+ readonly name: "task eval:triggers";
369
+ readonly summary: "Trigger routing coverage for Skills Index rules";
370
+ readonly refs: "(#1586)";
371
+ readonly description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).";
372
+ readonly usage: "task eval:triggers [-- --json] [--project-root PATH]";
373
+ readonly flags: readonly [readonly ["--json", "(off)", "Emit the TriggerEvalReport JSON."], readonly ["--project-root PATH", "(cwd)", "Project root override (Taskfile threads USER_WORKING_DIR)."]];
374
+ readonly examples: readonly ["task eval:triggers", "task eval:triggers -- --json"];
375
+ readonly see_also: readonly ["task eval:health", "task verify:eval-triggers-relocation", "#1862"];
376
+ readonly placeholder: false;
377
+ };
367
378
  readonly "task eval:run": {
368
379
  readonly name: "task eval:run";
369
380
  readonly summary: "Tier 2 golden corpus eval for a model";
@@ -508,7 +519,7 @@ export declare const registryData: {
508
519
  readonly placeholder: false;
509
520
  };
510
521
  };
511
- readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:run", "task eval:report"]]];
522
+ readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"]]];
512
523
  readonly categoriesScope: readonly [readonly ["Promote / demote", readonly ["task scope:promote", "task scope:demote"]], readonly ["Activate / complete", readonly ["task scope:activate", "task scope:complete", "task scope:fail", "task scope:cancel", "task scope:block", "task scope:unblock"]], readonly ["Reversibility", readonly ["task scope:undo", "task scope:restore"]], readonly ["Decomposition", readonly ["task scope:decompose"]]];
513
524
  readonly scriptSubcommandMap: {
514
525
  readonly triage_actions: {
@@ -548,6 +548,24 @@ export const registryData = {
548
548
  see_also: ["task eval:run", "task eval:report", "#1703"],
549
549
  placeholder: false,
550
550
  },
551
+ "task eval:triggers": {
552
+ name: "task eval:triggers",
553
+ summary: "Trigger routing coverage for Skills Index rules",
554
+ refs: "(#1586)",
555
+ description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).",
556
+ usage: "task eval:triggers [-- --json] [--project-root PATH]",
557
+ flags: [
558
+ ["--json", "(off)", "Emit the TriggerEvalReport JSON."],
559
+ [
560
+ "--project-root PATH",
561
+ "(cwd)",
562
+ "Project root override (Taskfile threads USER_WORKING_DIR).",
563
+ ],
564
+ ],
565
+ examples: ["task eval:triggers", "task eval:triggers -- --json"],
566
+ see_also: ["task eval:health", "task verify:eval-triggers-relocation", "#1862"],
567
+ placeholder: false,
568
+ },
551
569
  "task eval:run": {
552
570
  name: "task eval:run",
553
571
  summary: "Tier 2 golden corpus eval for a model",
@@ -829,7 +847,10 @@ export const registryData = {
829
847
  "task triage:metrics",
830
848
  ],
831
849
  ],
832
- ["Framework eval (#1703)", ["task eval:health", "task eval:run", "task eval:report"]],
850
+ [
851
+ "Framework eval (#1703)",
852
+ ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"],
853
+ ],
833
854
  ],
834
855
  categoriesScope: [
835
856
  ["Promote / demote", ["task scope:promote", "task scope:demote"]],
@@ -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