@deftai/directive-core 0.79.4 → 0.81.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 (84) hide show
  1. package/dist/doctor/checks.d.ts +6 -0
  2. package/dist/doctor/checks.js +139 -0
  3. package/dist/doctor/main.js +2 -1
  4. package/dist/hooks/dispatcher.d.ts +18 -3
  5. package/dist/hooks/dispatcher.js +173 -62
  6. package/dist/hooks/readonly.d.ts +7 -0
  7. package/dist/hooks/readonly.js +83 -0
  8. package/dist/hooks/tools.d.ts +7 -0
  9. package/dist/hooks/tools.js +16 -0
  10. package/dist/init-deposit/agent-hooks.d.ts +4 -1
  11. package/dist/init-deposit/agent-hooks.js +77 -21
  12. package/dist/policy/index.d.ts +3 -0
  13. package/dist/policy/index.js +45 -1
  14. package/dist/policy/product-signal.d.ts +45 -0
  15. package/dist/policy/product-signal.js +210 -0
  16. package/dist/policy/runtime-authority.d.ts +54 -0
  17. package/dist/policy/runtime-authority.js +180 -0
  18. package/dist/policy/staleness-tickler.d.ts +20 -0
  19. package/dist/policy/staleness-tickler.js +151 -0
  20. package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
  21. package/dist/pr-merge-readiness/ci-gate.js +38 -5
  22. package/dist/pr-merge-readiness/gh.d.ts +4 -0
  23. package/dist/pr-merge-readiness/gh.js +10 -3
  24. package/dist/pr-merge-readiness/index.d.ts +2 -1
  25. package/dist/pr-merge-readiness/index.js +2 -1
  26. package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
  27. package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
  28. package/dist/pr-watch/constants.d.ts +5 -0
  29. package/dist/pr-watch/constants.js +6 -1
  30. package/dist/pr-watch/main.js +8 -0
  31. package/dist/pr-watch/probe.js +8 -0
  32. package/dist/pr-watch/types.d.ts +7 -0
  33. package/dist/pr-watch/watch.js +8 -1
  34. package/dist/product-signal/actor-name.d.ts +18 -0
  35. package/dist/product-signal/actor-name.js +94 -0
  36. package/dist/product-signal/consent.d.ts +30 -0
  37. package/dist/product-signal/consent.js +116 -0
  38. package/dist/product-signal/gates.d.ts +17 -0
  39. package/dist/product-signal/gates.js +66 -0
  40. package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
  41. package/dist/product-signal/github-private-sink-adapter.js +274 -0
  42. package/dist/product-signal/headless.d.ts +8 -0
  43. package/dist/product-signal/headless.js +32 -0
  44. package/dist/product-signal/install-context.d.ts +13 -0
  45. package/dist/product-signal/install-context.js +41 -0
  46. package/dist/product-signal/local-signal-summary.d.ts +7 -0
  47. package/dist/product-signal/local-signal-summary.js +173 -0
  48. package/dist/product-signal/payload.d.ts +90 -0
  49. package/dist/product-signal/payload.js +124 -0
  50. package/dist/product-signal/sink-bootstrap.d.ts +29 -0
  51. package/dist/product-signal/sink-bootstrap.js +108 -0
  52. package/dist/product-signal/submit-adapter.d.ts +14 -0
  53. package/dist/product-signal/submit-adapter.js +2 -0
  54. package/dist/product-signal/submit.d.ts +54 -0
  55. package/dist/product-signal/submit.js +290 -0
  56. package/dist/scm/gh-rest.d.ts +3 -1
  57. package/dist/scm/gh-rest.js +22 -0
  58. package/dist/scope/main.js +12 -0
  59. package/dist/session/release-availability.d.ts +21 -0
  60. package/dist/session/release-availability.js +109 -0
  61. package/dist/session/ritual-sentinel.d.ts +14 -0
  62. package/dist/session/ritual-sentinel.js +28 -0
  63. package/dist/session/session-start.d.ts +10 -0
  64. package/dist/session/session-start.js +18 -0
  65. package/dist/staleness-tickler/escalation.d.ts +14 -0
  66. package/dist/staleness-tickler/escalation.js +106 -0
  67. package/dist/staleness-tickler/idle.d.ts +18 -0
  68. package/dist/staleness-tickler/idle.js +47 -0
  69. package/dist/staleness-tickler/index.d.ts +8 -0
  70. package/dist/staleness-tickler/index.js +8 -0
  71. package/dist/staleness-tickler/probe-directive.d.ts +32 -0
  72. package/dist/staleness-tickler/probe-directive.js +106 -0
  73. package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
  74. package/dist/staleness-tickler/probe-xbrief.js +103 -0
  75. package/dist/staleness-tickler/run.d.ts +31 -0
  76. package/dist/staleness-tickler/run.js +227 -0
  77. package/dist/staleness-tickler/state.d.ts +10 -0
  78. package/dist/staleness-tickler/state.js +46 -0
  79. package/dist/staleness-tickler/types.d.ts +85 -0
  80. package/dist/staleness-tickler/types.js +5 -0
  81. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  82. package/dist/vbrief-validate/plan-hooks.js +50 -0
  83. package/dist/verify-env/agent-hooks.js +4 -2
  84. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
