@deftai/directive-core 0.77.0 → 0.79.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 (126) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.d.ts +1 -0
  8. package/dist/doctor/main.js +48 -0
  9. package/dist/doctor/payload-staleness.js +7 -1
  10. package/dist/doctor/types.d.ts +3 -0
  11. package/dist/eval/triggers.d.ts +76 -0
  12. package/dist/eval/triggers.js +259 -0
  13. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  14. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  15. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  16. package/dist/eval-triggers-relocation/index.js +2 -0
  17. package/dist/hooks/dispatcher.d.ts +43 -0
  18. package/dist/hooks/dispatcher.js +173 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.js +3 -0
  21. package/dist/hooks/scope.d.ts +12 -0
  22. package/dist/hooks/scope.js +46 -0
  23. package/dist/hooks/tools.d.ts +4 -0
  24. package/dist/hooks/tools.js +23 -0
  25. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  26. package/dist/init-deposit/agent-hooks.js +238 -0
  27. package/dist/init-deposit/hygiene.js +4 -0
  28. package/dist/init-deposit/index.d.ts +1 -0
  29. package/dist/init-deposit/index.js +1 -0
  30. package/dist/init-deposit/init-deposit.js +4 -0
  31. package/dist/init-deposit/prettierignore.d.ts +21 -0
  32. package/dist/init-deposit/prettierignore.js +83 -0
  33. package/dist/init-deposit/refresh.d.ts +3 -1
  34. package/dist/init-deposit/refresh.js +31 -40
  35. package/dist/init-deposit/scaffold.js +6 -12
  36. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  37. package/dist/init-deposit/xbrief-projections.js +117 -0
  38. package/dist/intake/reconcile-issues.d.ts +11 -0
  39. package/dist/intake/reconcile-issues.js +166 -51
  40. package/dist/issue-sync/index.d.ts +2 -0
  41. package/dist/issue-sync/index.js +2 -0
  42. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  43. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  44. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  45. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  46. package/dist/platform/index.d.ts +1 -0
  47. package/dist/platform/index.js +1 -0
  48. package/dist/platform/platform-capabilities.d.ts +5 -0
  49. package/dist/platform/platform-capabilities.js +9 -0
  50. package/dist/platform/shell-context.d.ts +30 -0
  51. package/dist/platform/shell-context.js +111 -0
  52. package/dist/pr-monitor/cadence.d.ts +6 -0
  53. package/dist/pr-monitor/cadence.js +13 -0
  54. package/dist/pr-monitor/index.d.ts +1 -1
  55. package/dist/pr-monitor/index.js +1 -1
  56. package/dist/pr-monitor/monitor.d.ts +7 -0
  57. package/dist/pr-monitor/monitor.js +59 -13
  58. package/dist/preflight-cache/evaluate.d.ts +3 -8
  59. package/dist/preflight-cache/evaluate.js +19 -19
  60. package/dist/release/build-dist.js +1 -0
  61. package/dist/release/constants.d.ts +2 -0
  62. package/dist/release/constants.js +8 -1
  63. package/dist/release/flags.js +26 -0
  64. package/dist/release/issue-state-fetch.d.ts +27 -0
  65. package/dist/release/issue-state-fetch.js +146 -0
  66. package/dist/release/main.js +1 -0
  67. package/dist/release/native-steps.js +6 -4
  68. package/dist/release/pipeline.js +15 -5
  69. package/dist/release/preflight.d.ts +6 -2
  70. package/dist/release/preflight.js +19 -5
  71. package/dist/release/types.d.ts +3 -1
  72. package/dist/release-e2e/constants.js +6 -6
  73. package/dist/release-e2e/flags.d.ts +2 -0
  74. package/dist/release-e2e/flags.js +8 -4
  75. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  76. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  77. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  78. package/dist/release-e2e/main.js +15 -9
  79. package/dist/release-e2e/types.d.ts +4 -2
  80. package/dist/resolution/classify.d.ts +1 -1
  81. package/dist/resolution/classify.js +36 -11
  82. package/dist/scope/project-definition-sync.js +2 -150
  83. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  84. package/dist/scope/registry-artifact-sync.js +159 -0
  85. package/dist/scope/specification-sync.d.ts +3 -0
  86. package/dist/scope/specification-sync.js +16 -0
  87. package/dist/scope/transition.js +40 -12
  88. package/dist/session/cache-recovery.d.ts +19 -0
  89. package/dist/session/cache-recovery.js +26 -0
  90. package/dist/session/session-start.d.ts +2 -0
  91. package/dist/session/session-start.js +14 -3
  92. package/dist/session/verify-session-ritual.d.ts +17 -0
  93. package/dist/session/verify-session-ritual.js +64 -0
  94. package/dist/triage/help/registry-data.d.ts +12 -1
  95. package/dist/triage/help/registry-data.js +22 -1
  96. package/dist/triage/welcome/default-mode.js +2 -0
  97. package/dist/triage/welcome/onboard.js +2 -0
  98. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  99. package/dist/ts-check-lane/run-lane.js +9 -0
  100. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  101. package/dist/vbrief-reconcile/labels.js +14 -0
  102. package/dist/vbrief-reconcile/main.d.ts +1 -0
  103. package/dist/vbrief-reconcile/main.js +13 -3
  104. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  105. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  106. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  107. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  108. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  109. package/dist/verify-env/agent-hooks.d.ts +11 -0
  110. package/dist/verify-env/agent-hooks.js +46 -0
  111. package/dist/verify-env/command-spawn.d.ts +5 -0
  112. package/dist/verify-env/command-spawn.js +28 -9
  113. package/dist/verify-env/index.d.ts +1 -0
  114. package/dist/verify-env/index.js +1 -0
  115. package/dist/verify-env/verify-hooks-installed.js +5 -5
  116. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  117. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  118. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  119. package/dist/vitest-runner/coverage-debt.js +155 -0
  120. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  121. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  122. package/dist/xbrief-migrate/detect.js +46 -1
  123. package/dist/xbrief-migrate/migrate-project.js +19 -4
  124. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  125. package/dist/xbrief-migrate/migration-containment.js +37 -0
  126. package/package.json +19 -3
