@deftai/directive-core 0.78.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 (99) 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.js +13 -2
  8. package/dist/doctor/payload-staleness.js +7 -1
  9. package/dist/hooks/dispatcher.d.ts +1 -1
  10. package/dist/hooks/dispatcher.js +19 -17
  11. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  12. package/dist/init-deposit/agent-hooks.js +10 -0
  13. package/dist/init-deposit/hygiene.js +1 -0
  14. package/dist/init-deposit/init-deposit.js +2 -0
  15. package/dist/init-deposit/prettierignore.d.ts +21 -0
  16. package/dist/init-deposit/prettierignore.js +83 -0
  17. package/dist/init-deposit/refresh.d.ts +3 -1
  18. package/dist/init-deposit/refresh.js +29 -40
  19. package/dist/init-deposit/scaffold.js +6 -12
  20. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  21. package/dist/init-deposit/xbrief-projections.js +117 -0
  22. package/dist/intake/reconcile-issues.d.ts +10 -0
  23. package/dist/intake/reconcile-issues.js +115 -2
  24. package/dist/issue-sync/index.d.ts +2 -0
  25. package/dist/issue-sync/index.js +2 -0
  26. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  27. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  28. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  29. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  30. package/dist/platform/index.d.ts +1 -0
  31. package/dist/platform/index.js +1 -0
  32. package/dist/platform/platform-capabilities.d.ts +5 -0
  33. package/dist/platform/platform-capabilities.js +9 -0
  34. package/dist/platform/shell-context.d.ts +30 -0
  35. package/dist/platform/shell-context.js +111 -0
  36. package/dist/pr-monitor/cadence.d.ts +6 -0
  37. package/dist/pr-monitor/cadence.js +13 -0
  38. package/dist/pr-monitor/index.d.ts +1 -1
  39. package/dist/pr-monitor/index.js +1 -1
  40. package/dist/pr-monitor/monitor.d.ts +7 -0
  41. package/dist/pr-monitor/monitor.js +59 -13
  42. package/dist/preflight-cache/evaluate.d.ts +3 -8
  43. package/dist/preflight-cache/evaluate.js +19 -19
  44. package/dist/release/constants.d.ts +2 -0
  45. package/dist/release/constants.js +8 -1
  46. package/dist/release/flags.js +26 -0
  47. package/dist/release/issue-state-fetch.d.ts +27 -0
  48. package/dist/release/issue-state-fetch.js +146 -0
  49. package/dist/release/main.js +1 -0
  50. package/dist/release/native-steps.js +6 -4
  51. package/dist/release/pipeline.js +15 -5
  52. package/dist/release/preflight.d.ts +6 -2
  53. package/dist/release/preflight.js +19 -5
  54. package/dist/release/types.d.ts +3 -1
  55. package/dist/release-e2e/constants.js +6 -6
  56. package/dist/release-e2e/flags.d.ts +2 -0
  57. package/dist/release-e2e/flags.js +8 -4
  58. package/dist/release-e2e/main.js +15 -9
  59. package/dist/release-e2e/types.d.ts +4 -2
  60. package/dist/resolution/classify.d.ts +1 -1
  61. package/dist/resolution/classify.js +36 -11
  62. package/dist/scope/project-definition-sync.js +2 -150
  63. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  64. package/dist/scope/registry-artifact-sync.js +159 -0
  65. package/dist/scope/specification-sync.d.ts +3 -0
  66. package/dist/scope/specification-sync.js +16 -0
  67. package/dist/scope/transition.js +40 -12
  68. package/dist/session/cache-recovery.d.ts +19 -0
  69. package/dist/session/cache-recovery.js +26 -0
  70. package/dist/session/session-start.d.ts +2 -0
  71. package/dist/session/session-start.js +14 -3
  72. package/dist/session/verify-session-ritual.d.ts +1 -0
  73. package/dist/session/verify-session-ritual.js +1 -0
  74. package/dist/triage/welcome/default-mode.js +2 -0
  75. package/dist/triage/welcome/onboard.js +2 -0
  76. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  77. package/dist/ts-check-lane/run-lane.js +9 -0
  78. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  79. package/dist/vbrief-reconcile/labels.js +14 -0
  80. package/dist/vbrief-reconcile/main.d.ts +1 -0
  81. package/dist/vbrief-reconcile/main.js +13 -3
  82. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  83. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  84. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  85. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  86. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  87. package/dist/verify-env/agent-hooks.js +4 -3
  88. package/dist/verify-env/verify-hooks-installed.js +5 -5
  89. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  90. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  91. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  92. package/dist/vitest-runner/coverage-debt.js +155 -0
  93. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  94. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  95. package/dist/xbrief-migrate/detect.js +46 -1
  96. package/dist/xbrief-migrate/migrate-project.js +19 -4
  97. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  98. package/dist/xbrief-migrate/migration-containment.js +37 -0
  99. package/package.json +7 -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
  }
