@deftai/directive-core 0.79.3 → 0.80.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 (77) hide show
  1. package/dist/coverage-hotspots/evaluate.d.ts +49 -0
  2. package/dist/coverage-hotspots/evaluate.js +262 -0
  3. package/dist/coverage-hotspots/index.d.ts +3 -0
  4. package/dist/coverage-hotspots/index.js +3 -0
  5. package/dist/coverage-hotspots/thresholds.d.ts +5 -0
  6. package/dist/coverage-hotspots/thresholds.js +48 -0
  7. package/dist/hooks/dispatcher.d.ts +25 -3
  8. package/dist/hooks/dispatcher.js +216 -65
  9. package/dist/hooks/readonly.d.ts +7 -0
  10. package/dist/hooks/readonly.js +83 -0
  11. package/dist/hooks/tools.d.ts +7 -0
  12. package/dist/hooks/tools.js +16 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  16. package/dist/init-deposit/agent-hooks.js +77 -21
  17. package/dist/init-deposit/xbrief-projections.d.ts +7 -0
  18. package/dist/init-deposit/xbrief-projections.js +29 -1
  19. package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
  20. package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
  21. package/dist/policy/index.d.ts +2 -0
  22. package/dist/policy/index.js +31 -1
  23. package/dist/policy/runtime-authority.d.ts +54 -0
  24. package/dist/policy/runtime-authority.js +180 -0
  25. package/dist/policy/staleness-tickler.d.ts +20 -0
  26. package/dist/policy/staleness-tickler.js +151 -0
  27. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  28. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  29. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  30. package/dist/pr-merge-readiness/gh.js +10 -3
  31. package/dist/pr-merge-readiness/index.d.ts +2 -1
  32. package/dist/pr-merge-readiness/index.js +2 -1
  33. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  34. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  35. package/dist/pr-wait-mergeable/cascade.js +8 -1
  36. package/dist/pr-wait-mergeable/classify.js +5 -1
  37. package/dist/pr-wait-mergeable/wrappers.js +15 -4
  38. package/dist/pr-watch/constants.d.ts +15 -0
  39. package/dist/pr-watch/constants.js +16 -1
  40. package/dist/pr-watch/main.js +12 -0
  41. package/dist/pr-watch/probe.js +13 -1
  42. package/dist/pr-watch/types.d.ts +9 -0
  43. package/dist/pr-watch/watch.js +24 -1
  44. package/dist/scope/main.js +12 -0
  45. package/dist/scope/undo.js +12 -2
  46. package/dist/session/release-availability.d.ts +21 -0
  47. package/dist/session/release-availability.js +109 -0
  48. package/dist/session/ritual-sentinel.d.ts +14 -0
  49. package/dist/session/ritual-sentinel.js +28 -0
  50. package/dist/session/session-start.d.ts +10 -0
  51. package/dist/session/session-start.js +18 -0
  52. package/dist/staleness-tickler/escalation.d.ts +14 -0
  53. package/dist/staleness-tickler/escalation.js +106 -0
  54. package/dist/staleness-tickler/idle.d.ts +18 -0
  55. package/dist/staleness-tickler/idle.js +47 -0
  56. package/dist/staleness-tickler/index.d.ts +8 -0
  57. package/dist/staleness-tickler/index.js +8 -0
  58. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  59. package/dist/staleness-tickler/probe-directive.js +106 -0
  60. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  61. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  62. package/dist/staleness-tickler/run.d.ts +31 -0
  63. package/dist/staleness-tickler/run.js +227 -0
  64. package/dist/staleness-tickler/state.d.ts +10 -0
  65. package/dist/staleness-tickler/state.js +46 -0
  66. package/dist/staleness-tickler/types.d.ts +85 -0
  67. package/dist/staleness-tickler/types.js +5 -0
  68. package/dist/triage/bootstrap/cache-module.d.ts +25 -0
  69. package/dist/triage/bootstrap/cache-module.js +39 -0
  70. package/dist/triage/bootstrap/index.d.ts +1 -0
  71. package/dist/triage/bootstrap/index.js +7 -82
  72. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  73. package/dist/vbrief-validate/plan-hooks.js +50 -0
  74. package/dist/verify-env/agent-hooks.js +4 -2
  75. package/dist/xbrief-migrate/agents-header.d.ts +0 -11
  76. package/dist/xbrief-migrate/agents-header.js +10 -1
  77. package/package.json +3 -3