@@ -0,0 +1,44 @@
1
+ /**
2
+ * empty-populate.ts -- auto-fetch GitHub when the triage cache is empty (#2575).
3
+ *
4
+ * Empty/missing cache is the one case where a live list beats a false-empty
5
+ * "what's next?" answer. Non-empty caches keep the 24h freshness gate unchanged.
6
+ */
7
+ import type { FetchAllReportImpl } from "./fetch.js";
8
+ export interface EmptyPopulateResult {
9
+ readonly skipped: boolean;
10
+ readonly skipReason: string | null;
11
+ readonly repo: string | null;
12
+ readonly populated: boolean;
13
+ readonly succeeded: number | null;
14
+ readonly message: string;
15
+ }
16
+ export interface EmptyPopulateOptions {
17
+ readonly repo?: string | null;
18
+ readonly cacheRoot?: string;
19
+ readonly source?: string;
20
+ readonly inferRepoFn?: (cwd: string) => string | null;
21
+ readonly fetchFn?: (options: {
22
+ source: string;
23
+ repo: string;
24
+ cacheRoot: string;
25
+ force: boolean;
26
+ }) => FetchAllReportImpl;
27
+ readonly seedFn?: (projectRoot: string) => {
28
+ ok: boolean;
29
+ };
30
+ readonly backfillFn?: (projectRoot: string, repo: string | null) => {
31
+ ok: boolean;
32
+ };
33
+ }
34
+ /** True when `.deft-cache/github-issue/` has zero cached issue entries. */
35
+ export declare function isTriageCacheEmpty(projectRoot: string, cacheRoot?: string): boolean;
36
+ /**
37
+ * When the triage cache is empty, mirror `triage:bootstrap` populate steps:
38
+ * fetch-all from GitHub, seed the candidates log, backfill lifecycle accepts.
39
+ * Never auto-accepts new issues beyond existing xBRIEF backfill.
40
+ */
41
+ export declare function maybeAutoPopulateEmptyCache(projectRoot: string, options?: EmptyPopulateOptions): EmptyPopulateResult;
42
+ /** Idempotent hydrate hook for triage read paths (#2575). */
43
+ export declare function ensureTriageCacheHydrated(projectRoot: string, options?: EmptyPopulateOptions): EmptyPopulateResult;
44
+ //# sourceMappingURL=empty-populate.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * empty-populate.ts -- auto-fetch GitHub when the triage cache is empty (#2575).
3
+ *
4
+ * Empty/missing cache is the one case where a live list beats a false-empty
5
+ * "what's next?" answer. Non-empty caches keep the 24h freshness gate unchanged.
6
+ */
7
+ import { join, resolve } from "node:path";
8
+ import { stepSeedCandidatesLog } from "../triage/bootstrap/gitignore.js";
9
+ import { inferRepoFromGit, stepBackfillAuditLog } from "../triage/bootstrap/index.js";
10
+ import { ENV_TRIAGE_REPO } from "../triage/queue/constants.js";
11
+ import { CACHE_DIR_NAME, CACHE_SOURCE, iterCachedIssues } from "../triage/summary/index.js";
12
+ import { cacheFetchAll } from "./fetch.js";
13
+ /** True when `.deft-cache/github-issue/` has zero cached issue entries. */
14
+ export function isTriageCacheEmpty(projectRoot, cacheRoot) {
15
+ const root = resolve(projectRoot);
16
+ const resolvedCacheRoot = cacheRoot ?? join(root, CACHE_DIR_NAME);
17
+ return iterCachedIssues(resolvedCacheRoot).length === 0;
18
+ }
19
+ /**
20
+ * When the triage cache is empty, mirror `triage:bootstrap` populate steps:
21
+ * fetch-all from GitHub, seed the candidates log, backfill lifecycle accepts.
22
+ * Never auto-accepts new issues beyond existing xBRIEF backfill.
23
+ */
24
+ export function maybeAutoPopulateEmptyCache(projectRoot, options = {}) {
25
+ const root = resolve(projectRoot);
26
+ const source = options.source ?? CACHE_SOURCE;
27
+ const cacheRoot = options.cacheRoot ?? join(root, CACHE_DIR_NAME);
28
+ if (!isTriageCacheEmpty(root, cacheRoot)) {
29
+ return {
30
+ skipped: true,
31
+ skipReason: "cache-non-empty",
32
+ repo: null,
33
+ populated: false,
34
+ succeeded: null,
35
+ message: "cache already populated",
36
+ };
37
+ }
38
+ const infer = options.inferRepoFn ?? inferRepoFromGit;
39
+ const envRepo = (process.env[ENV_TRIAGE_REPO] ?? "").trim();
40
+ const repo = options.repo ?? (envRepo.length > 0 ? envRepo : null) ?? infer(root);
41
+ if (repo === null) {
42
+ return {
43
+ skipped: true,
44
+ skipReason: "repo-not-resolved",
45
+ repo: null,
46
+ populated: false,
47
+ succeeded: null,
48
+ message: "could not infer repo for empty-cache auto-populate",
49
+ };
50
+ }
51
+ const fetch = options.fetchFn ?? cacheFetchAll;
52
+ let report;
53
+ try {
54
+ report = fetch({ source, repo, cacheRoot, force: true });
55
+ }
56
+ catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ return {
59
+ skipped: true,
60
+ skipReason: "fetch-failed",
61
+ repo,
62
+ populated: false,
63
+ succeeded: null,
64
+ message,
65
+ };
66
+ }
67
+ const seed = options.seedFn ?? stepSeedCandidatesLog;
68
+ const seedOutcome = seed(root);
69
+ if (!seedOutcome.ok) {
70
+ return {
71
+ skipped: true,
72
+ skipReason: "seed-failed",
73
+ repo,
74
+ populated: false,
75
+ succeeded: report.issuesWritten,
76
+ message: "candidates log seed failed after fetch-all",
77
+ };
78
+ }
79
+ const backfill = options.backfillFn ?? stepBackfillAuditLog;
80
+ try {
81
+ const backfillOutcome = backfill(root, repo);
82
+ if (!backfillOutcome.ok) {
83
+ return {
84
+ skipped: true,
85
+ skipReason: "backfill-failed",
86
+ repo,
87
+ populated: false,
88
+ succeeded: report.issuesWritten,
89
+ message: "audit log backfill failed after fetch-all",
90
+ };
91
+ }
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ return {
96
+ skipped: true,
97
+ skipReason: "backfill-failed",
98
+ repo,
99
+ populated: false,
100
+ succeeded: report.issuesWritten,
101
+ message,
102
+ };
103
+ }
104
+ return {
105
+ skipped: false,
106
+ skipReason: null,
107
+ repo,
108
+ populated: true,
109
+ succeeded: report.issuesWritten,
110
+ message: `auto-populated empty triage cache from GitHub (${repo})`,
111
+ };
112
+ }
113
+ /** Idempotent hydrate hook for triage read paths (#2575). */
114
+ export function ensureTriageCacheHydrated(projectRoot, options = {}) {
115
+ return maybeAutoPopulateEmptyCache(projectRoot, options);
116
+ }
117
+ //# sourceMappingURL=empty-populate.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./constants.js";
2
+ export * from "./empty-populate.js";
2
3
  export * from "./errors.js";
