@deftai/directive-core 0.84.0 → 0.86.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 (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -1,5 +1,22 @@
1
1
  import { type RunGhApiFn } from "../scm/gh-rest.js";
2
+ import { type Clock } from "./time.js";
2
3
  import type { FetchAllReport, StateRefreshReport } from "./types.js";
4
+ /** Repo-level stamp proving a successful open-only enumeration (#2826). */
5
+ export declare const OPEN_INVENTORY_STAMP = "open-inventory.json";
6
+ export interface OpenInventoryStamp {
7
+ readonly fetched_at: string;
8
+ readonly open_count: number;
9
+ }
10
+ export declare function openInventoryStampPath(cacheRoot: string, source: string, repo: string): string;
11
+ export declare function readOpenInventoryStamp(cacheRoot: string, source: string, repo: string): OpenInventoryStamp | null;
12
+ export declare function writeOpenInventoryStamp(options: {
13
+ cacheRoot: string;
14
+ source: string;
15
+ repo: string;
16
+ openCount: number;
17
+ fetchedAt?: Date;
18
+ clock?: Clock;
19
+ }): void;
3
20
  export declare const REST_MAX_PER_PAGE = 100;
4
21
  export declare const REST_PAGINATION_MAX_PAGES = 100;
5
22
  export declare const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
@@ -4,9 +4,49 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
4
4
  import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
5
5
  import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
6
6
  import { CacheError, CacheFetchError } from "./errors.js";
7
+ import { atomicWriteText } from "./io.js";
7
8
  import { pythonJsonLine } from "./json.js";
8
9
  import { cachePut, isFresh, validateRepo } from "./operations.js";
9
10
  import { entryDir } from "./paths.js";
11
+ import { systemClock, utcIso } from "./time.js";
12
+ /** Repo-level stamp proving a successful open-only enumeration (#2826). */
13
+ export const OPEN_INVENTORY_STAMP = "open-inventory.json";
14
+ export function openInventoryStampPath(cacheRoot, source, repo) {
15
+ const [owner, name] = repo.split("/", 2);
16
+ return join(cacheRoot, source, owner, name, OPEN_INVENTORY_STAMP);
17
+ }
18
+ export function readOpenInventoryStamp(cacheRoot, source, repo) {
19
+ const path = openInventoryStampPath(cacheRoot, source, repo);
20
+ if (!existsSync(path))
21
+ return null;
22
+ try {
23
+ const raw = JSON.parse(readFileSync(path, "utf8"));
24
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
25
+ return null;
26
+ const fetchedAt = raw.fetched_at;
27
+ const openCount = raw.open_count;
28
+ if (typeof fetchedAt !== "string")
29
+ return null;
30
+ if (typeof openCount !== "number" || openCount !== 0)
31
+ return null;
32
+ return { fetched_at: fetchedAt, open_count: openCount };
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export function writeOpenInventoryStamp(options) {
39
+ if (options.openCount !== 0)
40
+ return;
41
+ validateRepo(options.repo);
42
+ const clock = options.clock ?? systemClock;
43
+ const fetchedAt = options.fetchedAt ?? clock.now();
44
+ const payload = {
45
+ fetched_at: utcIso(clock, fetchedAt),
46
+ open_count: 0,
47
+ };
48
+ atomicWriteText(openInventoryStampPath(options.cacheRoot, options.source, options.repo), `${JSON.stringify(payload)}\n`);
49
+ }
10
50
  export const REST_MAX_PER_PAGE = 100;
11
51
  export const REST_PAGINATION_MAX_PAGES = 100;
12
52
  export const DEFAULT_RETRY_AFTER_FALLBACK_S = 60;
@@ -308,6 +348,16 @@ export function runFetchAll(options) {
308
348
  if (processed % batchSize === 0)
309
349
  maybeSleep(delayMs);
310
350
  }
351
+ // #2826: zero open issues is a successful inventory answer — persist freshness
352
+ // metadata so preflight-cache does not treat minFetchedAt=null as Infinity stale.
353
+ if (total === 0 && report.issuesFailed === 0) {
354
+ writeOpenInventoryStamp({
355
+ cacheRoot,
356
+ source,
357
+ repo: options.repo,
358
+ openCount: 0,
359
+ });
360
+ }
311
361
  return report;
312
362
  }
313
363
  export function cacheFetchAll(options) {
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { CANONICAL_GITIGNORE_BASELINE } from "../init-deposit/gitignore.js";
3
- import { detectLegacyLayout, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
3
+ import { detectDualLayout, detectLegacyLayout, dualLayoutSignpostLine, legacyLayoutSignpostLine, } from "../init-deposit/legacy-detect.js";
4
4
  import { detectCanonicalVendoredManifest, isNpmManaged, NPM_MANAGED_SENTINEL_KEY, NPM_MANAGED_SENTINEL_VALUE, } from "../init-deposit/migrate.js";
5
5
  import { resolveLifecycleRoot } from "../layout/resolve.js";
6
6
  import { findSkillPathsInText } from "../text/redos-safe.js";
@@ -300,6 +300,21 @@ export function checkLegacyLayout(projectRoot, seams = {}) {
300
300
  ...(seams.isFile ? { isFile: seams.isFile } : {}),
301
301
  ...(seams.isDir ? { isDir: seams.isDir } : {}),
302
302
  };
303
+ const dualLayout = detectDualLayout(projectRoot, legacySeams);
304
+ if (dualLayout !== null) {
305
+ return {
306
+ name: "legacy-layout",
307
+ status: "fail",
308
+ detail: dualLayoutSignpostLine(dualLayout),
309
+ data: {
310
+ legacy_layout: true,
311
+ legacy_layout_kind: dualLayout.kind,
312
+ evidence: [...dualLayout.evidence],
313
+ upgrading_doc_url: UPGRADING_DOC_URL,
314
+ go_bridge_releases_url: GO_BRIDGE_RELEASES_URL,
315
+ },
316
+ };
317
+ }
303
318
  const detection = detectLegacyLayout(projectRoot, legacySeams);
304
319
  if (!detection.legacy) {
305
320
  return {
@@ -10,11 +10,13 @@ export declare const TASKFILE_INCLUDE_SNIPPET = "version: '3'\n\nincludes:\n de
10
10
  export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--network", "--project-root", "-h", "--help"];
11
11
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
12
12
  export declare const NPM_PACKAGE_NAME = "@deftai/directive";
13
+ export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
14
+ export declare const NPM_REGISTRY_MIRROR_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md#corporate-or-mirrored-npm-registry";
13
15
  export declare const NETWORK_DISCLOSURE_LINE: string;
14
16
  export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
15
- export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
17
+ export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "xbrief"];
16
18
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
17
- export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "vbrief"];
19
+ export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "xbrief"];
18
20
  export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/agents-entry.md", "content/skills/deft-directive-build/SKILL.md"];
19
21
  export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
20
22
  /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
@@ -28,12 +28,16 @@ export const DOCTOR_ALLOWED_FLAGS = [
28
28
  ];
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
+ export const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
32
+ export const NPM_REGISTRY_MIRROR_DOC_URL = `${UPGRADING_DOC_URL}#corporate-or-mirrored-npm-registry`;
31
33
  // #2182: payload-staleness is the only doctor check that can reach a network
32
34
  // 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
- // and requires the explicit `--network` flag; this line discloses exactly
35
- // which tool + registry class it may contact BEFORE the check runs, and the
36
- // skip line tells an offline run how to opt in.
35
+ // release-tag install with the latest stable package). The #2808 baseline
36
+ // registry-routing check uses only offline `npm config get` reads.
37
+ // Payload-staleness is OFF by default (offline tier) and requires the explicit
38
+ // `--network` flag; this line discloses exactly which tool + registry class it
39
+ // may contact BEFORE the check runs, and the skip line tells an offline run how
40
+ // to opt in.
37
41
  export const NETWORK_DISCLOSURE_LINE = "[deft doctor] --network: this run may contact your git remote (framework " +
38
42
  "repo host) and the npm registry (registry.npmjs.org) to " +
39
43
  `look up the latest ${NPM_PACKAGE_NAME} version.`;
@@ -42,9 +46,9 @@ export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (def
42
46
  "(discloses tool + registry before contacting either).";
43
47
  // Engine / lifecycle dirs that stay at the framework root (NOT relocated by
44
48
  // #1875). Shippable-content dirs moved under content/ -- see EXPECTED_CONTENT_DIRS.
45
- export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "vbrief"];
49
+ export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "xbrief"];
46
50
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
47
- export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "vbrief"];
51
+ export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "xbrief"];
48
52
  // Post-#1875 content/ move: these framework-internal markers now live under
