@deftai/directive-core 0.76.0 → 0.78.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 (70) hide show
  1. package/dist/agents-md-budget/evaluate.d.ts +8 -1
  2. package/dist/agents-md-budget/evaluate.js +54 -19
  3. package/dist/content-contracts/skills/skill-frontmatter.js +4 -0
  4. package/dist/doctor/constants.js +3 -3
  5. package/dist/doctor/index.d.ts +1 -0
  6. package/dist/doctor/index.js +1 -0
  7. package/dist/doctor/main.d.ts +1 -0
  8. package/dist/doctor/main.js +39 -1
  9. package/dist/doctor/payload-staleness.js +62 -63
  10. package/dist/doctor/release-availability.d.ts +28 -0
  11. package/dist/doctor/release-availability.js +35 -0
  12. package/dist/doctor/types.d.ts +3 -0
  13. package/dist/eval/crud-telemetry.d.ts +5 -4
  14. package/dist/eval/crud-telemetry.js +9 -5
  15. package/dist/eval/health.d.ts +5 -4
  16. package/dist/eval/health.js +24 -16
  17. package/dist/eval/readback.js +2 -8
  18. package/dist/eval/triggers.d.ts +76 -0
  19. package/dist/eval/triggers.js +259 -0
  20. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  21. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  22. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  23. package/dist/eval-triggers-relocation/index.js +2 -0
  24. package/dist/hooks/dispatcher.d.ts +43 -0
  25. package/dist/hooks/dispatcher.js +171 -0
  26. package/dist/hooks/index.d.ts +3 -0
  27. package/dist/hooks/index.js +3 -0
  28. package/dist/hooks/scope.d.ts +12 -0
  29. package/dist/hooks/scope.js +46 -0
  30. package/dist/hooks/tools.d.ts +4 -0
  31. package/dist/hooks/tools.js +23 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  35. package/dist/init-deposit/agent-hooks.js +228 -0
  36. package/dist/init-deposit/hygiene.js +3 -0
  37. package/dist/init-deposit/index.d.ts +1 -0
  38. package/dist/init-deposit/index.js +1 -0
  39. package/dist/init-deposit/init-deposit.js +2 -0
  40. package/dist/init-deposit/refresh.js +2 -0
  41. package/dist/intake/reconcile-issues.d.ts +1 -0
  42. package/dist/intake/reconcile-issues.js +51 -49
  43. package/dist/metrics/index.d.ts +2 -0
  44. package/dist/metrics/index.js +2 -0
  45. package/dist/metrics/resolve-metrics-home.d.ts +50 -0
  46. package/dist/metrics/resolve-metrics-home.js +125 -0
  47. package/dist/plan-sequence/index.d.ts +3 -0
  48. package/dist/plan-sequence/index.js +3 -0
  49. package/dist/plan-sequence/store.d.ts +6 -0
  50. package/dist/plan-sequence/store.js +29 -0
  51. package/dist/plan-sequence/types.d.ts +79 -0
  52. package/dist/plan-sequence/types.js +220 -0
  53. package/dist/release/build-dist.js +1 -0
  54. package/dist/release/version.d.ts +2 -0
  55. package/dist/release/version.js +42 -0
  56. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  57. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  58. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  59. package/dist/release-e2e/npm-ops.js +20 -6
  60. package/dist/session/verify-session-ritual.d.ts +16 -0
  61. package/dist/session/verify-session-ritual.js +63 -0
  62. package/dist/triage/help/registry-data.d.ts +12 -1
  63. package/dist/triage/help/registry-data.js +22 -1
  64. package/dist/verify-env/agent-hooks.d.ts +11 -0
  65. package/dist/verify-env/agent-hooks.js +45 -0
  66. package/dist/verify-env/command-spawn.d.ts +29 -0
  67. package/dist/verify-env/command-spawn.js +97 -0
  68. package/dist/verify-env/index.d.ts +2 -0
  69. package/dist/verify-env/index.js +2 -0
  70. package/package.json +19 -3
@@ -4,7 +4,11 @@ export type OutputStream = "stdout" | "stderr" | "none";
4
4
  /**
5
5
  * Layered absolute north-star for the always-on managed surface (#2372 / #2450 / #2452).
6
6
  *
7
- * `ABSOLUTE_MANAGED_MAX_BYTES` is the relocation north-star (<=8192 B / ~2k tok).
7
+ * `ABSOLUTE_MANAGED_MAX_BYTES` is the managed-section relocation north-star (<=8192 B / ~2k tok)
8
+ * — Phase-2 success bar. `COMBINED_ALWAYS_ON_MAX_BYTES` is the Phase-3 combined always-on
9
+ * north-star (managed + DD-3 + hooks), raised to 9 KB after diminishing returns on further
10
+ * thinning (#2531 closeout).
11
+ *
8
12
  * When `plan.policy.agentsMdBudget.absoluteMaxBytes` is set, growth past that seeded
9
13
  * ratchet fails closed; distance-to-north-star is always reported. Optional release-gate
10
14
  * north-star enforcement: DEFT_AGENTS_MD_BUDGET_ENFORCE_NORTH_STAR=1 (waiver:
@@ -17,6 +21,9 @@ export type OutputStream = "stdout" | "stderr" | "none";
17
21
  */