@@ -0,0 +1,106 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
4
+ import { evaluateReleaseAvailability, } from "../doctor/release-availability.js";
5
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
6
+ function defaultReadText(path) {
7
+ try {
8
+ return readFileSync(path, "utf8");
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ function defaultNpmView() {
15
+ const result = spawnSync("npm", [
16
+ "view",
17
+ "@deftai/directive",
18
+ "version",
19
+ `--registry=${PUBLIC_NPM_REGISTRY}`,
20
+ "--ignore-scripts",
21
+ ], { encoding: "utf8", timeout: 5_000 });
22
+ const version = (result.stdout ?? "").trim().split(/\r?\n/)[0]?.trim() ?? "";
23
+ return { ok: result.status === 0 && version.length > 0, version };
24
+ }
25
+ /** Parse `X.Y.Z` or `vX.Y.Z` core semver (ignores pre-release suffix). */
26
+ export function parseSemverCore(version) {
27
+ const trimmed = version.trim().replace(/^v/i, "");
28
+ const core = trimmed.split("-")[0] ?? "";
29
+ const parts = core.split(".");
30
+ if (parts.length !== 3) {
31
+ return null;
32
+ }
33
+ const major = Number(parts[0]);
34
+ const minor = Number(parts[1]);
35
+ const patch = Number(parts[2]);
36
+ if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) {
37
+ return null;
38
+ }
39
+ return { major, minor, patch };
40
+ }
41
+ export function computeDirectiveDistance(installedVersion, latestVersion) {
42
+ const installed = parseSemverCore(installedVersion);
43
+ const latest = parseSemverCore(latestVersion);
44
+ if (installed === null || latest === null) {
45
+ return { majorBehind: false, minorDistance: 0, patchDistance: 0 };
46
+ }
47
+ if (latest.major > installed.major) {
48
+ return {
49
+ majorBehind: true,
50
+ minorDistance: 0,
51
+ patchDistance: 0,
52
+ };
53
+ }
54
+ if (latest.major < installed.major) {
55
+ return { majorBehind: false, minorDistance: 0, patchDistance: 0 };
56
+ }
57
+ return {
58
+ majorBehind: false,
59
+ minorDistance: Math.max(0, latest.minor - installed.minor),
60
+ patchDistance: Math.max(0, latest.patch - installed.patch),
61
+ };
62
+ }
63
+ /**
64
+ * Probe Directive payload staleness via the public npm registry (#1692 substrate).
65
+ * Skips network when `DEFT_NO_NETWORK=1`.
66
+ */
67
+ export function probeDirectiveStaleness(projectRoot, options = {}) {
68
+ const env = options.env ?? process.env;
69
+ const disclosure = `[deft staleness] Checking ${PUBLIC_NPM_REGISTRY} for Directive release drift.`;
70
+ if (env.DEFT_NO_NETWORK === "1") {
71
+ return null;
72
+ }
73
+ const readText = options.readText ?? defaultReadText;
74
+ const isFile = options.isFile ?? existsSync;
75
+ const manifestPath = locateManifest(projectRoot, null, isFile);
76
+ const manifestText = manifestPath ? readText(manifestPath) : null;
77
+ if (manifestText === null) {
78
+ return null;
79
+ }
80
+ const manifest = parseInstallManifest(manifestText);
81
+ const installed = (manifest.tag ?? manifest.ref ?? "").trim();
82
+ const notApplicable = evaluateReleaseAvailability(installed, null);
83
+ if (notApplicable.status === "not-applicable") {
84
+ return null;
85
+ }
86
+ let npmResult;
87
+ try {
88
+ npmResult = (options.runNpmView ?? defaultNpmView)();
89
+ }
90
+ catch {
91
+ npmResult = { ok: false, version: "" };
92
+ }
93
+ const availability = evaluateReleaseAvailability(installed, npmResult.ok ? npmResult.version : null);
94
+ const stale = availability.status === "available";
95
+ let distance = { majorBehind: false, minorDistance: 0, patchDistance: 0 };
96
+ if (availability.status === "available" || availability.status === "current") {
97
+ distance = computeDirectiveDistance(availability.installedVersion, availability.latestVersion);
98
+ }
99
+ return {
100
+ availability,
101
+ ...distance,
102
+ stale,
103
+ registryDisclosure: disclosure,
104
+ };
105
+ }
106
+ //# sourceMappingURL=probe-directive.js.map
@@ -0,0 +1,12 @@
1
+ import type { XbriefDrift, XbriefSchemaDistance } from "./types.js";
2
+ export interface ProbeXbriefOptions {
3
+ readonly targetVersion?: string;
4
+ readonly readText?: (path: string) => string | null;
5
+ readonly isFile?: (path: string) => boolean;
6
+ readonly resolveDefinitionPath?: (projectRoot: string) => string;
7
+ }
8
+ /** Classify declared xBRIEF schema distance vs the installed framework schema. */
9
+ export declare function classifyXbriefSchemaDistance(declaredVersion: string | null, targetVersion: string): XbriefSchemaDistance;
10
+ /** Probe xBRIEF schema staleness from PROJECT-DEFINITION declared version. */
11
+ export declare function probeXbriefStaleness(projectRoot: string, options?: ProbeXbriefOptions): XbriefDrift;
12
+ //# sourceMappingURL=probe-xbrief.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { VBRIEF_VERSION } from "@deftai/directive-types";
3
+ import { resolveProjectDefinitionPath } from "../layout/resolve.js";
4
+ import { readDeclaredArtifactVersion } from "../xbrief-migrate/transforms.js";
5
+ function defaultReadText(path) {
6
+ try {
7
+ return readFileSync(path, "utf8");
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ function parseSchemaParts(version) {
14
+ const trimmed = version.trim();
15
+ const parts = trimmed.split(".");
16
+ if (parts.length < 2) {
17
+ return null;
18
+ }
19
+ const major = Number(parts[0]);
20
+ const minor = Number(parts[1]);
21
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) {
22
+ return null;
23
+ }
24
+ return { major, minor };
25
+ }
26
+ /** Classify declared xBRIEF schema distance vs the installed framework schema. */
27
+ export function classifyXbriefSchemaDistance(declaredVersion, targetVersion) {
28
+ if (declaredVersion === null) {
29
+ return "behind-major";
30
+ }
31
+ const declared = parseSchemaParts(declaredVersion);
32
+ const target = parseSchemaParts(targetVersion);
33
+ if (declared === null || target === null) {
34
+ return "behind-major";
35
+ }
36
+ if (declared.major < target.major) {
37
+ return "behind-major";
38
+ }
39
+ if (declared.major === target.major) {
40
+ const minorGap = target.minor - declared.minor;
41
+ if (minorGap >= 2) {
42
+ return "behind-major";
43
+ }
44
+ if (minorGap >= 1) {
45
+ return "behind-minor";
46
+ }
47
+ }
48
+ return "current";
49
+ }
50
+ /** Probe xBRIEF schema staleness from PROJECT-DEFINITION declared version. */
51
+ export function probeXbriefStaleness(projectRoot, options = {}) {
52
+ const targetVersion = options.targetVersion ?? VBRIEF_VERSION;
53
+ const readText = options.readText ?? defaultReadText;
54
+ const isFile = options.isFile ?? existsSync;
55
+ let definitionPath;
56
+ try {
57
+ definitionPath =
58
+ options.resolveDefinitionPath?.(projectRoot) ?? resolveProjectDefinitionPath(projectRoot);
59
+ }
60
+ catch {
61
+ return {
62
+ declaredVersion: null,
63
+ targetVersion,
64
+ distance: "behind-major",
65
+ stale: true,
66
+ };
67
+ }
68
+ if (!isFile(definitionPath)) {
69
+ return {
70
+ declaredVersion: null,
71
+ targetVersion,
72
+ distance: "behind-major",
73
+ stale: true,
74
+ };
75
+ }
76
+ const text = readText(definitionPath);
77
+ if (text === null) {
78
+ return {
79
+ declaredVersion: null,
80
+ targetVersion,
81
+ distance: "behind-major",
82
+ stale: true,
83
+ };
84
+ }
85
+ let declared = null;
86
+ try {
87
+ const parsed = JSON.parse(text);
88
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
89
+ declared = readDeclaredArtifactVersion(parsed);
90
+ }
91
+ }
92
+ catch {
93
+ declared = null;
94
+ }
95
+ const distance = classifyXbriefSchemaDistance(declared, targetVersion);
96
+ return {
97
+ declaredVersion: declared,
98
+ targetVersion,
99
+ distance,
100
+ stale: distance !== "current",
101
+ };
102
+ }
103
+ //# sourceMappingURL=probe-xbrief.js.map
@@ -0,0 +1,31 @@
1
+ import { type IdleGateOptions } from "./idle.js";
2
+ import { type ProbeDirectiveOptions } from "./probe-directive.js";
3
+ import { type ProbeXbriefOptions } from "./probe-xbrief.js";
4
+ import type { StalenessProbeResult, StalenessTicklerRunResult } from "./types.js";
5
+ export declare const DIRECTIVE_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
6
+ export declare const XBRIEF_MIGRATE_COMMAND = "deft migrate:xbrief";
7
+ export interface StalenessTicklerOptions {
8
+ readonly now?: Date;
9
+ readonly env?: NodeJS.ProcessEnv;
10
+ readonly idle?: IdleGateOptions;
11
+ readonly probeDirective?: ProbeDirectiveOptions;
12
+ readonly probeXbrief?: ProbeXbriefOptions;
13
+ readonly askConsent?: (prompt: string) => "yes" | "no" | "snooze" | "remind";
14
+ readonly isInteractive?: boolean;
15
+ readonly executeUpgrade?: boolean;
16
+ readonly runUpgrade?: () => {
17
+ ok: boolean;
18
+ message: string;
19
+ };
20
+ readonly runMigrate?: (projectRoot: string) => {
21
+ ok: boolean;
22
+ message: string;
23
+ };
24
+ }
25
+ export declare function probeStalenessDimensions(projectRoot: string, options?: StalenessTicklerOptions): StalenessProbeResult;
26
+ /**
27
+ * Maybe run the staleness tickler at a safe idle point (#2488 + #2489).
28
+ * Best-effort and non-fatal — callers should wrap in try/catch.
29
+ */
30
+ export declare function maybeRunStalenessTickler(projectRoot: string, options?: StalenessTicklerOptions): StalenessTicklerRunResult;
31
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,227 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { loadProjectDefinition } from "../policy/resolve.js";
3
+ import { loadStalenessTicklerPolicy } from "../policy/staleness-tickler.js";
4
+ import { runXbriefMigration } from "../xbrief-migrate/migrate-project.js";
5
+ import { holdTierOnUnverified, isSnoozeActive, mergeHeldXbriefDistance, resolveTier, scoreDrift, shouldPromptDespiteSnooze, snoozeWindowMs, } from "./escalation.js";
6
+ import { isInteractiveSession, isSafeIdlePoint, shouldSkipTicklerEntirely, } from "./idle.js";
7
+ import { probeDirectiveStaleness } from "./probe-directive.js";
8
+ import { probeXbriefStaleness } from "./probe-xbrief.js";
9
+ import { loadStalenessTicklerState, saveStalenessTicklerState } from "./state.js";
10
+ export const DIRECTIVE_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
11
+ export const XBRIEF_MIGRATE_COMMAND = "deft migrate:xbrief";
12
+ const TIER_TONE = {
13
+ quiet: "note",
14
+ notice: "recommend",
15
+ strong: "strongly recommend",
16
+ assert: "assert",
17
+ };
18
+ function defaultAskConsent(_prompt) {
19
+ return "snooze";
20
+ }
21
+ function defaultRunUpgrade() {
22
+ const result = spawnSync("npm", ["i", "-g", "@deftai/directive@latest"], {
23
+ encoding: "utf8",
24
+ timeout: 120_000,
25
+ });
26
+ if (result.status === 0) {
27
+ return { ok: true, message: "Directive upgrade completed." };
28
+ }
29
+ const stderr = (result.stderr ?? "").trim();
30
+ return {
31
+ ok: false,
32
+ message: stderr.length > 0 ? stderr : `npm exited with status ${result.status ?? "unknown"}`,
33
+ };
34
+ }
35
+ function defaultRunMigrate(projectRoot) {
36
+ const io = { writeOut: () => { }, writeErr: () => { } };
37
+ const outcome = runXbriefMigration({ projectRoot }, io);
38
+ if (outcome.kind === "migrated") {
39
+ return {
40
+ ok: true,
41
+ message: `xBRIEF migration completed (${outcome.files} files; backup ${outcome.backupDir}).`,
42
+ };
43
+ }
44
+ if (outcome.kind === "converged" || outcome.kind === "noop") {
45
+ return { ok: true, message: outcome.message };
46
+ }
47
+ return { ok: false, message: outcome.message };
48
+ }
49
+ export function probeStalenessDimensions(projectRoot, options = {}) {
50
+ const directiveProbe = probeDirectiveStaleness(projectRoot, {
51
+ env: options.env,
52
+ ...options.probeDirective,
53
+ });
54
+ const xbrief = probeXbriefStaleness(projectRoot, options.probeXbrief);
55
+ const directive = directiveProbe
56
+ ? {
57
+ availability: directiveProbe.availability,
58
+ majorBehind: directiveProbe.majorBehind,
59
+ minorDistance: directiveProbe.minorDistance,
60
+ patchDistance: directiveProbe.patchDistance,
61
+ stale: directiveProbe.stale,
62
+ }
63
+ : {
64
+ availability: {
65
+ status: "unverified",
66
+ installedVersion: "0.0.0",
67
+ latestVersion: null,
68
+ resolver: "npm-view",
69
+ },
70
+ majorBehind: false,
71
+ minorDistance: 0,
72
+ patchDistance: 0,
73
+ stale: false,
74
+ };
75
+ const anyStale = directive.stale || xbrief.stale;
76
+ return {
77
+ directive,
78
+ xbrief,
79
+ anyStale,
80
+ directiveRegistryDisclosure: directiveProbe?.registryDisclosure,
81
+ };
82
+ }
83
+ function formatDimensionLines(probe) {
84
+ const lines = [];
85
+ if (probe.directive.stale && probe.directive.availability.status === "available") {
86
+ lines.push(`[deft staleness] Directive payload behind: installed v${probe.directive.availability.installedVersion}, ` +
87
+ `latest v${probe.directive.availability.latestVersion}. Run \`${DIRECTIVE_UPGRADE_COMMAND}\`.`);
88
+ }
89
+ if (probe.xbrief.stale) {
90
+ lines.push(`[deft staleness] xBRIEF schema ${probe.xbrief.distance}: declared ${probe.xbrief.declaredVersion ?? "unknown"}, ` +
91
+ `framework ${probe.xbrief.targetVersion}. Run \`${XBRIEF_MIGRATE_COMMAND}\`.`);
92
+ }
93
+ return lines;
94
+ }
95
+ function buildPromptLine(tier, probe) {
96
+ const tone = TIER_TONE[tier];
97
+ const actions = [];
98
+ if (probe.directive.stale) {
99
+ actions.push("upgrade Directive");
100
+ }
101
+ if (probe.xbrief.stale) {
102
+ actions.push("migrate xBRIEF");
103
+ }
104
+ const actionText = actions.join(" and ");
105
+ if (tier === "assert") {
106
+ return (`[deft staleness:${tone}] Version drift is compounding (${actionText}). ` +
107
+ "Plain snooze is unavailable — choose yes, opt-out via plan.policy.stalenessTickler.optOut, " +
108
+ "or remind after next release.");
109
+ }
110
+ return `[deft staleness:${tone}] ${actionText} now? [y/n/s]`;
111
+ }
112
+ function nextStateAfterPrompt(previous, tier, score, now, choice, policy, probe) {
113
+ const deferralCount = choice === "snooze" || choice === "no" || choice === "headless-advisory"
114
+ ? (previous.deferralCount ?? 0) + 1
115
+ : (previous.deferralCount ?? 0);
116
+ const windowMs = choice === "remind" ? policy.snooze.quietMs : snoozeWindowMs(tier, deferralCount, policy);
117
+ const snoozedUntil = new Date(now.getTime() + windowMs).toISOString();
118
+ return {
119
+ firstDetectedAt: previous.firstDetectedAt ?? now.toISOString(),
120
+ lastTier: tier,
121
+ lastScore: score,
122
+ lastPromptAt: now.toISOString(),
123
+ deferralCount,
124
+ snoozedUntil,
125
+ heldDirectiveLatest: probe.directive.availability.status === "available" ||
126
+ probe.directive.availability.status === "current"
127
+ ? probe.directive.availability.latestVersion
128
+ : (previous.heldDirectiveLatest ?? null),
129
+ heldXbriefDistance: mergeHeldXbriefDistance(probe.xbrief.distance, previous.heldXbriefDistance),
130
+ remindAfterNextRelease: choice === "remind" ? true : previous.remindAfterNextRelease,
131
+ };
132
+ }
133
+ /**
134
+ * Maybe run the staleness tickler at a safe idle point (#2488 + #2489).
135
+ * Best-effort and non-fatal — callers should wrap in try/catch.
136
+ */
137
+ export function maybeRunStalenessTickler(projectRoot, options = {}) {
138
+ const env = options.env ?? process.env;
139
+ if (shouldSkipTicklerEntirely(env)) {
140
+ return { lines: [], prompted: false, skippedReason: "ritual-skip" };
141
+ }
142
+ const [data] = loadProjectDefinition(projectRoot);
143
+ const policy = loadStalenessTicklerPolicy(data);
144
+ const idle = isSafeIdlePoint(projectRoot, policy, { env, ...options.idle });
145
+ if (!idle.ok) {
146
+ return { lines: [], prompted: false, skippedReason: idle.reason };
147
+ }
148
+ const now = options.now ?? new Date();
149
+ const state = loadStalenessTicklerState(projectRoot);
150
+ const probe = probeStalenessDimensions(projectRoot, options);
151
+ if (!probe.anyStale) {
152
+ if (state.firstDetectedAt !== undefined ||
153
+ state.lastTier !== undefined ||
154
+ state.snoozedUntil !== undefined) {
155
+ saveStalenessTicklerState(projectRoot, {});
156
+ }
157
+ return { lines: [], prompted: false, skippedReason: "current" };
158
+ }
159
+ const directiveUnverified = probe.directive.availability.status === "unverified";
160
+ const xbriefDistance = directiveUnverified && state.heldXbriefDistance !== undefined
161
+ ? mergeHeldXbriefDistance(probe.xbrief.distance, state.heldXbriefDistance)
162
+ : probe.xbrief.distance;
163
+ const ageMs = state.firstDetectedAt !== undefined
164
+ ? Math.max(0, now.getTime() - Date.parse(state.firstDetectedAt))
165
+ : 0;
166
+ const inputs = {
167
+ directive: probe.directive,
168
+ xbrief: {
169
+ ...probe.xbrief,
170
+ distance: xbriefDistance,
171
+ stale: xbriefDistance !== "current",
172
+ },
173
+ ageMs,
174
+ deferralCount: state.deferralCount ?? 0,
175
+ };
176
+ const computedScore = scoreDrift(inputs, policy);
177
+ const computedTier = resolveTier(inputs, policy);
178
+ const { score, tier } = holdTierOnUnverified(computedTier, computedScore, state, directiveUnverified);
179
+ if (isSnoozeActive(state, now) && !shouldPromptDespiteSnooze(tier, state)) {
180
+ return { lines: [], prompted: false, skippedReason: "snoozed" };
181
+ }
182
+ const lines = [];
183
+ if (probe.directiveRegistryDisclosure) {
184
+ lines.push(probe.directiveRegistryDisclosure);
185
+ }
186
+ lines.push(...formatDimensionLines(probe));
187
+ lines.push(buildPromptLine(tier, probe));
188
+ const interactive = options.isInteractive ?? isInteractiveSession(env);
189
+ let choice;
190
+ if (!interactive) {
191
+ lines.push("[deft staleness] Non-interactive session — recommendation recorded; " +
192
+ "decline with plan.policy.stalenessTickler.optOut or wait for snooze window.");
193
+ choice = "headless-advisory";
194
+ }
195
+ else {
196
+ const ask = options.askConsent ?? defaultAskConsent;
197
+ const response = ask(lines.join("\n"));
198
+ if (tier === "assert" && response === "snooze") {
199
+ choice = "remind";
200
+ lines.push("[deft staleness] Assert tier: treating snooze as remind-after-next-release.");
201
+ }
202
+ else {
203
+ choice = response;
204
+ }
205
+ }
206
+ if (choice === "yes") {
207
+ const runUpgrade = options.runUpgrade ?? defaultRunUpgrade;
208
+ const runMigrate = options.runMigrate ?? defaultRunMigrate;
209
+ if (probe.directive.stale) {
210
+ lines.push(`[deft staleness] Running \`${DIRECTIVE_UPGRADE_COMMAND}\`…`);
211
+ const upgrade = runUpgrade();
212
+ lines.push(upgrade.ok
213
+ ? `[deft staleness] ${upgrade.message}`
214
+ : `[deft staleness] Upgrade failed (non-fatal): ${upgrade.message}`);
215
+ }
216
+ if (probe.xbrief.stale) {
217
+ lines.push(`[deft staleness] Running xBRIEF migration…`);
218
+ const migrate = runMigrate(projectRoot);
219
+ lines.push(migrate.ok
220
+ ? `[deft staleness] ${migrate.message}`
221
+ : `[deft staleness] Migration failed (non-fatal): ${migrate.message}`);
222
+ }
223
+ }
224
+ saveStalenessTicklerState(projectRoot, nextStateAfterPrompt(state, tier, score, now, choice, policy, probe));
225
+ return { lines, prompted: true };
226
+ }
227
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,10 @@
1
+ import type { StalenessTicklerState } from "./types.js";
2
+ export declare const STATE_RELATIVE_PATH: string;
3
+ export interface StateIo {
4
+ readonly readText?: (path: string) => string | null;
5
+ readonly writeText?: (path: string, content: string) => void;
6
+ }
7
+ export declare function parseStalenessTicklerState(text: string | null): StalenessTicklerState;
8
+ export declare function loadStalenessTicklerState(projectRoot: string, io?: StateIo): StalenessTicklerState;
9
+ export declare function saveStalenessTicklerState(projectRoot: string, state: StalenessTicklerState, io?: StateIo): void;
10
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "staleness-tickler-state.json");
4
+ function defaultReadText(path) {
5
+ try {
6
+ return readFileSync(path, "utf8");
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ function defaultWriteText(path, content) {
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ const temporary = `${path}.${process.pid}.tmp`;
15
+ writeFileSync(temporary, content, "utf8");
16
+ renameSync(temporary, path);
17
+ }
18
+ export function parseStalenessTicklerState(text) {
19
+ if (text === null) {
20
+ return {};
21
+ }
22
+ try {
23
+ const parsed = JSON.parse(text);
24
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
25
+ return parsed;
26
+ }
27
+ }
28
+ catch {
29
+ // stale state must never block operator work
30
+ }
31
+ return {};
32
+ }
33
+ export function loadStalenessTicklerState(projectRoot, io = {}) {
34
+ const path = join(projectRoot, STATE_RELATIVE_PATH);
35
+ return parseStalenessTicklerState((io.readText ?? defaultReadText)(path));
36
+ }
37
+ export function saveStalenessTicklerState(projectRoot, state, io = {}) {
38
+ const path = join(projectRoot, STATE_RELATIVE_PATH);
39
+ try {
40
+ (io.writeText ?? defaultWriteText)(path, `${JSON.stringify(state, null, 2)}\n`);
41
+ }
42
+ catch {
43
+ // persistence is best-effort
44
+ }
45
+ }
46
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,85 @@
1
+ import type { ReleaseAvailabilityResult } from "../doctor/release-availability.js";
2
+ /** Escalation tiers for the staleness tickler (#2489). */
3
+ export declare const STALENESS_TICKLER_TIERS: readonly ["quiet", "notice", "strong", "assert"];
4
+ export type StalenessTicklerTier = (typeof STALENESS_TICKLER_TIERS)[number];
5
+ /** xBRIEF schema distance relative to the installed framework schema. */
6
+ export declare const XBRIEF_SCHEMA_DISTANCES: readonly ["current", "behind-minor", "behind-major"];
7
+ export type XbriefSchemaDistance = (typeof XBRIEF_SCHEMA_DISTANCES)[number];
8
+ export interface ParsedSemverCore {
9
+ readonly major: number;
10
+ readonly minor: number;
11
+ readonly patch: number;
12
+ }
13
+ export interface DirectiveDrift {
14
+ readonly availability: ReleaseAvailabilityResult;
15
+ readonly majorBehind: boolean;
16
+ readonly minorDistance: number;
17
+ readonly patchDistance: number;
18
+ readonly stale: boolean;
19
+ }
20
+ export interface XbriefDrift {
21
+ readonly declaredVersion: string | null;
22
+ readonly targetVersion: string;
23
+ readonly distance: XbriefSchemaDistance;
24
+ readonly stale: boolean;
25
+ }
26
+ export interface DriftInputs {
27
+ readonly directive: DirectiveDrift;
28
+ readonly xbrief: XbriefDrift;
29
+ readonly ageMs: number;
30
+ readonly deferralCount: number;
31
+ }
32
+ export interface DriftScoreResult {
33
+ readonly score: number;
34
+ readonly tier: StalenessTicklerTier;
35
+ }
36
+ export interface StalenessTicklerState {
37
+ readonly firstDetectedAt?: string;
38
+ readonly lastTier?: StalenessTicklerTier;
39
+ readonly lastScore?: number;
40
+ readonly lastPromptAt?: string;
41
+ readonly deferralCount?: number;
42
+ readonly snoozedUntil?: string;
43
+ readonly heldDirectiveLatest?: string | null;
44
+ readonly heldXbriefDistance?: XbriefSchemaDistance;
45
+ readonly remindAfterNextRelease?: boolean;
46
+ }
47
+ export interface StalenessTicklerWeights {
48
+ readonly directiveMajor: number;
49
+ readonly directiveMinor: number;
50
+ readonly directivePatch: number;
51
+ readonly schemaMajor: number;
52
+ readonly schemaMinor: number;
53
+ readonly agePerDay: number;
54
+ readonly deferral: number;
55
+ }
56
+ export interface StalenessTicklerTierThresholds {
57
+ readonly noticeMinorThreshold: number;
58
+ readonly strongAgeMs: number;
59
+ readonly assertDeferralCap: number;
60
+ }
61
+ export interface StalenessTicklerSnoozePolicy {
62
+ readonly quietMs: number;
63
+ readonly noticeMs: number;
64
+ readonly strongMs: number;
65
+ readonly maxWidenMultiplier: number;
66
+ }
67
+ export interface StalenessTicklerPolicy {
68
+ readonly enabled: boolean;
69
+ readonly optOut: boolean;
70
+ readonly weights: StalenessTicklerWeights;
71
+ readonly tiers: StalenessTicklerTierThresholds;
72
+ readonly snooze: StalenessTicklerSnoozePolicy;
73
+ }
74
+ export interface StalenessProbeResult {
75
+ readonly directive: DirectiveDrift;
76
+ readonly xbrief: XbriefDrift;
77
+ readonly anyStale: boolean;
78
+ readonly directiveRegistryDisclosure?: string;
79
+ }
80
+ export interface StalenessTicklerRunResult {
81
+ readonly lines: readonly string[];
82
+ readonly prompted: boolean;
83
+ readonly skippedReason?: string;
84
+ }
85
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** Escalation tiers for the staleness tickler (#2489). */
2
+ export const STALENESS_TICKLER_TIERS = ["quiet", "notice", "strong", "assert"];
3
+ /** xBRIEF schema distance relative to the installed framework schema. */
4
+ export const XBRIEF_SCHEMA_DISTANCES = ["current", "behind-minor", "behind-major"];
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * TypeScript default cache module for triage:bootstrap (#2684).
3
+ *
4
+ * npm / packaged consumers never ship scripts/cache.py; bootstrap must use the
5
+ * same cacheFetchAll path as empty-cache auto-populate (#2575).
6
+ */
7
+ import { type FetchAllReportImpl } from "../../cache/fetch.js";
8
+ import type { CacheModule } from "./types.js";
9
+ export type CacheFetchAllFn = (options: {
10
+ source: string;
11
+ repo: string;
12
+ batchSize?: number;
13
+ delayMs?: number;
14
+ state?: string;
15
+ limit?: number;
16
+ labels?: readonly string[];
17
+ author?: string | null;
18
+ cacheRoot?: string;
19
+ force?: boolean;
20
+ }) => FetchAllReportImpl;
21
+ /** Adapt TS cache reports to the bootstrap `FetchAllReport` shape. */
22
+ export declare function bootstrapCacheModule(fetchAll?: CacheFetchAllFn): CacheModule;
23
+ /** Default module for packaged and source checkouts — never gates on Python. */
24
+ export declare function loadDefaultCacheModule(): CacheModule;
25
+ //# sourceMappingURL=cache-module.d.ts.map