@deftai/directive-core 0.84.0 → 0.85.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 (69) 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 +2 -0
  5. package/dist/doctor/constants.js +8 -4
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.js +13 -4
  9. package/dist/doctor/npm-registry.d.ts +23 -0
  10. package/dist/doctor/npm-registry.js +128 -0
  11. package/dist/doctor/npm-view.d.ts +10 -0
  12. package/dist/doctor/npm-view.js +31 -0
  13. package/dist/doctor/payload-staleness.js +2 -9
  14. package/dist/doctor/signpost-checks.d.ts +2 -1
  15. package/dist/doctor/signpost-checks.js +2 -0
  16. package/dist/doctor/types.d.ts +9 -0
  17. package/dist/eval-health-relocation/evaluate.js +15 -1
  18. package/dist/fs/projection-containment.js +1 -1
  19. package/dist/hooks/cursor-hooks.d.ts +6 -20
  20. package/dist/hooks/cursor-hooks.js +7 -92
  21. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  22. package/dist/init-deposit/agent-hooks.js +15 -37
  23. package/dist/init-deposit/hygiene.d.ts +22 -0
  24. package/dist/init-deposit/hygiene.js +136 -4
  25. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  26. package/dist/init-deposit/legacy-detect.js +143 -11
  27. package/dist/init-deposit/refresh.d.ts +2 -0
  28. package/dist/init-deposit/refresh.js +18 -5
  29. package/dist/policy/index.d.ts +2 -0
  30. package/dist/policy/index.js +2 -0
  31. package/dist/policy/org-force-on-migration.d.ts +44 -0
  32. package/dist/policy/org-force-on-migration.js +240 -0
  33. package/dist/policy/product-signal.d.ts +3 -1
  34. package/dist/policy/product-signal.js +15 -4
  35. package/dist/policy/value-feedback.d.ts +18 -1
  36. package/dist/policy/value-feedback.js +78 -5
  37. package/dist/preflight-cache/evaluate.js +12 -1
  38. package/dist/product-signal/consent-prompt.d.ts +13 -0
  39. package/dist/product-signal/consent-prompt.js +30 -0
  40. package/dist/product-signal/submit.js +22 -1
  41. package/dist/render/index.d.ts +1 -0
  42. package/dist/render/index.js +1 -0
  43. package/dist/render/rule-map-template.d.ts +2 -0
  44. package/dist/render/rule-map-template.js +407 -0
  45. package/dist/render/rule-map.d.ts +2 -0
  46. package/dist/render/rule-map.js +703 -0
  47. package/dist/review-monitor/constants.d.ts +7 -1
  48. package/dist/review-monitor/constants.js +40 -14
  49. package/dist/review-monitor/github-lease.d.ts +45 -0
  50. package/dist/review-monitor/github-lease.js +106 -0
  51. package/dist/review-monitor/index.d.ts +2 -0
  52. package/dist/review-monitor/index.js +2 -0
  53. package/dist/review-monitor/lease-comment.d.ts +36 -0
  54. package/dist/review-monitor/lease-comment.js +152 -0
  55. package/dist/review-monitor/record.d.ts +61 -13
  56. package/dist/review-monitor/record.js +379 -132
  57. package/dist/review-monitor/verify.d.ts +2 -0
  58. package/dist/review-monitor/verify.js +32 -13
  59. package/dist/scm/gh-rest.d.ts +7 -1
  60. package/dist/scm/gh-rest.js +35 -0
  61. package/dist/session/session-start.js +5 -0
  62. package/dist/task-surface/index.js +53 -13
  63. package/dist/xbrief-migrate/constants.d.ts +6 -0
  64. package/dist/xbrief-migrate/constants.js +6 -0
  65. package/dist/xbrief-migrate/index.d.ts +2 -2
  66. package/dist/xbrief-migrate/index.js +2 -2
  67. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  68. package/dist/xbrief-migrate/migrate-project.js +18 -1
  69. 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,6 +10,8 @@ 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
17
  export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