18
22
  export declare const ABSOLUTE_MANAGED_MAX_BYTES = 8192;
19
23
  export declare const ABSOLUTE_MANAGED_MAX_TOKENS = 2000;
24
+ /** Phase-3 combined always-on north-star (managed + DD-3 + hooks) — 9 KB (#2531). */
25
+ export declare const COMBINED_ALWAYS_ON_MAX_BYTES = 9216;
26
+ export declare const COMBINED_ALWAYS_ON_MAX_TOKENS = 2304;
20
27
  /** Bootstrap host hooks are 0 B until #2438 ships. */
21
28
  export declare const BOOTSTRAP_HOOK_BYTES = 0;
22
29
  /** Rough UTF-8 bytes-per-token estimate for advisory reporting (~8192 B ≈ ~2048 tok). */
@@ -7,7 +7,11 @@ import { measureSkillFrontmatter } from "./skill-frontmatter.js";
7
7
  /**
8
8
  * Layered absolute north-star for the always-on managed surface (#2372 / #2450 / #2452).
9
9
  *
10
- * `ABSOLUTE_MANAGED_MAX_BYTES` is the relocation north-star (<=8192 B / ~2k tok).
10
+ * `ABSOLUTE_MANAGED_MAX_BYTES` is the managed-section relocation north-star (<=8192 B / ~2k tok)
11
+ * — Phase-2 success bar. `COMBINED_ALWAYS_ON_MAX_BYTES` is the Phase-3 combined always-on
12
+ * north-star (managed + DD-3 + hooks), raised to 9 KB after diminishing returns on further
13
+ * thinning (#2531 closeout).
14
+ *
11
15
  * When `plan.policy.agentsMdBudget.absoluteMaxBytes` is set, growth past that seeded
12
16
  * ratchet fails closed; distance-to-north-star is always reported. Optional release-gate
13
17
  * north-star enforcement: DEFT_AGENTS_MD_BUDGET_ENFORCE_NORTH_STAR=1 (waiver:
@@ -20,6 +24,9 @@ import { measureSkillFrontmatter } from "./skill-frontmatter.js";
20
24
  */
21
25
  export const ABSOLUTE_MANAGED_MAX_BYTES = 8192;
22
26
  export const ABSOLUTE_MANAGED_MAX_TOKENS = 2000;
27
+ /** Phase-3 combined always-on north-star (managed + DD-3 + hooks) — 9 KB (#2531). */
28
+ export const COMBINED_ALWAYS_ON_MAX_BYTES = 9216;
29
+ export const COMBINED_ALWAYS_ON_MAX_TOKENS = 2304;
23
30
  /** Bootstrap host hooks are 0 B until #2438 ships. */
24
31
  export const BOOTSTRAP_HOOK_BYTES = 0;
25
32
  /** Rough UTF-8 bytes-per-token estimate for advisory reporting (~8192 B ≈ ~2048 tok). */
@@ -186,7 +193,7 @@ function formatNorthStarOverage(measure) {
186
193
  return formatNorthStarOverageBytes(measure.bytes - ABSOLUTE_MANAGED_MAX_BYTES, measure.estimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS);
187
194
  }
188
195
  function formatCombinedNorthStarOverage(bootstrap) {
189
- return formatNorthStarOverageBytes(bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES, bootstrap.totalEstimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS);
196
+ return formatNorthStarOverageBytes(bootstrap.totalBytes - COMBINED_ALWAYS_ON_MAX_BYTES, bootstrap.totalEstimatedTokens - COMBINED_ALWAYS_ON_MAX_TOKENS);
190
197
  }
191
198
  function formatNorthStarDistance(measure) {
192
199
  const overBytes = measure.bytes - ABSOLUTE_MANAGED_MAX_BYTES;
@@ -200,14 +207,14 @@ function formatNorthStarDistance(measure) {
200
207
  `${formatNorthStarOverage(measure)}).`);
201
208
  }
202
209
  function formatCombinedNorthStarDistance(bootstrap) {
203
- const overBytes = bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES;
204
- const overTokens = bootstrap.totalEstimatedTokens - ABSOLUTE_MANAGED_MAX_TOKENS;
210
+ const overBytes = bootstrap.totalBytes - COMBINED_ALWAYS_ON_MAX_BYTES;
211
+ const overTokens = bootstrap.totalEstimatedTokens - COMBINED_ALWAYS_ON_MAX_TOKENS;
205
212
  if (overBytes <= 0 && overTokens <= 0) {
206
213
  return (`north-star: combined always-on ${bootstrap.totalBytes} bytes ` +
207
214
  `(~${bootstrap.totalEstimatedTokens} tok) within ` +
208
- `≤${ABSOLUTE_MANAGED_MAX_BYTES} B / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok target.`);
215
+ `≤${COMBINED_ALWAYS_ON_MAX_BYTES} B / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok target.`);
209
216
  }