49
53
  // content/ in the SOURCE repo. They identify a deft source checkout (a consumer
50
54
  // would never reproduce them); the C1 flatten means a consumer deposit has no
@@ -6,6 +6,7 @@ export * from "./flags.js";
6
6
  export * from "./json.js";
7
7
  export * from "./main.js";
8
8
  export * from "./manifest.js";
9
+ export * from "./npm-registry.js";
9
10
  export * from "./paths.js";
10
11
  export * from "./payload-staleness.js";
11
12
  export * from "./release-availability.js";
@@ -6,6 +6,7 @@ export * from "./flags.js";
6
6
  export * from "./json.js";
7
7
  export * from "./main.js";
8
8
  export * from "./manifest.js";
9
+ export * from "./npm-registry.js";
9
10
  export * from "./paths.js";
10
11
  export * from "./payload-staleness.js";
11
12
  export * from "./release-availability.js";
@@ -2,7 +2,8 @@ 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
+ export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
6
+ export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
6
7
  /**
7
8
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
8
9
  * (#2267). The `directive` surface always works; `task deft:X` is optional and
@@ -7,6 +7,7 @@ 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
9
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
10
+ import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
10
11
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
11
12
  import { runChecks } from "./checks.js";
12
13
  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";
@@ -14,8 +15,9 @@ import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeSta
14
15
  import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
15
16
  import { pythonJsonDump } from "./json.js";
16
17
  import { parseInstallRootFromAgentsMd } from "./manifest.js";
18
+ import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
17
19
  import { createPlainSink } from "./output.js";
18
- import { readTextSafe, resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
20
+ import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
19
21
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
20
22
  import { runLocalSignpostChecks } from "./signpost-checks.js";
21
23
  import { classifyTaskfileInclude, formatMissingIncludeSnippet, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
@@ -56,7 +58,7 @@ export function cmdDoctor(args, seams = {}) {
56
58
  const quietMode = flags.quiet;
57
59
  const fullMode = flags.full;
58
60
  const projectRoot = resolvePath(flags.projectRoot ?? process.cwd());
59
- const frameworkRoot = seams.frameworkRoot ?? resolveDefaultFrameworkRoot();
61
+ const frameworkRoot = seams.frameworkRoot ?? resolveFrameworkRootForProject(projectRoot);
60
62
  const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
61
63
  const whichFn = seams.whichFn ?? defaultWhich;
62
64
  const nowFn = seams.now ?? (() => new Date());
@@ -90,7 +92,7 @@ export function cmdDoctor(args, seams = {}) {
90
92
  }
91
93
  const signpostWarnings = throttleFindings.filter((f) => f.severity === "warning").length;
92
94
  if (signpostWarnings > 0 && !jsonMode) {
93
- throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local layout / npm-migration note(s) above (throttle-skipped full probe).`);
95
+ throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local configuration / layout note(s) above (throttle-skipped full probe).`);
94
96
  }
95
97
  return decision.dirty ? 1 : 0;
96
98
  }
@@ -164,13 +166,31 @@ export function cmdDoctor(args, seams = {}) {
164
166
  sink.blank();
165
167
  }
166
168
  sink.info("Checking agent-host hook registration...");
167
- runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
169
+ const findingCountBeforeHooks = findings.length;
170
+ const hooksHealthy = runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
171
+ if (fullMode && hooksHealthy) {
172
+ const lastFinding = findings[findings.length - 1];
173
+ if (findings.length > findingCountBeforeHooks &&
174
+ lastFinding?.check === "agent-hooks-registration" &&
175
+ lastFinding?.status === "registered") {
176
+ findings.pop();
177
+ }
178
+ runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams);
179
+ }
180
+ if (consumerContext) {
181
+ if (!jsonMode) {
182
+ sink.blank();
183
+ }
184
+ sink.info("Checking npm registry routing...");
185
+ runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
186
+ }
168
187
  if (!jsonMode) {
169
188
  sink.blank();
170
189
  }
171
- // #2182: payload-staleness is the only doctor check that can reach a
172
- // registry (git verifies the pin; npm compares stable release availability).
173
- // It stays in the OFFLINE
190
+ // #2182: payload-staleness is the only doctor check that can reach a network
191
+ // endpoint (git verifies the pin; npm compares stable release availability).
192
+ // The baseline #2808 registry-routing diagnostic above is an offline
193
+ // `npm config get` read. Payload-staleness stays in the OFFLINE
174
194
  // tier (skipped) unless the operator explicitly opts into the NETWORK tier
175
195
  // via `--network`, and the tool + registry class is disclosed BEFORE the
176
196
  // check runs -- never silently, never as a side effect of a read-only
@@ -344,40 +364,81 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
344
364
  const reason = "maintainer source checkout; project hook deposit is consumer-only";
345
365
  sink.info(`${checkName}: skip -- ${reason}`);
346
366
  addFinding({ severity: "skip", message: reason, check: checkName, status: "skip" });
347
- return;
367
+ return false;
348
368
  }
349
369
  try {
350
370
  const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
351
- if (result.code === 0) {
352
- const message = `${checkName}: registered and structurally valid; ` +
353
- "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
354
- sink.success(message);
371
+ if (result.code !== 0) {
372
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
373
+ sink.warn(message);
355
374
  addFinding({
356
- severity: "skip",
375
+ severity: "warning",
357
376
  message,
358
377
  check: checkName,
359
- status: "registered",
378
+ status: result.code === 2 ? "unavailable" : "incomplete",
360
379
  registrations: result.registrations,
361
- trust_status: "not-verifiable",
362
- trust_review: "Open `/hooks` in Codex and review the project hook commands.",
380
+ suggestion: "deft update",
363
381
  });
364
- return;
382
+ return false;
365
383
  }
366
- const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
367
- sink.warn(message);
384
+ const message = `${checkName}: registered and structurally valid; ` +
385
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
386
+ sink.success(message);
368
387
  addFinding({
369
- severity: "warning",
388
+ severity: "skip",
370
389
  message,
371
390
  check: checkName,
372
- status: result.code === 2 ? "unavailable" : "incomplete",
391
+ status: "registered",
373
392
  registrations: result.registrations,
374
- suggestion: "deft update",
393
+ trust_status: "not-verifiable",
394
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
375
395
  });
396
+ return true;
376
397
  }
377
398
  catch (cause) {
378
399
  const message = `${checkName}: probe failed -- ${String(cause)}`;
379
400
  sink.warn(message);
380
401
  addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
402
+ return false;
403
+ }
404
+ }
405
+ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams) {
406
+ const checkName = "agent-hooks-registration";
407
+ const liveCheckName = "agent-hooks-live-probe";
408
+ try {
409
+ const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
410
+ const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot);
411
+ if (liveResult.code !== 0) {
412
+ const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
413
+ sink.warn(message);
414
+ addFinding({
415
+ severity: "warning",
416
+ message,
417
+ check: liveCheckName,
418
+ status: liveResult.code === 2 ? "unavailable" : "non-functional",
419
+ cases: liveResult.cases,
420
+ suggestion: "npm i -g @deftai/directive@latest && deft update",
421
+ });
422
+ return;
423
+ }
424
+ const message = `${checkName}: registered, structurally valid, and live probe passed; ` +
425
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
426
+ sink.success(message);
427
+ addFinding({
428
+ severity: "skip",
429
+ message,
430
+ check: liveCheckName,
431
+ status: "registered-and-functional",
432
+ registrations: result.registrations,
433
+ trust_status: "not-verifiable",
434
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
435
+ live_probe: "passed",
436
+ });
437
+ }
438
+ catch (cause) {
439
+ const message = `${liveCheckName}: probe failed -- ${String(cause)}`;
440
+ sink.warn(message);
441
+ addFinding({ severity: "warning", message, check: liveCheckName, suggestion: "deft update" });
381
442
  }
382
443
  }
383
444
  function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
@@ -0,0 +1,23 @@
1
+ import type { OutputSink } from "./output.js";
2
+ import type { Finding, NpmConfigGet, NpmConfigGetResult } from "./types.js";
3
+ /** Seams for the read-only npm registry routing diagnostic. */
4
+ export interface NpmRegistryMirrorSeams {
5
+ readonly runNpmConfigGet?: NpmConfigGet;
6
+ }
7
+ /**
8
+ * Read one npm configuration key without a shell.
9
+ *
10
+ * `npm config get` reads local/user configuration only; it does not resolve a
11
+ * package or contact a registry. Failures are returned explicitly so doctor can
12
+ * skip this advisory without turning a missing npm binary into a hard error.
13
+ */
14
+ export declare function defaultNpmConfigGet(key: string, cwd: string): NpmConfigGetResult;
15
+ /**
16
+ * Warn when `@deftai/*` resolves through a non-public npm registry.
17
+ *
18
+ * The finding intentionally records only whether the scoped or default key won
19
+ * precedence. It never includes the configured URL, which may contain internal
20
+ * hostnames or credentials.
21
+ */
22
+ export declare function runNpmRegistryMirrorCheck(projectRoot: string, sink: OutputSink, addFinding: (finding: Finding) => void, seams?: NpmRegistryMirrorSeams): void;
23
+ //# sourceMappingURL=npm-registry.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { NPM_REGISTRY_MIRROR_DOC_URL, PUBLIC_NPM_REGISTRY } from "./constants.js";
3
+ const CHECK_NAME = "npm-registry-mirror";
4
+ const DEFT_SCOPE_REGISTRY_KEY = "@deftai:registry";
5
+ const DEFAULT_REGISTRY_KEY = "registry";
6
+ const UNSET_NPM_VALUES = new Set(["", "null", "undefined"]);
7
+ /**
8
+ * Read one npm configuration key without a shell.
9
+ *
10
+ * `npm config get` reads local/user configuration only; it does not resolve a
11
+ * package or contact a registry. Failures are returned explicitly so doctor can
12
+ * skip this advisory without turning a missing npm binary into a hard error.
13
+ */
14
+ export function defaultNpmConfigGet(key, cwd) {
15
+ try {
16
+ const proc = spawnSync("npm", ["config", "get", key], {
17
+ cwd,
18
+ encoding: "utf8",
19
+ shell: false,
20
+ timeout: 5_000,
21
+ windowsHide: true,
22
+ });
23
+ return {
24
+ ok: proc.status === 0,
25
+ value: typeof proc.stdout === "string" ? proc.stdout.trim() : "",
26
+ };
27
+ }
28
+ catch {
29
+ return { ok: false, value: "" };
30
+ }
31
+ }
32
+ function configuredValue(value) {
33
+ const trimmed = value.trim();
34
+ return UNSET_NPM_VALUES.has(trimmed.toLowerCase()) ? null : trimmed;
35
+ }
36
+ function effectiveRegistry(runConfigGet, projectRoot) {
37
+ let scoped;
38
+ try {
39
+ scoped = runConfigGet(DEFT_SCOPE_REGISTRY_KEY, projectRoot);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ if (!scoped.ok) {
45
+ return null;
46
+ }
47
+ const scopedValue = configuredValue(scoped.value);
48
+ if (scopedValue !== null) {
49
+ return { source: "scoped", value: scopedValue };
50
+ }
51
+ let fallback;
52
+ try {
53
+ fallback = runConfigGet(DEFAULT_REGISTRY_KEY, projectRoot);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ if (!fallback.ok) {
59
+ return null;
60
+ }
61
+ const fallbackValue = configuredValue(fallback.value);
62
+ return fallbackValue === null ? null : { source: "default", value: fallbackValue };
63
+ }
64
+ function isValidRegistryUrl(value) {
65
+ try {
66
+ const parsed = new URL(value);
67
+ return (parsed.protocol === "https:" || parsed.protocol === "http:") && parsed.hostname !== "";
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ function isCanonicalPublicRegistry(value) {
74
+ try {
75
+ const parsed = new URL(value);
76
+ return (parsed.protocol === "https:" &&
77
+ parsed.hostname === "registry.npmjs.org" &&
78
+ parsed.port === "" &&
79
+ (parsed.pathname === "" || parsed.pathname === "/") &&
80
+ parsed.username === "" &&
81
+ parsed.password === "" &&
82
+ parsed.search === "" &&
83
+ parsed.hash === "");
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function skipFinding(addFinding) {
90
+ addFinding({
91
+ severity: "skip",
92
+ message: `${CHECK_NAME}: skip -- npm registry configuration is unavailable or invalid`,
93
+ check: CHECK_NAME,
94
+ status: "skip",
95
+ });
96
+ }
97
+ /**
98
+ * Warn when `@deftai/*` resolves through a non-public npm registry.
99
+ *
100
+ * The finding intentionally records only whether the scoped or default key won
101
+ * precedence. It never includes the configured URL, which may contain internal
102
+ * hostnames or credentials.
103
+ */
104
+ export function runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams = {}) {
105
+ const selected = effectiveRegistry(seams.runNpmConfigGet ?? defaultNpmConfigGet, projectRoot);
106
+ if (selected === null || !isValidRegistryUrl(selected.value)) {
107
+ skipFinding(addFinding);
108
+ return;
109
+ }
110
+ if (isCanonicalPublicRegistry(selected.value)) {
111
+ return;
112
+ }
113
+ const message = `${CHECK_NAME}: @deftai packages resolve through a non-public npm registry. ` +
114
+ "Corporate mirrors can return E404/ETARGET or silently serve stale @latest metadata. " +
115
+ `Where organization policy permits, retry with \`npm i -g @deftai/directive@latest --registry=${PUBLIC_NPM_REGISTRY}\` ` +
116
+ `or add \`@deftai:registry=${PUBLIC_NPM_REGISTRY}\` to \`.npmrc\`. ` +
117
+ `Otherwise ask your registry administrator to sync the @deftai packages. See ${NPM_REGISTRY_MIRROR_DOC_URL}.`;
118
+ sink.warn(message);
119
+ addFinding({
120
+ severity: "warning",
121
+ message,
122
+ check: CHECK_NAME,
123
+ status: "non-public",
124
+ registry_source: selected.source,
125
+ suggestion: NPM_REGISTRY_MIRROR_DOC_URL,
126
+ });
127
+ }
128
+ //# sourceMappingURL=npm-registry.js.map
@@ -0,0 +1,10 @@
1
+ export interface NpmViewVersionResult {
2
+ readonly ok: boolean;
3
+ readonly version: string;
4
+ }
5
+ /**
6
+ * Query the canonical public registry for the latest published Directive
7
+ * version, independent of the consumer's configured corporate mirror.
8
+ */
9
+ export declare function defaultNpmViewVersion(): NpmViewVersionResult;
10
+ //# sourceMappingURL=npm-view.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { NPM_PACKAGE_NAME, PUBLIC_NPM_REGISTRY } from "./constants.js";
3
+ /**
4
+ * Query the canonical public registry for the latest published Directive
5
+ * version, independent of the consumer's configured corporate mirror.
6
+ */
7
+ export function defaultNpmViewVersion() {
8
+ try {
9
+ const proc = spawnSync("npm", [
10
+ "view",
11
+ NPM_PACKAGE_NAME,
12
+ "version",
13
+ `--registry=${PUBLIC_NPM_REGISTRY}`,
14
+ "--ignore-scripts",
15
+ ], {
16
+ encoding: "utf8",
17
+ shell: false,
18
+ timeout: 15_000,
19
+ windowsHide: true,
20
+ });
21
+ if (proc.error !== undefined || proc.status !== 0) {
22
+ return { ok: false, version: "" };
23
+ }
24
+ const version = (typeof proc.stdout === "string" ? proc.stdout : "").trim().split("\n")[0]?.trim() ?? "";
25
+ return { ok: version.length > 0, version };
26
+ }
27
+ catch {
28
+ return { ok: false, version: "" };
29
+ }
30
+ }
31
+ //# sourceMappingURL=npm-view.js.map
@@ -2,8 +2,9 @@ import { spawnSync } from "node:child_process";
2
2
  import { dirname, join } from "node:path";
3
3
  import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/migrate.js";
4
4
  import { isPublishable } from "../release/version.js";
5
- import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
5
+ import { CANONICAL_UPGRADE_COMMAND, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
6
6
  import { locateManifest, parseInstallManifest } from "./manifest.js";
7
+ import { defaultNpmViewVersion } from "./npm-view.js";
7
8
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
8
9
  import { evaluateReleaseAvailability } from "./release-availability.js";
9
10
  function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
@@ -40,14 +41,6 @@ function parseRemoteSha(stdout) {
40
41
  const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
41
42
  return firstLine.trim().split(/\s+/)[0] ?? "";
42
43
  }
43
- function defaultNpmViewVersion() {
44
- const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
45
- encoding: "utf8",
46
- timeout: 15_000,
47
- });
48
- const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
49
- return { ok: proc.status === 0 && version.length > 0, version };
50
- }
51
44
  function emitReleaseAvailable(checkName, installedVersion, latestVersion, ref, upgradeCommand, sink, addFinding) {
52
45
  const msg = `Newer framework release available (installed v${installedVersion}; ` +
53
46
  `latest v${latestVersion} from npm registry). ` +
@@ -1,7 +1,8 @@
1
1
  import { type CheckSeams } from "./checks.js";
2
+ import { type NpmRegistryMirrorSeams } from "./npm-registry.js";
2
3
  import type { OutputSink } from "./output.js";
3
4
  import type { Finding } from "./types.js";
4
- export interface LocalSignpostSeams extends CheckSeams {
5
+ export interface LocalSignpostSeams extends CheckSeams, NpmRegistryMirrorSeams {
5
6
  readonly runningInsideDeftRepo?: (root: string) => boolean;
6
7
  }
7
8
  /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
@@ -1,4 +1,5 @@
1
1
  import { checkCanonicalVendoredNpmSignpost, checkLegacyLayout } from "./checks.js";
2
+ import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
2
3
  import { runningInsideDeftRepo } from "./paths.js";
3
4
  /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
4
5
  export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}) {
@@ -6,6 +7,7 @@ export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}
6
7
  if (insideDeft) {
7
8
  return;
8
9
  }
10
+ runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
9
11
  for (const result of [
10
12
  checkLegacyLayout(projectRoot, seams),
11
13
  checkCanonicalVendoredNpmSignpost(projectRoot, seams),