@deftai/directive-core 0.92.0 → 0.93.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 (52) hide show
  1. package/dist/doctor/index.d.ts +1 -0
  2. package/dist/doctor/index.js +1 -0
  3. package/dist/doctor/main.js +12 -0
  4. package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
  5. package/dist/doctor/openclaw-l2-adapter.js +199 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2 -0
  8. package/dist/init-deposit/hygiene.js +22 -0
  9. package/dist/init-deposit/index.d.ts +3 -0
  10. package/dist/init-deposit/index.js +3 -0
  11. package/dist/init-deposit/init-deposit.js +10 -0
  12. package/dist/init-deposit/refresh.js +11 -0
  13. package/dist/init-deposit/scaffold.js +3 -133
  14. package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
  15. package/dist/init-deposit/skill-discovery-deposit.js +287 -0
  16. package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
  17. package/dist/init-deposit/skill-discovery-hosts.js +217 -0
  18. package/dist/init-deposit/slash-deposit.d.ts +46 -0
  19. package/dist/init-deposit/slash-deposit.js +174 -0
  20. package/dist/policy/host-slash-commands.d.ts +28 -0
  21. package/dist/policy/host-slash-commands.js +103 -0
  22. package/dist/policy/index.d.ts +1 -0
  23. package/dist/policy/index.js +47 -7
  24. package/dist/slash/emitters.d.ts +102 -0
  25. package/dist/slash/emitters.js +148 -0
  26. package/dist/slash/generator.d.ts +98 -0
  27. package/dist/slash/generator.js +145 -0
  28. package/dist/slash/index.d.ts +16 -0
  29. package/dist/slash/index.js +16 -0
  30. package/dist/slash/openclaw-adapter.d.ts +64 -0
  31. package/dist/slash/openclaw-adapter.js +198 -0
  32. package/dist/slash/openclaw-deposit.d.ts +73 -0
  33. package/dist/slash/openclaw-deposit.js +279 -0
  34. package/dist/slash/openclaw-slugs.d.ts +52 -0
  35. package/dist/slash/openclaw-slugs.js +126 -0
  36. package/dist/slash/product-set.d.ts +50 -0
  37. package/dist/slash/product-set.js +142 -0
  38. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  39. package/dist/vbrief-validate/plan-hooks.js +50 -0
  40. package/dist/xbrief/create.d.ts +36 -0
  41. package/dist/xbrief/create.js +285 -0
  42. package/dist/xbrief/index.d.ts +14 -0
  43. package/dist/xbrief/index.js +42 -0
  44. package/dist/xbrief/paths.d.ts +37 -0
  45. package/dist/xbrief/paths.js +123 -0
  46. package/dist/xbrief/styles.d.ts +36 -0
  47. package/dist/xbrief/styles.js +235 -0
  48. package/dist/xbrief/types.d.ts +50 -0
  49. package/dist/xbrief/types.js +17 -0
  50. package/dist/xbrief/verify.d.ts +30 -0
  51. package/dist/xbrief/verify.js +251 -0
  52. package/package.json +11 -3
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Multi-host init/update deposit for native slash command files (#3054 / epic #55).
3
+ *
4
+ * Wires #3053 emitters into the init/update deposit path:
5
+ * - Default enabled set = hosts with real emitters (L6)
6
+ * - Per-host opt-out via `plan.policy.hostSlashCommands`
7
+ * - Idempotent managed rewrite (no duplicate pile-up)
8
+ * - Prefer commit of **managed product paths only** (L8) — exact allowlist, not whole dirs
9
+ *
10
+ * Parallel to agent-hooks deposit; does not touch hook JSON configs.
11
+ *
12
+ * Ownership: only create/update/remove files that are missing or still look like
13
+ * Directive thin wrappers (`isThinWrapperMarkdown`). Consumer-customized content
14
+ * at a product filename is left untouched.
15
+ */
16
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
17
+ import { basename, dirname, join, resolve } from "node:path";
18
+ import { assertDepositContained } from "../deposit/contain.js";
19
+ import { containedWrite } from "../fs/contained-write.js";
20
+ import { isHostSlashCommandDepositEnabled, loadHostSlashCommandsPolicyFromProject, } from "../policy/host-slash-commands.js";
21
+ import { emitHostCommandFiles, getHostCommandLayout, isThinWrapperMarkdown, listSlashEmitterHosts, } from "../slash/index.js";
22
+ import { listProductCommands, logicalIdToFilename } from "../slash/product-set.js";
23
+ /**
24
+ * Write thin-wrapper command/prompt files for every policy-enabled host.
25
+ *
26
+ * Idempotent: skips files whose on-disk bytes already match emission.
27
+ * Ownership-safe: never overwrites non-thin consumer customizations at product paths.
28
+ * Opt-out: removes managed thin wrappers only (leaves user customizations alone).
29
+ */
30
+ export function writeSlashCommandDeposit(projectRoot, io = { printf: () => undefined }, policy = loadHostSlashCommandsPolicyFromProject(projectRoot)) {
31
+ const rootAbs = resolve(projectRoot);
32
+ const writtenPaths = [];
33
+ const removedPaths = [];
34
+ const preservedCustomPaths = [];
35
+ const depositedHosts = [];
36
+ const skippedHosts = [];
37
+ for (const hostId of listSlashEmitterHosts()) {
38
+ if (!isHostSlashCommandDepositEnabled(hostId, policy)) {
39
+ skippedHosts.push(hostId);
40
+ removedPaths.push(...stripManagedHostCommandFiles(rootAbs, hostId));
41
+ continue;
42
+ }
43
+ depositedHosts.push(hostId);
44
+ const files = emitHostCommandFiles(hostId);
45
+ for (const file of files) {
46
+ const outcome = writeHostCommandFileIfChanged(rootAbs, file);
47
+ if (outcome === "written")
48
+ writtenPaths.push(file.relativePath);
49
+ if (outcome === "preserved")
50
+ preservedCustomPaths.push(file.relativePath);
51
+ }
52
+ }
53
+ if (writtenPaths.length > 0) {
54
+ const hostSummary = depositedHosts.join(", ");
55
+ io.printf(`Installed Directive slash commands for hosts [${hostSummary}]: ${writtenPaths.length} file(s)\n`);
56
+ }
57
+ if (preservedCustomPaths.length > 0) {
58
+ io.printf(`Preserved non-managed slash command customizations: ${preservedCustomPaths.join(", ")}\n`);
59
+ }
60
+ if (removedPaths.length > 0) {
61
+ io.printf(`Removed Directive-managed slash commands (plan.policy.hostSlashCommands opt-out): ${removedPaths.join(", ")}\n`);
62
+ }
63
+ if (writtenPaths.length === 0 && removedPaths.length === 0) {
64
+ if (depositedHosts.length === 0) {
65
+ io.printf("Directive slash commands: all hosts opted out via plan.policy.hostSlashCommands.\n");
66
+ }
67
+ else {
68
+ io.printf("Directive slash commands already current.\n");
69
+ }
70
+ }
71
+ return {
72
+ changed: writtenPaths.length + removedPaths.length > 0,
73
+ writtenPaths,
74
+ removedPaths,
75
+ preservedCustomPaths,
76
+ depositedHosts,
77
+ skippedHosts,
78
+ };
79
+ }
80
+ /**
81
+ * Exact repo-relative product command paths (all emitter hosts).
82
+ * Used by installerManagedMatchers for L8 prefer-commit staging without
83
+ * claiming whole host command directories (consumer custom files stay app-owned).
84
+ */
85
+ export function slashCommandManagedExactPaths() {
86
+ const paths = [];
87
+ for (const hostId of listSlashEmitterHosts()) {
88
+ const dir = getHostCommandLayout(hostId).relativeDir;
89
+ for (const cmd of listProductCommands()) {
90
+ paths.push(`${dir}/${logicalIdToFilename(cmd.logicalId)}`);
91
+ }
92
+ }
93
+ return paths;
94
+ }
95
+ function isManagedThinContent(raw, file) {
96
+ return raw === file.contents || isThinWrapperMarkdown(raw, file.dispatchPath);
97
+ }
98
+ function writeHostCommandFileIfChanged(projectRoot, file) {
99
+ const absolute = join(projectRoot, file.relativePath);
100
+ assertDepositContained(projectRoot, absolute);
101
+ if (existsSync(absolute)) {
102
+ let raw;
103
+ try {
104
+ raw = readFileSync(absolute, "utf8");
105
+ }
106
+ catch {
107
+ // Unreadable existing file: do not clobber consumer content.
108
+ return "preserved";
109
+ }
110
+ if (raw === file.contents)
111
+ return "unchanged";
112
+ // Ownership gate: only rewrite managed thin wrappers.
113
+ if (!isManagedThinContent(raw, file)) {
114
+ return "preserved";
115
+ }
116
+ }
117
+ const parent = dirname(absolute);
118
+ mkdirSync(parent, { recursive: true });
119
+ // Atomic replace via temp under project root (#2951).
120
+ const tmpName = `${basename(absolute)}.deft-${process.pid}.tmp`;
121
+ const temporary = join(parent, tmpName);
122
+ try {
123
+ containedWrite({
124
+ root: projectRoot,
125
+ target: temporary,
126
+ data: file.contents,
127
+ mode: "replace",
128
+ });
129
+ renameSync(temporary, absolute);
130
+ }
131
+ catch (err) {
132
+ try {
133
+ rmSync(temporary, { force: true });
134
+ }
135
+ catch {
136
+ /* best-effort cleanup */
137
+ }
138
+ throw err;
139
+ }
140
+ return "written";
141
+ }
142
+ /**
143
+ * On opt-out, remove only product-set managed thin wrappers for that host.
144
+ * User-authored or non-thin files under the host command dir are left untouched.
145
+ */
146
+ function stripManagedHostCommandFiles(projectRoot, hostId) {
147
+ const removed = [];
148
+ const files = emitHostCommandFiles(hostId);
149
+ for (const file of files) {
150
+ const absolute = join(projectRoot, file.relativePath);
151
+ assertDepositContained(projectRoot, absolute);
152
+ if (!existsSync(absolute))
153
+ continue;
154
+ let raw;
155
+ try {
156
+ raw = readFileSync(absolute, "utf8");
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ if (!isManagedThinContent(raw, file)) {
162
+ continue;
163
+ }
164
+ try {
165
+ rmSync(absolute, { force: true });
166
+ removed.push(file.relativePath);
167
+ }
168
+ catch {
169
+ /* best-effort */
170
+ }
171
+ }
172
+ return removed;
173
+ }
174
+ //# sourceMappingURL=slash-deposit.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Per-host slash-command deposit policy (#3054 / epic #55 L6).
3
+ *
4
+ * Parallel to {@link plan.policy.hostHooks}: default multi-host enablement for
5
+ * every host that has a real emitter (no stubs). Per-host `false` opts out.
6
+ */
7
+ import { type SlashEmitterHostId } from "../slash/emitters.js";
8
+ export declare const FIELD_HOST_SLASH_COMMANDS = "plan.policy.hostSlashCommands";
9
+ export declare const FIELD_HOST_SLASH_COMMANDS_CLI_ALIAS = "hostSlashCommands";
10
+ /** Per-host Directive slash-command deposit toggles (#3054). */
11
+ export type HostSlashCommandsPolicy = Record<SlashEmitterHostId, boolean>;
12
+ /** Default = all hosts with real emitters enabled (L6; not single-host-only). */
13
+ export declare const DEFAULT_HOST_SLASH_COMMANDS_POLICY: HostSlashCommandsPolicy;
14
+ export interface HostSlashCommandsPolicyField {
15
+ readonly name: string;
16
+ readonly current: HostSlashCommandsPolicy;
17
+ readonly default: HostSlashCommandsPolicy;
18
+ readonly source: string;
19
+ }
20
+ /** Resolve typed slash-command deposit policy from raw PROJECT-DEFINITION value. */
21
+ export declare function resolveHostSlashCommandsPolicy(raw: unknown): HostSlashCommandsPolicy;
22
+ export declare function validateHostSlashCommands(value: unknown): string[];
23
+ export declare function isHostSlashCommandDepositEnabled(host: SlashEmitterHostId, policy?: HostSlashCommandsPolicy): boolean;
24
+ /** Inspector row for `policy:show --field=hostSlashCommands`. */
25
+ export declare function inspectHostSlashCommands(data: Record<string, unknown> | null): HostSlashCommandsPolicyField;
26
+ /** Resolve slash-command deposit policy from PROJECT-DEFINITION on disk. */
27
+ export declare function loadHostSlashCommandsPolicyFromProject(projectRoot: string): HostSlashCommandsPolicy;
28
+ //# sourceMappingURL=host-slash-commands.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Per-host slash-command deposit policy (#3054 / epic #55 L6).
3
+ *
4
+ * Parallel to {@link plan.policy.hostHooks}: default multi-host enablement for
5
+ * every host that has a real emitter (no stubs). Per-host `false` opts out.
6
+ */
7
+ import { isSlashEmitterHostId, SLASH_EMITTER_HOSTS, } from "../slash/emitters.js";
8
+ import { readPlanPolicy } from "./plan-extensions.js";
9
+ import { loadProjectDefinition } from "./resolve.js";
10
+ export const FIELD_HOST_SLASH_COMMANDS = "plan.policy.hostSlashCommands";
11
+ export const FIELD_HOST_SLASH_COMMANDS_CLI_ALIAS = "hostSlashCommands";
12
+ /** Default = all hosts with real emitters enabled (L6; not single-host-only). */
13
+ export const DEFAULT_HOST_SLASH_COMMANDS_POLICY = Object.freeze({
14
+ claude: true,
15
+ cursor: true,
16
+ grok: true,
17
+ codex: true,
18
+ });
19
+ function readHostBoolean(rec, host, fallback) {
20
+ if (host in rec && typeof rec[host] === "boolean") {
21
+ return rec[host];
22
+ }
23
+ return fallback;
24
+ }
25
+ /** Resolve typed slash-command deposit policy from raw PROJECT-DEFINITION value. */
26
+ export function resolveHostSlashCommandsPolicy(raw) {
27
+ if (raw === null || raw === undefined) {
28
+ return { ...DEFAULT_HOST_SLASH_COMMANDS_POLICY };
29
+ }
30
+ if (typeof raw !== "object" || Array.isArray(raw)) {
31
+ return { ...DEFAULT_HOST_SLASH_COMMANDS_POLICY };
32
+ }
33
+ const rec = raw;
34
+ return {
35
+ claude: readHostBoolean(rec, "claude", DEFAULT_HOST_SLASH_COMMANDS_POLICY.claude),
36
+ cursor: readHostBoolean(rec, "cursor", DEFAULT_HOST_SLASH_COMMANDS_POLICY.cursor),
37
+ grok: readHostBoolean(rec, "grok", DEFAULT_HOST_SLASH_COMMANDS_POLICY.grok),
38
+ codex: readHostBoolean(rec, "codex", DEFAULT_HOST_SLASH_COMMANDS_POLICY.codex),
39
+ };
40
+ }
41
+ export function validateHostSlashCommands(value) {
42
+ if (value === null || value === undefined) {
43
+ return [];
44
+ }
45
+ if (typeof value !== "object" || Array.isArray(value)) {
46
+ return [`${FIELD_HOST_SLASH_COMMANDS} must be an object; got ${typeof value}`];
47
+ }
48
+ const rec = value;
49
+ const errors = [];
50
+ for (const host of SLASH_EMITTER_HOSTS) {
51
+ if (host in rec && typeof rec[host] !== "boolean") {
52
+ errors.push(`${FIELD_HOST_SLASH_COMMANDS}.${host} must be a boolean`);
53
+ }
54
+ }
55
+ for (const key of Object.keys(rec)) {
56
+ if (!isSlashEmitterHostId(key)) {
57
+ errors.push(`${FIELD_HOST_SLASH_COMMANDS}.${key} is not a slash emitter host (${SLASH_EMITTER_HOSTS.join(", ")})`);
58
+ }
59
+ }
60
+ return errors;
61
+ }
62
+ export function isHostSlashCommandDepositEnabled(host, policy = DEFAULT_HOST_SLASH_COMMANDS_POLICY) {
63
+ return policy[host];
64
+ }
65
+ function fieldFromResolved(resolved, source) {
66
+ return {
67
+ name: FIELD_HOST_SLASH_COMMANDS,
68
+ current: resolved,
69
+ default: { ...DEFAULT_HOST_SLASH_COMMANDS_POLICY },
70
+ source,
71
+ };
72
+ }
73
+ /** Inspector row for `policy:show --field=hostSlashCommands`. */
74
+ export function inspectHostSlashCommands(data) {
75
+ if (data === null) {
76
+ return fieldFromResolved({ ...DEFAULT_HOST_SLASH_COMMANDS_POLICY }, "default");
77
+ }
78
+ const policyBlock = readPlanPolicy(data.plan);
79
+ if (typeof policyBlock !== "object" ||
80
+ policyBlock === null ||
81
+ Array.isArray(policyBlock) ||
82
+ !("hostSlashCommands" in policyBlock)) {
83
+ return fieldFromResolved({ ...DEFAULT_HOST_SLASH_COMMANDS_POLICY }, "default");
84
+ }
85
+ const resolved = resolveHostSlashCommandsPolicy(policyBlock.hostSlashCommands);
86
+ return fieldFromResolved(resolved, "typed");
87
+ }
88
+ /** Resolve slash-command deposit policy from PROJECT-DEFINITION on disk. */
89
+ export function loadHostSlashCommandsPolicyFromProject(projectRoot) {
90
+ const [data] = loadProjectDefinition(projectRoot);
91
+ if (data === null) {
92
+ return { ...DEFAULT_HOST_SLASH_COMMANDS_POLICY };
93
+ }
94
+ const policyBlock = readPlanPolicy(data.plan);
95
+ if (typeof policyBlock !== "object" ||
96
+ policyBlock === null ||
97
+ Array.isArray(policyBlock) ||
98
+ !("hostSlashCommands" in policyBlock)) {
99
+ return { ...DEFAULT_HOST_SLASH_COMMANDS_POLICY };
100
+ }
101
+ return resolveHostSlashCommandsPolicy(policyBlock.hostSlashCommands);
102
+ }
103
+ //# sourceMappingURL=host-slash-commands.js.map
@@ -6,6 +6,7 @@ export * from "./deft-directive-disable.js";
6
6
  export * from "./delivery-branch.js";
7
7
  export * from "./disclosure.js";
8
8
  export * from "./host-hooks.js";
9
+ export * from "./host-slash-commands.js";
9
10
  export * from "./hotfix-criteria.js";
10
11
  export * from "./intent-ceiling.js";
11
12
  export * from "./no-deft-directive.js";
@@ -1,5 +1,8 @@
1
+ import { FIELD_HOST_SKILL_DISCOVERY, FIELD_HOST_SKILL_DISCOVERY_CLI_ALIAS, inspectHostSkillDiscovery, } from "../init-deposit/skill-discovery-hosts.js";
2
+ import { FIELD_OPENCLAW_PRODUCT_COMMANDS, FIELD_OPENCLAW_PRODUCT_COMMANDS_CLI_ALIAS, inspectOpenClawProductCommands, } from "../slash/openclaw-deposit.js";
1
3
  import { FIELD_DELIVERY_BRANCH, FIELD_DELIVERY_BRANCH_CLI_ALIAS, inspectDeliveryBranch, } from "./delivery-branch.js";
2
4
  import { FIELD_HOST_HOOKS, FIELD_HOST_HOOKS_CLI_ALIAS, inspectHostHooks } from "./host-hooks.js";
5
+ import { FIELD_HOST_SLASH_COMMANDS, FIELD_HOST_SLASH_COMMANDS_CLI_ALIAS, inspectHostSlashCommands, } from "./host-slash-commands.js";
3
6
  import { FIELD_HOTFIX_CRITERIA, FIELD_HOTFIX_CRITERIA_CLI_ALIAS, inspectHotfixCriteria, } from "./hotfix-criteria.js";
4
7
  import { readPlanPolicy } from "./plan-extensions.js";
5
8
  import { FIELD_PRODUCT_SIGNAL, FIELD_PRODUCT_SIGNAL_CLI_ALIAS, inspectProductSignal, } from "./product-signal.js";
@@ -17,6 +20,7 @@ export * from "./deft-directive-disable.js";
17
20
  export * from "./delivery-branch.js";
18
21
  export * from "./disclosure.js";
19
22
  export * from "./host-hooks.js";
23
+ export * from "./host-slash-commands.js";
20
24
  export * from "./hotfix-criteria.js";
21
25
  export * from "./intent-ceiling.js";
22
26
  export * from "./no-deft-directive.js";
@@ -282,6 +286,33 @@ function inspectHostHooksField(data) {
282
286
  source: field.source,
283
287
  };
284
288
  }
289
+ function inspectHostSlashCommandsField(data) {
290
+ const field = inspectHostSlashCommands(data);
291
+ return {
292
+ name: field.name,
293
+ current: field.current,
294
+ default: field.default,
295
+ source: field.source,
296
+ };
297
+ }
298
+ function inspectOpenClawProductCommandsField(data) {
299
+ const field = inspectOpenClawProductCommands(data);
300
+ return {
301
+ name: field.name,
302
+ current: field.current,
303
+ default: field.default,
304
+ source: field.source,
305
+ };
306
+ }
307
+ function inspectHostSkillDiscoveryField(data) {
308
+ const field = inspectHostSkillDiscovery(data);
309
+ return {
310
+ name: field.name,
311
+ current: field.current,
312
+ default: field.default,
313
+ source: field.source,
314
+ };
315
+ }
285
316
  function inspectRequireHumanMergeField(data, projectRoot) {
286
317
  const field = inspectRequireHumanMerge(data, projectRoot);
287
318
  return {
@@ -323,6 +354,9 @@ const REGISTERED_POLICIES = [
323
354
  inspectSwarmSubagentBackend,
324
355
  inspectDeliveryBranchField,
325
356
  inspectHostHooksField,
357
+ inspectHostSlashCommandsField,
358
+ inspectOpenClawProductCommandsField,
359
+ inspectHostSkillDiscoveryField,
326
360
  inspectStalenessTicklerField,
327
361
  inspectRuntimeAuthorityField,
328
362
  inspectProductSignalField,
@@ -347,13 +381,19 @@ export function inspectOnePolicy(name, projectRoot) {
347
381
  ? FIELD_RUNTIME_AUTHORITY
348
382
  : name === FIELD_HOST_HOOKS_CLI_ALIAS
349
383
  ? FIELD_HOST_HOOKS
350
- : name === FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS
351
- ? FIELD_REQUIRE_HUMAN_MERGE
352
- : name === FIELD_HOTFIX_CRITERIA_CLI_ALIAS
353
- ? FIELD_HOTFIX_CRITERIA
354
- : name === FIELD_DELIVERY_BRANCH_CLI_ALIAS
355
- ? FIELD_DELIVERY_BRANCH
356
- : name;
384
+ : name === FIELD_HOST_SLASH_COMMANDS_CLI_ALIAS
385
+ ? FIELD_HOST_SLASH_COMMANDS
386
+ : name === FIELD_OPENCLAW_PRODUCT_COMMANDS_CLI_ALIAS
387
+ ? FIELD_OPENCLAW_PRODUCT_COMMANDS
388
+ : name === FIELD_HOST_SKILL_DISCOVERY_CLI_ALIAS
389
+ ? FIELD_HOST_SKILL_DISCOVERY
390
+ : name === FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS
391
+ ? FIELD_REQUIRE_HUMAN_MERGE
392
+ : name === FIELD_HOTFIX_CRITERIA_CLI_ALIAS
393
+ ? FIELD_HOTFIX_CRITERIA
394
+ : name === FIELD_DELIVERY_BRANCH_CLI_ALIAS
395
+ ? FIELD_DELIVERY_BRANCH
396
+ : name;
357
397
  for (const field of inspectAllPolicies(projectRoot)) {
358
398
  if (field.name === normalized)
359
399
  return field;
@@ -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