@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,279 @@
1
+ /**
2
+ * OpenClaw L2 product-command skill deposit (#3064 D4–D5).
3
+ *
4
+ * Writes thin user-invocable skills into the OpenClaw **main workspace skills**
5
+ * root (copy/stage — not symlink-escape into npm). Idempotent managed rewrite;
6
+ * preserves consumer custom skills at the same slug.
7
+ *
8
+ * Primary operator path: `deft doctor --fix` when OpenClaw is detected.
9
+ * init/update also deposits when OC signals are present and policy is on.
10
+ *
11
+ * ⊗ Fake project `.openclaw/commands/` file emitter.
12
+ */
13
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join, resolve } from "node:path";
16
+ import { detectOpenClaw, isEscapingSkillSymlink, listInScopeSkillsDirs, } from "../doctor/openclaw-skills.js";
17
+ import { containedWrite } from "../fs/contained-write.js";
18
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
19
+ import { loadProjectDefinition } from "../policy/resolve.js";
20
+ import { generateOpenClawSkillArtifacts, isManagedOpenClawL2Skill, listOpenClawManagedSkillSlugs, } from "./openclaw-adapter.js";
21
+ /** Typed policy key: opt-out of OpenClaw L2 product-command adapter deposit. */
22
+ export const FIELD_OPENCLAW_PRODUCT_COMMANDS = "plan.policy.openClawProductCommands";
23
+ export const FIELD_OPENCLAW_PRODUCT_COMMANDS_CLI_ALIAS = "openClawProductCommands";
24
+ /** Default: adapter on when OpenClaw is detected (real adapter — D5). */
25
+ export const DEFAULT_OPENCLAW_PRODUCT_COMMANDS = true;
26
+ function defaultIsDir(path) {
27
+ try {
28
+ return statSync(path).isDirectory();
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** Resolve openClawProductCommands from raw policy value. */
35
+ export function resolveOpenClawProductCommandsPolicy(raw) {
36
+ if (typeof raw === "boolean")
37
+ return raw;
38
+ return DEFAULT_OPENCLAW_PRODUCT_COMMANDS;
39
+ }
40
+ export function validateOpenClawProductCommands(value) {
41
+ if (value === null || value === undefined)
42
+ return [];
43
+ if (typeof value !== "boolean") {
44
+ return [`${FIELD_OPENCLAW_PRODUCT_COMMANDS} must be a boolean; got ${typeof value}`];
45
+ }
46
+ return [];
47
+ }
48
+ export function inspectOpenClawProductCommands(data) {
49
+ if (data === null) {
50
+ return {
51
+ name: FIELD_OPENCLAW_PRODUCT_COMMANDS,
52
+ current: DEFAULT_OPENCLAW_PRODUCT_COMMANDS,
53
+ default: DEFAULT_OPENCLAW_PRODUCT_COMMANDS,
54
+ source: "default",
55
+ };
56
+ }
57
+ const policyBlock = readPlanPolicy(data.plan);
58
+ if (typeof policyBlock !== "object" ||
59
+ policyBlock === null ||
60
+ Array.isArray(policyBlock) ||
61
+ !("openClawProductCommands" in policyBlock)) {
62
+ return {
63
+ name: FIELD_OPENCLAW_PRODUCT_COMMANDS,
64
+ current: DEFAULT_OPENCLAW_PRODUCT_COMMANDS,
65
+ default: DEFAULT_OPENCLAW_PRODUCT_COMMANDS,
66
+ source: "default",
67
+ };
68
+ }
69
+ return {
70
+ name: FIELD_OPENCLAW_PRODUCT_COMMANDS,
71
+ current: resolveOpenClawProductCommandsPolicy(policyBlock.openClawProductCommands),
72
+ default: DEFAULT_OPENCLAW_PRODUCT_COMMANDS,
73
+ source: "typed",
74
+ };
75
+ }
76
+ export function loadOpenClawProductCommandsPolicyFromProject(projectRoot) {
77
+ const [data] = loadProjectDefinition(projectRoot);
78
+ if (data === null)
79
+ return DEFAULT_OPENCLAW_PRODUCT_COMMANDS;
80
+ return inspectOpenClawProductCommands(data).current;
81
+ }
82
+ function skillDirLstatKind(path) {
83
+ try {
84
+ const st = lstatSync(path);
85
+ if (st.isSymbolicLink())
86
+ return "symlink";
87
+ if (st.isDirectory())
88
+ return "dir";
89
+ if (st.isFile())
90
+ return "file";
91
+ return "other";
92
+ }
93
+ catch {
94
+ return "missing";
95
+ }
96
+ }
97
+ function writeSkillArtifact(skillsDir, artifact) {
98
+ const skillDir = join(skillsDir, artifact.slug);
99
+ const skillFile = join(skillDir, "SKILL.md");
100
+ // #3064 Greptile P1 / #3008 spirit: never write through an escaping skill-dir symlink.
101
+ const dirKind = skillDirLstatKind(skillDir);
102
+ if (dirKind === "symlink" && isEscapingSkillSymlink(skillsDir, skillDir)) {
103
+ return "preserved";
104
+ }
105
+ if (existsSync(skillFile)) {
106
+ // Leaf symlink on SKILL.md that escapes skills root — refuse (containedWrite also fails closed).
107
+ if (skillDirLstatKind(skillFile) === "symlink" &&
108
+ isEscapingSkillSymlink(skillsDir, skillFile)) {
109
+ return "preserved";
110
+ }
111
+ let raw;
112
+ try {
113
+ raw = readFileSync(skillFile, "utf8");
114
+ }
115
+ catch {
116
+ return "preserved";
117
+ }
118
+ if (raw === artifact.skillMarkdown)
119
+ return "unchanged";
120
+ // Ownership: only rewrite managed thin L2 skills.
121
+ if (!isManagedOpenClawL2Skill(raw)) {
122
+ return "preserved";
123
+ }
124
+ }
125
+ mkdirSync(skillDir, { recursive: true });
126
+ // Re-check after mkdir: concurrent symlink plant is fail-closed.
127
+ if (skillDirLstatKind(skillDir) === "symlink" && isEscapingSkillSymlink(skillsDir, skillDir)) {
128
+ return "preserved";
129
+ }
130
+ const tmpName = `SKILL.md.deft-${process.pid}-${Date.now().toString(36)}.tmp`;
131
+ const tmp = join(skillDir, tmpName);
132
+ try {
133
+ // Contained write under skills root (#2951) — refuses symlink escape on the write path.
134
+ containedWrite({
135
+ root: skillsDir,
136
+ target: tmp,
137
+ data: artifact.skillMarkdown,
138
+ mode: "replace",
139
+ });
140
+ renameSync(tmp, skillFile);
141
+ }
142
+ catch (err) {
143
+ try {
144
+ rmSync(tmp, { force: true });
145
+ }
146
+ catch {
147
+ /* best-effort */
148
+ }
149
+ // Containment / symlink refusals: treat as preserved (do not clobber outside root).
150
+ const msg = err instanceof Error ? err.message : String(err);
151
+ if (/contained write refused|symlink|escape/i.test(msg)) {
152
+ return "preserved";
153
+ }
154
+ throw err;
155
+ }
156
+ return "written";
157
+ }
158
+ function stripManagedSkills(skillsDir) {
159
+ const removed = [];
160
+ for (const slug of listOpenClawManagedSkillSlugs()) {
161
+ const skillDir = join(skillsDir, slug);
162
+ const skillFile = join(skillDir, "SKILL.md");
163
+ if (!existsSync(skillFile))
164
+ continue;
165
+ let raw;
166
+ try {
167
+ raw = readFileSync(skillFile, "utf8");
168
+ }
169
+ catch {
170
+ continue;
171
+ }
172
+ if (!isManagedOpenClawL2Skill(raw))
173
+ continue;
174
+ try {
175
+ rmSync(skillDir, { recursive: true, force: true });
176
+ removed.push(skillDir);
177
+ }
178
+ catch {
179
+ /* best-effort */
180
+ }
181
+ }
182
+ return removed;
183
+ }
184
+ /**
185
+ * Deposit OpenClaw L2 product-command skills into workspace skills roots.
186
+ *
187
+ * Fail-closed when OpenClaw is not detected (unless forceDeposit for tests).
188
+ * Policy false → remove managed skills only (opt-out).
189
+ */
190
+ export function depositOpenClawL2ProductCommands(options = {}) {
191
+ const printf = options.printf ?? (() => undefined);
192
+ const env = options.env ?? process.env;
193
+ const home = options.homeDir ?? homedir();
194
+ const isDir = options.isDir ?? defaultIsDir;
195
+ const policy = options.policy ??
196
+ (options.projectRoot
197
+ ? loadOpenClawProductCommandsPolicyFromProject(options.projectRoot)
198
+ : DEFAULT_OPENCLAW_PRODUCT_COMMANDS);
199
+ const detect = detectOpenClaw(env, { homeDir: home, isDir });
200
+ if (!detect.detected && !options.forceDeposit) {
201
+ printf("OpenClaw L2 product commands: skip -- OpenClaw not detected\n");
202
+ return {
203
+ changed: false,
204
+ writtenPaths: [],
205
+ removedPaths: [],
206
+ preservedCustomPaths: [],
207
+ skillsDirs: [],
208
+ skipped: true,
209
+ skipReason: "openclaw-not-detected",
210
+ detect,
211
+ };
212
+ }
213
+ const skillsDirs = options.skillsDirs !== undefined
214
+ ? [...options.skillsDirs]
215
+ : listInScopeSkillsDirs(detect.stateDir, options.allAgents === true, {
216
+ isDir,
217
+ readDir: options.readDir,
218
+ });
219
+ const writtenPaths = [];
220
+ const removedPaths = [];
221
+ const preservedCustomPaths = [];
222
+ if (!policy) {
223
+ for (const skillsDir of skillsDirs) {
224
+ mkdirSync(skillsDir, { recursive: true });
225
+ removedPaths.push(...stripManagedSkills(skillsDir));
226
+ }
227
+ if (removedPaths.length > 0) {
228
+ printf(`Removed Directive-managed OpenClaw L2 product skills (plan.policy.openClawProductCommands opt-out): ${removedPaths.length}\n`);
229
+ }
230
+ else {
231
+ printf("OpenClaw L2 product commands: opted out (policy false); nothing to remove\n");
232
+ }
233
+ return {
234
+ changed: removedPaths.length > 0,
235
+ writtenPaths,
236
+ removedPaths,
237
+ preservedCustomPaths,
238
+ skillsDirs,
239
+ skipped: false,
240
+ detect,
241
+ };
242
+ }
243
+ const artifacts = generateOpenClawSkillArtifacts();
244
+ for (const skillsDir of skillsDirs) {
245
+ mkdirSync(skillsDir, { recursive: true });
246
+ for (const artifact of artifacts) {
247
+ const outcome = writeSkillArtifact(skillsDir, artifact);
248
+ const abs = join(skillsDir, artifact.slug);
249
+ if (outcome === "written")
250
+ writtenPaths.push(abs);
251
+ if (outcome === "preserved")
252
+ preservedCustomPaths.push(abs);
253
+ }
254
+ }
255
+ if (writtenPaths.length > 0) {
256
+ printf(`Installed OpenClaw L2 product-command skills (${artifacts.length} per skills root; router + ${PRODUCT_COUNT_LABEL}): ${writtenPaths.length} path(s)\n`);
257
+ }
258
+ else if (preservedCustomPaths.length === 0) {
259
+ printf("OpenClaw L2 product-command skills already current.\n");
260
+ }
261
+ if (preservedCustomPaths.length > 0) {
262
+ printf(`Preserved non-managed OpenClaw skills at product slugs: ${preservedCustomPaths.join(", ")}\n`);
263
+ }
264
+ return {
265
+ changed: writtenPaths.length + removedPaths.length > 0,
266
+ writtenPaths,
267
+ removedPaths,
268
+ preservedCustomPaths,
269
+ skillsDirs,
270
+ skipped: false,
271
+ detect,
272
+ };
273
+ }
274
+ const PRODUCT_COUNT_LABEL = "13 product";
275
+ /** Resolve absolute main skills dir for a state dir (test helper re-export path). */
276
+ export function resolveOpenClawMainSkillsDirForState(stateDir) {
277
+ return join(resolve(stateDir), "workspace", "skills");
278
+ }
279
+ //# sourceMappingURL=openclaw-deposit.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Stable logicalId → OpenClaw slug map for L2 product commands (#3064 D2).
3
+ *
4
+ * OpenClaw sanitizes skill/command names to `a-z0-9_` (max 32). Colons in
5
+ * `/deft:directive:run:interview` cannot survive raw. This table is the single
6
+ * bijective map for the L2 set of 13 — do not invent ad-hoc sanitize at emit time.
7
+ *
8
+ * Example: `/deft:directive:run:interview` → `deft_run_interview`
9
+ *
10
+ * ⊗ Add `openclaw` to HOST_COMMAND_LAYOUTS / SLASH_EMITTER_HOSTS as a fake
11
+ * project file emitter (L6 / #3064 non-goal).
12
+ */
13
+ import { type ProductCommand } from "./product-set.js";
14
+ /** OpenClaw skill/command name max length (host sanitize). */
15
+ export declare const OPENCLAW_SLUG_MAX_LEN: 32;
16
+ /** Regex for a valid OpenClaw skill slug. */
17
+ export declare const OPENCLAW_SLUG_PATTERN: RegExp;
18
+ /**
19
+ * Router skill slug — preferred native-menu entry (D3).
20
+ * Keep short for Telegram BOT_COMMANDS budget.
21
+ */
22
+ export declare const OPENCLAW_ROUTER_SLUG: "deft";
23
+ /** Public frozen map (logicalId → openClawSlug). */
24
+ export declare const OPENCLAW_SLUG_BY_LOGICAL_ID: Readonly<Record<string, string>>;
25
+ /** Reverse map for dispatch / router lookup. */
26
+ export declare const OPENCLAW_LOGICAL_ID_BY_SLUG: Readonly<Record<string, string>>;
27
+ export interface OpenClawSlugEntry {
28
+ readonly logicalId: string;
29
+ readonly openClawSlug: string;
30
+ readonly filenameStem: string;
31
+ readonly description: string;
32
+ }
33
+ /** Assert slug shape: a-z0-9_, length 1..32, no colons. */
34
+ export declare function isValidOpenClawSlug(slug: string): boolean;
35
+ /**
36
+ * Look up the OpenClaw slug for a product logical id.
37
+ * Throws when the id is not in the L2 map (no ad-hoc sanitize fallback).
38
+ */
39
+ export declare function logicalIdToOpenClawSlug(logicalId: string): string;
40
+ /** Reverse lookup slug → logical id, or undefined. */
41
+ export declare function openClawSlugToLogicalId(slug: string): string | undefined;
42
+ /**
43
+ * Stable ordered list of L2 slug entries (count === PRODUCT_COMMAND_COUNT).
44
+ * Built from {@link listProductCommands} so the name table is not duplicated.
45
+ */
46
+ export declare function listOpenClawSlugEntries(commands?: readonly ProductCommand[]): readonly OpenClawSlugEntry[];
47
+ /**
48
+ * Validate the L2 map: cardinality, bijectivity, slug shape, product-set coverage.
49
+ * Throws on any violation (used by unit tests and deposit preflight).
50
+ */
51
+ export declare function assertOpenClawSlugMapIntegrity(commands?: readonly ProductCommand[]): void;
52
+ //# sourceMappingURL=openclaw-slugs.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Stable logicalId → OpenClaw slug map for L2 product commands (#3064 D2).
3
+ *
4
+ * OpenClaw sanitizes skill/command names to `a-z0-9_` (max 32). Colons in
5
+ * `/deft:directive:run:interview` cannot survive raw. This table is the single
6
+ * bijective map for the L2 set of 13 — do not invent ad-hoc sanitize at emit time.
7
+ *
8
+ * Example: `/deft:directive:run:interview` → `deft_run_interview`
9
+ *
10
+ * ⊗ Add `openclaw` to HOST_COMMAND_LAYOUTS / SLASH_EMITTER_HOSTS as a fake
11
+ * project file emitter (L6 / #3064 non-goal).
12
+ */
13
+ import { listProductCommands, PRODUCT_COMMAND_COUNT } from "./product-set.js";
14
+ /** OpenClaw skill/command name max length (host sanitize). */
15
+ export const OPENCLAW_SLUG_MAX_LEN = 32;
16
+ /** Regex for a valid OpenClaw skill slug. */
17
+ export const OPENCLAW_SLUG_PATTERN = /^[a-z0-9_]+$/;
18
+ /**
19
+ * Router skill slug — preferred native-menu entry (D3).
20
+ * Keep short for Telegram BOT_COMMANDS budget.
21
+ */
22
+ export const OPENCLAW_ROUTER_SLUG = "deft";
23
+ /**
24
+ * Stable logical slash id → OpenClaw slug for the L2 product set.
25
+ *
26
+ * Derived from product-set order; shorter than hyphen-stem → underscore so
27
+ * every entry stays well under 32 chars (D2 example pattern).
28
+ */
29
+ const OPENCLAW_SLUG_BY_LOGICAL_ID_INNER = Object.freeze({
30
+ "/deft:directive:change": "deft_change",
31
+ "/deft:directive:change:apply": "deft_change_apply",
32
+ "/deft:directive:change:verify": "deft_change_verify",
33
+ "/deft:directive:change:archive": "deft_change_archive",
34
+ "/deft:directive:run:interview": "deft_run_interview",
35
+ "/deft:directive:run:yolo": "deft_run_yolo",
36
+ "/deft:directive:run:map": "deft_run_map",
37
+ "/deft:directive:run:discuss": "deft_run_discuss",
38
+ "/deft:directive:run:research": "deft_run_research",
39
+ "/deft:directive:run:speckit": "deft_run_speckit",
40
+ "/deft:directive:run:probe": "deft_run_probe",
41
+ "/deft:continue": "deft_continue",
42
+ "/deft:checkpoint": "deft_checkpoint",
43
+ });
44
+ /** Public frozen map (logicalId → openClawSlug). */
45
+ export const OPENCLAW_SLUG_BY_LOGICAL_ID = OPENCLAW_SLUG_BY_LOGICAL_ID_INNER;
46
+ /** Reverse map for dispatch / router lookup. */
47
+ export const OPENCLAW_LOGICAL_ID_BY_SLUG = Object.freeze(Object.fromEntries(Object.entries(OPENCLAW_SLUG_BY_LOGICAL_ID_INNER).map(([logicalId, slug]) => [slug, logicalId])));
48
+ /** Assert slug shape: a-z0-9_, length 1..32, no colons. */
49
+ export function isValidOpenClawSlug(slug) {
50
+ if (slug.length === 0 || slug.length > OPENCLAW_SLUG_MAX_LEN)
51
+ return false;
52
+ if (slug.includes(":"))
53
+ return false;
54
+ return OPENCLAW_SLUG_PATTERN.test(slug);
55
+ }
56
+ /**
57
+ * Look up the OpenClaw slug for a product logical id.
58
+ * Throws when the id is not in the L2 map (no ad-hoc sanitize fallback).
59
+ */
60
+ export function logicalIdToOpenClawSlug(logicalId) {
61
+ const slug = OPENCLAW_SLUG_BY_LOGICAL_ID_INNER[logicalId];
62
+ if (slug === undefined) {
63
+ throw new Error(`No OpenClaw slug mapping for logicalId: ${logicalId}`);
64
+ }
65
+ return slug;
66
+ }
67
+ /** Reverse lookup slug → logical id, or undefined. */
68
+ export function openClawSlugToLogicalId(slug) {
69
+ return OPENCLAW_LOGICAL_ID_BY_SLUG[slug];
70
+ }
71
+ /**
72
+ * Stable ordered list of L2 slug entries (count === PRODUCT_COMMAND_COUNT).
73
+ * Built from {@link listProductCommands} so the name table is not duplicated.
74
+ */
75
+ export function listOpenClawSlugEntries(commands = listProductCommands()) {
76
+ return commands.map((c) => ({
77
+ logicalId: c.logicalId,
78
+ openClawSlug: logicalIdToOpenClawSlug(c.logicalId),
79
+ filenameStem: c.filenameStem,
80
+ description: c.description,
81
+ }));
82
+ }
83
+ /**
84
+ * Validate the L2 map: cardinality, bijectivity, slug shape, product-set coverage.
85
+ * Throws on any violation (used by unit tests and deposit preflight).
86
+ */
87
+ export function assertOpenClawSlugMapIntegrity(commands = listProductCommands()) {
88
+ if (commands.length !== PRODUCT_COMMAND_COUNT) {
89
+ throw new Error(`OpenClaw slug map expects ${PRODUCT_COMMAND_COUNT} product commands, got ${commands.length}`);
90
+ }
91
+ const entries = listOpenClawSlugEntries(commands);
92
+ if (entries.length !== PRODUCT_COMMAND_COUNT) {
93
+ throw new Error(`OpenClaw slug map size ${entries.length} !== ${PRODUCT_COMMAND_COUNT}`);
94
+ }
95
+ const slugs = new Set();
96
+ for (const e of entries) {
97
+ if (!isValidOpenClawSlug(e.openClawSlug)) {
98
+ throw new Error(`Invalid OpenClaw slug for ${e.logicalId}: ${e.openClawSlug}`);
99
+ }
100
+ if (slugs.has(e.openClawSlug)) {
101
+ throw new Error(`Duplicate OpenClaw slug: ${e.openClawSlug}`);
102
+ }
103
+ slugs.add(e.openClawSlug);
104
+ if (OPENCLAW_LOGICAL_ID_BY_SLUG[e.openClawSlug] !== e.logicalId) {
105
+ throw new Error(`Reverse map mismatch for slug ${e.openClawSlug}`);
106
+ }
107
+ }
108
+ if (slugs.has(OPENCLAW_ROUTER_SLUG)) {
109
+ throw new Error(`Router slug "${OPENCLAW_ROUTER_SLUG}" collides with an L2 product slug (reserve for router)`);
110
+ }
111
+ if (!isValidOpenClawSlug(OPENCLAW_ROUTER_SLUG)) {
112
+ throw new Error(`Invalid router slug: ${OPENCLAW_ROUTER_SLUG}`);
113
+ }
114
+ // Map keys must cover exactly the product set (no orphans, no missing).
115
+ const mapKeys = new Set(Object.keys(OPENCLAW_SLUG_BY_LOGICAL_ID_INNER));
116
+ for (const c of commands) {
117
+ if (!mapKeys.has(c.logicalId)) {
118
+ throw new Error(`Product command missing OpenClaw slug map entry: ${c.logicalId}`);
119
+ }
120
+ mapKeys.delete(c.logicalId);
121
+ }
122
+ if (mapKeys.size > 0) {
123
+ throw new Error(`OpenClaw slug map has orphan keys: ${[...mapKeys].join(", ")}`);
124
+ }
125
+ }
126
+ //# sourceMappingURL=openclaw-slugs.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Locked product slash-command table for native multi-host registration (#3052 / epic #55).
3
+ *
4
+ * Authoritative locks: LockedDecisions L1–L10 on #55 (especially L1 names, L2 set of 13,
5
+ * L3 no native aliases, L4 logical-id ↔ hyphen filename map).
6
+ *
7
+ * Emitters (#3053) and deposit (#3054) MUST consume this table (or the generator IR built
8
+ * from it) — do not maintain a second product name list.
9
+ */
10
+ /** Primary load path on slash invoke (L5: one clear artifact). */
11
+ export type SlashDispatchKind = "strategy" | "skill" | "commands" | "resilience" | "session";
12
+ /**
13
+ * One canonical product slash command (L2).
14
+ *
15
+ * `logicalId` is the external slash form (host display / autocomplete).
16
+ * `filenameStem` is the portable on-disk name without extension (L4).
17
+ * `dispatchPath` is content-root-relative; emitters may prefix deposit roots
18
+ * (e.g. `.deft/core/`) without changing this table.
19
+ */
20
+ export interface ProductCommand {
21
+ readonly logicalId: string;
22
+ readonly filenameStem: string;
23
+ readonly description: string;
24
+ readonly dispatchKind: SlashDispatchKind;
25
+ readonly dispatchPath: string;
26
+ readonly argumentHint?: string;
27
+ }
28
+ /** Locked product-set cardinality (L2). */
29
+ export declare const PRODUCT_COMMAND_COUNT: 13;
30
+ /**
31
+ * Canonical L2 product command set — exactly 13 entries, stable order.
32
+ *
33
+ * ⊗ Expand without a product decision that amends L2 on #55.
34
+ * ⊗ Emit native alias files for legacy `/deft:change` forms (L3).
35
+ */
36
+ export declare const PRODUCT_COMMANDS: readonly ProductCommand[];
37
+ /**
38
+ * Map a canonical logical slash id to the portable hyphen filename stem (L4).
39
+ *
40
+ * `/deft:directive:run:interview` → `deft-directive-run-interview`
41
+ * `/deft:continue` → `deft-continue`
42
+ */
43
+ export declare function logicalIdToFilenameStem(logicalId: string): string;
44
+ /** Filename with `.md` extension for host command deposit (L4). */
45
+ export declare function logicalIdToFilename(logicalId: string): string;
46
+ /** Frozen product table for emitters (#3053) — single source of truth. */
47
+ export declare function listProductCommands(): readonly ProductCommand[];
48
+ /** Look up one product command by logical id, or undefined. */
49
+ export declare function getProductCommand(logicalId: string): ProductCommand | undefined;
50
+ //# sourceMappingURL=product-set.d.ts.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Locked product slash-command table for native multi-host registration (#3052 / epic #55).
3
+ *
4
+ * Authoritative locks: LockedDecisions L1–L10 on #55 (especially L1 names, L2 set of 13,
5
+ * L3 no native aliases, L4 logical-id ↔ hyphen filename map).
6
+ *
7
+ * Emitters (#3053) and deposit (#3054) MUST consume this table (or the generator IR built
8
+ * from it) — do not maintain a second product name list.
9
+ */
10
+ /** Locked product-set cardinality (L2). */
11
+ export const PRODUCT_COMMAND_COUNT = 13;
12
+ /**
13
+ * Canonical L2 product command set — exactly 13 entries, stable order.
14
+ *
15
+ * ⊗ Expand without a product decision that amends L2 on #55.
16
+ * ⊗ Emit native alias files for legacy `/deft:change` forms (L3).
17
+ */
18
+ export const PRODUCT_COMMANDS = Object.freeze([
19
+ {
20
+ logicalId: "/deft:directive:change",
21
+ filenameStem: "deft-directive-change",
22
+ description: "Create a scoped change proposal under history/changes/",
23
+ dispatchKind: "commands",
24
+ dispatchPath: "commands.md",
25
+ argumentHint: "<name>",
26
+ },
27
+ {
28
+ logicalId: "/deft:directive:change:apply",
29
+ filenameStem: "deft-directive-change-apply",
30
+ description: "Implement tasks from the active change proposal",
31
+ dispatchKind: "commands",
32
+ dispatchPath: "commands.md",
33
+ },
34
+ {
35
+ logicalId: "/deft:directive:change:verify",
36
+ filenameStem: "deft-directive-change-verify",
37
+ description: "Verify the active change against acceptance criteria",
38
+ dispatchKind: "commands",
39
+ dispatchPath: "commands.md",
40
+ },
41
+ {
42
+ logicalId: "/deft:directive:change:archive",
43
+ filenameStem: "deft-directive-change-archive",
44
+ description: "Archive the completed change under history/archive/",
45
+ dispatchKind: "commands",
46
+ dispatchPath: "commands.md",
47
+ },
48
+ {
49
+ logicalId: "/deft:directive:run:interview",
50
+ filenameStem: "deft-directive-run-interview",
51
+ description: "Structured interview with Light/Full sizing gate",
52
+ dispatchKind: "strategy",
53
+ dispatchPath: "strategies/interview.md",
54
+ argumentHint: "<name>",
55
+ },
56
+ {
57
+ logicalId: "/deft:directive:run:yolo",
58
+ filenameStem: "deft-directive-run-yolo",
59
+ description: "Auto-pilot interview; agent picks options",
60
+ dispatchKind: "strategy",
61
+ dispatchPath: "strategies/yolo.md",
62
+ argumentHint: "<name>",
63
+ },
64
+ {
65
+ logicalId: "/deft:directive:run:map",
66
+ filenameStem: "deft-directive-run-map",
67
+ description: "Brownfield codebase mapping strategy",
68
+ dispatchKind: "strategy",
69
+ dispatchPath: "strategies/map.md",
70
+ },
71
+ {
72
+ logicalId: "/deft:directive:run:discuss",
73
+ filenameStem: "deft-directive-run-discuss",
74
+ description: "Feynman-style alignment and decision locking",
75
+ dispatchKind: "strategy",
76
+ dispatchPath: "strategies/discuss.md",
77
+ argumentHint: "<topic>",
78
+ },
79
+ {
80
+ logicalId: "/deft:directive:run:research",
81
+ filenameStem: "deft-directive-run-research",
82
+ description: "Research domain before planning (don't hand-roll)",
83
+ dispatchKind: "strategy",
84
+ dispatchPath: "strategies/research.md",
85
+ argumentHint: "<domain>",
86
+ },
87
+ {
88
+ logicalId: "/deft:directive:run:speckit",
89
+ filenameStem: "deft-directive-run-speckit",
90
+ description: "Five-phase large/complex specification workflow",
91
+ dispatchKind: "strategy",
92
+ dispatchPath: "strategies/speckit.md",
93
+ argumentHint: "<name>",
94
+ },
95
+ {
96
+ logicalId: "/deft:directive:run:probe",
97
+ filenameStem: "deft-directive-run-probe",
98
+ description: "Adversarial one-question plan stress-testing",
99
+ dispatchKind: "skill",
100
+ dispatchPath: "skills/deft-directive-probe/SKILL.md",
101
+ },
102
+ {
103
+ logicalId: "/deft:continue",
104
+ filenameStem: "deft-continue",
105
+ description: "Resume from the continue checkpoint",
106
+ dispatchKind: "resilience",
107
+ dispatchPath: "resilience/continue-here.md",
108
+ },
109
+ {
110
+ logicalId: "/deft:checkpoint",
111
+ filenameStem: "deft-checkpoint",
112
+ description: "Save session state to xbrief/continue.xbrief.json",
113
+ dispatchKind: "session",
114
+ dispatchPath: "xbrief/continue.xbrief.json",
115
+ },
116
+ ]);
117
+ /**
118
+ * Map a canonical logical slash id to the portable hyphen filename stem (L4).
119
+ *
120
+ * `/deft:directive:run:interview` → `deft-directive-run-interview`
121
+ * `/deft:continue` → `deft-continue`
122
+ */
123
+ export function logicalIdToFilenameStem(logicalId) {
124
+ const trimmed = logicalId.trim();
125
+ if (!trimmed.startsWith("/")) {
126
+ throw new Error(`logicalId must start with '/': ${logicalId}`);
127
+ }
128
+ return trimmed.slice(1).replace(/:/g, "-");
129
+ }
130
+ /** Filename with `.md` extension for host command deposit (L4). */
131
+ export function logicalIdToFilename(logicalId) {
132
+ return `${logicalIdToFilenameStem(logicalId)}.md`;
133
+ }
134
+ /** Frozen product table for emitters (#3053) — single source of truth. */
135
+ export function listProductCommands() {
136
+ return PRODUCT_COMMANDS;
137
+ }
138
+ /** Look up one product command by logical id, or undefined. */
139
+ export function getProductCommand(logicalId) {
140
+ return PRODUCT_COMMANDS.find((c) => c.logicalId === logicalId);
141
+ }
142
+ //# sourceMappingURL=product-set.js.map
@@ -212,6 +212,7 @@ const VERIFY_TASK_VERBS = new Set([
212
212
  "verify:forward-coverage",
213
213
  "verify:story-ready",
214
214
  "verify:review-monitor",
215
+ "verify:l4-owner",
215
216
  "pr:watch",
216
217
  "pr:merge-ready",
217
218
  "coverage:hotspots",