3
4
  export * from "./fetch.js";
4
5
  export * from "./io.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./constants.js";
2
+ export * from "./empty-populate.js";
2
3
  export * from "./errors.js";
3
4
  export * from "./fetch.js";
4
5
  export * from "./io.js";
@@ -2,10 +2,20 @@
2
2
  export declare const REMOTE_PROBE_DEFAULT_TIMEOUT = 5;
3
3
  /** Baked-in canonical upstream (#1320). Never probe consumer origin. */
4
4
  export declare const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
5
+ /** SSH form of the canonical upstream — allowlisted alongside HTTPS (#2601). */
6
+ export declare const DEFT_UPSTREAM_SSH_URL = "git@github.com:deftai/directive.git";
5
7
  /** Reject git ls-remote targets that could be parsed as options (#CodeQL). */
6
8
  export declare function isSafeGitLsRemoteTarget(url: string): boolean;
7
9
  /** Return a trimmed upstream URL safe for `git ls-remote`, or null when rejected. */
8
10
  export declare function sanitizeGitLsRemoteTarget(url: string): string | null;
11
+ /** Extract hostname from an https or git@ upstream URL. */
12
+ export declare function extractUpstreamHostname(url: string): string | null;
13
+ /** Reject localhost, link-local, and RFC1918 targets for blind SSRF hardening (#2601). */
14
+ export declare function isPrivateOrLinkLocalUpstreamHost(hostname: string): boolean;
15
+ /** True when a sanitized upstream URL is on the canonical allowlist (#2601). */
16
+ export declare function isAllowlistedSafeUpstreamUrl(safeUrl: string): boolean;
17
+ /** True when a manifest-controlled upstream URL is safe to probe (#2601). */
18
+ export declare function isAllowlistedUpstreamUrl(url: string): boolean;
9
19
  export type RemoteProbeStatus = "ok" | "behind" | "skipped" | "no-upstream" | "no-tags" | "error";
