@deftai/directive-core 0.92.0 → 0.94.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 (111) hide show
  1. package/dist/cache/fetch.d.ts +29 -0
  2. package/dist/cache/fetch.js +131 -4
  3. package/dist/cache/operations.d.ts +8 -0
  4. package/dist/cache/operations.js +67 -4
  5. package/dist/check/cached-orchestrator.js +11 -0
  6. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  7. package/dist/check/consumer-gate-integrity.js +265 -0
  8. package/dist/check/index.d.ts +1 -0
  9. package/dist/check/index.js +1 -0
  10. package/dist/check/orchestrator.js +10 -0
  11. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  12. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  13. package/dist/doctor/index.d.ts +1 -0
  14. package/dist/doctor/index.js +1 -0
  15. package/dist/doctor/main.js +69 -0
  16. package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
  17. package/dist/doctor/openclaw-l2-adapter.js +199 -0
  18. package/dist/finish-loop/pr-finish-loop.js +1 -0
  19. package/dist/hooks/dispatcher.d.ts +24 -2
  20. package/dist/hooks/dispatcher.js +122 -11
  21. package/dist/hooks/readonly.d.ts +14 -0
  22. package/dist/hooks/readonly.js +126 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/init-deposit/hygiene.js +22 -0
  26. package/dist/init-deposit/index.d.ts +3 -0
  27. package/dist/init-deposit/index.js +3 -0
  28. package/dist/init-deposit/init-deposit.js +10 -0
  29. package/dist/init-deposit/refresh.js +11 -0
  30. package/dist/init-deposit/scaffold.js +3 -133
  31. package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
  32. package/dist/init-deposit/skill-discovery-deposit.js +287 -0
  33. package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
  34. package/dist/init-deposit/skill-discovery-hosts.js +217 -0
  35. package/dist/init-deposit/slash-deposit.d.ts +46 -0
  36. package/dist/init-deposit/slash-deposit.js +174 -0
  37. package/dist/intake/issue-ingest.d.ts +20 -0
  38. package/dist/intake/issue-ingest.js +58 -0
  39. package/dist/policy/host-slash-commands.d.ts +28 -0
  40. package/dist/policy/host-slash-commands.js +103 -0
  41. package/dist/policy/index.d.ts +2 -0
  42. package/dist/policy/index.js +61 -7
  43. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  44. package/dist/policy/min-greptile-confidence.js +123 -0
  45. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  46. package/dist/pr-merge-readiness/compute.js +17 -4
  47. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  48. package/dist/pr-merge-readiness/evaluate.js +7 -4
  49. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  50. package/dist/pr-merge-readiness/mergeability.js +14 -2
  51. package/dist/pr-monitor/main.d.ts +2 -0
  52. package/dist/pr-monitor/main.js +31 -1
  53. package/dist/pr-monitor/monitor.js +5 -1
  54. package/dist/pr-monitor/readiness.js +3 -1
  55. package/dist/pr-monitor/types.d.ts +8 -0
  56. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  57. package/dist/pr-wait-mergeable/cascade.js +28 -1
  58. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  59. package/dist/pr-wait-mergeable/main.js +16 -0
  60. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  61. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  62. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  63. package/dist/pr-watch/constants.d.ts +4 -1
  64. package/dist/pr-watch/constants.js +5 -2
  65. package/dist/pr-watch/main.js +1 -0
  66. package/dist/pr-watch/probe.d.ts +1 -1
  67. package/dist/pr-watch/probe.js +4 -1
  68. package/dist/pr-watch/types.d.ts +7 -1
  69. package/dist/pr-watch/watch.js +3 -2
  70. package/dist/review-monitor/index.d.ts +1 -0
  71. package/dist/review-monitor/index.js +1 -0
  72. package/dist/review-monitor/l4-owner.d.ts +46 -0
  73. package/dist/review-monitor/l4-owner.js +262 -0
  74. package/dist/slash/emitters.d.ts +102 -0
  75. package/dist/slash/emitters.js +148 -0
  76. package/dist/slash/generator.d.ts +98 -0
  77. package/dist/slash/generator.js +145 -0
  78. package/dist/slash/index.d.ts +16 -0
  79. package/dist/slash/index.js +16 -0
  80. package/dist/slash/openclaw-adapter.d.ts +64 -0
  81. package/dist/slash/openclaw-adapter.js +198 -0
  82. package/dist/slash/openclaw-deposit.d.ts +73 -0
  83. package/dist/slash/openclaw-deposit.js +279 -0
  84. package/dist/slash/openclaw-slugs.d.ts +52 -0
  85. package/dist/slash/openclaw-slugs.js +126 -0
  86. package/dist/slash/product-set.d.ts +50 -0
  87. package/dist/slash/product-set.js +142 -0
  88. package/dist/tool-events/classify.js +1 -0
  89. package/dist/triage/welcome/writers.js +16 -7
  90. package/dist/umbrella-current-shape/index.d.ts +45 -0
  91. package/dist/umbrella-current-shape/index.js +162 -8
  92. package/dist/vbrief-reconcile/index.d.ts +2 -2
  93. package/dist/vbrief-reconcile/index.js +1 -1
  94. package/dist/vbrief-reconcile/types.d.ts +14 -0
  95. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  96. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  97. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  98. package/dist/vbrief-validate/plan-hooks.js +50 -0
  99. package/dist/xbrief/create.d.ts +36 -0
  100. package/dist/xbrief/create.js +285 -0
  101. package/dist/xbrief/index.d.ts +14 -0
  102. package/dist/xbrief/index.js +42 -0
  103. package/dist/xbrief/paths.d.ts +37 -0
  104. package/dist/xbrief/paths.js +123 -0
  105. package/dist/xbrief/styles.d.ts +36 -0
  106. package/dist/xbrief/styles.js +235 -0
  107. package/dist/xbrief/types.d.ts +50 -0
  108. package/dist/xbrief/types.js +17 -0
  109. package/dist/xbrief/verify.d.ts +30 -0
  110. package/dist/xbrief/verify.js +251 -0
  111. package/package.json +11 -3
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Owner Continuity / L4 owner gate (#3090).
3
+ *
4
+ * Exit 0 only when a fresh sticky review-owner lease exists on the PR,
5
+ * or the caller asserts `review_cycle: done` after Step 6.
6
+ * Freeform started/pending/initiated values are rejected.
7
+ * skipped / n/a / parent-retained are process evidence only — they do NOT
8
+ * satisfy this machine gate when --pr is required (lease-or-done).
9
+ */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { resolveRepo } from "../triage/queue/repo.js";
13
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY } from "./constants.js";
14
+ import { fetchActiveMonitorFromGithub } from "./record.js";
15
+ export const L4_OWNER_HELP = "usage: task verify:l4-owner -- --pr <N> [options]\n" +
16
+ "\n" +
17
+ "Owner Continuity / L4 owner gate (#3090): after drive-to:merge-ready /\n" +
18
+ "babysit / shepherd claims, exit 0 only when a sticky GitHub review-owner\n" +
19
+ "lease is fresh on the PR, or the caller asserts --review-cycle done\n" +
20
+ "(Step 6 fail-closed all-of on HEAD). Freeform started/pending/initiated\n" +
21
+ "are rejected. skipped / n/a / parent-retained do NOT satisfy this machine\n" +
22
+ "gate (process A/B/C still applies). Silent hold exits 1.\n" +
23
+ "\n" +
24
+ "options:\n" +
25
+ " -h, --help Show this help and exit 0\n" +
26
+ " --pr N Pull request number (required unless --help)\n" +
27
+ " --repo OWNER/REPO Repository (optional; inferred from origin)\n" +
28
+ " --head-sha SHA Expected HEAD SHA (optional freshness check)\n" +
29
+ " --project-root PATH Project root (default: cwd)\n" +
30
+ " --review-cycle VALUE done | in_progress:<pr>#<ref> | skipped:<reason> | n/a\n" +
31
+ " --json Emit structured JSON on stdout\n" +
32
+ "\n" +
33
+ "exit codes:\n" +
34
+ " 0 READY Fresh sticky lease, or --review-cycle done\n" +
35
+ " 1 NOT READY Silent hold / illegal evidence / lease missing\n" +
36
+ " 2 CONFIG Usage / path / GitHub fetch error\n";
37
+ const FORBIDDEN_FREEFORM = new Set(["started", "pending", "initiated", "start", "in_progress"]);
38
+ export function parseReviewCycleEvidence(raw) {
39
+ if (raw === null || raw === undefined || raw.trim() === "") {
40
+ return { ok: true, value: null };
41
+ }
42
+ const value = raw.trim();
43
+ const lower = value.toLowerCase();
44
+ if (FORBIDDEN_FREEFORM.has(lower)) {
45
+ return {
46
+ ok: false,
47
+ reason: `illegal freeform review_cycle value '${value}' (#3090); ` +
48
+ "use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
49
+ };
50
+ }
51
+ if (value === "done" || value === "n/a") {
52
+ return { ok: true, value };
53
+ }
54
+ if (value.startsWith("in_progress:")) {
55
+ const rest = value.slice("in_progress:".length);
56
+ if (!rest.includes("#") || rest.endsWith("#") || rest.startsWith("#")) {
57
+ return {
58
+ ok: false,
59
+ reason: "in_progress requires form in_progress:<pr>#<monitor_or_lease_ref> (#3090)",
60
+ };
61
+ }
62
+ const prPart = rest.slice(0, rest.indexOf("#"));
63
+ if (!/^\d+$/.test(prPart) || Number(prPart) <= 0) {
64
+ return {
65
+ ok: false,
66
+ reason: "in_progress:<pr> must be a positive integer PR number (#3090)",
67
+ };
68
+ }
69
+ return { ok: true, value: value };
70
+ }
71
+ if (value.startsWith("skipped:")) {
72
+ const rest = value.slice("skipped:".length).trim();
73
+ if (rest.length === 0) {
74
+ return { ok: false, reason: "skipped requires a non-empty reason (#3090)" };
75
+ }
76
+ return { ok: true, value: value };
77
+ }
78
+ return {
79
+ ok: false,
80
+ reason: `unknown review_cycle value '${value}' (#3090); ` +
81
+ "use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
82
+ };
83
+ }
84
+ /** Parse in_progress:<pr>#<ref> into parts; null if not that form. */
85
+ export function parseInProgressEvidence(value) {
86
+ if (!value.startsWith("in_progress:")) {
87
+ return null;
88
+ }
89
+ const rest = value.slice("in_progress:".length);
90
+ const hash = rest.indexOf("#");
91
+ if (hash <= 0) {
92
+ return null;
93
+ }
94
+ const prPart = rest.slice(0, hash);
95
+ const ref = rest.slice(hash + 1);
96
+ if (!/^\d+$/.test(prPart) || ref.length === 0) {
97
+ return null;
98
+ }
99
+ return { pr: Number(prPart), ref };
100
+ }
101
+ function oneLine(value) {
102
+ return value.replace(/\r?\n/g, " ");
103
+ }
104
+ export function evaluateL4OwnerGate(args) {
105
+ const projectRoot = resolve(args.projectRoot);
106
+ let isDir = false;
107
+ try {
108
+ isDir = existsSync(projectRoot) && statSync(projectRoot).isDirectory();
109
+ }
110
+ catch {
111
+ isDir = false;
112
+ }
113
+ if (!isDir) {
114
+ return {
115
+ exitCode: EXIT_CONFIG_ERROR,
116
+ message: `verify_l4_owner: --project-root is not a directory: ${oneLine(projectRoot)}`,
117
+ monitorRecord: null,
118
+ reviewCycle: null,
119
+ path: "config",
120
+ };
121
+ }
122
+ const parsed = parseReviewCycleEvidence(args.reviewCycle);
123
+ if (!parsed.ok) {
124
+ return {
125
+ exitCode: EXIT_NOT_READY,
126
+ message: `verify_l4_owner: ${oneLine(parsed.reason)}`,
127
+ monitorRecord: null,
128
+ reviewCycle: null,
129
+ path: "illegal",
130
+ };
131
+ }
132
+ // Machine gate is lease-or-done only (#3090). skipped/n/a are handoff enum values
133
+ // but must not green-light ownership of an open PR without lease or Step 6 done.
134
+ if (parsed.value === "n/a" || parsed.value?.startsWith("skipped:")) {
135
+ return {
136
+ exitCode: EXIT_NOT_READY,
137
+ message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} does not satisfy the machine ` +
138
+ `gate for PR #${args.pr} (#3090). Exit 0 requires a fresh sticky lease or --review-cycle done.`,
139
+ monitorRecord: null,
140
+ reviewCycle: parsed.value,
141
+ path: "none",
142
+ };
143
+ }
144
+ if (parsed.value === "done") {
145
+ return {
146
+ exitCode: EXIT_READY,
147
+ message: `verify_l4_owner: review_cycle=done asserted for PR #${args.pr} (#3090). ` +
148
+ "Caller MUST have satisfied Step 6 fail-closed all-of on HEAD before this claim.",
149
+ monitorRecord: null,
150
+ reviewCycle: "done",
151
+ path: "done",
152
+ };
153
+ }
154
+ // Validate in_progress PR binding when evidence is present (before lease fetch).
155
+ if (parsed.value?.startsWith("in_progress:")) {
156
+ const parts = parseInProgressEvidence(parsed.value);
157
+ if (parts === null) {
158
+ return {
159
+ exitCode: EXIT_NOT_READY,
160
+ message: `verify_l4_owner: malformed in_progress evidence ${oneLine(parsed.value)} (#3090).`,
161
+ monitorRecord: null,
162
+ reviewCycle: parsed.value,
163
+ path: "illegal",
164
+ };
165
+ }
166
+ if (parts.pr !== args.pr) {
167
+ return {
168
+ exitCode: EXIT_NOT_READY,
169
+ message: `verify_l4_owner: review_cycle PR #${parts.pr} does not match --pr ${args.pr} (#3090). ` +
170
+ "Ownership evidence must bind to the target PR.",
171
+ monitorRecord: null,
172
+ reviewCycle: parsed.value,
173
+ path: "none",
174
+ };
175
+ }
176
+ }
177
+ const repo = resolveRepo(args.repo ?? null, projectRoot);
178
+ if (repo === null) {
179
+ return {
180
+ exitCode: EXIT_CONFIG_ERROR,
181
+ message: "verify_l4_owner: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
182
+ monitorRecord: null,
183
+ reviewCycle: parsed.value,
184
+ path: "config",
185
+ };
186
+ }
187
+ const now = args.now ?? new Date();
188
+ const githubMonitor = fetchActiveMonitorFromGithub(repo, args.pr, {
189
+ now,
190
+ headSha: args.headSha ?? null,
191
+ seams: args.seams,
192
+ });
193
+ if (githubMonitor !== null && typeof githubMonitor === "object" && "error" in githubMonitor) {
194
+ return {
195
+ exitCode: EXIT_CONFIG_ERROR,
196
+ message: `verify_l4_owner: ${oneLine(githubMonitor.error)}`,
197
+ monitorRecord: null,
198
+ reviewCycle: parsed.value,
199
+ path: "config",
200
+ };
201
+ }
202
+ if (githubMonitor !== null) {
203
+ const ref = parsed.value ??
204
+ `in_progress:${args.pr}#${githubMonitor.monitor_agent_id}`;
205
+ return {
206
+ exitCode: EXIT_READY,
207
+ message: `verify_l4_owner: active GitHub review-owner lease for PR #${args.pr} ` +
208
+ `(monitor_agent_id=${oneLine(githubMonitor.monitor_agent_id)}, owner=${oneLine(githubMonitor.owner)}) ` +
209
+ "— review_cycle path=lease (#3090).",
210
+ monitorRecord: githubMonitor,
211
+ reviewCycle: ref,
212
+ path: "lease",
213
+ };
214
+ }
215
+ // parent-retained is process path B — machine gate cannot verify it (lease-or-done only).
216
+ if (parsed.value?.startsWith("in_progress:")) {
217
+ const parts = parseInProgressEvidence(parsed.value);
218
+ if (parts?.ref === "parent-retained") {
219
+ return {
220
+ exitCode: EXIT_NOT_READY,
221
+ message: `verify_l4_owner: parent-retained for PR #${args.pr} is process path B only (#3090). ` +
222
+ "Machine gate requires a sticky <!-- deft:review-owner --> lease or --review-cycle done. " +
223
+ "Keep parent dual-source poll/fix; do not emit L4 status:pass as terminal.",
224
+ monitorRecord: null,
225
+ reviewCycle: parsed.value,
226
+ path: "none",
227
+ };
228
+ }
229
+ return {
230
+ exitCode: EXIT_NOT_READY,
231
+ message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} claims in_progress but no sticky ` +
232
+ `<!-- deft:review-owner --> lease found for PR #${args.pr} (#3090 / #2797).\n` +
233
+ " Register: deft review-monitor:register --pr <N> --monitor-agent-id <id> --platform-primitive …",
234
+ monitorRecord: null,
235
+ reviewCycle: parsed.value,
236
+ path: "none",
237
+ };
238
+ }
239
+ return {
240
+ exitCode: EXIT_NOT_READY,
241
+ message: `verify_l4_owner: silent hold on PR #${args.pr} — no fresh sticky lease and no ` +
242
+ "review_cycle=done (#3090 Owner Continuity Gate).\n" +
243
+ " Same turn MUST end in A (monitor+lease), B (parent-retained + next dual-source action),\n" +
244
+ " or C (explicit BLOCKED/FAILED finish). Check-run SUCCESS alone is not CLEAN.",
245
+ monitorRecord: null,
246
+ reviewCycle: null,
247
+ path: "none",
248
+ };
249
+ }
250
+ export function l4OwnerResultToJson(result) {
251
+ return {
252
+ exit_code: result.exitCode,
253
+ message: result.message,
254
+ monitor_agent_id: result.monitorRecord?.monitor_agent_id ?? null,
255
+ monitor_owner: result.monitorRecord?.owner ?? null,
256
+ monitor_record: result.monitorRecord,
257
+ path: result.path,
258
+ ready: result.exitCode === EXIT_READY,
259
+ review_cycle: result.reviewCycle,
260
+ };
261
+ }
262
+ //# sourceMappingURL=l4-owner.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Per-host native command/prompt emitters (#3053 / epic #55).
3
+ *
4
+ * Maps shared thin-wrapper IR from {@link generateThinWrappers} onto each
5
+ * supported host’s repo-relative path layout. Pure generate-to-path records;
6
+ * filesystem deposit is init-deposit `writeSlashCommandDeposit` (#3054).
7
+ *
8
+ * ## Host id → output directory / file pattern
9
+ *
10
+ * | Host id | Relative directory | Filename pattern |
11
+ * |---------|----------------------|--------------------|
12
+ * | claude | `.claude/commands/` | `{hyphen-stem}.md` |
13
+ * | cursor | `.cursor/commands/` | `{hyphen-stem}.md` |
14
+ * | grok | `.grok/commands/` | `{hyphen-stem}.md` |
15
+ * | codex | `.codex/prompts/` | `{hyphen-stem}.md` |
16
+ *
17
+ * All four use the shared thin-wrapper markdown template (L5). Adding a host is
18
+ * an additive layout entry + optional frontmatter reshape — not a new product
19
+ * name table (L2 lives in product-set / generator only).
20
+ *
21
+ * ⊗ Empty stub dirs for “enabled but no emitter” (L6).
22
+ * ⊗ Native legacy alias files (L3).
23
+ */
24
+ import { type ThinWrapperIR } from "./generator.js";
25
+ /**
26
+ * Hosts with a real slash/prompt emitter layout (aligned with hook host ids).
27
+ * Default enabled set for deposit (#3054) is this list when policy opts in.
28
+ */
29
+ export declare const SLASH_EMITTER_HOSTS: readonly ["claude", "cursor", "grok", "codex"];
30
+ export type SlashEmitterHostId = (typeof SLASH_EMITTER_HOSTS)[number];
31
+ /** Repo-relative layout for one host’s native command/prompt files. */
32
+ export interface HostCommandLayout {
33
+ readonly hostId: SlashEmitterHostId;
34
+ /**
35
+ * Repo-relative directory (posix, no trailing slash).
36
+ * Example: `.claude/commands`
37
+ */
38
+ readonly relativeDir: string;
39
+ /** Human-readable file pattern (always `{stem}.md` for v1). */
40
+ readonly filePattern: string;
41
+ /**
42
+ * Kind of native surface the host loads (documentation + deposit policy).
43
+ * Does not change file contents in v1 — all emit shared thin markdown.
44
+ */
45
+ readonly surfaceKind: "commands" | "prompts";
46
+ }
47
+ /**
48
+ * Documented host id → directory / file pattern mapping (issue #3053 AC).
49
+ * Frozen; additive registration only.
50
+ */
51
+ export declare const HOST_COMMAND_LAYOUTS: Readonly<Record<SlashEmitterHostId, HostCommandLayout>>;
52
+ /** One host-native file ready for deposit (#3054) or tests. */
53
+ export interface HostEmittedFile {
54
+ readonly hostId: SlashEmitterHostId;
55
+ /** Canonical slash id from the product set. */
56
+ readonly logicalId: string;
57
+ /** Hyphen filename stem (L4). */
58
+ readonly filenameStem: string;
59
+ /** Basename including `.md`. */
60
+ readonly filename: string;
61
+ /** Repo-relative posix path, e.g. `.claude/commands/deft-continue.md`. */
62
+ readonly relativePath: string;
63
+ /** Thin-wrapper file contents (frontmatter + body). */
64
+ readonly contents: string;
65
+ readonly description: string;
66
+ readonly dispatchPath: string;
67
+ }
68
+ /** Type guard for {@link SlashEmitterHostId}. */
69
+ export declare function isSlashEmitterHostId(value: string): value is SlashEmitterHostId;
70
+ /** Stable list of hosts that have real emitters (no stubs). */
71
+ export declare function listSlashEmitterHosts(): readonly SlashEmitterHostId[];
72
+ /** Look up the documented layout for a host, or throw. */
73
+ export declare function getHostCommandLayout(hostId: SlashEmitterHostId): HostCommandLayout;
74
+ /**
75
+ * Build the repo-relative path for one IR entry under a host layout (L4 filenames).
76
+ */
77
+ export declare function hostRelativePath(hostId: SlashEmitterHostId, filename: string): string;
78
+ /**
79
+ * Emit host-native file records for one host from shared thin-wrapper IR.
80
+ *
81
+ * Does not redefine the L2 product table — defaults to {@link generateThinWrappers}.
82
+ * Contents stay thin (L5); paths use L4 hyphen names under the host layout.
83
+ */
84
+ export declare function emitHostCommandFiles(hostId: SlashEmitterHostId, wrappers?: readonly ThinWrapperIR[]): readonly HostEmittedFile[];
85
+ /**
86
+ * Emit for every host that has a real emitter (default: all of {@link SLASH_EMITTER_HOSTS}).
87
+ *
88
+ * Returns a map keyed by host id; each value has count === product set when using
89
+ * default IR.
90
+ */
91
+ export declare function emitAllHostCommandFiles(hosts?: readonly SlashEmitterHostId[], wrappers?: readonly ThinWrapperIR[]): ReadonlyMap<SlashEmitterHostId, readonly HostEmittedFile[]>;
92
+ /**
93
+ * Host-specific markdown reshape hook.
94
+ *
95
+ * v1: all hosts share the generator’s host-agnostic `fileMarkdown` (description +
96
+ * optional argument-hint + thin body). Future hosts may remap frontmatter keys
97
+ * here without touching the product name table.
98
+ */
99
+ export declare function renderHostFileContents(_hostId: SlashEmitterHostId, wrapper: ThinWrapperIR): string;
100
+ /** Assert emitted contents remain thin pointers (for tests and deposit validation). */
101
+ export declare function assertThinHostEmission(files: readonly HostEmittedFile[]): void;
102
+ //# sourceMappingURL=emitters.d.ts.map
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Per-host native command/prompt emitters (#3053 / epic #55).
3
+ *
4
+ * Maps shared thin-wrapper IR from {@link generateThinWrappers} onto each
5
+ * supported host’s repo-relative path layout. Pure generate-to-path records;
6
+ * filesystem deposit is init-deposit `writeSlashCommandDeposit` (#3054).
7
+ *
8
+ * ## Host id → output directory / file pattern
9
+ *
10
+ * | Host id | Relative directory | Filename pattern |
11
+ * |---------|----------------------|--------------------|
12
+ * | claude | `.claude/commands/` | `{hyphen-stem}.md` |
13
+ * | cursor | `.cursor/commands/` | `{hyphen-stem}.md` |
14
+ * | grok | `.grok/commands/` | `{hyphen-stem}.md` |
15
+ * | codex | `.codex/prompts/` | `{hyphen-stem}.md` |
16
+ *
17
+ * All four use the shared thin-wrapper markdown template (L5). Adding a host is
18
+ * an additive layout entry + optional frontmatter reshape — not a new product
19
+ * name table (L2 lives in product-set / generator only).
20
+ *
21
+ * ⊗ Empty stub dirs for “enabled but no emitter” (L6).
22
+ * ⊗ Native legacy alias files (L3).
23
+ */
24
+ import { generateThinWrappers, isThinWrapperMarkdown } from "./generator.js";
25
+ import { PRODUCT_COMMAND_COUNT } from "./product-set.js";
26
+ /**
27
+ * Hosts with a real slash/prompt emitter layout (aligned with hook host ids).
28
+ * Default enabled set for deposit (#3054) is this list when policy opts in.
29
+ */
30
+ export const SLASH_EMITTER_HOSTS = ["claude", "cursor", "grok", "codex"];
31
+ /**
32
+ * Documented host id → directory / file pattern mapping (issue #3053 AC).
33
+ * Frozen; additive registration only.
34
+ */
35
+ export const HOST_COMMAND_LAYOUTS = Object.freeze({
36
+ claude: Object.freeze({
37
+ hostId: "claude",
38
+ relativeDir: ".claude/commands",
39
+ filePattern: "{stem}.md",
40
+ surfaceKind: "commands",
41
+ }),
42
+ cursor: Object.freeze({
43
+ hostId: "cursor",
44
+ relativeDir: ".cursor/commands",
45
+ filePattern: "{stem}.md",
46
+ surfaceKind: "commands",
47
+ }),
48
+ grok: Object.freeze({
49
+ hostId: "grok",
50
+ relativeDir: ".grok/commands",
51
+ filePattern: "{stem}.md",
52
+ surfaceKind: "commands",
53
+ }),
54
+ codex: Object.freeze({
55
+ hostId: "codex",
56
+ relativeDir: ".codex/prompts",
57
+ filePattern: "{stem}.md",
58
+ surfaceKind: "prompts",
59
+ }),
60
+ });
61
+ /** Type guard for {@link SlashEmitterHostId}. */
62
+ export function isSlashEmitterHostId(value) {
63
+ return SLASH_EMITTER_HOSTS.includes(value);
64
+ }
65
+ /** Stable list of hosts that have real emitters (no stubs). */
66
+ export function listSlashEmitterHosts() {
67
+ return SLASH_EMITTER_HOSTS;
68
+ }
69
+ /** Look up the documented layout for a host, or throw. */
70
+ export function getHostCommandLayout(hostId) {
71
+ const layout = HOST_COMMAND_LAYOUTS[hostId];
72
+ if (layout === undefined) {
73
+ throw new Error(`No slash emitter layout for host: ${hostId}`);
74
+ }
75
+ return layout;
76
+ }
77
+ /**
78
+ * Build the repo-relative path for one IR entry under a host layout (L4 filenames).
79
+ */
80
+ export function hostRelativePath(hostId, filename) {
81
+ const layout = getHostCommandLayout(hostId);
82
+ // Defensive: refuse path separators in filename so deposit cannot escape relativeDir.
83
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
84
+ throw new Error(`Invalid command filename for host emit: ${filename}`);
85
+ }
86
+ return `${layout.relativeDir}/${filename}`;
87
+ }
88
+ /**
89
+ * Emit host-native file records for one host from shared thin-wrapper IR.
90
+ *
91
+ * Does not redefine the L2 product table — defaults to {@link generateThinWrappers}.
92
+ * Contents stay thin (L5); paths use L4 hyphen names under the host layout.
93
+ */
94
+ export function emitHostCommandFiles(hostId, wrappers = generateThinWrappers()) {
95
+ if (!isSlashEmitterHostId(hostId)) {
96
+ throw new Error(`Unknown slash emitter host: ${String(hostId)}`);
97
+ }
98
+ // Snapshot layout once; additive hosts must register before emit.
99
+ getHostCommandLayout(hostId);
100
+ return wrappers.map((w) => {
101
+ const contents = renderHostFileContents(hostId, w);
102
+ return {
103
+ hostId,
104
+ logicalId: w.logicalId,
105
+ filenameStem: w.filenameStem,
106
+ filename: w.filename,
107
+ relativePath: hostRelativePath(hostId, w.filename),
108
+ contents,
109
+ description: w.description,
110
+ dispatchPath: w.dispatchPath,
111
+ };
112
+ });
113
+ }
114
+ /**
115
+ * Emit for every host that has a real emitter (default: all of {@link SLASH_EMITTER_HOSTS}).
116
+ *
117
+ * Returns a map keyed by host id; each value has count === product set when using
118
+ * default IR.
119
+ */
120
+ export function emitAllHostCommandFiles(hosts = SLASH_EMITTER_HOSTS, wrappers = generateThinWrappers()) {
121
+ const out = new Map();
122
+ for (const hostId of hosts) {
123
+ out.set(hostId, emitHostCommandFiles(hostId, wrappers));
124
+ }
125
+ return out;
126
+ }
127
+ /**
128
+ * Host-specific markdown reshape hook.
129
+ *
130
+ * v1: all hosts share the generator’s host-agnostic `fileMarkdown` (description +
131
+ * optional argument-hint + thin body). Future hosts may remap frontmatter keys
132
+ * here without touching the product name table.
133
+ */
134
+ export function renderHostFileContents(_hostId, wrapper) {
135
+ return wrapper.fileMarkdown;
136
+ }
137
+ /** Assert emitted contents remain thin pointers (for tests and deposit validation). */
138
+ export function assertThinHostEmission(files) {
139
+ if (files.length !== PRODUCT_COMMAND_COUNT) {
140
+ throw new Error(`Expected ${PRODUCT_COMMAND_COUNT} host command files, got ${files.length}`);
141
+ }
142
+ for (const f of files) {
143
+ if (!isThinWrapperMarkdown(f.contents, f.dispatchPath)) {
144
+ throw new Error(`Non-thin emission for ${f.relativePath}`);
145
+ }
146
+ }
147
+ }
148
+ //# sourceMappingURL=emitters.js.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Host-agnostic thin-wrapper generator for product slash commands (#3052 / epic #55).
3
+ *
4
+ * Emits IR + markdown templates that per-host emitters (#3053) format into native
5
+ * command/prompt/workflow files. Wrappers stay thin (L5): frontmatter description +
6
+ * short dispatch pointer — never inlined strategy/skill/commands.md bodies.
7
+ *
8
+ * ## Token / context budgets (from #55 token design rules)
9
+ *
10
+ * | When | Target |
11
+ * |---|---|
12
+ * | Idle (user never invokes `/deft…`) | ~0 from command files |
13
+ * | Catalog (`/` menu: name + description × N) | ≤ ~1k tok for the product set |
14
+ * | Single invoke | ~40–100 tok thin wrapper body |
15
+ * | After dispatch | cost of the target strategy/skill (unchanged) |
16
+ *
17
+ * Multi-host deposit does not multiply tokens in one session: each host reads only
18
+ * its own command directory. Real spend is the loaded artifact after invoke.
19
+ */
20
+ import { logicalIdToFilename, logicalIdToFilenameStem, PRODUCT_COMMAND_COUNT, type ProductCommand } from "./product-set.js";
21
+ /** Rough UTF-8 bytes-per-token estimate (aligned with agents-md-budget). */
22
+ export declare const BYTES_PER_TOKEN_ESTIMATE = 4;
23
+ /** L5: invoke wrapper body budget (≈100 tokens). */
24
+ export declare const MAX_WRAPPER_BODY_TOKENS = 100;
25
+ /** Per-command catalog description hard cap for tests (order-of-magnitude 20–50). */
26
+ export declare const MAX_DESCRIPTION_TOKENS = 50;
27
+ /** Full product-set description catalog budget (≤ ~1k tok). */
28
+ export declare const MAX_CATALOG_TOKENS = 1000;
29
+ /**
30
+ * Host-agnostic intermediate representation for one thin wrapper.
31
+ *
32
+ * Emitters consume this shape without re-listing product names.
33
+ */
34
+ export interface ThinWrapperIR {
35
+ /** Canonical slash id, e.g. `/deft:directive:run:interview`. */
36
+ readonly logicalId: string;
37
+ /** Hyphen stem without extension. */
38
+ readonly filenameStem: string;
39
+ /** On-disk filename including `.md`. */
40
+ readonly filename: string;
41
+ /** Catalog description (host frontmatter `description`). */
42
+ readonly description: string;
43
+ readonly dispatchKind: ProductCommand["dispatchKind"];
44
+ /** Content-root-relative primary load path. */
45
+ readonly dispatchPath: string;
46
+ readonly argumentHint?: string;
47
+ /** Body markdown only (no frontmatter). */
48
+ readonly bodyMarkdown: string;
49
+ /** Full file: YAML frontmatter + body (host-agnostic template). */
50
+ readonly fileMarkdown: string;
51
+ /** Estimated body tokens (UTF-8 bytes / {@link BYTES_PER_TOKEN_ESTIMATE}). */
52
+ readonly estimatedBodyTokens: number;
53
+ /** Estimated description tokens. */
54
+ readonly estimatedDescriptionTokens: number;
55
+ }
56
+ /** Aggregate token budget report for the product set. */
57
+ export interface TokenBudgetReport {
58
+ readonly commandCount: number;
59
+ readonly catalogTokens: number;
60
+ readonly maxBodyTokens: number;
61
+ readonly maxDescriptionTokens: number;
62
+ readonly withinBodyBudget: boolean;
63
+ readonly withinDescriptionBudget: boolean;
64
+ readonly withinCatalogBudget: boolean;
65
+ readonly ok: boolean;
66
+ }
67
+ /** Estimate tokens from a UTF-8 string (bytes / 4). */
68
+ export declare function estimateTokens(text: string): number;
69
+ /**
70
+ * Render the thin body only: short dispatch pointer, no inlined target content.
71
+ *
72
+ * Keeps invoke cost in the ~40–100 token band (L5).
73
+ */
74
+ export declare function renderThinWrapperBody(command: ProductCommand): string;
75
+ /**
76
+ * Render host-agnostic file markdown: description frontmatter + thin body.
77
+ *
78
+ * Per-host emitters may re-shape frontmatter keys; the body semantics stay shared.
79
+ */
80
+ export declare function renderThinWrapperFile(command: ProductCommand): string;
81
+ /** Build {@link ThinWrapperIR} for one product command. */
82
+ export declare function generateThinWrapper(command: ProductCommand): ThinWrapperIR;
83
+ /**
84
+ * Generate thin wrappers for the full L2 product set (stable order, count === 13).
85
+ *
86
+ * This is the primary API for #3053 emitters.
87
+ */
88
+ export declare function generateThinWrappers(commands?: readonly ProductCommand[]): readonly ThinWrapperIR[];
89
+ /** Measure catalog + per-wrapper body budgets for the generated set. */
90
+ export declare function measureTokenBudget(wrappers?: readonly ThinWrapperIR[]): TokenBudgetReport;
91
+ /**
92
+ * Structural check that a wrapper file looks like a thin pointer template.
93
+ * Used by unit tests and available to emitters for deposit validation.
94
+ */
95
+ export declare function isThinWrapperMarkdown(fileMarkdown: string, dispatchPath: string): boolean;
96
+ /** Re-export mapping helpers for emitter convenience without a second import path. */
97
+ export { logicalIdToFilename, logicalIdToFilenameStem, PRODUCT_COMMAND_COUNT };
98
+ //# sourceMappingURL=generator.d.ts.map