210
- return (`north-star: combined always-on ≤${ABSOLUTE_MANAGED_MAX_BYTES} B / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
217
+ return (`north-star: combined always-on ≤${COMBINED_ALWAYS_ON_MAX_BYTES} B / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok ` +
211
218
  `(current ${bootstrap.totalBytes} bytes / ~${bootstrap.totalEstimatedTokens} tok — ` +
212
219
  `${formatCombinedNorthStarOverage(bootstrap)}).`);
213
220
  }
@@ -224,12 +231,24 @@ function formatBootstrapItemization(bootstrap) {
224
231
  }
225
232
  function formatAbsoluteAdvisory(bootstrap) {
226
233
  const measure = bootstrap.managed;
234
+ const overManagedBytes = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES;
235
+ const overManagedTokens = measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
236
+ const overCombinedBytes = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES;
237
+ const overCombinedTokens = bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
238
+ const violationLines = [];
239
+ if (overManagedBytes || overManagedTokens) {
240
+ violationLines.push(` Managed section alone: ${measure.bytes} bytes (~${measure.estimatedTokens} tok) exceeds the north-star ` +
241
+ `of ${ABSOLUTE_MANAGED_MAX_BYTES} bytes / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
242
+ `(OVER: ${formatNorthStarOverage(measure)}).`);
243
+ }
244
+ if (overCombinedBytes || overCombinedTokens) {
245
+ violationLines.push(` Combined always-on: ${bootstrap.totalBytes} bytes (~${bootstrap.totalEstimatedTokens} tok) exceeds the north-star ` +
246
+ `of ${COMBINED_ALWAYS_ON_MAX_BYTES} bytes / ~${COMBINED_ALWAYS_ON_MAX_TOKENS} tok ` +
247
+ `(OVER: ${formatCombinedNorthStarOverage(bootstrap)}).`);
248
+ }
227
249
  return (`⚠ verify:agents-md-budget: always-on bootstrap advisory — ` +
228
250
  `${formatBootstrapItemization(bootstrap)}.\n` +
229
- ` Managed section alone: ${measure.bytes} bytes (~${measure.estimatedTokens} tok) exceeds the north-star ` +
230
- `of ${ABSOLUTE_MANAGED_MAX_BYTES} bytes / ~${ABSOLUTE_MANAGED_MAX_TOKENS} tok ` +
231
- `(OVER: ${formatNorthStarOverage(measure)}).\n` +
232
- ` ${formatCombinedNorthStarDistance(bootstrap)}\n` +
251
+ `${violationLines.join("\n")}\n` +
233
252
  " Advisory only — set plan.policy.agentsMdBudget.absoluteMaxBytes to enable " +
234
253
  "fail-closed managed growth ratchet (#2452).\n" +
235
254
  " Optional DD-3 ratchet: plan.policy.agentsMdBudget.skillFrontmatterMaxBytes (#2463).\n" +
@@ -268,13 +287,29 @@ function formatSkillFrontmatterRefusal(bootstrap, skillFrontmatterMaxBytes, proj
268
287
  }
269
288
  function formatNorthStarRefusal(bootstrap, projectRoot) {
270
289
  const measure = bootstrap.managed;
271
- const overBytes = bootstrap.totalBytes - ABSOLUTE_MANAGED_MAX_BYTES;
272
- return (`❌ verify:agents-md-budget: combined always-on surface exceeds the north-star ceiling ` +
290
+ const overNorthStarManaged = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES ||
291
+ measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
292
+ const overNorthStarCombined = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES ||
293
+ bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
294
+ const headline = overNorthStarManaged && overNorthStarCombined
295
+ ? "managed and combined always-on surfaces exceed the north-star ceiling"
296
+ : overNorthStarManaged
297
+ ? "managed section exceeds the north-star ceiling"
298
+ : "combined always-on surface exceeds the north-star ceiling";
299
+ const detailLines = [];
300
+ if (overNorthStarManaged) {
301
+ detailLines.push(` managed section: ${measure.bytes}/${ABSOLUTE_MANAGED_MAX_BYTES} bytes ` +
302
+ `(OVER: ${formatNorthStarOverage(measure)})`);
303
+ }
304
+ if (overNorthStarCombined) {
305
+ detailLines.push(` combined always-on: ${bootstrap.totalBytes}/${COMBINED_ALWAYS_ON_MAX_BYTES} bytes ` +
306
+ `(OVER: ${formatCombinedNorthStarOverage(bootstrap)})`);
307
+ }
308
+ return (`❌ verify:agents-md-budget: ${headline} ` +
273
309
  `(project_root=${projectRoot}, release-gate mode).\n` +
274
310
  ` ${formatBootstrapItemization(bootstrap)}\n` +
275
- ` managed section: ${measure.bytes}/${ABSOLUTE_MANAGED_MAX_BYTES} bytes\n` +
276
- ` combined OVER by ${overBytes} bytes vs north-star\n` +
277
- " Thin the managed section and/or tier DD-3 skills toward the <=8192 B target,\n" +
311
+ `${detailLines.join("\n")}\n` +
312
+ " Thin the managed section and/or tier DD-3 skills toward the combined ≤9 KB target,\n" +
278
313
  " or set DEFT_ALLOW_ABSOLUTE_BUDGET_WAIVER=1 for a time-boxed operator waiver (#2452).");
279
314
  }
280
315
  function enforceNorthStarEnabled() {
@@ -287,8 +322,8 @@ function attachNorthStarNote(result, bootstrap, options) {
287
322
  const measure = bootstrap.managed;
288
323
  const overManagedBytes = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES;
289
324
  const overManagedTokens = measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
290
- const overCombinedBytes = bootstrap.totalBytes > ABSOLUTE_MANAGED_MAX_BYTES;
291
- const overCombinedTokens = bootstrap.totalEstimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
325
+ const overCombinedBytes = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES;
326
+ const overCombinedTokens = bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
292
327
  if (options.advisoryOnly) {
293
328
  if (!overManagedBytes && !overManagedTokens && !overCombinedBytes && !overCombinedTokens) {
294
329
  return result;
@@ -445,8 +480,8 @@ export function evaluate(projectRoot, options = {}) {
445
480
  }
446
481
  const overNorthStarManaged = measure.bytes > ABSOLUTE_MANAGED_MAX_BYTES ||
447
482
  measure.estimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
448
- const overNorthStarCombined = bootstrap.totalBytes > ABSOLUTE_MANAGED_MAX_BYTES ||
449
- bootstrap.totalEstimatedTokens > ABSOLUTE_MANAGED_MAX_TOKENS;
483
+ const overNorthStarCombined = bootstrap.totalBytes > COMBINED_ALWAYS_ON_MAX_BYTES ||
484
+ bootstrap.totalEstimatedTokens > COMBINED_ALWAYS_ON_MAX_TOKENS;
450
485
  if (enforceNorthStarEnabled() &&
451
486
  (overNorthStarManaged || overNorthStarCombined) &&
452
487
  !northStarWaiverActive()) {
@@ -11,6 +11,10 @@ const EXCLUDED_PARTS = new Set([
11
11
  "node_modules",
12
12
  "__pycache__",
13
13
  ".pytest_cache",
14
+ ".deft-scratch",
15
+ ".deft-cache",
16
+ "coverage",
17
+ "dist",
14
18
  ]);
15
19
  function frontmatter(text) {
16
20
  if (!text.startsWith("---")) {
@@ -29,13 +29,13 @@ export const DOCTOR_ALLOWED_FLAGS = [
29
29
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
30
30
  export const NPM_PACKAGE_NAME = "@deftai/directive";
31
31
  // #2182: payload-staleness is the only doctor check that can reach a network
32
- // endpoint (git ls-remote against the framework's git remote, falling back to
33
- // `npm view` against the npm registry). It is OFF by default (offline tier)
32
+ // endpoint (git ls-remote verifies the pinned ref and `npm view` compares a
33
+ // release-tag install with the latest stable package). It is OFF by default (offline tier)
34
34
  // and requires the explicit `--network` flag; this line discloses exactly
35
35
  // which tool + registry class it may contact BEFORE the check runs, and the
36
36
  // skip line tells an offline run how to opt in.
37
37
  export const NETWORK_DISCLOSURE_LINE = "[deft doctor] --network: this run may contact your git remote (framework " +
38
- "repo host) and, as a fallback, the npm registry (registry.npmjs.org) to " +
38
+ "repo host) and the npm registry (registry.npmjs.org) to " +
39
39
  `look up the latest ${NPM_PACKAGE_NAME} version.`;
40
40
  export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (default). Run `deft doctor --network` to check " +
41
41
  "framework currency against your git remote and the npm registry " +
@@ -8,6 +8,7 @@ export * from "./main.js";
8
8
  export * from "./manifest.js";
9
9
  export * from "./paths.js";
10
10
  export * from "./payload-staleness.js";
11
+ export * from "./release-availability.js";
11
12
  export * from "./taskfile.js";
12
13
  export * from "./types.js";
13
14
  export * from "./which.js";
@@ -8,6 +8,7 @@ export * from "./main.js";
8
8
  export * from "./manifest.js";
9
9
  export * from "./paths.js";
10
10
  export * from "./payload-staleness.js";
11
+ export * from "./release-availability.js";
11
12
  export * from "./taskfile.js";
12
13
  export * from "./types.js";
13
14
  export * from "./which.js";
@@ -2,6 +2,7 @@ import { type ResolutionFacts } from "../resolution/index.js";
2
2
  import { createPlainSink } from "./output.js";
3
3
  import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
4
4
  export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
5
+ export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
5
6
  /**
6
7
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
7
8
  * (#2267). The `directive` surface always works; `task deft:X` is optional and
@@ -6,6 +6,7 @@ import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "..
6
6
  import { loadProjectDefinition } from "../policy/resolve.js";
7
7
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
8
8
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
9
+ import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
9
10
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
10
11
  import { runChecks } from "./checks.js";
11
12
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
@@ -162,8 +163,14 @@ export function cmdDoctor(args, seams = {}) {
162
163
  if (!jsonMode) {
163
164
  sink.blank();
164
165
  }
166
+ sink.info("Checking agent-host hook registration...");
167
+ runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
168
+ if (!jsonMode) {
169
+ sink.blank();
170
+ }
165
171
  // #2182: payload-staleness is the only doctor check that can reach a
166
- // registry (git remote, then npm as a fallback). It stays in the OFFLINE
172
+ // registry (git verifies the pin; npm compares stable release availability).
173
+ // It stays in the OFFLINE
167
174
  // tier (skipped) unless the operator explicitly opts into the NETWORK tier
168
175
  // via `--network`, and the tool + registry class is disclosed BEFORE the
169
176
  // check runs -- never silently, never as a side effect of a read-only
@@ -331,6 +338,37 @@ export function cmdDoctor(args, seams = {}) {
331
338
  sink.finalWarn(`System check completed with ${warningCount} warning(s).`);
332
339
  return 0;
333
340
  }
341
+ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams) {
342
+ const checkName = "agent-hooks-registration";
343
+ if (!consumerContext) {
344
+ const reason = "maintainer source checkout; project hook deposit is consumer-only";
345
+ sink.info(`${checkName}: skip -- ${reason}`);
346
+ addFinding({ severity: "skip", message: reason, check: checkName, status: "skip" });
347
+ return;
348
+ }
349
+ try {
350
+ const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
351
+ if (result.code === 0) {
352
+ sink.success(`${checkName}: healthy`);
353
+ return;
354
+ }
355
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
356
+ sink.warn(message);
357
+ addFinding({
358
+ severity: "warning",
359
+ message,
360
+ check: checkName,
361
+ status: result.code === 2 ? "unavailable" : "non-functional",
362
+ registrations: result.registrations,
363
+ suggestion: "deft update",
364
+ });
365
+ }
366
+ catch (cause) {
367
+ const message = `${checkName}: probe failed -- ${String(cause)}`;
368
+ sink.warn(message);
369
+ addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
370
+ }
371
+ }
334
372
  function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
335
373
  if (runningInsideDeftRepo(projectRoot, seams)) {
336
374
  sink.info("Skipping install-integrity checks -- running inside the deft framework repo (no install manifest in the source checkout).");
@@ -4,6 +4,7 @@ import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/m
4
4
  import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
5
5
  import { locateManifest, parseInstallManifest } from "./manifest.js";
6
6
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
7
+ import { evaluateReleaseAvailability } from "./release-availability.js";
7
8
  function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
8
9
  try {
9
10
  const agents = join(projectRoot, "AGENTS.md");
@@ -38,42 +39,6 @@ function parseRemoteSha(stdout) {
38
39
  const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
39
40
  return firstLine.trim().split(/\s+/)[0] ?? "";
40
41
  }
41
- function parseSemver(version) {
42
- const normalized = version.trim().replace(/^v/i, "");
43
- const parts = [];
44
- for (const segment of normalized.split(".")) {
45
- const numeric = Number.parseInt(segment.split("-")[0] ?? "", 10);
46
- if (Number.isNaN(numeric)) {
47
- break;
48
- }
49
- parts.push(numeric);
50
- }
51
- return parts.length > 0 ? parts : [0];
52
- }
53
- function semverLessThan(left, right) {
54
- const a = parseSemver(left);
55
- const b = parseSemver(right);
56
- const len = Math.max(a.length, b.length);
57
- for (let i = 0; i < len; i += 1) {
58
- const av = a[i] ?? 0;
59
- const bv = b[i] ?? 0;
60
- if (av < bv) {
61
- return true;
62
- }
63
- if (av > bv) {
64
- return false;
65
- }
66
- }
67
- return false;
68
- }
69
- function manifestVersion(ref, tag) {
70
- const candidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
71
- const normalized = candidate.replace(/^v/i, "");
72
- if (!/^\d+(?:\.\d+)*/.test(normalized)) {
73
- return "";
74
- }
75
- return normalized;
76
- }
77
42
  function defaultNpmViewVersion() {
78
43
  const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
79
44
  encoding: "utf8",
@@ -82,6 +47,25 @@ function defaultNpmViewVersion() {
82
47
  const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
83
48
  return { ok: proc.status === 0 && version.length > 0, version };
84
49
  }
50
+ function emitReleaseAvailable(checkName, installedVersion, latestVersion, ref, upgradeCommand, sink, addFinding) {
51
+ const msg = `Newer framework release available (installed v${installedVersion}; ` +
52
+ `latest v${latestVersion} from npm registry). ` +
53
+ `Recommendation: run \`${upgradeCommand}\`.`;
54
+ sink.warn(msg);
55
+ addFinding({
56
+ severity: "warning",
57
+ message: msg,
58
+ check: checkName,
59
+ status: "stale",
60
+ staleness_kind: "newer-release",
61
+ ref,
62
+ installed_version: installedVersion,
63
+ latest_version: latestVersion,
64
+ remote_version: latestVersion,
65
+ resolver: "npm-view",
66
+ suggestion: upgradeCommand,
67
+ });
68
+ }
85
69
  function emitUnverified(checkName, reason, sink, addFinding) {
86
70
  const msg = `payload currency UNVERIFIED — ${reason}`;
87
71
  sink.warn(msg);
@@ -101,8 +85,8 @@ function resolveUpgradeCommand(projectRoot, manifest, isFile, pm = "npm") {
101
85
  }
102
86
  return vendored ? VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND : CANONICAL_UPGRADE_COMMAND;
103
87
  }
104
- function emitStale(checkName, installedLabel, remoteLabel, ref, upgradeCommand, sink, addFinding, extras = {}, behindWord = "remote") {
105
- const msg = `Framework payload is stale (installed ${installedLabel} behind ${behindWord} ${remoteLabel} for ref '${ref}'). ` +
88
+ function emitStale(checkName, installedLabel, remoteLabel, ref, upgradeCommand, sink, addFinding, extras = {}) {
89
+ const msg = `Framework payload is stale (installed ${installedLabel} behind remote ${remoteLabel} for ref '${ref}'). ` +
106
90
  `Recommendation: run \`${upgradeCommand}\` from any shell with Node ≥ 20.`;
107
91
  sink.warn(msg);
108
92
  addFinding({
@@ -209,37 +193,52 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
209
193
  catch {
210
194
  remoteResult = { ok: false, stdout: "" };
211
195
  }
212
- if (remoteResult.ok) {
213
- const remoteSha = parseRemoteSha(remoteResult.stdout);
214
- if (remoteSha) {
215
- if (installedSha === remoteSha) {
216
- sink.info(`${checkName}: current (sha matches remote)`);
217
- return;
218
- }
219
- emitStale(checkName, `sha ${installedSha.slice(0, 8)}...`, `sha ${remoteSha.slice(0, 8)}...`, ref, upgradeCommand, sink, addFinding, { installed_sha: installedSha, remote_sha: remoteSha, resolver: "git-ls-remote" });
220
- return;
221
- }
196
+ const remoteSha = remoteResult.ok ? parseRemoteSha(remoteResult.stdout) : "";
197
+ if (remoteSha && installedSha !== remoteSha) {
198
+ emitStale(checkName, `sha ${installedSha.slice(0, 8)}...`, `sha ${remoteSha.slice(0, 8)}...`, ref, upgradeCommand, sink, addFinding, {
199
+ installed_sha: installedSha,
200
+ remote_sha: remoteSha,
201
+ resolver: "git-ls-remote",
202
+ staleness_kind: "pinned-ref-moved",
203
+ });
204
+ return;
222
205
  }
223
- const npmResult = runNpmView();
224
- const installedVersion = manifestVersion(ref, tag);
225
- if (npmResult.ok && installedVersion) {
226
- if (semverLessThan(installedVersion, npmResult.version)) {
227
- emitStale(checkName, `v${installedVersion}`, `v${npmResult.version}`, ref, upgradeCommand, sink, addFinding, {
228
- installed_version: installedVersion,
229
- remote_version: npmResult.version,
230
- resolver: "npm-view",
231
- }, "npm registry");
232
- return;
233
- }
234
- if (installedVersion === npmResult.version.replace(/^v/i, "")) {
235
- sink.info(`${checkName}: current (version matches npm registry)`);
206
+ const installedCandidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
207
+ const applicability = evaluateReleaseAvailability(installedCandidate, null);
208
+ if (applicability.status === "not-applicable") {
209
+ if (remoteSha && installedSha === remoteSha) {
210
+ sink.info(`${checkName}: current (sha matches remote; ref is not a release tag)`);
236
211
  return;
237
212
  }
238
- sink.info(`${checkName}: current (installed version >= npm registry)`);
213
+ const reason = remoteResult.ok
214
+ ? "ls-remote produced no sha; ref is not a release tag"
215
+ : "could not reach remote; ref is not a release tag";
216
+ sink.info(`${checkName}: skip -- ${reason}`);
217
+ emitUnverified(checkName, reason, sink, addFinding);
218
+ return;
219
+ }
220
+ let npmResult;
221
+ try {
222
+ npmResult = runNpmView();
223
+ }
224
+ catch {
225
+ npmResult = { ok: false, version: "" };
226
+ }
227
+ const availability = evaluateReleaseAvailability(installedCandidate, npmResult.ok ? npmResult.version : null);
228
+ if (availability.status === "available") {
229
+ emitReleaseAvailable(checkName, availability.installedVersion, availability.latestVersion, ref, upgradeCommand, sink, addFinding);
230
+ return;
231
+ }
232
+ if (availability.status === "current") {
233
+ sink.info(`${checkName}: current (installed release >= npm latest)`);
234
+ return;
235
+ }
236
+ if (availability.status === "prerelease-ignored") {
237
+ sink.info(`${checkName}: current (npm candidate is a prerelease; stable install retained)`);
239
238
  return;
240
239
  }
241
240
  const reason = remoteResult.ok
242
- ? "ls-remote produced no sha and npm registry fallback unavailable"
241
+ ? "npm registry release lookup unavailable or returned a non-publishable version"
243
242
  : "could not reach remote (git ls-remote / npm view both unavailable)";
244
243
  sink.info(`${checkName}: skip -- ${reason}`);
245
244
  emitUnverified(checkName, reason, sink, addFinding);
@@ -0,0 +1,28 @@
1
+ /** Outcomes produced by the reusable latest-release comparison. */
2
+ export type ReleaseAvailabilityStatus = "available" | "current" | "prerelease-ignored" | "not-applicable" | "unverified";
3
+ /** Network resolver that supplied the latest published version. */
4
+ export type ReleaseAvailabilityResolver = "npm-view";
5
+ /** Typed result consumed by doctor today and safe-idle upgrade prompting later. */
6
+ export type ReleaseAvailabilityResult = {
7
+ readonly status: "available" | "current" | "prerelease-ignored";
8
+ readonly installedVersion: string;
9
+ readonly latestVersion: string;
10
+ readonly resolver: ReleaseAvailabilityResolver;
11
+ } | {
12
+ readonly status: "not-applicable";
13
+ readonly installedVersion: null;
14
+ readonly latestVersion: string | null;
15
+ readonly resolver: ReleaseAvailabilityResolver;
16
+ } | {
17
+ readonly status: "unverified";
18
+ readonly installedVersion: string;
19
+ readonly latestVersion: null;
20
+ readonly resolver: ReleaseAvailabilityResolver;
21
+ };
22
+ /**
23
+ * Compare an installed release with a resolver-provided latest release.
24
+ * Stable installs deliberately ignore prerelease candidates even when their
25
+ * numeric core is newer.
26
+ */
27
+ export declare function evaluateReleaseAvailability(installed: string, latest: string | null, resolver?: ReleaseAvailabilityResolver): ReleaseAvailabilityResult;
28
+ //# sourceMappingURL=release-availability.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { comparePublishableVersions, isPrereleaseTag, isPublishable } from "../release/version.js";
2
+ function normalizePublishableVersion(version) {
3
+ if (version === null)
4
+ return null;
5
+ const candidate = version.trim().replace(/^refs\/tags\//, "");
6
+ if (!candidate || !isPublishable(candidate))
7
+ return null;
8
+ return candidate.replace(/^v/, "");
9
+ }
10
+ /**
11
+ * Compare an installed release with a resolver-provided latest release.
12
+ * Stable installs deliberately ignore prerelease candidates even when their
13
+ * numeric core is newer.
14
+ */
15
+ export function evaluateReleaseAvailability(installed, latest, resolver = "npm-view") {
16
+ const installedVersion = normalizePublishableVersion(installed);
17
+ const latestVersion = normalizePublishableVersion(latest);
18
+ if (installedVersion === null) {
19
+ return { status: "not-applicable", installedVersion, latestVersion, resolver };
20
+ }
21
+ if (latestVersion === null) {
22
+ return { status: "unverified", installedVersion, latestVersion, resolver };
23
+ }
24
+ if (!isPrereleaseTag(installedVersion) && isPrereleaseTag(latestVersion)) {
25
+ return { status: "prerelease-ignored", installedVersion, latestVersion, resolver };
26
+ }
27
+ const comparison = comparePublishableVersions(installedVersion, latestVersion);
28
+ return {
29
+ status: comparison < 0 ? "available" : "current",
30
+ installedVersion,
31
+ latestVersion,
32
+ resolver,
33
+ };
34
+ }
35
+ //# sourceMappingURL=release-availability.js.map
@@ -3,6 +3,7 @@ import type { ShadowedPlanExtension } from "../policy/plan-extensions.js";
3
3
  import type { EngineProbeResult } from "../resolution/classify.js";
4
4
  import type { ResolutionMode } from "../resolution/index.js";
5
5
  import type { ResolveUserMdResult } from "../user-config/resolve-user-md.js";
6
+ import type { AgentHookHealthResult } from "../verify-env/agent-hooks.js";
6
7
  export declare const EXIT_CLEAN = 0;
7
8
  export declare const EXIT_DRIFT = 1;
8
9
  export declare const EXIT_CONFIG_ERROR = 2;
@@ -135,5 +136,7 @@ export interface DoctorSeams {
135
136
  * its `plan` object; returns [] when no project definition is present.
136
137
  */
137
138
  readonly detectPlanExtensionShadows?: (projectRoot: string) => readonly ShadowedPlanExtension[];
139
+ /** Read-only agent-host hook registration probe (#2438). */
140
+ readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
138
141
  }
139
142
  //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,5 @@
1
- export declare const CRUD_METRICS_HISTORY_REL = "results/crud-metrics.jsonl";
1
+ /** Relative path under the resolved metrics home for helped / value telemetry (#2545). */
2
+ export declare const CRUD_METRICS_HISTORY_REL = "helped/crud-metrics.jsonl";
2
3
  export type CrudOperation = "create" | "read" | "update" | "delete";
3
4
  export type ByteDiffMinimality = "surgical" | "whole-file-rewrite";
4
5
  export interface CrudOperationMetric {
@@ -31,9 +32,9 @@ export declare function classifyByteDiffMinimality(before: string, after: string
31
32
  readonly kind: ByteDiffMinimality;
32
33
  readonly changedRatio: number;
33
34
  };
34
- /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1). */
35
- export declare function crudMetricsHistoryPath(projectRoot: string): string;
36
- /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1). */
35
+ /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1 / #2545). */
36
+ export declare function crudMetricsHistoryPath(projectRoot: string): string | null;
37
+ /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1 / #2545). */
37
38
  export declare function persistCrudMetrics(projectRoot: string, metrics: readonly CrudOperationMetric[]): void;
38
39
  /**
39
40
  * Instrumented vBRIEF/xBRIEF CRUD chokepoint (#1703 Tier 1).
@@ -1,10 +1,11 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { readCorePackageVersion } from "../engine-version.js";
4
- import { resolveEvalPath } from "../layout/resolve.js";
4
+ import { helpedMetricsHistoryPath } from "../metrics/resolve-metrics-home.js";
5
5
  import { scanVbrief } from "../vbrief-validate/conformance.js";
6
6
  import { validateVbriefSchema } from "../vbrief-validate/schema.js";
7
- export const CRUD_METRICS_HISTORY_REL = "results/crud-metrics.jsonl";
7
+ /** Relative path under the resolved metrics home for helped / value telemetry (#2545). */
8
+ export const CRUD_METRICS_HISTORY_REL = "helped/crud-metrics.jsonl";
8
9
  /** Whole-file rewrites change at least half of the bytes or re-serialize with high drift. */
9
10
  export const BYTE_DIFF_WHOLE_FILE_THRESHOLD = 0.5;
10
11
  function isRecord(value) {
@@ -85,16 +86,19 @@ function sanitizeInlineMessage(message) {
85
86
  function ensureParentDir(path) {
86
87
  mkdirSync(dirname(path), { recursive: true });
87
88
  }
88
- /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1). */
89
+ /** Absolute path to the versioned CRUD metrics ledger (#1703 Tier 1 / #2545). */
89
90
  export function crudMetricsHistoryPath(projectRoot) {
90
- return resolveEvalPath(projectRoot, CRUD_METRICS_HISTORY_REL);
91
+ return helpedMetricsHistoryPath(projectRoot);
91
92
  }
92
- /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1). */
93
+ /** Append CRUD operation metrics to the versioned ledger (#1703 Tier 1 / #2545). */
93
94
  export function persistCrudMetrics(projectRoot, metrics) {
94
95
  if (metrics.length === 0) {
95
96
  return;
96
97
  }
97
98
  const path = crudMetricsHistoryPath(projectRoot);
99
+ if (path === null) {
100
+ return;
101
+ }
98
102
  mkdirSync(dirname(path), { recursive: true });
99
103
  for (const metric of metrics) {
100
104
  appendFileSync(path, `${JSON.stringify(metric)}\n`, "utf8");
@@ -1,5 +1,6 @@
1
1
  export declare const HEALTH_SCHEMA_VERSION: 1;
2
- export declare const HEALTH_HISTORY_REL = "results/health-history.jsonl";
2
+ /** Relative path under the resolved metrics home for eval:health history (#2545). */
3
+ export declare const HEALTH_HISTORY_REL = "health/health-history.jsonl";
3
4
  /** One static Tier-0 gate probe aggregated into the health score. */
4
5
  export interface GateProbeResult {
5
6
  readonly id: string;
@@ -37,15 +38,15 @@ export interface EvaluateHealthResult {
37
38
  readonly report: HealthReport | null;
38
39
  readonly message: string;
39
40
  }
40
- /** Absolute path to the versioned health history ledger. */
41
- export declare function healthHistoryPath(projectRoot: string): string;
41
+ /** Absolute path to the versioned health history ledger (#2545). */
42
+ export declare function healthHistoryPath(projectRoot: string): string | null;
42
43
  /** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
43
44
  export declare function detectWipCapUnsatisfiableNudge(projectRoot: string): ContradictionEvidence | null;
44
45
  /** Run all registered contradictory-gate detectors. */
45
46
  export declare function detectContradictoryGates(projectRoot: string): ContradictionEvidence[];
46
47
  /** Compute a 0-100 score from gate pass rate minus contradiction penalty. */
47
48
  export declare function computeHealthScore(gates: readonly GateProbeResult[], contradictions: readonly ContradictionEvidence[]): number;
48
- /** Append one health run to the versioned ledger (#1703 Tier 0). */
49
+ /** Append one health run to the versioned ledger (#1703 Tier 0 / #2545). */
49
50
  export declare function persistHealthRun(projectRoot: string, report: HealthReport): void;
50
51
  /** Aggregate Tier-0 static gates into a versioned framework health score (#1703). */
51
52
  export declare function evaluateHealth(options?: EvaluateHealthOptions): EvaluateHealthResult;