@deftai/directive-core 0.91.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 (92) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/index.d.ts +1 -0
  6. package/dist/doctor/index.js +1 -0
  7. package/dist/doctor/main.js +49 -0
  8. package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
  9. package/dist/doctor/openclaw-l2-adapter.js +199 -0
  10. package/dist/hooks/dispatcher.d.ts +6 -1
  11. package/dist/hooks/dispatcher.js +30 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +2 -0
  14. package/dist/init-deposit/gitignore.js +3 -0
  15. package/dist/init-deposit/hygiene.d.ts +43 -1
  16. package/dist/init-deposit/hygiene.js +134 -10
  17. package/dist/init-deposit/index.d.ts +3 -0
  18. package/dist/init-deposit/index.js +3 -0
  19. package/dist/init-deposit/init-deposit.js +10 -0
  20. package/dist/init-deposit/refresh.js +11 -0
  21. package/dist/init-deposit/scaffold.js +6 -134
  22. package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
  23. package/dist/init-deposit/skill-discovery-deposit.js +287 -0
  24. package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
  25. package/dist/init-deposit/skill-discovery-hosts.js +217 -0
  26. package/dist/init-deposit/slash-deposit.d.ts +46 -0
  27. package/dist/init-deposit/slash-deposit.js +174 -0
  28. package/dist/orchestration/probe-session.d.ts +5 -1
  29. package/dist/orchestration/probe-session.js +24 -13
  30. package/dist/platform/agents-md.js +1 -1
  31. package/dist/policy/deft-directive-disable.d.ts +86 -0
  32. package/dist/policy/deft-directive-disable.js +167 -0
  33. package/dist/policy/delivery-branch.d.ts +33 -0
  34. package/dist/policy/delivery-branch.js +124 -0
  35. package/dist/policy/host-slash-commands.d.ts +28 -0
  36. package/dist/policy/host-slash-commands.js +103 -0
  37. package/dist/policy/index.d.ts +3 -0
  38. package/dist/policy/index.js +61 -5
  39. package/dist/scope/brief-io.d.ts +3 -1
  40. package/dist/scope/brief-io.js +5 -3
  41. package/dist/scope/delivery-evidence.d.ts +112 -0
  42. package/dist/scope/delivery-evidence.js +419 -0
  43. package/dist/scope/index.d.ts +1 -0
  44. package/dist/scope/index.js +1 -0
  45. package/dist/scope/main.d.ts +5 -0
  46. package/dist/scope/main.js +98 -3
  47. package/dist/scope/registry-artifact-sync.js +4 -1
  48. package/dist/scope/transition.d.ts +11 -1
  49. package/dist/scope/transition.js +30 -4
  50. package/dist/session/ritual-sentinel.js +21 -12
  51. package/dist/session/session-start-hook.d.ts +3 -0
  52. package/dist/session/session-start-hook.js +21 -0
  53. package/dist/session/session-start.js +31 -0
  54. package/dist/slash/emitters.d.ts +102 -0
  55. package/dist/slash/emitters.js +148 -0
  56. package/dist/slash/generator.d.ts +98 -0
  57. package/dist/slash/generator.js +145 -0
  58. package/dist/slash/index.d.ts +16 -0
  59. package/dist/slash/index.js +16 -0
  60. package/dist/slash/openclaw-adapter.d.ts +64 -0
  61. package/dist/slash/openclaw-adapter.js +198 -0
  62. package/dist/slash/openclaw-deposit.d.ts +73 -0
  63. package/dist/slash/openclaw-deposit.js +279 -0
  64. package/dist/slash/openclaw-slugs.d.ts +52 -0
  65. package/dist/slash/openclaw-slugs.js +126 -0
  66. package/dist/slash/product-set.d.ts +50 -0
  67. package/dist/slash/product-set.js +142 -0
  68. package/dist/swarm/complete-cohort.d.ts +20 -1
  69. package/dist/swarm/complete-cohort.js +56 -9
  70. package/dist/swarm/finalize-cohort-cli.js +9 -0
  71. package/dist/swarm/finalize-cohort.d.ts +8 -0
  72. package/dist/swarm/finalize-cohort.js +217 -21
  73. package/dist/user-config/experimental-rules.d.ts +43 -0
  74. package/dist/user-config/experimental-rules.js +162 -0
  75. package/dist/user-config/index.d.ts +1 -0
  76. package/dist/user-config/index.js +1 -0
  77. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  78. package/dist/vbrief-validate/plan-hooks.js +50 -0
  79. package/dist/verify-source/contained-writes.js +1 -1
  80. package/dist/xbrief/create.d.ts +36 -0
  81. package/dist/xbrief/create.js +285 -0
  82. package/dist/xbrief/index.d.ts +14 -0
  83. package/dist/xbrief/index.js +42 -0
  84. package/dist/xbrief/paths.d.ts +37 -0
  85. package/dist/xbrief/paths.js +123 -0
  86. package/dist/xbrief/styles.d.ts +36 -0
  87. package/dist/xbrief/styles.js +235 -0
  88. package/dist/xbrief/types.d.ts +50 -0
  89. package/dist/xbrief/types.js +17 -0
  90. package/dist/xbrief/verify.d.ts +30 -0
  91. package/dist/xbrief/verify.js +251 -0
  92. package/package.json +11 -3