@@ -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.`;
@@ -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";
@@ -14,6 +14,7 @@ import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeSta
14
14
  import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
15
15
  import { pythonJsonDump } from "./json.js";
16
16
  import { parseInstallRootFromAgentsMd } from "./manifest.js";
17
+ import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
17
18
  import { createPlainSink } from "./output.js";
18
19
  import { readTextSafe, resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
19
20
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
@@ -90,7 +91,7 @@ export function cmdDoctor(args, seams = {}) {
90
91
  }
91
92
  const signpostWarnings = throttleFindings.filter((f) => f.severity === "warning").length;
92
93
  if (signpostWarnings > 0 && !jsonMode) {
93
- throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local layout / npm-migration note(s) above (throttle-skipped full probe).`);
94
+ throttleSink.finalWarn(`Signpost advisory: ${signpostWarnings} local configuration / layout note(s) above (throttle-skipped full probe).`);
94
95
  }
95
96
  return decision.dirty ? 1 : 0;
96
97
  }
@@ -165,12 +166,20 @@ export function cmdDoctor(args, seams = {}) {
165
166
  }
166
167
  sink.info("Checking agent-host hook registration...");
167
168
  runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
169
+ if (consumerContext) {
170
+ if (!jsonMode) {
171
+ sink.blank();
172
+ }
173
+ sink.info("Checking npm registry routing...");
174
+ runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
175
+ }
168
176
  if (!jsonMode) {
169
177
  sink.blank();
170
178
  }
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
179
+ // #2182: payload-staleness is the only doctor check that can reach a network
180
+ // endpoint (git verifies the pin; npm compares stable release availability).
181
+ // The baseline #2808 registry-routing diagnostic above is an offline
182
+ // `npm config get` read. Payload-staleness stays in the OFFLINE
174
183
  // tier (skipped) unless the operator explicitly opts into the NETWORK tier
175
184
  // via `--network`, and the tool + registry class is disclosed BEFORE the
176
185
  // check runs -- never silently, never as a side effect of a read-only
@@ -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),
@@ -30,6 +30,13 @@ export interface Finding {
30
30
  readonly status?: string;
31
31
  readonly [key: string]: unknown;
32
32
  }
33
+ /** Result of an offline `npm config get` query. */
34
+ export interface NpmConfigGetResult {
35
+ readonly ok: boolean;
36
+ readonly value: string;
37
+ }
38
+ /** Injectable npm-config reader used by doctor and its throttle-safe signposts. */
39
+ export type NpmConfigGet = (key: string, cwd: string) => NpmConfigGetResult;
33
40
  /**
34
41
  * The single read-only decision surface derived from the shared keystone
35
42
  * `plan()` (#2267 / epic #2203). `plan()` is the ONE classifier: doctor never
@@ -99,6 +106,8 @@ export interface DoctorSeams {
99
106
  ok: boolean;
100
107
  version: string;
101
108
  };
109
+ /** Offline npm configuration reader for effective-registry diagnostics (#2808). */
110
+ readonly runNpmConfigGet?: NpmConfigGet;
102
111
  readonly agentsRefreshPlan?: (projectRoot: string) => Record<string, unknown>;
103
112
  readonly agentsMdAdvisoryEvaluate?: (projectRoot: string) => AdvisoryEvaluateResult;
104
113
  readonly readState?: (projectRoot: string) => DoctorState | null;
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { evaluateHealth } from "../eval/health.js";
5
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
5
6
  import { resolveEvalPath } from "../layout/resolve.js";
6
7
  import { matchAny } from "../orchestration/pathspec.js";
7
8
  /** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
@@ -57,6 +58,7 @@ export function readHealthBaseline(projectRoot) {
57
58
  /** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
58
59
  export function writeHealthBaseline(projectRoot, report) {
59
60
  const path = healthBaselinePath(projectRoot);
61
+ assertWriteTargetSafe(projectRoot, path);
60
62
  mkdirSync(dirname(path), { recursive: true });
61
63
  writeFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
62
64
  }
@@ -170,7 +172,19 @@ export function evaluate(options = {}) {
170
172
  stream: "stderr",
171
173
  };
172
174
  }
173
- writeHealthBaseline(projectRoot, report);
175
+ try {
176
+ writeHealthBaseline(projectRoot, report);
177
+ }
178
+ catch (err) {
179
+ if (err instanceof ProjectionContainmentError) {
180
+ return {
181
+ code: 2,
182
+ message: `❌ verify:eval-health-relocation: ${err.message}`,
183
+ stream: "stderr",
184
+ };
185
+ }
186
+ throw err;
187
+ }
174
188
  return {
175
189
  code: 0,
176
190
  message: `✓ verify:eval-health-relocation: seeded baseline at ${HEALTH_BASELINE_REL} ` +
@@ -91,7 +91,7 @@ export function assertProjectionContained(projectDir, targetPath) {
91
91
  try {
92
92
  deepestExistingReal = realpathSync(current);
93
93
  }
94
- catch (err) {
94
+ catch {
95
95
  throw new ProjectionContainmentError(`projection write refused: could not resolve ${current} on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
96
96
  }
97
97
  }
@@ -1,23 +1,9 @@
1
- /** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
1
+ /** Cursor's ApplyPatch payload is parsed directly by the shared hook executable. */
2
2
  export declare const APPLY_PATCH_TOOL_NAMES: readonly ["ApplyPatch", "apply_patch"];