10
20
  export interface RemoteProbeResult {
11
21
  readonly status: RemoteProbeStatus;
@@ -7,6 +7,9 @@ import { resolveVersion } from "../doctor/paths.js";
7
7
  export const REMOTE_PROBE_DEFAULT_TIMEOUT = 5.0;
8
8
  /** Baked-in canonical upstream (#1320). Never probe consumer origin. */
9
9
  export const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
10
+ /** SSH form of the canonical upstream — allowlisted alongside HTTPS (#2601). */
11
+ export const DEFT_UPSTREAM_SSH_URL = "git@github.com:deftai/directive.git";
12
+ const ALLOWED_UPSTREAM_URLS = new Set([DEFT_UPSTREAM_URL, DEFT_UPSTREAM_SSH_URL]);
10
13
  const MANIFEST_UPSTREAM_URL_KEYS = [
11
14
  "source_url",
12
15
  "url",
@@ -32,6 +35,83 @@ export function sanitizeGitLsRemoteTarget(url) {
32
35
  }
33
36
  return null;
34
37
  }
38
+ /** Extract hostname from an https or git@ upstream URL. */
39
+ export function extractUpstreamHostname(url) {
40
+ const trimmed = url.trim();
41
+ const httpsMatch = /^https?:\/\/([^/:?#]+)/i.exec(trimmed);
42
+ if (httpsMatch?.[1]) {
43
+ return httpsMatch[1].toLowerCase();
44
+ }
45
+ const sshMatch = /^git@([^:/]+):/i.exec(trimmed);
46
+ if (sshMatch?.[1]) {
47
+ return sshMatch[1].toLowerCase();
48
+ }
49
+ return null;
50
+ }
51
+ function parseIpv4Octets(hostname) {
52
+ const parts = hostname.split(".");
53
+ if (parts.length !== 4) {
54
+ return null;
55
+ }
56
+ const octets = [];
57
+ for (const part of parts) {
58
+ if (!/^\d+$/.test(part)) {
59
+ return null;
60
+ }
61
+ const value = Number(part);
62
+ if (value > 255) {
63
+ return null;
64
+ }
65
+ octets.push(value);
66
+ }
67
+ return octets;
68
+ }
69
+ /** Reject localhost, link-local, and RFC1918 targets for blind SSRF hardening (#2601). */
70
+ export function isPrivateOrLinkLocalUpstreamHost(hostname) {
71
+ const host = hostname.trim().toLowerCase();
72
+ if (host.length === 0) {
73
+ return true;
74
+ }
75
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
76
+ return true;
77
+ }
78
+ if (host === "::1" || host === "[::1]") {
79
+ return true;
80
+ }
81
+ const octets = parseIpv4Octets(host);
82
+ if (octets !== null) {
83
+ const [a, b] = octets;
84
+ if (a === 127)
85
+ return true;
86
+ if (a === 10)
87
+ return true;
88
+ if (a === 172 && b !== undefined && b >= 16 && b <= 31)
89
+ return true;
90
+ if (a === 192 && b === 168)
91
+ return true;
92
+ if (a === 169 && b === 254)
93
+ return true;
94
+ if (a === 0)
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ /** True when a sanitized upstream URL is on the canonical allowlist (#2601). */
100
+ export function isAllowlistedSafeUpstreamUrl(safeUrl) {
101
+ if (!ALLOWED_UPSTREAM_URLS.has(safeUrl)) {
102
+ return false;
103
+ }
104
+ const hostname = extractUpstreamHostname(safeUrl);
105
+ if (hostname === null || isPrivateOrLinkLocalUpstreamHost(hostname)) {
106
+ return false;
107
+ }
108
+ return true;
109
+ }
110
+ /** True when a manifest-controlled upstream URL is safe to probe (#2601). */
111
+ export function isAllowlistedUpstreamUrl(url) {
112
+ const safe = sanitizeGitLsRemoteTarget(url);
113
+ return safe !== null && isAllowlistedSafeUpstreamUrl(safe);
114
+ }
35
115
  function normalizePrereleaseForSort(pre) {
36
116
  const dot = pre.indexOf(".");
37
117
  if (dot <= 0) {
@@ -134,7 +214,7 @@ export function resolveUpstreamUrl(projectRoot) {
134
214
  const value = manifest[key];
135
215
  if (typeof value === "string" && value.trim()) {
136
216
  const safe = sanitizeGitLsRemoteTarget(value);
137
- if (safe) {
217
+ if (safe && isAllowlistedSafeUpstreamUrl(safe)) {
138
218
  return safe;
139
219
  }
140
220
  }
@@ -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,6 +163,11 @@ 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
172
  // registry (git verifies the pin; npm compares stable release availability).
167
173
  // It stays in the OFFLINE
@@ -332,6 +338,48 @@ export function cmdDoctor(args, seams = {}) {
332
338
  sink.finalWarn(`System check completed with ${warningCount} warning(s).`);
333
339
  return 0;
334
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
+ const message = `${checkName}: registered and structurally valid; ` +
353
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
354
+ sink.success(message);
355
+ addFinding({
356
+ severity: "skip",
357
+ message,
358
+ check: checkName,
359
+ status: "registered",
360
+ registrations: result.registrations,
361
+ trust_status: "not-verifiable",
362
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
363
+ });
364
+ return;
365
+ }
366
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
367
+ sink.warn(message);
368
+ addFinding({
369
+ severity: "warning",
370
+ message,
371
+ check: checkName,
372
+ status: result.code === 2 ? "unavailable" : "incomplete",
373
+ registrations: result.registrations,
374
+ suggestion: "deft update",
375
+ });
376
+ }
377
+ catch (cause) {
378
+ const message = `${checkName}: probe failed -- ${String(cause)}`;
379
+ sink.warn(message);
380
+ addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
381
+ }
382
+ }
335
383
  function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
336
384
  if (runningInsideDeftRepo(projectRoot, seams)) {
337
385
  sink.info("Skipping install-integrity checks -- running inside the deft framework repo (no install manifest in the source checkout).");
@@ -1,6 +1,7 @@
1
1
  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
+ import { isPublishable } from "../release/version.js";
4
5
  import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
5
6
  import { locateManifest, parseInstallManifest } from "./manifest.js";
6
7
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
@@ -203,7 +204,12 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
203
204
  });
204
205
  return;
205
206
  }
206
- const installedCandidate = (tag || ref).trim().replace(/^refs\/tags\//, "");
207
+ const normalizedRef = ref.trim().replace(/^refs\/tags\//, "");
208
+ const normalizedTag = tag.trim().replace(/^refs\/tags\//, "");
209
+ // Branch pins (ref not a publishable tag) must not fall through to npm via a stale tag (#2538).
210
+ const installedCandidate = isPublishable(normalizedRef)
211
+ ? normalizedTag || normalizedRef
212
+ : normalizedRef;
207
213
  const applicability = evaluateReleaseAvailability(installedCandidate, null);
208
214
  if (applicability.status === "not-applicable") {
209
215
  if (remoteSha && installedSha === remoteSha) {
@@ -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
@@ -0,0 +1,76 @@
1
+ /** Relative path to skill-pi-trigger-eval compatible case rows (#1586). */
2
+ export declare const TRIGGER_CASES_REL = "evals/trigger-cases.jsonl";
3
+ /** Default Skills Index source for AGENTS.md skill routing (#1586). */
4
+ export declare const SKILLS_INDEX_REL = "REFERENCES.md";
5
+ /** Parsed Skills Index row. */
6
+ export interface SkillsIndexEntry {
7
+ readonly skillId: string;
8
+ readonly triggers: readonly string[];
9
+ }
10
+ /** One row in evals/trigger-cases.jsonl (skill-pi-trigger-eval eval-set + skill id). */
11
+ export interface TriggerCaseRow {
12
+ readonly id: string;
13
+ readonly query: string;
14
+ readonly skill: string;
15
+ readonly should_trigger: boolean;
16
+ }
17
+ /** Outcome for a single trigger case. */
18
+ export interface TriggerCaseResult {
19
+ readonly id: string;
20
+ readonly query: string;
21
+ readonly skill: string;
22
+ readonly should_trigger: boolean;
23
+ readonly actual: boolean;
24
+ readonly pass: boolean;
25
+ readonly matchedTrigger?: string;
26
+ readonly winnerSkill?: string;
27
+ }
28
+ /** Aggregate eval:triggers report. */
29
+ export interface TriggerEvalReport {
30
+ readonly total: number;
31
+ readonly passed: number;
32
+ readonly failed: number;
33
+ readonly pass_rate: number | null;
34
+ readonly results: readonly TriggerCaseResult[];
35
+ readonly coverageErrors: readonly string[];
36
+ }
37
+ export interface RunTriggerEvalOptions {
38
+ readonly projectRoot?: string;
39
+ readonly casesPath?: string;
40
+ readonly indexPath?: string;
41
+ readonly cases?: readonly TriggerCaseRow[];
42
+ readonly indexText?: string;
43
+ }
44
+ export interface RunTriggerEvalResult {
45
+ readonly code: 0 | 1 | 2;
46
+ readonly report: TriggerEvalReport | null;
47
+ readonly message: string;
48
+ }
49
+ /** Normalize user text for deterministic substring trigger matching. */
50
+ export declare function normalizeTriggerText(text: string): string;
51
+ /** Collapse embedded newlines before interpolating user/data into log lines. */
52
+ export declare function sanitizeTriggerLogLine(text: string): string;
53
+ /** Extract backtick trigger phrases from a Skills Index triggers cell. */
54
+ export declare function parseTriggerCell(cell: string): string[];
55
+ /** Parse the Skills Index markdown table in REFERENCES.md. */
56
+ export declare function parseSkillsIndex(markdown: string): SkillsIndexEntry[];
57
+ interface TriggerMatch {
58
+ readonly skillId: string;
59
+ readonly trigger: string;
60
+ }
61
+ /** Find the winning skill for a query using longest-trigger-wins disambiguation. */
62
+ export declare function resolveTriggerWinner(query: string, index: readonly SkillsIndexEntry[]): TriggerMatch | null;
63
+ /** True when the Skills Index routing would send *query* to *skillId*. */
64
+ export declare function wouldRouteToSkill(query: string, skillId: string, index: readonly SkillsIndexEntry[]): boolean;
65
+ /** Validate one JSONL row against the skill-pi-trigger-eval row contract. */
66
+ export declare function parseTriggerCaseRow(raw: unknown, lineNumber: number): TriggerCaseRow | string;
67
+ /** Load evals/trigger-cases.jsonl. */
68
+ export declare function loadTriggerCases(path: string): TriggerCaseRow[] | {
69
+ error: string;
70
+ };
71
+ /** Verify at least two positive and one negative case per indexed skill. */
72
+ export declare function validateTriggerCoverage(cases: readonly TriggerCaseRow[], index: readonly SkillsIndexEntry[]): string[];
73
+ /** Run deterministic trigger routing eval (offline skill-pi-trigger-eval counterpart). */
74
+ export declare function runTriggerEval(options?: RunTriggerEvalOptions): RunTriggerEvalResult;
75
+ export {};
76
+ //# sourceMappingURL=triggers.d.ts.map