@@ -8,6 +8,7 @@ import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief
8
8
  import { stampCompletionMetadata } from "./capacity-stamp.js";
9
9
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
10
10
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
11
+ import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
11
12
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
12
13
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
13
14
  import { utcNowIso } from "./vbrief-json.js";
@@ -48,7 +49,7 @@ function stampEnvelopeUpdated(data, nowIso) {
48
49
  }
49
50
  }
50
51
  }
51
- export function runTransition(action, filePath, now = new Date()) {
52
+ export function runTransition(action, filePath, now = new Date(), options = {}) {
52
53
  if (!(action in TRANSITIONS)) {
53
54
  const valid = Object.keys(TRANSITIONS).sort().join(", ");
54
55
  return { ok: false, message: `Unknown action '${action}'. Valid actions: ${valid}` };
@@ -95,9 +96,15 @@ export function runTransition(action, filePath, now = new Date()) {
95
96
  const requiredStatus = STATUS_PRECONDITIONS[act];
96
97
  if (requiredStatus !== undefined) {
97
98
  if (currentStatus === targetStatus) {
99
+ // Surface legacy delivery disposition on already-completed briefs (#3041).
100
+ let dispositionSuffix = "";
101
+ if (act === "complete" && currentFolder === "completed") {
102
+ const disposition = classifyStoredDeliveryDisposition(planObj);
103
+ dispositionSuffix = ` (deliveryDisposition=${disposition})`;
104
+ }
98
105
  return {
99
106
  ok: true,
100
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
107
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
101
108
  };
102
109
  }
103
110
  if (currentStatus !== requiredStatus) {
@@ -131,6 +138,25 @@ export function runTransition(action, filePath, now = new Date()) {
131
138
  }
132
139
  }
133
140
  const nowIso = utcNowIso(now);
141
+ // #3041: fail closed before mutating a code-bearing complete without delivery evidence.
142
+ if (act === "complete") {
143
+ const gate = evaluateDeliveryGate({
144
+ projectRoot,
145
+ plan: planObj,
146
+ nowIso,
147
+ evidence: options.deliveryEvidence,
148
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
149
+ runGit: options.runGit,
150
+ verifier: options.verifier ?? "scope:complete",
151
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
152
+ });
153
+ if (!gate.ok) {
154
+ return { ok: false, message: gate.message };
155
+ }
156
+ if (gate.provenance !== null) {
157
+ stampDeliveryProvenance(planObj, gate.provenance);
158
+ }
159
+ }
134
160
  planObj.status = targetStatus;
135
161
  planObj.updated = nowIso;
136
162
  // Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
@@ -153,7 +179,7 @@ export function runTransition(action, filePath, now = new Date()) {
153
179
  }
154
180
  // #2578: stamp terminal status at the destination path in the same write as
155
181
  // folder placement — never leave a non-terminal status under completed/.
156
- const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
182
+ const writeResult = atomicWriteBrief(destPath, data, vbriefRoot, { projectRoot });
157
183
  if (!writeResult.ok) {
158
184
  return { ok: false, message: writeResult.message };
159
185
  }
@@ -193,7 +219,7 @@ export function runTransition(action, filePath, now = new Date()) {
193
219
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
194
220
  };
195
221
  }
196
- const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
222
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
197
223
  if (!writeResult.ok) {
198
224
  return { ok: false, message: writeResult.message };
199
225
  }
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import { containedWrite } from "../fs/contained-write.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
5
6
  import { stableJson } from "./json.js";
6
7
  import { RITUAL_STATE_CONTRACT } from "./posture.js";
@@ -95,21 +96,29 @@ function validateSteps(raw, key) {
95
96
  }
96
97
  return [steps, null];
97
98
  }
98
- function atomicWriteJson(targetPath, payload, prefix) {
99
- const dir = join(targetPath, "..");
100
- mkdirSync(dir, { recursive: true });
101
- const tmpBase = `${prefix}${process.pid}.json.tmp`;
99
+ /**
100
+ * Contained atomic JSON write for ritual/sentinel state (#3042).
101
+ * Containment root is projectRoot (not dirname(target)) so a force-added `.deft`
102
+ * directory symlink fails closed before temp+rename.
103
+ */
104
+ function atomicWriteJson(projectRoot, targetPath, payload, prefix) {
105
+ const root = resolve(projectRoot);
106
+ const abs = resolve(targetPath);
107
+ // Refuse leaf/parent symlinks on the final path before temp+rename publish.
108
+ assertWriteTargetSafe(root, abs);
109
+ const dir = dirname(abs);
110
+ const tmpBase = `${prefix}${process.pid}.${basename(abs)}.tmp`;
102
111
  const tmpName = join(dir, tmpBase);
103
112
  const text = `${stableJson(payload, 2)}\n`;
104
113
  try {
105
- // #2980 wave D: ritual-state product write routes through containedWrite.
114
+ // #2980 wave D / #3042: product write routes through containedWrite under projectRoot.
106
115
  containedWrite({
107
- root: resolve(dir),
108
- target: tmpBase,
116
+ root,
117
+ target: tmpName,
109
118
  data: text,
110
119
  mode: "create",
111
120
  });
112
- renameSync(tmpName, targetPath);
121
+ renameSync(tmpName, abs);
113
122
  }
114
123
  catch (err) {
115
124
  try {
@@ -191,7 +200,7 @@ export function readRitualState(projectRoot) {
191
200
  }
192
201
  export function writeRitualState(projectRoot, payload) {
193
202
  const stateFile = ritualStatePath(projectRoot);
194
- atomicWriteJson(stateFile, payload, ".ritual-state.");
203
+ atomicWriteJson(projectRoot, stateFile, payload, ".ritual-state.");
195
204
  return stateFile;
196
205
  }
197
206
  /** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
@@ -303,7 +312,7 @@ export function writeSentinel(projectRoot, input) {
303
312
  .replace(`${LEGACY_ARTIFACT_DIR}/active/`, `${MIGRATED_ARTIFACT_DIR}/active/`),
304
313
  lastBranch: input.lastBranch,
305
314
  };
306
- atomicWriteJson(sentinelFile, payload, ".last-session.");
315
+ atomicWriteJson(projectRoot, sentinelFile, payload, ".last-session.");
307
316
  return sentinelFile;
308
317
  }
309
318
  function formatElapsed(deltaMs) {
@@ -1,3 +1,4 @@
1
+ import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
1
2
  import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
2
3
  import { writeSentinel } from "./ritual-sentinel.js";
3
4
  export interface SessionStartHookOptions {
@@ -7,6 +8,8 @@ export interface SessionStartHookOptions {
7
8
  readonly writeSentinelFn?: typeof writeSentinel;
8
9
  /** Test seam for #2926 opt-out detection. */
9
10
  readonly detectNoDeftDirectiveFn?: typeof detectNoDeftDirective;
11
+ /** Test seam for #3039 test kill-switch detection. */
12
+ readonly detectDeftDirectiveDisableFn?: typeof detectDeftDirectiveDisable;
10
13
  }
11
14
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
12
15
  export declare function runSessionStartHookWrite(projectRoot: string, options?: SessionStartHookOptions): {
@@ -1,9 +1,30 @@
1
1
  import { resolveVersion } from "../doctor/paths.js";
2
+ import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
2
3
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
3
4
  import { detectBranch } from "./git.js";
4
5
  import { detectLatestActiveVbrief, writeSentinel } from "./ritual-sentinel.js";
5
6
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
6
7
  export function runSessionStartHookWrite(projectRoot, options = {}) {
8
+ const detectKill = options.detectDeftDirectiveDisableFn ?? detectDeftDirectiveDisable;
9
+ // #3039: local (untracked) kill-switch — skip ritual bookkeeping (deposit OK).
10
+ // Tracked flags do not short-circuit (enforcement stays on).
11
+ const kill = detectKill(projectRoot);
12
+ const killActive = options.detectDeftDirectiveDisableFn !== undefined
13
+ ? kill.active
14
+ : isDeftDirectiveDisableActive(projectRoot);
15
+ if (killActive) {
16
+ const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
17
+ const optOut = detectOptOut(projectRoot);
18
+ const message = formatDeftDirectiveDisableMessage({
19
+ permanentOptOutAlsoPresent: optOut.present,
20
+ trackedByGit: false,
21
+ });
22
+ return {
23
+ code: 0,
24
+ stdout: `${message}\n`,
25
+ stderr: "",
26
+ };
27
+ }
7
28
  const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
8
29
  // #2926: root opt-out wins — host SessionStart must not write ritual bookkeeping.
9
30
  const optOut = detectOptOut(projectRoot);
@@ -3,6 +3,7 @@ import { runningInsideDeftRepo } from "../doctor/paths.js";
3
3
  import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
+ import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
6
7
  import { disclosureLine } from "../policy/disclosure.js";
7
8
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
8
9
  import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
@@ -543,6 +544,36 @@ export function runSessionStart(projectRoot, options = {}) {
543
544
  const runGit = options.runGit ?? defaultGitRunner;
544
545
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
545
546
  const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
547
+ // #3039: local (untracked) test kill-switch — skip ritual write; deposit may remain.
548
+ // Recovery requires delete + new session. Tracked flags do not short-circuit.
549
+ if (isDeftDirectiveDisableActive(projectRoot)) {
550
+ const killSwitch = detectDeftDirectiveDisable(projectRoot);
551
+ const optOutAlso = detectNoDeftDirective(projectRoot);
552
+ const message = formatDeftDirectiveDisableMessage({
553
+ permanentOptOutAlsoPresent: optOutAlso.present,
554
+ trackedByGit: false,
555
+ });
556
+ const lines = message.split("\n");
557
+ return {
558
+ code: 0,
559
+ payload: {
560
+ ready: false,
561
+ exit_code: 0,
562
+ disabled: true,
563
+ disabled_via: DEFT_DIRECTIVE_DISABLE_FLAG_NAME,
564
+ status: DEFT_DIRECTIVE_DISABLE_STATUS,
565
+ kill_switch: true,
566
+ inconsistent: false,
567
+ deposit_present: killSwitch.depositPresent,
568
+ tracked_by_git: false,
569
+ permanent_opt_out_also_present: optOutAlso.present,
570
+ posture,
571
+ environment: environmentContextToDict(environment),
572
+ message,
573
+ },
574
+ lines,
575
+ };
576
+ }
546
577
  // #2926: official root opt-out wins locally — skip Directive session ritual.
547
578
  // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
548
579
  // automation does not treat opt-out as "session fully initialized for work".
@@ -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