3
4
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
4
5
  import { canonicalLogPath, readAll } from "./audit-log.js";
5
6
  import { TRANSITIONS } from "./constants.js";
@@ -110,6 +111,17 @@ export function lifecycleMain(argv) {
110
111
  catch {
111
112
  /* best-effort drift warning + reconcile; lifecycle success remains authoritative */
112
113
  }
114
+ try {
115
+ const rootForTickler = resolveProjectRoot(projectRoot) ??
116
+ dirname(dirname(dirname(completedPathForScopeMove(filePath))));
117
+ const tickler = maybeRunStalenessTickler(rootForTickler);
118
+ for (const line of tickler.lines) {
119
+ process.stdout.write(`${line}\n`);
120
+ }
121
+ }
122
+ catch {
123
+ /* best-effort staleness tickler; lifecycle success remains authoritative */
124
+ }
113
125
  }
114
126
  return 0;
115
127
  }
@@ -0,0 +1,21 @@
1
+ export interface ReleaseAvailabilityProbeOptions {
2
+ readonly now?: Date;
3
+ readonly env?: NodeJS.ProcessEnv;
4
+ readonly readText?: (path: string) => string | null;
5
+ readonly isFile?: (path: string) => boolean;
6
+ readonly runNpmView?: () => {
7
+ ok: boolean;
8
+ version: string;
9
+ };
10
+ readonly readState?: (path: string) => string | null;
11
+ readonly writeState?: (path: string, content: string) => void;
12
+ }
13
+ export interface ReleaseAvailabilityProbeResult {
14
+ readonly lines: readonly string[];
15
+ }
16
+ /**
17
+ * Probe the public npm registry separately from doctor. Doctor stays offline
18
+ * by default (#2182); mutable session start may emit this bounded advisory.
19
+ */
20
+ export declare function probeSessionReleaseAvailability(projectRoot: string, options?: ReleaseAvailabilityProbeOptions): ReleaseAvailabilityProbeResult;
21
+ //# sourceMappingURL=release-availability.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
5
+ import { runningInsideDeftRepo } from "../doctor/paths.js";
6
+ import { evaluateReleaseAvailability } from "../doctor/release-availability.js";
7
+ const THROTTLE_MS = 24 * 60 * 60 * 1000;
8
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
9
+ const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "release-availability-state.json");
10
+ function defaultReadText(path) {
11
+ try {
12
+ return readFileSync(path, "utf8");
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function defaultNpmView() {
19
+ const result = spawnSync("npm", [
20
+ "view",
21
+ "@deftai/directive",
22
+ "version",
23
+ `--registry=${PUBLIC_NPM_REGISTRY}`,
24
+ "--ignore-scripts",
25
+ ], {
26
+ encoding: "utf8",
27
+ timeout: 5_000,
28
+ });
29
+ const version = (result.stdout ?? "").trim().split(/\r?\n/)[0]?.trim() ?? "";
30
+ return { ok: result.status === 0 && version.length > 0, version };
31
+ }
32
+ function parseState(text) {
33
+ if (text === null)
34
+ return {};
35
+ try {
36
+ const parsed = JSON.parse(text);
37
+ if (parsed !== null && typeof parsed === "object") {
38
+ return parsed;
39
+ }
40
+ }
41
+ catch {
42
+ // A stale advisory state is never allowed to block session start.
43
+ }
44
+ return {};
45
+ }
46
+ function isThrottled(state, latestVersion, now) {
47
+ if (state.latestVersion !== latestVersion || !state.notifiedAt)
48
+ return false;
49
+ const notifiedAt = Date.parse(state.notifiedAt);
50
+ return Number.isFinite(notifiedAt) && now.getTime() - notifiedAt < THROTTLE_MS;
51
+ }
52
+ function defaultWriteState(path, content) {
53
+ mkdirSync(dirname(path), { recursive: true });
54
+ const temporary = `${path}.${process.pid}.tmp`;
55
+ writeFileSync(temporary, content, "utf8");
56
+ renameSync(temporary, path);
57
+ }
58
+ /**
59
+ * Probe the public npm registry separately from doctor. Doctor stays offline
60
+ * by default (#2182); mutable session start may emit this bounded advisory.
61
+ */
62
+ export function probeSessionReleaseAvailability(projectRoot, options = {}) {
63
+ const env = options.env ?? process.env;
64
+ if (env.DEFT_NO_NETWORK === "1") {
65
+ return { lines: ["[deft release] skipped (DEFT_NO_NETWORK=1)."] };
66
+ }
67
+ if (runningInsideDeftRepo(projectRoot)) {
68
+ return { lines: [] };
69
+ }
70
+ const readText = options.readText ?? defaultReadText;
71
+ const isFile = options.isFile ?? existsSync;
72
+ const manifestPath = locateManifest(projectRoot, null, isFile);
73
+ const manifestText = manifestPath ? readText(manifestPath) : null;
74
+ if (manifestText === null)
75
+ return { lines: [] };
76
+ const manifest = parseInstallManifest(manifestText);
77
+ const installed = (manifest.tag ?? manifest.ref ?? "").trim();
78
+ if (evaluateReleaseAvailability(installed, null).status === "not-applicable") {
79
+ return { lines: [] };
80
+ }
81
+ const lines = [
82
+ `[deft release] Checking ${PUBLIC_NPM_REGISTRY} for a newer published Directive release.`,
83
+ ];
84
+ let npmResult;
85
+ try {
86
+ npmResult = (options.runNpmView ?? defaultNpmView)();
87
+ }
88
+ catch {
89
+ npmResult = { ok: false, version: "" };
90
+ }
91
+ const availability = evaluateReleaseAvailability(installed, npmResult.ok ? npmResult.version : null);
92
+ if (availability.status !== "available")
93
+ return { lines };
94
+ const statePath = join(projectRoot, STATE_RELATIVE_PATH);
95
+ const state = parseState((options.readState ?? defaultReadText)(statePath));
96
+ const now = options.now ?? new Date();
97
+ if (isThrottled(state, availability.latestVersion, now))
98
+ return { lines: [] };
99
+ const message = `[deft release] Newer Directive release available: v${availability.latestVersion} ` +
100
+ `(installed v${availability.installedVersion}). Run \`npm i -g @deftai/directive@latest\`.`;
101
+ try {
102
+ (options.writeState ?? defaultWriteState)(statePath, `${JSON.stringify({ latestVersion: availability.latestVersion, notifiedAt: now.toISOString() }, null, 2)}\n`);
103
+ }
104
+ catch {
105
+ // The advisory remains useful if its best-effort throttle state cannot persist.
106
+ }
107
+ return { lines: [...lines, message] };
108
+ }
109
+ //# sourceMappingURL=release-availability.js.map
@@ -43,6 +43,20 @@ export declare function newRitualStatePayload(input: {
43
43
  }): Record<string, unknown>;
44
44
  export declare function readRitualState(projectRoot: string): [RitualState | null, string | null];
45
45
  export declare function writeRitualState(projectRoot: string, payload: Record<string, unknown>): string;
46
+ /** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
47
+ export declare const RITUAL_STALE_EPOCH: Date;
48
+ export interface MarkRitualStaleAfterCompactResult {
49
+ readonly changed: boolean;
50
+ readonly statePath: string;
51
+ readonly message: string;
52
+ }
53
+ /**
54
+ * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
55
+ * Reuses ritual-state age semantics — no parallel policy stack.
56
+ */
57
+ export declare function markRitualStaleAfterCompact(projectRoot: string, input?: {
58
+ now?: Date;
59
+ }): MarkRitualStaleAfterCompactResult;
46
60
  export declare function recordRitualStep(projectRoot: string, input: {
47
61
  tier: "quick" | "gated";
48
62
  stepName: string;
@@ -184,6 +184,34 @@ export function writeRitualState(projectRoot, payload) {
184
184
  atomicWriteJson(stateFile, payload, ".ritual-state.");
185
185
  return stateFile;
186
186
  }
187
+ /** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
188
+ export const RITUAL_STALE_EPOCH = new Date("1970-01-01T00:00:00Z");
189
+ /**
190
+ * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
191
+ * Reuses ritual-state age semantics — no parallel policy stack.
192
+ */
193
+ export function markRitualStaleAfterCompact(projectRoot, input = {}) {
194
+ const now = input.now ?? new Date();
195
+ const statePath = ritualStatePath(projectRoot);
196
+ const [state, err] = readRitualState(projectRoot);
197
+ if (state === null) {
198
+ return {
199
+ changed: false,
200
+ statePath,
201
+ message: err ?? "no ritual state to invalidate after compaction",
202
+ };
203
+ }
204
+ const payload = { ...state.raw };
205
+ payload.started_at = timestampIso(RITUAL_STALE_EPOCH);
206
+ payload.compact_resume_at = timestampIso(now);
207
+ writeRitualState(projectRoot, payload);
208
+ return {
209
+ changed: true,
210
+ statePath,
211
+ message: "Marked session ritual stale after context compaction; run session:start and " +
212
+ "verify:session-ritual -- --tier=gated before direct writes.",
213
+ };
214
+ }
187
215
  export function recordRitualStep(projectRoot, input) {
188
216
  const [state, err] = readRitualState(projectRoot);
189
217
  if (state === null) {
@@ -1,6 +1,7 @@
1
1
  import { type EnvironmentContext } from "../platform/shell-context.js";
2
2
  import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
3
3
  import type { GitRunner } from "./git.js";
4
+ import { type ReleaseAvailabilityProbeOptions } from "./release-availability.js";
4
5
  import { ritualStatePath } from "./ritual-sentinel.js";
5
6
  export declare const SESSION_POSTURES: readonly ["read-only", "mutation"];
6
7
  export type SessionPosture = (typeof SESSION_POSTURES)[number];
@@ -42,6 +43,15 @@ export interface SessionStartOptions {
42
43
  };
43
44
  readonly resolveUserMd?: (projectRoot: string) => ResolveUserMdResult;
44
45
  readonly probeEnvironment?: () => EnvironmentContext;
46
+ readonly probeReleaseAvailability?: (projectRoot: string, options: ReleaseAvailabilityProbeOptions) => {
47
+ lines: readonly string[];
48
+ };
49
+ readonly runStalenessTickler?: (projectRoot: string, options: {
50
+ now?: Date;
51
+ }) => {
52
+ lines: readonly string[];
53
+ prompted: boolean;
54
+ };
45
55
  }
46
56
  export declare function parseDeferrals(rawValues: readonly string[]): {
47
57
  deferrals: Record<string, string>;
@@ -5,11 +5,13 @@ import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposi
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
6
  import { disclosureLine } from "../policy/disclosure.js";
7
7
  import { resolvePolicy } from "../policy/resolve.js";
8
+ import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
8
9
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
9
10
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
10
11
  import { emitSessionValueReadback } from "../value/readback.js";
11
12
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
12
13
  import { defaultGitRunner, gitHead, worktreePath } from "./git.js";
14
+ import { probeSessionReleaseAvailability, } from "./release-availability.js";
13
15
  import { newRitualStatePayload, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
14
16
  export const SESSION_POSTURES = ["read-only", "mutation"];
15
17
  export const READ_ONLY_POSTURE = "read-only";
@@ -341,6 +343,22 @@ export function runSessionStart(projectRoot, options = {}) {
341
343
  lines.push(message);
342
344
  }
343
345
  }
346
+ try {
347
+ const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
348
+ lines.push(...releaseAvailability.lines);
349
+ }
350
+ catch {
351
+ // Release availability is a best-effort operator advisory, never a session blocker.
352
+ }
353
+ try {
354
+ const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
355
+ now: instant,
356
+ });
357
+ lines.push(...tickler.lines);
358
+ }
359
+ catch {
360
+ // Staleness tickler is best-effort and must never block session start.
361
+ }
344
362
  if (!runningInsideDeftRepo(projectRoot) && shouldEmitMigrateNudge(projectRoot)) {
345
363
  lines.push(MIGRATE_COMPLETION_NUDGE);
346
364
  }
@@ -0,0 +1,14 @@
1
+ import type { DriftInputs, DriftScoreResult, StalenessTicklerPolicy, StalenessTicklerState, StalenessTicklerTier, XbriefSchemaDistance } from "./types.js";
2
+ /** Compute a weighted drift score from independent axes (#2489). */
3
+ export declare function scoreDrift(inputs: DriftInputs, policy: StalenessTicklerPolicy): number;
4
+ /** Map drift inputs to an escalation tier using typed thresholds. */
5
+ export declare function resolveTier(inputs: DriftInputs, policy: StalenessTicklerPolicy): StalenessTicklerTier;
6
+ /** Hold the last known tier when detection is unverified (#2489 monotonicity). */
7
+ export declare function holdTierOnUnverified(computedTier: StalenessTicklerTier, computedScore: number, state: StalenessTicklerState, directiveUnverified: boolean): DriftScoreResult;
8
+ /** Snooze window widens with deferrals up to `maxWidenMultiplier`. */
9
+ export declare function snoozeWindowMs(tier: StalenessTicklerTier, deferralCount: number, policy: StalenessTicklerPolicy): number;
10
+ export declare function isSnoozeActive(state: StalenessTicklerState, now: Date): boolean;
11
+ /** Assert tier re-prompts at idle even when a plain snooze would still be active. */
12
+ export declare function shouldPromptDespiteSnooze(tier: StalenessTicklerTier, state: StalenessTicklerState): boolean;
13
+ export declare function mergeHeldXbriefDistance(current: XbriefSchemaDistance, held: XbriefSchemaDistance | undefined): XbriefSchemaDistance;
14
+ //# sourceMappingURL=escalation.d.ts.map
@@ -0,0 +1,106 @@
1
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
2
+ /** Compute a weighted drift score from independent axes (#2489). */
3
+ export function scoreDrift(inputs, policy) {
4
+ const { weights } = policy;
5
+ let score = 0;
6
+ if (inputs.directive.stale) {
7
+ if (inputs.directive.majorBehind) {
8
+ score += weights.directiveMajor;
9
+ }
10
+ score += inputs.directive.minorDistance * weights.directiveMinor;
11
+ score += inputs.directive.patchDistance * weights.directivePatch;
12
+ }
13
+ if (inputs.xbrief.stale) {
14
+ if (inputs.xbrief.distance === "behind-major") {
15
+ score += weights.schemaMajor;
16
+ }
17
+ else if (inputs.xbrief.distance === "behind-minor") {
18
+ score += weights.schemaMinor;
19
+ }
20
+ }
21
+ score += (inputs.ageMs / MS_PER_DAY) * weights.agePerDay;
22
+ score += inputs.deferralCount * weights.deferral;
23
+ return score;
24
+ }
25
+ function baseTierFromInputs(inputs, policy) {
26
+ const { tiers } = policy;
27
+ if (inputs.directive.majorBehind ||
28
+ inputs.xbrief.distance === "behind-major" ||
29
+ inputs.ageMs >= tiers.strongAgeMs) {
30
+ return "strong";
31
+ }
32
+ if (inputs.directive.minorDistance >= tiers.noticeMinorThreshold ||
33
+ inputs.xbrief.distance === "behind-minor") {
34
+ return "notice";
35
+ }
36
+ return "quiet";
37
+ }
38
+ /** Map drift inputs to an escalation tier using typed thresholds. */
39
+ export function resolveTier(inputs, policy) {
40
+ const base = baseTierFromInputs(inputs, policy);
41
+ if (base === "strong" && inputs.deferralCount >= policy.tiers.assertDeferralCap) {
42
+ return "assert";
43
+ }
44
+ return base;
45
+ }
46
+ /** Hold the last known tier when detection is unverified (#2489 monotonicity). */
47
+ export function holdTierOnUnverified(computedTier, computedScore, state, directiveUnverified) {
48
+ if (!directiveUnverified || state.lastTier === undefined) {
49
+ return { score: computedScore, tier: computedTier };
50
+ }
51
+ const heldTier = state.lastTier;
52
+ const heldScore = state.lastScore ?? computedScore;
53
+ const tierRank = (tier) => tier === "quiet" ? 0 : tier === "notice" ? 1 : tier === "strong" ? 2 : 3;
54
+ if (tierRank(heldTier) > tierRank(computedTier)) {
55
+ return { score: heldScore, tier: heldTier };
56
+ }
57
+ return { score: computedScore, tier: computedTier };
58
+ }
59
+ function baseSnoozeMs(tier, policy) {
60
+ switch (tier) {
61
+ case "quiet":
62
+ return policy.snooze.quietMs;
63
+ case "notice":
64
+ return policy.snooze.noticeMs;
65
+ case "strong":
66
+ return policy.snooze.strongMs;
67
+ case "assert":
68
+ return policy.snooze.strongMs;
69
+ default:
70
+ return policy.snooze.noticeMs;
71
+ }
72
+ }
73
+ /** Snooze window widens with deferrals up to `maxWidenMultiplier`. */
74
+ export function snoozeWindowMs(tier, deferralCount, policy) {
75
+ const base = baseSnoozeMs(tier, policy);
76
+ const multiplier = Math.min(1 + deferralCount, Math.max(1, policy.snooze.maxWidenMultiplier));
77
+ if (tier === "assert") {
78
+ return Math.min(base, policy.snooze.strongMs);
79
+ }
80
+ return Math.round(base * multiplier);
81
+ }
82
+ export function isSnoozeActive(state, now) {
83
+ if (!state.snoozedUntil) {
84
+ return false;
85
+ }
86
+ const until = Date.parse(state.snoozedUntil);
87
+ return Number.isFinite(until) && now.getTime() < until;
88
+ }
89
+ /** Assert tier re-prompts at idle even when a plain snooze would still be active. */
90
+ export function shouldPromptDespiteSnooze(tier, state) {
91
+ if (tier !== "assert") {
92
+ return false;
93
+ }
94
+ if (state.remindAfterNextRelease) {
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ export function mergeHeldXbriefDistance(current, held) {
100
+ if (held === undefined) {
101
+ return current;
102
+ }
103
+ const rank = (distance) => distance === "current" ? 0 : distance === "behind-minor" ? 1 : 2;
104
+ return rank(held) > rank(current) ? held : current;
105
+ }
106
+ //# sourceMappingURL=escalation.js.map
@@ -0,0 +1,18 @@
1
+ import type { StalenessTicklerPolicy } from "./types.js";
2
+ export interface IdleGateOptions {
3
+ readonly env?: NodeJS.ProcessEnv;
4
+ readonly readPorcelain?: (projectRoot: string) => string | null;
5
+ readonly countInFlight?: (projectRoot: string) => number;
6
+ readonly insideDeftRepo?: (projectRoot: string) => boolean;
7
+ }
8
+ export interface IdleGateResult {
9
+ readonly ok: boolean;
10
+ readonly reason?: string;
11
+ }
12
+ /** True when every #2488 idle gate passes for an interactive tickler prompt. */
13
+ export declare function isSafeIdlePoint(projectRoot: string, policy: StalenessTicklerPolicy, options?: IdleGateOptions): IdleGateResult;
14
+ /** Skip the tickler entirely in CI/headless ritual-skip contexts. */
15
+ export declare function shouldSkipTicklerEntirely(env?: NodeJS.ProcessEnv): boolean;
16
+ /** Whether stdin/stdout are TTYs suitable for a blocking consent prompt. */
17
+ export declare function isInteractiveSession(env?: NodeJS.ProcessEnv, isTty?: () => boolean): boolean;
18
+ //# sourceMappingURL=idle.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { runningInsideDeftRepo } from "../doctor/paths.js";
2
+ import { gitPorcelain } from "../story-ready/git.js";
3
+ import { countFilesystemInFlight } from "../triage/summary/index.js";
4
+ function ritualSkipActive(env) {
5
+ return env.DEFT_SESSION_RITUAL_SKIP === "1";
6
+ }
7
+ /** True when every #2488 idle gate passes for an interactive tickler prompt. */
8
+ export function isSafeIdlePoint(projectRoot, policy, options = {}) {
9
+ const env = options.env ?? process.env;
10
+ if (ritualSkipActive(env)) {
11
+ return { ok: false, reason: "DEFT_SESSION_RITUAL_SKIP=1" };
12
+ }
13
+ if (!policy.enabled || policy.optOut) {
14
+ return { ok: false, reason: "policy-disabled" };
15
+ }
16
+ const insideDeft = options.insideDeftRepo ?? ((root) => runningInsideDeftRepo(root));
17
+ if (insideDeft(projectRoot)) {
18
+ return { ok: false, reason: "framework-repo" };
19
+ }
20
+ const porcelain = (options.readPorcelain ?? gitPorcelain)(projectRoot);
21
+ if (porcelain === null) {
22
+ return { ok: false, reason: "git-undeterminable" };
23
+ }
24
+ if (porcelain.trim().length > 0) {
25
+ return { ok: false, reason: "dirty-tree" };
26
+ }
27
+ const inFlight = (options.countInFlight ?? countFilesystemInFlight)(projectRoot);
28
+ if (inFlight > 0) {
29
+ return { ok: false, reason: "story-in-flight" };
30
+ }
31
+ return { ok: true };
32
+ }
33
+ /** Skip the tickler entirely in CI/headless ritual-skip contexts. */
34
+ export function shouldSkipTicklerEntirely(env = process.env) {
35
+ return ritualSkipActive(env);
36
+ }
37
+ /** Whether stdin/stdout are TTYs suitable for a blocking consent prompt. */
38
+ export function isInteractiveSession(env = process.env, isTty = () => process.stdin.isTTY === true && process.stdout.isTTY === true) {
39
+ if (env.DEFT_SESSION_RITUAL_SKIP === "1") {
40
+ return false;
41
+ }
42
+ if (env.CI === "true" || env.CI === "1") {
43
+ return false;
44
+ }
45
+ return isTty();
46
+ }
47
+ //# sourceMappingURL=idle.js.map
@@ -0,0 +1,8 @@
1
+ export * from "./escalation.js";
2
+ export * from "./idle.js";
3
+ export * from "./probe-directive.js";
4
+ export * from "./probe-xbrief.js";
5
+ export { DIRECTIVE_UPGRADE_COMMAND, maybeRunStalenessTickler, probeStalenessDimensions, XBRIEF_MIGRATE_COMMAND, } from "./run.js";
6
+ export * from "./state.js";
7
+ export * from "./types.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ export * from "./escalation.js";
2
+ export * from "./idle.js";
3
+ export * from "./probe-directive.js";
4
+ export * from "./probe-xbrief.js";
5
+ export { DIRECTIVE_UPGRADE_COMMAND, maybeRunStalenessTickler, probeStalenessDimensions, XBRIEF_MIGRATE_COMMAND, } from "./run.js";
6
+ export * from "./state.js";
7
+ export * from "./types.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,32 @@
1
+ import { type ReleaseAvailabilityResult } from "../doctor/release-availability.js";
2
+ import type { ParsedSemverCore } from "./types.js";
3
+ export interface ProbeDirectiveOptions {
4
+ readonly env?: NodeJS.ProcessEnv;
5
+ readonly readText?: (path: string) => string | null;
6
+ readonly isFile?: (path: string) => boolean;
7
+ readonly runNpmView?: () => {
8
+ ok: boolean;
9
+ version: string;
10
+ };
11
+ }
12
+ /** Parse `X.Y.Z` or `vX.Y.Z` core semver (ignores pre-release suffix). */
13
+ export declare function parseSemverCore(version: string): ParsedSemverCore | null;
14
+ export declare function computeDirectiveDistance(installedVersion: string, latestVersion: string): {
15
+ majorBehind: boolean;
16
+ minorDistance: number;
17
+ patchDistance: number;
18
+ };
19
+ export interface ProbeDirectiveResult {
20
+ readonly availability: ReleaseAvailabilityResult;
21
+ readonly majorBehind: boolean;
22
+ readonly minorDistance: number;
23
+ readonly patchDistance: number;
24
+ readonly stale: boolean;
25
+ readonly registryDisclosure: string;
26
+ }
27
+ /**
28
+ * Probe Directive payload staleness via the public npm registry (#1692 substrate).
29
+ * Skips network when `DEFT_NO_NETWORK=1`.
30
+ */
31
+ export declare function probeDirectiveStaleness(projectRoot: string, options?: ProbeDirectiveOptions): ProbeDirectiveResult | null;
32
+ //# sourceMappingURL=probe-directive.d.ts.map
@@ -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