3
3
  export declare const APPLY_PATCH_HOOK_MATCHER: string;
4
- /** Generic Cursor write matcher excludes ApplyPatch — adapter owns it (#2764). */
5
- export declare const CURSOR_GENERIC_WRITE_TOOL_NAMES: ("Edit" | "Write" | "WriteFile" | "CreateFile" | "MultiEdit" | "NotebookEdit" | "StrReplace" | "SearchReplace" | "Delete" | "DeleteFile" | "ApplyPatch" | "apply_patch")[];
6
- export declare const CURSOR_GENERIC_WRITE_HOOK_MATCHER: string;
7
- export declare const CURSOR_APPLY_PATCH_ADAPTER_RELATIVE = ".cursor/hooks/deft-cursor-hook-adapter.mjs";
8
- export declare const DEFT_CURSOR_ADAPTER_COMMAND_MARKER = "deft-cursor-hook-adapter.mjs";
9
- export declare const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = "node .cursor/hooks/deft-cursor-hook-adapter.mjs ApplyPatch";
10
- /** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
11
- export declare const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = "#!/usr/bin/env node\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst projectRoot = resolve(process.cwd());\nconst stdin = readFileSync(0, \"utf8\");\n\nfunction deftCommand() {\n for (const candidate of [\"deft\", \"directive\"]) {\n const probe = spawnSync(candidate, [\"--version\"], {\n encoding: \"utf8\",\n stdio: \"ignore\",\n shell: process.platform === \"win32\",\n windowsHide: true,\n });\n if (probe.error === undefined && probe.status === 0) return candidate;\n }\n process.stderr.write(\n \"Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n\",\n );\n process.exit(2);\n}\n\nconst cli = deftCommand();\nconst result = spawnSync(\n cli,\n [\n \"hook:dispatch\",\n \"--host\",\n \"cursor\",\n \"--event\",\n \"tool.before\",\n \"--project-root\",\n projectRoot,\n ],\n {\n input: stdin,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n shell: process.platform === \"win32\",\n windowsHide: true,\n },\n);\n\nif (result.stderr) process.stderr.write(result.stderr);\nif (result.error) {\n process.stderr.write(String(result.error));\n process.exit(2);\n}\nif (result.status !== 0 && result.status !== null) {\n if (result.stdout) process.stdout.write(result.stdout);\n process.exit(result.status);\n}\n// Cursor failClosed treats empty stdout as failure \u2014 normalize allow.\nconst out = (result.stdout ?? \"\").trim();\nprocess.stdout.write((out.length > 0 ? out : '{\"permission\":\"allow\"}') + \"\\n\");\nprocess.exit(0);\n";
12
- export interface CursorPreToolUseEntry {
13
- readonly command: string;
14
- readonly matcher?: string;
15
- readonly failClosed?: boolean;
16
- readonly timeout?: number;
17
- }
18
- export declare function cursorApplyPatchAdapterEntry(): CursorPreToolUseEntry;
19
- /** True when generic and adapter matchers share no tool tokens. */
20
- export declare function cursorApplyPatchMatchersDisjoint(): boolean;
21
- /** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
22
- export declare function assertCursorApplyPatchMatchersDisjoint(): void;
4
+ /**
5
+ * Cursor uses one disjoint direct-write registration: hook-dispatch recognizes
6
+ * JSON and free-form ApplyPatch payloads, avoiding an adapter process (#2790).
7
+ */
8
+ export declare const CURSOR_DIRECT_WRITE_HOOK_MATCHER: string;
23
9
  //# sourceMappingURL=cursor-hooks.d.ts.map