@@ -349,7 +349,18 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
349
349
  try {
350
350
  const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
351
351
  if (result.code === 0) {
352
- sink.success(`${checkName}: healthy`);
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
+ });
353
364
  return;
354
365
  }
355
366
  const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
@@ -358,7 +369,7 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
358
369
  severity: "warning",
359
370
  message,
360
371
  check: checkName,
361
- status: result.code === 2 ? "unavailable" : "non-functional",
372
+ status: result.code === 2 ? "unavailable" : "incomplete",
362
373
  registrations: result.registrations,
363
374
  suggestion: "deft update",
364
375
  });
@@ -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) {
@@ -1,7 +1,7 @@
1
1
  import { type VerifyResult } from "../session/verify-session-ritual.js";
2
2
  import { type ActiveScopeInspection } from "./scope.js";
3
3
  export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
4
- export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor"];
4
+ export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
5
5
  export type HookHost = (typeof HOOK_HOSTS)[number];
6
6
  export declare const HOOK_EVENTS: readonly ["session.start", "tool.before"];
7
7
  export type HookEvent = (typeof HOOK_EVENTS)[number];
@@ -4,7 +4,7 @@ import { inspectSessionRitual } from "../session/verify-session-ritual.js";
4
4
  import { inspectActiveScope } from "./scope.js";
5
5
  import { isDirectWriteTool } from "./tools.js";
6
6
  export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
7
- export const HOOK_HOSTS = ["claude", "grok", "cursor"];
7
+ export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
8
8
  export const HOOK_EVENTS = ["session.start", "tool.before"];
9
9
  function record(value) {
10
10
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -150,22 +150,24 @@ export function decideHook(input, seams = {}) {
150
150
  export function renderHostDecision(host, decision) {
151
151
  if (decision.verdict === "allow")
152
152
  return "";
153
- if (host === "claude") {
154
- return JSON.stringify({
155
- hookSpecificOutput: {
156
- hookEventName: "PreToolUse",
157
- permissionDecision: "deny",
158
- permissionDecisionReason: decision.message,
159
- },
160
- });
153
+ switch (host) {
154
+ case "claude":
155
+ case "codex":
156
+ return JSON.stringify({
157
+ hookSpecificOutput: {
158
+ hookEventName: "PreToolUse",
159
+ permissionDecision: "deny",
160
+ permissionDecisionReason: decision.message,
161
+ },
162
+ });
163
+ case "grok":
164
+ return JSON.stringify({ decision: "deny", reason: decision.message });
165
+ case "cursor":
166
+ return JSON.stringify({
167
+ permission: "deny",
168
+ user_message: decision.message,
169
+ agent_message: decision.message,
170
+ });
161
171
  }
162
- if (host === "grok") {
163
- return JSON.stringify({ decision: "deny", reason: decision.message });
164
- }
165
- return JSON.stringify({
166
- permission: "deny",
167
- user_message: decision.message,
168
- agent_message: decision.message,
169
- });
170
172
  }
171
173
  //# sourceMappingURL=dispatcher.js.map
@@ -2,7 +2,7 @@ import type { HookHost } from "../hooks/dispatcher.js";
2
2
  import type { InitDepositIo } from "./constants.js";
3
3
  export declare const DIRECT_WRITE_HOOK_MATCHER: string;
4
4
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
- export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json"];
5
+ export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
6
6
  export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
7
7
  export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
8
8
  export interface AgentHookInspection {
@@ -8,6 +8,7 @@ export const AGENT_HOOK_PATHS = [
8
8
  ".claude/settings.json",
9
9
  ".grok/hooks/deft.json",
10
10
  ".cursor/hooks.json",
11
+ ".codex/hooks.json",
11
12
  ];
12
13
  function object(value) {
13
14
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -125,6 +126,10 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
125
126
  merge: (config, path) => mergeNestedConfig(config, path, "grok"),
126
127
  },
127
128
  { path: AGENT_HOOK_PATHS[2], merge: mergeCursorConfig },
129
+ {
130
+ path: AGENT_HOOK_PATHS[3],
131
+ merge: (config, path) => mergeNestedConfig(config, path, "codex"),
132
+ },
128
133
  ];
129
134
  const prepared = definitions.map((definition) => {
130
135
  const absolute = join(projectRoot, definition.path);
@@ -187,6 +192,11 @@ export function inspectAgentHookDeposit(projectRoot) {
187
192
  valid: (config) => hasNestedRegistration(config, "grok"),
188
193
  },
189
194
  { host: "cursor", path: AGENT_HOOK_PATHS[2], valid: hasCursorRegistration },
195
+ {
196
+ host: "codex",
197
+ path: AGENT_HOOK_PATHS[3],
198
+ valid: (config) => hasNestedRegistration(config, "codex"),
199
+ },
190
200
  ];
191
201
  return definitions.map((definition) => {
192
202
  const absolute = join(projectRoot, definition.path);
@@ -24,6 +24,7 @@ export function installerManagedMatchers() {
24
24
  { exact: ".claude/settings.json" },
25
25
  { exact: ".grok/hooks/deft.json" },
26
26
  { exact: ".cursor/hooks.json" },
27
+ { exact: ".codex/hooks.json" },
27
28
  { exact: ".gitattributes" },
28
29
  { exact: ".gitignore" },
29
30
  { exact: "greptile.json" },
@@ -18,6 +18,7 @@ import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./giti
18
18
  import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
19
19
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
20
20
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
21
+ import { ensurePrettierIgnoreLines } from "./prettierignore.js";
21
22
  import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
22
23
  export function parseInitArgv(canonicalArgv, userArgv = []) {
23
24
  const args = [...canonicalArgv, ...userArgv];
@@ -137,6 +138,7 @@ export async function runInitDeposit(args, io, seams = {}) {
137
138
  await reconstituteDepositFromContent(contentRoot, deftDir, copyContent);
138
139
  await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
139
140
  ensureInitGitignoreLines(projectDir, io);
141
+ ensurePrettierIgnoreLines(projectDir, io);
140
142
  const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
141
143
  const version = readContentVersion(contentRoot, seams.readPackageVersion ?? readCorePackageVersion);
142
144
  const manifestFields = {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Consumer `.prettierignore` upkeep for install and npm-managed update (#2534).
3
+ *
4
+ * Managed `.deft/core/` is outside the consumer Prettier format gate. Official
5
+ * install and `directive update` heal the ignore entry idempotently without
6
+ * reformatting the vendored deposit.
7
+ *
8
+ * Refs #2534, #670.
9
+ */
10
+ import type { InitDepositIo } from "./constants.js";
11
+ /** Directory ignore entry for the hybrid deposit. */
12
+ export declare const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
13
+ export interface EnsurePrettierIgnoreResult {
14
+ readonly changed: boolean;
15
+ }
16
+ /**
17
+ * Ensure the consumer `.prettierignore` excludes managed `.deft/core/` from
18
+ * Prettier. Idempotent; preserves consumer preamble.
19
+ */
20
+ export declare function ensurePrettierIgnoreLines(projectDir: string, io: InitDepositIo): EnsurePrettierIgnoreResult;
21
+ //# sourceMappingURL=prettierignore.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Consumer `.prettierignore` upkeep for install and npm-managed update (#2534).
3
+ *
4
+ * Managed `.deft/core/` is outside the consumer Prettier format gate. Official
5
+ * install and `directive update` heal the ignore entry idempotently without
6
+ * reformatting the vendored deposit.
7
+ *
8
+ * Refs #2534, #670.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { assertProjectionContained } from "../fs/projection-containment.js";
13
+ import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
14
+ /** Directory ignore entry for the hybrid deposit. */
15
+ export const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
16
+ /** Alternate spellings that already cover the deposit ignore entry. */
17
+ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core", ".deft/core/**"]);
18
+ const DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER = "# Deft framework: the vendored payload is outside the consumer Prettier gate (#2534).\n";
19
+ function projectionTarget(projectDir) {
20
+ const target = join(projectDir, ".prettierignore");
21
+ assertProjectionContained(projectDir, target);
22
+ return target;
23
+ }
24
+ function prettierIgnoreCoversLine(present, line) {
25
+ if (present.has(line))
26
+ return true;
27
+ if (line === PRETTIERIGNORE_DEFT_CORE_LINE) {
28
+ return [...DEFT_CORE_COVERING_LINES].some((candidate) => present.has(candidate));
29
+ }
30
+ return false;
31
+ }
32
+ function collectPresentPrettierIgnoreLines(existing) {
33
+ const present = new Set();
34
+ for (const raw of existing.split("\n")) {
35
+ const stripped = stripGitignoreInlineComment(raw);
36
+ if (stripped)
37
+ present.add(stripped);
38
+ }
39
+ return present;
40
+ }
41
+ /**
42
+ * Ensure the consumer `.prettierignore` excludes managed `.deft/core/` from
43
+ * Prettier. Idempotent; preserves consumer preamble.
44
+ */
45
+ export function ensurePrettierIgnoreLines(projectDir, io) {
46
+ const path = projectionTarget(projectDir);
47
+ let existing = "";
48
+ if (existsSync(path)) {
49
+ try {
50
+ existing = readFileSync(path, { encoding: "utf8" });
51
+ }
52
+ catch (cause) {
53
+ throw new Error(`could not read .prettierignore: ${String(cause)}`);
54
+ }
55
+ }
56
+ const present = collectPresentPrettierIgnoreLines(existing);
57
+ const targetLines = [PRETTIERIGNORE_DEFT_CORE_LINE];
58
+ const additions = targetLines.filter((line) => !prettierIgnoreCoversLine(present, line));
59
+ if (additions.length === 0) {
60
+ io.printf(".prettierignore already excludes the managed deft framework deposit — skipping.\n");
61
+ return { changed: false };
62
+ }
63
+ let body = existing;
64
+ if (body !== "" && !body.endsWith("\n")) {
65
+ body += "\n";
66
+ }
67
+ if (body !== "" && !body.endsWith("\n\n")) {
68
+ body += "\n";
69
+ }
70
+ body += DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER;
71
+ for (const add of additions) {
72
+ body += `${add}\n`;
73
+ }
74
+ try {
75
+ writeFileSync(path, body, { encoding: "utf8", mode: 0o644 });
76
+ }
77
+ catch (cause) {
78
+ throw new Error(`could not write .prettierignore: ${String(cause)}`);
79
+ }
80
+ io.printf(`.prettierignore updated with Deft framework exclusions: ${additions.join(", ")}\n`);
81
+ return { changed: true };
82
+ }
83
+ //# sourceMappingURL=prettierignore.js.map
@@ -12,7 +12,7 @@ import { type ClassifySeams, type EngineInstallRunner, type EngineResolution, ty
12
12
  import { type GitLsFiles } from "./gitignore.js";
13
13
  import { type InitDepositArgs } from "./init-deposit.js";
14
14
  import { type LegacyLayoutDetection } from "./legacy-detect.js";
15
- import { type InitDepositIo } from "./scaffold.js";
15
+ import { type GitHooksSeams, type InitDepositIo } from "./scaffold.js";
16
16
  export interface RefreshDepositArgs extends InitDepositArgs {
17
17
  readonly upgrade: boolean;
18
18
  }
@@ -45,6 +45,8 @@ export interface RefreshDepositSeams {
45
45
  * upkeep so the refresh never invokes a destructive `git rm --cached` path.
46
46
  */
47
47
  gitLsFiles?: GitLsFiles;
48
+ /** #2530: injected git config seams for {@link writeConsumerGitHooks}. */
49
+ gitHooks?: GitHooksSeams;
48
50
  }
49
51
  /**
50
52
  * The four states `directive update` classifies an EXISTING install into BEFORE