@noir-ai/skills 1.3.0-beta.6 → 1.4.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.
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # @noir-ai/skills
2
2
 
3
- The builtin `noir-*` skill pack (31 skills across SDD lifecycle, power, session, git, and domain categories) plus a copy-and-validate compiler. `noir init` / `noir sync` emit the pack to the host's skill directory idempotently. There is no plugin or marketplace — only native `noir-` builtins.
3
+ The native `noir-*` skill pack (33 builtins plus 1 integration) and a copy-and-validate compiler. `noir init` / `noir sync` emit it idempotently for hosts with a skill surface: Claude uses `.claude/skills/`; Cursor uses `.cursor/rules/*.mdc`. There is no plugin or marketplace.
4
4
 
5
5
  Part of the **[Noir](https://github.com/agaaaptr/noir#readme)** toolkit — the discipline, context, and memory layer for any agentic CLI.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install @noir-ai/skills
10
+ npm install @noir-ai/skills@beta
11
11
  ```
12
12
 
13
13
  > Most users install the CLI instead, which emits the builtin skills via `noir init` / `noir sync`:
14
14
  >
15
15
  > ```bash
16
- > npm install -g @noir-ai/cli
16
+ > npm install -g @noir-ai/cli@beta
17
17
  > ```
18
18
 
19
19
  ## License
@@ -6,7 +6,7 @@ description: Use mid-session — to save in-flight state before a context-risky
6
6
  Save in-flight state so work survives a context-risky moment (compaction, handoff, or interruption). Persists the task's phase, progress, and next step into `.noir/` and records the checkpoint observably via `noir.checkpoint`.
7
7
 
8
8
  ## Procedure
9
- 1. **Find the task.** Locate the active task stub under `.noir/tasks/<id>.md` (the S4 engine's persisted state). If none, ask the user which task to checkpoint — or note "no active task — nothing to checkpoint" and stop.
9
+ 1. **Find the task.** Locate the active task stub under `.noir/tasks/<id>.md` (the engine's persisted state). If none, ask the user which task to checkpoint — or note "no active task — nothing to checkpoint" and stop.
10
10
  2. **Capture state.** Update `.noir/tasks/<id>.md`: set the current phase, a status line (e.g. `checkpointed @ phase N`), an `updated:` timestamp, and fill *done so far* + *next steps* + any blockers. Append a one-line history entry so a fresh session can pick up cleanly.
11
11
  3. **Record the checkpoint.** Call `noir.checkpoint` so the SDD engine records the save observably; the task stub is the source of truth, the checkpoint is the durable signal that survives context loss.
12
12
  4. **Memory (best-effort).** Save the checkpoint to Noir memory (phase + key decisions + next step) if the host's memory tooling is available. Skip cleanly if not — `.noir/tasks/<id>.md` remains the durable record either way.
@@ -9,7 +9,7 @@ Load project context at session start. Read-only — do not edit project files.
9
9
  1. **Read anchors.** Read `CLAUDE.md` / `AGENTS.md` (always-on context) to ground the session.
10
10
  2. **Project setup.** Detect stack + the test/run command from manifests and `CLAUDE.md`; note project type. Informational — flag anything non-conventional, do not mutate.
11
11
  3. **Git state.** `git rev-parse --abbrev-ref HEAD`; `git status --porcelain`; `git log --oneline -5`.
12
- 4. **Noir state.** Read `.noir/NOIR.md`; check `.noir/tasks/` for an in-flight task (the S4 engine's persisted state). If one exists, surface its phase via `noir.workflow_status` and offer to resume it through the SDD lifecycle — do not auto-resume.
12
+ 4. **Noir state.** Read `.noir/NOIR.md`; check `.noir/tasks/` for an in-flight task (the engine's persisted state). If one exists, surface its phase via `noir.workflow_status` and offer to resume it through the SDD lifecycle — do not auto-resume.
13
13
  5. **Recall memory.** Query Noir memory (or the host's recall tooling, if any); surface 2–4 top facts relevant to the current task. If empty, say so — `.noir/` artifacts are the durable fallback.
14
14
  6. **Print a brief** of essentials only (project type, stack, key commands, in-flight task) — no large dumps. Then await the user; do not auto-start work.
15
15
 
@@ -1,12 +1,23 @@
1
1
  ---
2
2
  name: noir-wrap
3
- description: Use when closing a session cleanly — run tests, curate docs, confirm commits, save memory.
3
+ description: Use when closing a session cleanly — run tests, curate docs, confirm commits, save memory, emit a host handoff.
4
4
  ---
5
5
 
6
6
  # noir-wrap
7
7
 
8
- > **Stub:** this skill ships as a valid, loadable placeholder in S5; its full playbook is deepened in a later slice.
8
+ Use when you are ending a session and want to leave the work in a clean, recoverable state and hand off to the host CLI with a ready-to-paste prompt.
9
9
 
10
- **When to use:** you are ending a session and want to leave the work in a clean, recoverable state.
10
+ ## Steps
11
11
 
12
- **For now:** run the project's test command, curate or delete ephemeral notes, confirm commits are made (and pushed or intentionally local), and save durable memory before closing.
12
+ 1. Run the project's test command (the host detects it; if unknown, `pnpm test` / `npm test`).
13
+ 2. Curate or delete ephemeral notes (scratch docs, dead branches, tmp files).
14
+ 3. Confirm commits are made — and pushed or intentionally local (Noir keeps commits local + conservative by default).
15
+ 4. Save durable memory before closing: observations, decisions, patterns the next session should recall. Prefer `noir memory save` (or the `noir.remember` MCP tool from the host) so cross-session recall works.
16
+ 5. Advance the workflow task if a gate is satisfied: `noir task advance --to <phase>` (the verify gate prints the handoff hint automatically).
17
+ 6. Emit the host handoff — run `noir handoff` (or the session-end alias `noir wrap`). This prints a ready-to-paste markdown prompt to STDOUT that names the active task, the next gate's skill, a bounded context/memory seed, and the exact host-launch directive. Pipe it straight into the host, or persist with `noir handoff --write` (the path is gitignored under `.noir/handoff/`).
18
+
19
+ ## Notes
20
+
21
+ - `noir handoff` reuses the same snapshot as `noir status` and the same phase→skill map as `noir task next`, so the handoff is always consistent with the live state.
22
+ - The handoff directive is TEXT ONLY — Noir never launches the host. Paste the block into the host CLI to resume.
23
+ - For a machine-readable handoff (e.g. a CI consumer), use `noir handoff --json`.
package/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ interface CompiledSkill {
75
75
  interface CompiledIntegration extends CompiledSkill {
76
76
  /** Present only when `runtimeEmitsHostMcp(declaration.runtime)` AND a non-null
77
77
  * `declaration.mcp` exists. The cli/daemon layer hands this to the host
78
- * adapter's `emitMcpConfig` overload (X-T2 seam); absent ⇒ skill-only. */
78
+ * adapter's `emitMcpConfig` overload; absent ⇒ skill-only. */
79
79
  hostMcp?: {
80
80
  serverName: string;
81
81
  command: string;
@@ -93,14 +93,78 @@ interface EmitSummary {
93
93
  * Additive — callers that ignore it (existing cli) still get the builtins in
94
94
  * `emitted`. */
95
95
  integrations?: string[];
96
- /** Stale `noir-*` directories removed from `dir` after emit (T2 cleanup).
96
+ /** Stale `noir-*` directories removed from `dir` after emit (cleanup).
97
97
  * Names a previous Noir version shipped but the current build no longer
98
98
  * does (builtin renamed/removed). Only the `noir-` managed namespace is
99
99
  * ever pruned — user skills without the prefix are NEVER touched. Empty
100
100
  * array when nothing was stale; undefined on callers that pre-date the
101
101
  * field (additive — old callers still get the rest of the summary). */
102
102
  pruned?: string[];
103
+ /** `noir-*` entries the prune step REFRAINED from removing because they
104
+ * look user-authored (hand-rolled noir-foo/SKILL.md without canonical Noir
105
+ * frontmatter). `assertNotUserOwned` guard. Names only — never user skills
106
+ * without the `noir-` prefix (those are always left alone). */
107
+ preservedUserOwned?: string[];
108
+ /** One record per emitted skill file that existed AND differed from the
109
+ * compiled bytes, with the resolution applied. Mirrors {@link ConflictContext}
110
+ * / {@link ConflictRecord} in @noir-ai/create so the CLI can lift the same
111
+ * structured report under `--json`. */
112
+ conflicts?: SkillConflict[];
103
113
  }
114
+ /**
115
+ * Resolution choice for a skill-file conflict. Mirrors
116
+ * @noir-ai/create's `ConflictResolution` literally so the CLI's
117
+ * `buildConflictOpts().onConflict` is structurally compatible, WITHOUT the
118
+ * skills package gaining a create dependency.
119
+ */
120
+ type SkillConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel';
121
+ /**
122
+ * Context passed to {@link SkillConflictResolver}. Mirrors
123
+ * @noir-ai/create's `ConflictContext` (relPath/existing/proposed/mode) so the
124
+ * CLI's clack resolver handles skill conflicts with the SAME code path as
125
+ * regenerate conflicts.
126
+ */
127
+ interface SkillConflictContext {
128
+ /** Path relative to the skills target dir (e.g. `noir-brainstorm/SKILL.md`). */
129
+ relPath: string;
130
+ /** The skill file's current on-disk bytes. */
131
+ existing: string;
132
+ /** The compiled bytes Noir would write. */
133
+ proposed: string;
134
+ /** Always `'skill'` — the artifact class (apply-to-all is per-class). */
135
+ mode: 'skill';
136
+ }
137
+ /**
138
+ * One record per skill file that existed AND differed from the compiled
139
+ * bytes. Mirrors @noir-ai/create's `ConflictRecord` (additive — old callers
140
+ * ignore it).
141
+ */
142
+ interface SkillConflict {
143
+ /** Path relative to the skills target dir. */
144
+ path: string;
145
+ /** Always `'skill'`. */
146
+ mode: 'skill';
147
+ /** LCS similarity ratio (0-1). */
148
+ similarity?: number;
149
+ /** sha256 hex (12 chars) of the on-disk bytes. */
150
+ existingSha: string;
151
+ /** sha256 hex (12 chars) of the proposed bytes. */
152
+ proposedSha: string;
153
+ /** Resolution applied. */
154
+ resolution: SkillConflictResolution;
155
+ }
156
+ /**
157
+ * The resolver callback the CLI injects (clack menu, diff preview,
158
+ * apply-to-all). Returns a bare resolution OR a rich `{resolution, applyToAll}`
159
+ * shape; the engine unwraps both. Structurally compatible with
160
+ * @noir-ai/create's `ConflictResolverReturn` so the CLI passes its single
161
+ * resolver through unchanged.
162
+ */
163
+ type SkillConflictResolverReturn = SkillConflictResolution | {
164
+ resolution: SkillConflictResolution;
165
+ applyToAll?: boolean;
166
+ };
167
+ type SkillConflictResolver = (ctx: SkillConflictContext) => Promise<SkillConflictResolverReturn> | SkillConflictResolverReturn;
104
168
  /**
105
169
  * The set of host-shaped compile targets the compiler knows how to emit. Was
106
170
  * `'claude'` only through v1.1; widened in S10 to the multi-host enum (mirrors
@@ -163,7 +227,7 @@ declare function validateSkill(skill: BuiltinSkill): ValidationResult;
163
227
  *
164
228
  * - `claude` | `agents-md` | `gemini` | `opencode` (the AGENTS.md-aligned
165
229
  * hosts): canonical format copied verbatim — `SKILL.md` plus its
166
- * `references/` siblings (DS-4). These hosts read the same SKILL.md shape;
230
+ * `references/` siblings. These hosts read the same SKILL.md shape;
167
231
  * the EMIT-LOCATION (which dir they land in) is the cli/adapter's job, not
168
232
  * the compiler's.
169
233
  * - `cursor`: transform into a Cursor `.mdc` rule — ONE file `<name>.mdc`
@@ -205,6 +269,21 @@ declare function emitSkillsToDir(targetDir: string, opts?: {
205
269
  * hosts have no skill concept, so emit is skipped upstream — but the
206
270
  * default-verbatim policy keeps the signature total). */
207
271
  target?: CompileTarget;
272
+ /** Policy for a skill file that exists AND differs from the compiled
273
+ * bytes, when no {@link onConflict} resolver is wired (or non-interactive).
274
+ * Default `'overwrite'` preserves the v1.2 behavior (every sync clobbers).
275
+ * `'preserve'` skips the differing file (the CI / non-TTY default the CLI
276
+ * threads via `buildConflictOpts`). */
277
+ conflictPolicy?: 'overwrite' | 'preserve';
278
+ /** Per-file conflict resolver. The CLI passes its single
279
+ * `buildConflictOpts().onConflict` (clack menu + diff preview + apply-to-
280
+ * all) through unchanged; structurally compatible with @noir-ai/create's
281
+ * `ConflictResolverReturn`. */
282
+ onConflict?: SkillConflictResolver;
283
+ /** Explicit interactivity flag (the engine reads THIS, not process.env,
284
+ * matching @noir-ai/create's hermetic-interactive contract). When `false`,
285
+ * the resolver is NEVER consulted (CI / --json never hangs on a prompt). */
286
+ interactive?: boolean;
208
287
  }): Promise<EmitSummary>;
209
288
 
210
289
  /** Auth shape. Locked: `env-var` only until keychain lands (Q4b — refuse OAuth,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/compiler.ts
2
- import { mkdir, readdir, rm, writeFile } from "fs/promises";
2
+ import { createHash } from "crypto";
3
+ import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
3
4
  import { basename, dirname as dirname2, join as join2 } from "path";
4
5
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
6
 
@@ -213,14 +214,71 @@ async function emitSkillsToDir(targetDir, opts = {}) {
213
214
  let references = 0;
214
215
  const emitted = [];
215
216
  const integrationNames = [];
217
+ const conflicts = [];
218
+ const memory = /* @__PURE__ */ new Map();
219
+ const policy = opts.conflictPolicy ?? "overwrite";
220
+ const interactive = opts.interactive ?? (process.env.NOIR_NON_INTERACTIVE === void 0 || process.env.NOIR_NON_INTERACTIVE === "");
221
+ const writeWithConflict = async (absDest, relDest, content, isReference) => {
222
+ let existing;
223
+ try {
224
+ existing = await readFile(absDest, "utf8");
225
+ } catch {
226
+ existing = void 0;
227
+ }
228
+ if (existing !== void 0 && existing !== content) {
229
+ const MODE = "skill";
230
+ let resolution;
231
+ const remembered = memory.get(MODE);
232
+ if (remembered !== void 0) {
233
+ resolution = remembered;
234
+ } else if (interactive && opts.onConflict !== void 0) {
235
+ const ctx = {
236
+ relPath: relDest,
237
+ existing,
238
+ proposed: content,
239
+ mode: MODE
240
+ };
241
+ const ret = await opts.onConflict(ctx);
242
+ const unwrapped = typeof ret === "string" ? ret : ret.resolution;
243
+ if (typeof ret !== "string" && ret.applyToAll === true) {
244
+ memory.set(MODE, unwrapped);
245
+ }
246
+ resolution = unwrapped;
247
+ } else {
248
+ resolution = policy === "preserve" ? "preserve" : "replace";
249
+ }
250
+ conflicts.push({
251
+ path: relDest,
252
+ mode: MODE,
253
+ similarity: similarity(existing, content),
254
+ existingSha: sha256Hex12(existing),
255
+ proposedSha: sha256Hex12(content),
256
+ resolution
257
+ });
258
+ if (resolution === "cancel") {
259
+ throw new Error(`skill emit cancelled by user at ${relDest}`);
260
+ }
261
+ if (resolution === "preserve") {
262
+ return;
263
+ }
264
+ if (resolution === "rename") {
265
+ await renameAside(absDest, ".local");
266
+ } else if (resolution === "duplicate") {
267
+ await writeFile(await uniqueAside(absDest, ".noir"), content, "utf8");
268
+ return;
269
+ }
270
+ }
271
+ await writeFile(absDest, content, "utf8");
272
+ if (isReference) references++;
273
+ };
216
274
  const flat = target === "cursor";
217
275
  for (const s of builtins) {
218
276
  const compiled = compileSkill(s, target);
219
277
  for (const f of compiled.files) {
220
278
  const dest = flat ? join2(targetDir, ...f.path) : join2(targetDir, s.name, ...f.path);
221
279
  await mkdir(dirname2(dest), { recursive: true });
222
- await writeFile(dest, f.content, "utf8");
223
- if (f.path[0] !== "SKILL.md") references++;
280
+ const rel = flat ? f.path.join("/") : [s.name, ...f.path].join("/");
281
+ await writeWithConflict(dest, rel, f.content, f.path[0] !== "SKILL.md");
224
282
  }
225
283
  emitted.push(s.name);
226
284
  }
@@ -230,8 +288,8 @@ async function emitSkillsToDir(targetDir, opts = {}) {
230
288
  for (const f of compiled.files) {
231
289
  const dest = flat ? join2(targetDir, ...f.path) : join2(targetDir, i.name, ...f.path);
232
290
  await mkdir(dirname2(dest), { recursive: true });
233
- await writeFile(dest, f.content, "utf8");
234
- if (f.path[0] !== "SKILL.md") references++;
291
+ const rel = flat ? f.path.join("/") : [i.name, ...f.path].join("/");
292
+ await writeWithConflict(dest, rel, f.content, f.path[0] !== "SKILL.md");
235
293
  }
236
294
  emitted.push(i.name);
237
295
  integrationNames.push(i.name);
@@ -239,6 +297,7 @@ async function emitSkillsToDir(targetDir, opts = {}) {
239
297
  }
240
298
  const keep = new Set(emitted);
241
299
  const pruned = [];
300
+ const preservedUserOwned = [];
242
301
  let dirEntries = [];
243
302
  try {
244
303
  dirEntries = await readdir(targetDir, { withFileTypes: true });
@@ -252,13 +311,25 @@ async function emitSkillsToDir(targetDir, opts = {}) {
252
311
  const mdcMatch = ent.name.match(/^(noir-[a-z0-9]+(?:-[a-z0-9]+)*)\.mdc$/);
253
312
  const skillName = mdcMatch?.[1];
254
313
  if (skillName && keep.has(skillName)) continue;
314
+ if (await isUserOwnedMdc(join2(targetDir, ent.name))) {
315
+ preservedUserOwned.push(ent.name);
316
+ continue;
317
+ }
255
318
  } else if (ent.isDirectory()) {
319
+ if (await isUserOwnedCursorLegacyDir(join2(targetDir, ent.name))) {
320
+ preservedUserOwned.push(ent.name);
321
+ continue;
322
+ }
256
323
  } else {
257
324
  continue;
258
325
  }
259
326
  } else {
260
327
  if (!ent.isDirectory()) continue;
261
328
  if (keep.has(ent.name)) continue;
329
+ if (await isUserOwnedSkillDir(join2(targetDir, ent.name))) {
330
+ preservedUserOwned.push(ent.name);
331
+ continue;
332
+ }
262
333
  }
263
334
  try {
264
335
  await rm(join2(targetDir, ent.name), { recursive: true, force: true });
@@ -266,7 +337,92 @@ async function emitSkillsToDir(targetDir, opts = {}) {
266
337
  } catch {
267
338
  }
268
339
  }
269
- return { dir: targetDir, emitted, references, integrations: integrationNames, pruned };
340
+ return {
341
+ dir: targetDir,
342
+ emitted,
343
+ references,
344
+ integrations: integrationNames,
345
+ pruned,
346
+ ...preservedUserOwned.length > 0 ? { preservedUserOwned } : {},
347
+ ...conflicts.length > 0 ? { conflicts } : {}
348
+ };
349
+ }
350
+ async function isUserOwnedSkillDir(dirAbs) {
351
+ try {
352
+ const skillMd = await readFile(join2(dirAbs, "SKILL.md"), "utf8");
353
+ const m = skillMd.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
354
+ if (!m) return true;
355
+ const fm = parseYaml(m[1] ?? "");
356
+ const dirName = basename(dirAbs);
357
+ return typeof fm?.name !== "string" || fm.name !== dirName;
358
+ } catch {
359
+ return true;
360
+ }
361
+ }
362
+ async function isUserOwnedMdc(fileAbs) {
363
+ try {
364
+ const md = await readFile(fileAbs, "utf8");
365
+ const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
366
+ if (!m) return true;
367
+ const fm = parseYaml(m[1] ?? "");
368
+ return fm?.alwaysApply === void 0;
369
+ } catch {
370
+ return true;
371
+ }
372
+ }
373
+ async function isUserOwnedCursorLegacyDir(dirAbs) {
374
+ try {
375
+ const entries = await readdir(dirAbs);
376
+ if (entries.length === 0) return true;
377
+ let sawCanonical = false;
378
+ for (const name of entries) {
379
+ if (!name.endsWith(".mdc")) continue;
380
+ if (!await isUserOwnedMdc(join2(dirAbs, name))) {
381
+ sawCanonical = true;
382
+ break;
383
+ }
384
+ }
385
+ return !sawCanonical;
386
+ } catch {
387
+ return true;
388
+ }
389
+ }
390
+ function sha256Hex12(s) {
391
+ return createHash("sha256").update(s, "utf8").digest("hex").slice(0, 12);
392
+ }
393
+ function similarity(a, b) {
394
+ if (a === b) return 1;
395
+ const A = a.split("\n");
396
+ const B = b.split("\n");
397
+ if (A.length === 0 && B.length === 0) return 1;
398
+ const n = A.length;
399
+ const m = B.length;
400
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
401
+ for (let i = n - 1; i >= 0; i--) {
402
+ const dpi = dp[i] ?? [];
403
+ const dpi1 = dp[i + 1] ?? [];
404
+ const ai = A[i] ?? "";
405
+ for (let j = m - 1; j >= 0; j--) {
406
+ const bj = B[j] ?? "";
407
+ dpi[j] = ai === bj ? (dpi1[j + 1] ?? 0) + 1 : Math.max(dpi1[j] ?? 0, dpi[j + 1] ?? 0);
408
+ }
409
+ }
410
+ const lcs = dp[0]?.[0] ?? 0;
411
+ return n + m === 0 ? 1 : 2 * lcs / (n + m);
412
+ }
413
+ async function renameAside(abs, suffix) {
414
+ const { rename } = await import("fs/promises");
415
+ const dest = await uniqueAsideAbs(abs, suffix);
416
+ await rename(abs, dest);
417
+ }
418
+ async function uniqueAside(abs, suffix) {
419
+ return uniqueAsideAbs(abs, suffix);
420
+ }
421
+ async function uniqueAsideAbs(abs, suffix) {
422
+ const { existsSync } = await import("fs");
423
+ let candidate = `${abs}${suffix}`;
424
+ for (let n = 1; existsSync(candidate); n++) candidate = `${abs}${suffix}.${n}`;
425
+ return candidate;
270
426
  }
271
427
 
272
428
  // src/residue.ts
@@ -280,8 +436,8 @@ var FORBIDDEN_RESIDUE = [
280
436
  "@uiigateway",
281
437
  // predecessor Angular specifics
282
438
  // NOTE: 'ClickUp'/'clickup' were forbidden during the predecessor-port era (the
283
- // ClickUp REST precedent lived in the deleted noir-workflow plugin). Slice X
284
- // reintroduces ClickUp as a first-class Noir integration (skills/integrations/noir-clickup),
439
+ // ClickUp REST precedent lived in the deleted noir-workflow plugin). ClickUp is
440
+ // now reintroduced as a first-class Noir integration (skills/integrations/noir-clickup),
285
441
  // so the token is no longer residue — it is the integration's legitimate subject.
286
442
  "<EXTREMELY-IMPORTANT",
287
443
  // Superpowers rhetoric
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/compiler.ts","../src/discover.ts","../src/integrations-schema.ts","../src/residue.ts"],"sourcesContent":["import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { discoverAll, discoverBuiltin } from './discover.js';\nimport { runtimeEmitsHostMcp } from './integrations-schema.js';\nimport type {\n BuiltinSkill,\n CompiledIntegration,\n CompiledSkill,\n CompileTarget,\n EmitSummary,\n IntegrationSkill,\n SkillFrontmatter,\n ValidationResult,\n} from './types.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\nconst NAME_RE = /^noir-[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst WHEN_START =\n /^(use|using|used|whenever|when|before|after|while|starting|encountering|completing|creating|about to|upon|during|to|for|on)\\b/i;\nconst MAX_DESC = 1024;\n\nexport function parseFrontmatter(md: string): SkillFrontmatter {\n const m = md.match(FRONTMATTER_RE);\n if (!m) throw new Error('Skill missing YAML frontmatter (expected --- ... ---)');\n // Group 1 always exists when the regex matches; guard past noUncheckedIndexedAccess.\n const yaml = m[1];\n if (yaml === undefined) throw new Error('Skill missing YAML frontmatter (expected --- ... ---)');\n const fm = parseYaml(yaml) as SkillFrontmatter;\n if (typeof fm?.name !== 'string' || typeof fm?.description !== 'string') {\n throw new Error('Skill frontmatter requires string `name` + `description`');\n }\n return fm;\n}\n\nexport function bodyOf(md: string): string {\n return md.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?/, '');\n}\n\n/** A WHEN description leads with its trigger. Requiring a leading cue — rather\n * than a loose \"contains when/before/after anywhere\" — avoids false positives\n * (\"A tool that decides when to run tests\") and accepts valid leads (\"Upon…\"). */\nexport function looksLikeWhenDescription(desc: string): boolean {\n return WHEN_START.test(desc.trim());\n}\n\nexport function validateSkill(skill: BuiltinSkill): ValidationResult {\n const errors: string[] = [];\n const { name, description } = skill.frontmatter;\n if (!name) errors.push('missing `name`');\n else if (!NAME_RE.test(name)) errors.push(`name \"${name}\" must match noir-<kebab>`);\n if (basename(skill.dir) !== name) {\n errors.push(`dir \"${basename(skill.dir)}\" must equal name \"${name}\"`);\n }\n if (!description?.trim()) errors.push('missing `description`');\n else if (description.length > MAX_DESC) errors.push(`description exceeds ${MAX_DESC} chars`);\n else if (!looksLikeWhenDescription(description)) {\n errors.push('description must state WHEN to trigger (e.g. \"Use when…\"), not WHAT it does');\n }\n for (const r of skill.references) {\n if (!/^[a-z0-9-]+\\.md$/i.test(r.name)) errors.push(`reference \"${r.name}\" must be <kebab>.md`);\n if (!r.content.trim()) errors.push(`reference \"${r.name}\" is empty`);\n }\n return { ok: errors.length === 0, errors };\n}\n\n/**\n * Compile a builtin skill into the host-shaped `CompiledSkill`. The validator\n * runs for every target (a malformed skill is malformed in any format).\n *\n * - `claude` | `agents-md` | `gemini` | `opencode` (the AGENTS.md-aligned\n * hosts): canonical format copied verbatim — `SKILL.md` plus its\n * `references/` siblings (DS-4). These hosts read the same SKILL.md shape;\n * the EMIT-LOCATION (which dir they land in) is the cli/adapter's job, not\n * the compiler's.\n * - `cursor`: transform into a Cursor `.mdc` rule — ONE file `<name>.mdc`\n * whose frontmatter carries the skill description, a wildcard globs entry,\n * and `alwaysApply: false` (the agent decides whether to pull the rule via\n * the description; never auto-applied). The body is the SKILL.md BODY\n * (frontmatter stripped). Cursor's rule format has no references concept,\n * so references are dropped (documented in the S10 spec risks — the body is\n * the surface).\n *\n * The `target` defaults to `'claude'` for backward compatibility with every\n * existing caller; the verbatim branch produces byte-identical output to v1.1\n * (the regression anchor).\n */\nexport function compileSkill(skill: BuiltinSkill, target: CompileTarget = 'claude'): CompiledSkill {\n const res = validateSkill(skill);\n if (!res.ok) throw new Error(`Cannot compile ${skill.name}: ${res.errors.join('; ')}`);\n if (target === 'cursor') {\n return { name: skill.name, files: [{ path: [`${skill.name}.mdc`], content: toMdc(skill) }] };\n }\n // claude / agents-md / gemini / opencode → canonical verbatim format.\n const files = [\n { path: ['SKILL.md'], content: skill.skillMd },\n ...skill.references.map((r) => ({ path: ['references', r.name], content: r.content })),\n ];\n return { name: skill.name, files };\n}\n\n/**\n * Cursor `.mdc` rule transform — frontmatter `{description, globs, alwaysApply:\n * false}` + the SKILL.md body. The description drives Cursor's agent-decided\n * rule selection (per the S10 spec open-decision default: `alwaysApply: false`).\n * Cursor's rule format has no references/ concept, so the body alone is the\n * surface; references are dropped (documented risk in the spec).\n *\n * Frontmatter is YAML-serialized via the `yaml` package (already a dep) so\n * description escaping (colons, quotes, multi-line) is correct without hand\n * rolling. `lineWidth: 0` keeps descriptions on a single quoted line (predictable\n * byte shape; Cursor tolerates either). */\nfunction toMdc(skill: BuiltinSkill): string {\n const frontmatter = stringifyYaml(\n {\n description: skill.frontmatter.description,\n globs: ['**/*'],\n alwaysApply: false,\n },\n { lineWidth: 0 },\n ).trimEnd();\n const body = bodyOf(skill.skillMd);\n // Cursor's `.mdc` shape: leading `---\\n`, frontmatter, closing `---\\n`, blank\n // line, body. Trailing newline so files concatenate cleanly on disk.\n return `---\\n${frontmatter}\\n---\\n${body.endsWith('\\n') ? body : `${body}\\n`}`;\n}\n\n/**\n * Compile an integration: validates the SKILL.md (same shape as builtins) +\n * renders the same per-skill files. When the integration's `runtime` widens\n * emission (`mcp-stdio`/`external-mcp`) AND it carries a non-null `mcp`\n * declaration, also expose a provider-neutral `hostMcp` server block the host\n * adapter may merge into the host MCP config (e.g. Claude's `.mcp.json`).\n *\n * For `gated-write-proxy` (ClickUp) + `none` → no `hostMcp` (writes route\n * through Noir's own MCP tool; the host MCP config is unchanged). For\n * `mcp-stdio` the hostMcp is exposed (the compiler widens emission); the\n * Claude adapter still does not emit a NEW server entry for it (Noir's own\n * `noir mcp serve` entry already covers it).\n */\nexport function compileIntegration(\n integration: IntegrationSkill,\n target: CompileTarget = 'claude',\n): CompiledIntegration {\n // Validate the skill half first (throws on a bad SKILL.md — same path as\n // builtins). compileSkill returns a base CompiledSkill; we layer hostMcp on.\n const base = compileSkill(integration, target);\n const { runtime, mcp } = integration.declaration;\n if (runtimeEmitsHostMcp(runtime) && mcp !== null) {\n return {\n ...base,\n hostMcp: {\n serverName: integration.name,\n command: mcp.command,\n args: mcp.args,\n transport: mcp.transport,\n url: mcp.url,\n env: mcp.env,\n },\n };\n }\n return base;\n}\n\nexport async function emitSkillsToDir(\n targetDir: string,\n opts: {\n builtinDir?: string;\n integrationsDir?: string;\n includeIntegrations?: boolean;\n /** S10 CompileTarget — selects the per-skill transform. Defaults to\n * `'claude'` (the v1.1 verbatim SKILL.md shape) so every existing caller\n * stays byte-identical. `'cursor'` compiles each skill to a `.mdc` rule\n * file (frontmatter `description`/`globs`/`alwaysApply:false` + the\n * SKILL.md body); the other targets map to the verbatim shape (their\n * hosts have no skill concept, so emit is skipped upstream — but the\n * default-verbatim policy keeps the signature total). */\n target?: CompileTarget;\n } = {},\n): Promise<EmitSummary> {\n const target: CompileTarget = opts.target ?? 'claude';\n const { builtins, integrations } = discoverAll({\n builtinDir: opts.builtinDir,\n integrationsDir: opts.integrationsDir,\n });\n const emitIntegrations = opts.includeIntegrations ?? true;\n // Validate the whole pack before writing anything (fail-fast, atomic-ish).\n for (const s of builtins) {\n const res = validateSkill(s);\n if (!res.ok) throw new Error(`Invalid builtin skill ${s.name}: ${res.errors.join('; ')}`);\n }\n if (emitIntegrations) {\n for (const i of integrations) {\n const res = validateSkill(i);\n if (!res.ok) throw new Error(`Invalid integration ${i.name}: ${res.errors.join('; ')}`);\n }\n }\n await mkdir(targetDir, { recursive: true });\n let references = 0;\n const emitted: string[] = [];\n const integrationNames: string[] = [];\n // C3: cursor skills land FLAT under targetDir (`.cursor/rules/<name>.mdc`) —\n // Cursor's rule loader scans `.cursor/rules/*.mdc` and does NOT recurse into\n // per-name subdirs. The verbatim branch (claude/agents-md/gemini/opencode)\n // keeps the canonical nested layout (`<name>/SKILL.md` + `<name>/references/`).\n const flat = target === 'cursor';\n for (const s of builtins) {\n const compiled = compileSkill(s, target);\n for (const f of compiled.files) {\n const dest = flat ? join(targetDir, ...f.path) : join(targetDir, s.name, ...f.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, f.content, 'utf8');\n if (f.path[0] !== 'SKILL.md') references++;\n }\n emitted.push(s.name);\n }\n if (emitIntegrations) {\n for (const i of integrations) {\n const compiled = compileIntegration(i, target);\n for (const f of compiled.files) {\n const dest = flat ? join(targetDir, ...f.path) : join(targetDir, i.name, ...f.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, f.content, 'utf8');\n if (f.path[0] !== 'SKILL.md') references++;\n }\n // `hostMcp` is NOT written per-skill — it is surfaced to the host adapter\n // (via discoverAll/compileIntegration) to merge into the host's single\n // MCP config (e.g. `.mcp.json`). Wiring lives in cli/daemon (X-T2 seam).\n emitted.push(i.name);\n integrationNames.push(i.name);\n }\n }\n\n // --- T2: prune stale `noir-*` entries from the managed namespace ----------\n // Idempotent hygiene: a previous Noir version may have shipped a builtin that\n // was since renamed/removed (e.g. `noir-old-thing`). Each `noir sync`\n // re-writes the CURRENT pack but a stale entry would otherwise linger forever.\n // After emit, scan `targetDir` for `noir-`-prefixed entries NOT in the emitted\n // set and remove them. ONLY the `noir-` namespace — user skills without the\n // prefix are NEVER touched (they are not Noir's to manage).\n //\n // C3 shape awareness: the nested layout (claude/agents-md/gemini/opencode)\n // writes one `noir-<name>/` DIR per skill → prune stale DIRS. The cursor flat\n // layout writes one `noir-<name>.mdc` FILE per skill → prune stale .mdc FILES.\n // Cursor ALSO clears legacy pre-C3 `noir-<name>/` dirs (nesting residue) so an\n // upgrade from nested→flat does not leave orphans under `.cursor/rules/`.\n const keep = new Set(emitted);\n const pruned: string[] = [];\n let dirEntries: import('node:fs').Dirent[] = [];\n try {\n dirEntries = await readdir(targetDir, { withFileTypes: true });\n } catch {\n dirEntries = []; // targetDir vanished between the mkdir above and now — nothing to prune.\n }\n for (const ent of dirEntries) {\n if (!ent.name.startsWith('noir-')) continue;\n if (flat) {\n // Cursor flat layout — prune stale FILES (`noir-<name>.mdc`) + legacy\n // nested DIRS (`noir-<name>/`) from a pre-C3 sync.\n if (ent.isFile()) {\n const mdcMatch = ent.name.match(/^(noir-[a-z0-9]+(?:-[a-z0-9]+)*)\\.mdc$/);\n const skillName = mdcMatch?.[1];\n if (skillName && keep.has(skillName)) continue;\n } else if (ent.isDirectory()) {\n // Legacy pre-C3 cursor nested dir — clear unconditionally (the flat\n // layout has NO `noir-*/` dirs under .cursor/rules/; any such dir is\n // stale by definition, regardless of name overlap with the current\n // pack — the fresh `.mdc` file is what's kept, not the dir).\n } else {\n continue;\n }\n } else {\n // Nested layout — prune stale DIRS only (the canonical shape).\n if (!ent.isDirectory()) continue;\n if (keep.has(ent.name)) continue;\n }\n // Best-effort removal: a failure to remove a stale entry must NOT fail the\n // emit (the fresh skills are already on disk and valid). Surface it via the\n // summary so a caller can warn; the next sync will try again.\n try {\n await rm(join(targetDir, ent.name), { recursive: true, force: true });\n pruned.push(ent.name);\n } catch {\n // Swallow — stale-entry pruning is hygienic, not correctness-critical.\n }\n }\n\n return { dir: targetDir, emitted, references, integrations: integrationNames, pruned };\n}\n\n// Convenience for callers/tests that already hold raw markdown (unused by emit path;\n// kept so adapters/tests can validate a single in-memory skill without a dir).\nexport { discoverAll, discoverBuiltin };\n","import { readdirSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseFrontmatter } from './compiler.js';\nimport { parseIntegration } from './integrations-schema.js';\nimport type { BuiltinSkill, IntegrationDeclaration, IntegrationSkill } from './types.js';\n\n// Package root = ONE level up from THIS module's directory. Works in every layout\n// because `builtin/` is always a sibling of the dir this code runs from:\n// - vitest: .../packages/skills/src/discover.ts -> HERE = src -> PKG_ROOT = package root\n// - built: .../packages/skills/dist/index.js -> HERE = dist -> PKG_ROOT = package root\n// (tsup bundles everything into dist/index.js; `dirname` still yields `dist`.)\nconst HERE = dirname(fileURLToPath(import.meta.url)); // .../src OR .../dist\nconst PKG_ROOT = dirname(HERE); // .../packages/skills\nexport const BUILTIN_DIR = join(PKG_ROOT, 'builtin');\nexport const INTEGRATIONS_DIR = join(PKG_ROOT, 'integrations');\n\n/** Read a `<name>/SKILL.md` + its optional `references/` directory. Shared by\n * builtin + integration discovery (both share the SKILL.md + references shape;\n * the difference is the `integration.json` declaration). */\nfunction readSkillMd(dir: string): Pick<BuiltinSkill, 'skillMd' | 'frontmatter' | 'references'> {\n const skillMd = readFileSync(join(dir, 'SKILL.md'), 'utf8');\n const frontmatter = parseFrontmatter(skillMd);\n const refsDir = join(dir, 'references');\n let references: BuiltinSkill['references'] = [];\n try {\n references = readdirSync(refsDir)\n .filter((f) => f.endsWith('.md'))\n .sort()\n .map((f) => ({ name: f, content: readFileSync(join(refsDir, f), 'utf8') }));\n } catch {\n references = []; // no references/ dir is fine\n }\n return { skillMd, frontmatter, references };\n}\n\nexport function discoverBuiltin(builtinDir: string = BUILTIN_DIR): BuiltinSkill[] {\n const dirs = readdirSync(builtinDir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name.startsWith('noir-'))\n .map((e) => e.name)\n .sort();\n return dirs.map((name) => {\n const dir = join(builtinDir, name);\n return { name, dir, ...readSkillMd(dir) };\n });\n}\n\n/**\n * Discover every shipped integration under `integrations/<name>/` (sibling of\n * `builtin/`). Each entry is a builtin-shaped skill (validated `SKILL.md` +\n * optional references) PLUS a parsed + validated `integration.json`\n * declaration. The dir is resolved relative to the package root by default —\n * when `integrationsDir` is overridden (tests), only that dir is scanned.\n *\n * Throws an aggregate error if any integration is malformed (missing\n * `SKILL.md`, bad frontmatter, or an `integration.json` that fails the Zod\n * schema) — fail-fast, atomic-ish, mirroring the builtin compiler's posture.\n */\nexport function discoverIntegrations(\n integrationsDir: string = INTEGRATIONS_DIR,\n): IntegrationSkill[] {\n let entries: string[];\n try {\n entries = readdirSync(integrationsDir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name.startsWith('noir-'))\n .map((e) => e.name)\n .sort();\n } catch {\n // No integrations/ dir (or unreadable) = no integrations shipped here.\n return [];\n }\n return entries.map((name) => {\n const dir = join(integrationsDir, name);\n const declarationRaw = JSON.parse(\n readFileSync(join(dir, 'integration.json'), 'utf8'),\n ) as unknown;\n let declaration: IntegrationDeclaration;\n try {\n declaration = parseIntegration(declarationRaw);\n } catch (err) {\n // ZodError → wrap with the integration's dir for an actionable message.\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Invalid integration.json in ${dir}: ${msg}`);\n }\n if (declaration.name !== name) {\n throw new Error(\n `Integration dir \"${name}\" must equal declaration name \"${declaration.name}\"`,\n );\n }\n return { name, dir, ...readSkillMd(dir), declaration, declarationRaw };\n });\n}\n\n/**\n * Discover the full shipped pack — builtins + integrations — in one call. Used\n * by `emitSkillsToDir` so `noir init`/`sync` emit BOTH (slice X goal). The\n * `integrationsDir` defaults to the sibling of `builtinDir` so a test fixture\n * that overrides only `builtinDir` cleanly resolves integrations to a\n * non-existent path (returns `[]`) instead of accidentally picking up the\n * shipped integrations.\n */\nexport function discoverAll(opts: { builtinDir?: string; integrationsDir?: string } = {}): {\n builtins: BuiltinSkill[];\n integrations: IntegrationSkill[];\n} {\n const builtinDir = opts.builtinDir ?? BUILTIN_DIR;\n const integrationsDir = opts.integrationsDir ?? join(dirname(builtinDir), 'integrations');\n return {\n builtins: discoverBuiltin(builtinDir),\n integrations: discoverIntegrations(integrationsDir),\n };\n}\n","// Slice X — `integration.json` schema. Each integration ships a declaration\n// alongside its `SKILL.md` so the compiler can validate the auth/runtime/SDD\n// contract uniformly and so the host adapter knows whether to widen emission\n// (host MCP config) or stay skill-only.\n//\n// Doctrine (slice-x spec §architecture + v1x §4.4): skill-only by default;\n// `gated-write-proxy` for stateless writes routed through a Noir MCP tool\n// (X-T3); `mcp-stdio` for a Noir-spawned stateful runtime (deferred, gated on\n// keychain); `external-mcp` for a config-only pointer at a first-party/community\n// MCP server (emits a host `.mcp.json` server entry, no Noir runtime).\n//\n// Zod v4 (`z.record(keyType, valueType)` is key-first). The schema is additive\n// and defaults to the safest tier — every optional field degrades to \"skill\n// only, manual paste, no host MCP wiring\" — so a minimal `{ \"name\":\n// \"noir-x\" }` declaration still parses + behaves as a pure playbook.\n\nimport * as z from 'zod';\n\n/** Auth shape. Locked: `env-var` only until keychain lands (Q4b — refuse OAuth,\n * never silently lower the security bar). `fallback:'manual-paste'` keeps the\n * no-token path honest (the playbook tells the user to paste a value); `'none'`\n * is for integrations that genuinely do not need a token (read-only public\n * endpoints). */\nexport const IntegrationAuthSchema = z.object({\n type: z.literal('env-var'),\n tokenEnv: z.string().min(1),\n fallback: z.enum(['manual-paste', 'none']).default('manual-paste'),\n});\n\n/** SDD two-way binding. `intakeFrom` declares the external artifact kind the\n * `noir-intake` skill pulls from; `writeBack` enumerates the fields\n * `noir-wrap`/`noir-document` push back at session end. Strings (not enums)\n * for `writeBack` so a per-integration vocabulary stays expressible without\n * churning the schema. */\nexport const IntegrationSddSchema = z\n .object({\n intakeFrom: z.enum(['task', 'issue', 'none']).optional(),\n writeBack: z.array(z.string()).default([]),\n })\n .default({ writeBack: [] });\n\n/** Host MCP declaration. Only meaningful when `runtime ∈ {mcp-stdio,\n * external-mcp}`; `null` for `none`/`gated-write-proxy` (ClickUp — writes go\n * through Noir's own MCP tool, never the host's). */\nexport const IntegrationMcpSchema = z\n .object({\n command: z.string().min(1),\n transport: z.enum(['stdio', 'http']),\n args: z.array(z.string()).optional(),\n url: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n })\n .nullable()\n .default(null);\n\n/** Full `integration.json` declaration. `name` enforces the `noir-` prefix\n * shared with builtins so the unified pack reads consistently. */\nexport const IntegrationDeclarationSchema = z.object({\n name: z.string().regex(/^noir-[a-z0-9]+(?:-[a-z0-9]+)*$/, 'must match noir-<kebab>'),\n auth: IntegrationAuthSchema,\n runtime: z.enum(['none', 'gated-write-proxy', 'mcp-stdio', 'external-mcp']),\n sdd: IntegrationSddSchema,\n mcp: IntegrationMcpSchema,\n});\n\nexport type IntegrationDeclaration = z.infer<typeof IntegrationDeclarationSchema>;\n\n/** Coerce + validate an `integration.json` payload. Throws a ZodError on a\n * malformed declaration — the caller (discover) wraps it with the\n * integration's dir for an actionable message. */\nexport function parseIntegration(json: unknown): IntegrationDeclaration {\n return IntegrationDeclarationSchema.parse(json);\n}\n\n/** Non-throwing variant for the hygiene tests + lint helpers. */\nexport function validateIntegration(\n obj: unknown,\n): { ok: true; value: IntegrationDeclaration } | { ok: false; errors: string[] } {\n const parsed = IntegrationDeclarationSchema.safeParse(obj);\n if (parsed.success) return { ok: true, value: parsed.data };\n return { ok: false, errors: parsed.error.issues.map((i: { message: string }) => i.message) };\n}\n\n/** Runtime → host-MCP emission predicate. The compiler widens emission (makes\n * the host MCP block available to the adapter) ONLY for these tiers. The\n * adapter ultimately decides per-host what to render — for Claude, `external-\n * mcp` becomes a `.mcp.json` server entry; `mcp-stdio` registers through the\n * existing `noir mcp serve --stdio` entry so no NEW server entry is emitted. */\nexport function runtimeEmitsHostMcp(runtime: IntegrationDeclaration['runtime']): boolean {\n return runtime === 'mcp-stdio' || runtime === 'external-mcp';\n}\n","// Tokens that must NOT survive a port (predecessor plugin internals + Superpowers\n// rhetoric). Shared by the compiler's lint helper and the hygiene tests (T2–T5).\nexport const FORBIDDEN_RESIDUE: readonly string[] = [\n 'workflow/<task', // predecessor state file\n 'noir-workflow.mode', // predecessor mode flag\n 'noir-workflow', // predecessor plugin name (as a plugin/path reference)\n '@uiigateway', // predecessor Angular specifics\n // NOTE: 'ClickUp'/'clickup' were forbidden during the predecessor-port era (the\n // ClickUp REST precedent lived in the deleted noir-workflow plugin). Slice X\n // reintroduces ClickUp as a first-class Noir integration (skills/integrations/noir-clickup),\n // so the token is no longer residue — it is the integration's legitimate subject.\n '<EXTREMELY-IMPORTANT', // Superpowers rhetoric\n 'SUBAGENT-STOP', // Superpowers rhetoric\n 'plugins/noir-workflow', // predecessor path\n];\n"],"mappings":"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","names":["dirname","join","join","dirname"]}
1
+ {"version":3,"sources":["../src/compiler.ts","../src/discover.ts","../src/integrations-schema.ts","../src/residue.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { discoverAll, discoverBuiltin } from './discover.js';\nimport { runtimeEmitsHostMcp } from './integrations-schema.js';\nimport type {\n BuiltinSkill,\n CompiledIntegration,\n CompiledSkill,\n CompileTarget,\n EmitSummary,\n IntegrationSkill,\n SkillConflict,\n SkillConflictContext,\n SkillConflictResolution,\n SkillConflictResolver,\n SkillFrontmatter,\n ValidationResult,\n} from './types.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\nconst NAME_RE = /^noir-[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst WHEN_START =\n /^(use|using|used|whenever|when|before|after|while|starting|encountering|completing|creating|about to|upon|during|to|for|on)\\b/i;\nconst MAX_DESC = 1024;\n\nexport function parseFrontmatter(md: string): SkillFrontmatter {\n const m = md.match(FRONTMATTER_RE);\n if (!m) throw new Error('Skill missing YAML frontmatter (expected --- ... ---)');\n // Group 1 always exists when the regex matches; guard past noUncheckedIndexedAccess.\n const yaml = m[1];\n if (yaml === undefined) throw new Error('Skill missing YAML frontmatter (expected --- ... ---)');\n const fm = parseYaml(yaml) as SkillFrontmatter;\n if (typeof fm?.name !== 'string' || typeof fm?.description !== 'string') {\n throw new Error('Skill frontmatter requires string `name` + `description`');\n }\n return fm;\n}\n\nexport function bodyOf(md: string): string {\n return md.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?/, '');\n}\n\n/** A WHEN description leads with its trigger. Requiring a leading cue — rather\n * than a loose \"contains when/before/after anywhere\" — avoids false positives\n * (\"A tool that decides when to run tests\") and accepts valid leads (\"Upon…\"). */\nexport function looksLikeWhenDescription(desc: string): boolean {\n return WHEN_START.test(desc.trim());\n}\n\nexport function validateSkill(skill: BuiltinSkill): ValidationResult {\n const errors: string[] = [];\n const { name, description } = skill.frontmatter;\n if (!name) errors.push('missing `name`');\n else if (!NAME_RE.test(name)) errors.push(`name \"${name}\" must match noir-<kebab>`);\n if (basename(skill.dir) !== name) {\n errors.push(`dir \"${basename(skill.dir)}\" must equal name \"${name}\"`);\n }\n if (!description?.trim()) errors.push('missing `description`');\n else if (description.length > MAX_DESC) errors.push(`description exceeds ${MAX_DESC} chars`);\n else if (!looksLikeWhenDescription(description)) {\n errors.push('description must state WHEN to trigger (e.g. \"Use when…\"), not WHAT it does');\n }\n for (const r of skill.references) {\n if (!/^[a-z0-9-]+\\.md$/i.test(r.name)) errors.push(`reference \"${r.name}\" must be <kebab>.md`);\n if (!r.content.trim()) errors.push(`reference \"${r.name}\" is empty`);\n }\n return { ok: errors.length === 0, errors };\n}\n\n/**\n * Compile a builtin skill into the host-shaped `CompiledSkill`. The validator\n * runs for every target (a malformed skill is malformed in any format).\n *\n * - `claude` | `agents-md` | `gemini` | `opencode` (the AGENTS.md-aligned\n * hosts): canonical format copied verbatim — `SKILL.md` plus its\n * `references/` siblings. These hosts read the same SKILL.md shape;\n * the EMIT-LOCATION (which dir they land in) is the cli/adapter's job, not\n * the compiler's.\n * - `cursor`: transform into a Cursor `.mdc` rule — ONE file `<name>.mdc`\n * whose frontmatter carries the skill description, a wildcard globs entry,\n * and `alwaysApply: false` (the agent decides whether to pull the rule via\n * the description; never auto-applied). The body is the SKILL.md BODY\n * (frontmatter stripped). Cursor's rule format has no references concept,\n * so references are dropped (documented in the S10 spec risks — the body is\n * the surface).\n *\n * The `target` defaults to `'claude'` for backward compatibility with every\n * existing caller; the verbatim branch produces byte-identical output to v1.1\n * (the regression anchor).\n */\nexport function compileSkill(skill: BuiltinSkill, target: CompileTarget = 'claude'): CompiledSkill {\n const res = validateSkill(skill);\n if (!res.ok) throw new Error(`Cannot compile ${skill.name}: ${res.errors.join('; ')}`);\n if (target === 'cursor') {\n return { name: skill.name, files: [{ path: [`${skill.name}.mdc`], content: toMdc(skill) }] };\n }\n // claude / agents-md / gemini / opencode → canonical verbatim format.\n const files = [\n { path: ['SKILL.md'], content: skill.skillMd },\n ...skill.references.map((r) => ({ path: ['references', r.name], content: r.content })),\n ];\n return { name: skill.name, files };\n}\n\n/**\n * Cursor `.mdc` rule transform — frontmatter `{description, globs, alwaysApply:\n * false}` + the SKILL.md body. The description drives Cursor's agent-decided\n * rule selection (per the S10 spec open-decision default: `alwaysApply: false`).\n * Cursor's rule format has no references/ concept, so the body alone is the\n * surface; references are dropped (documented risk in the spec).\n *\n * Frontmatter is YAML-serialized via the `yaml` package (already a dep) so\n * description escaping (colons, quotes, multi-line) is correct without hand\n * rolling. `lineWidth: 0` keeps descriptions on a single quoted line (predictable\n * byte shape; Cursor tolerates either). */\nfunction toMdc(skill: BuiltinSkill): string {\n const frontmatter = stringifyYaml(\n {\n description: skill.frontmatter.description,\n globs: ['**/*'],\n alwaysApply: false,\n },\n { lineWidth: 0 },\n ).trimEnd();\n const body = bodyOf(skill.skillMd);\n // Cursor's `.mdc` shape: leading `---\\n`, frontmatter, closing `---\\n`, blank\n // line, body. Trailing newline so files concatenate cleanly on disk.\n return `---\\n${frontmatter}\\n---\\n${body.endsWith('\\n') ? body : `${body}\\n`}`;\n}\n\n/**\n * Compile an integration: validates the SKILL.md (same shape as builtins) +\n * renders the same per-skill files. When the integration's `runtime` widens\n * emission (`mcp-stdio`/`external-mcp`) AND it carries a non-null `mcp`\n * declaration, also expose a provider-neutral `hostMcp` server block the host\n * adapter may merge into the host MCP config (e.g. Claude's `.mcp.json`).\n *\n * For `gated-write-proxy` (ClickUp) + `none` → no `hostMcp` (writes route\n * through Noir's own MCP tool; the host MCP config is unchanged). For\n * `mcp-stdio` the hostMcp is exposed (the compiler widens emission); the\n * Claude adapter still does not emit a NEW server entry for it (Noir's own\n * `noir mcp serve` entry already covers it).\n */\nexport function compileIntegration(\n integration: IntegrationSkill,\n target: CompileTarget = 'claude',\n): CompiledIntegration {\n // Validate the skill half first (throws on a bad SKILL.md — same path as\n // builtins). compileSkill returns a base CompiledSkill; we layer hostMcp on.\n const base = compileSkill(integration, target);\n const { runtime, mcp } = integration.declaration;\n if (runtimeEmitsHostMcp(runtime) && mcp !== null) {\n return {\n ...base,\n hostMcp: {\n serverName: integration.name,\n command: mcp.command,\n args: mcp.args,\n transport: mcp.transport,\n url: mcp.url,\n env: mcp.env,\n },\n };\n }\n return base;\n}\n\nexport async function emitSkillsToDir(\n targetDir: string,\n opts: {\n builtinDir?: string;\n integrationsDir?: string;\n includeIntegrations?: boolean;\n /** S10 CompileTarget — selects the per-skill transform. Defaults to\n * `'claude'` (the v1.1 verbatim SKILL.md shape) so every existing caller\n * stays byte-identical. `'cursor'` compiles each skill to a `.mdc` rule\n * file (frontmatter `description`/`globs`/`alwaysApply:false` + the\n * SKILL.md body); the other targets map to the verbatim shape (their\n * hosts have no skill concept, so emit is skipped upstream — but the\n * default-verbatim policy keeps the signature total). */\n target?: CompileTarget;\n /** Policy for a skill file that exists AND differs from the compiled\n * bytes, when no {@link onConflict} resolver is wired (or non-interactive).\n * Default `'overwrite'` preserves the v1.2 behavior (every sync clobbers).\n * `'preserve'` skips the differing file (the CI / non-TTY default the CLI\n * threads via `buildConflictOpts`). */\n conflictPolicy?: 'overwrite' | 'preserve';\n /** Per-file conflict resolver. The CLI passes its single\n * `buildConflictOpts().onConflict` (clack menu + diff preview + apply-to-\n * all) through unchanged; structurally compatible with @noir-ai/create's\n * `ConflictResolverReturn`. */\n onConflict?: SkillConflictResolver;\n /** Explicit interactivity flag (the engine reads THIS, not process.env,\n * matching @noir-ai/create's hermetic-interactive contract). When `false`,\n * the resolver is NEVER consulted (CI / --json never hangs on a prompt). */\n interactive?: boolean;\n } = {},\n): Promise<EmitSummary> {\n const target: CompileTarget = opts.target ?? 'claude';\n const { builtins, integrations } = discoverAll({\n builtinDir: opts.builtinDir,\n integrationsDir: opts.integrationsDir,\n });\n const emitIntegrations = opts.includeIntegrations ?? true;\n // Validate the whole pack before writing anything (fail-fast, atomic-ish).\n for (const s of builtins) {\n const res = validateSkill(s);\n if (!res.ok) throw new Error(`Invalid builtin skill ${s.name}: ${res.errors.join('; ')}`);\n }\n if (emitIntegrations) {\n for (const i of integrations) {\n const res = validateSkill(i);\n if (!res.ok) throw new Error(`Invalid integration ${i.name}: ${res.errors.join('; ')}`);\n }\n }\n await mkdir(targetDir, { recursive: true });\n let references = 0;\n const emitted: string[] = [];\n const integrationNames: string[] = [];\n const conflicts: SkillConflict[] = [];\n // Apply-to-all memory keyed by artifact CLASS. Skill emission shares one\n // decision across the run (a `noir sync` touching 8 skill files → 1 prompt)\n // when the resolver returns `{resolution, applyToAll: true}`.\n const memory = new Map<string, SkillConflictResolution>();\n const policy: 'overwrite' | 'preserve' = opts.conflictPolicy ?? 'overwrite';\n // Non-interactive guard. The bridge `NOIR_NON_INTERACTIVE` is set by the\n // bin's preAction under --json/--no-input; the explicit `interactive: false`\n // wins over the env (mirrors @noir-ai/create's hermetic flag). When false,\n // the resolver is NEVER consulted — differing files fall back to `policy`.\n const interactive =\n opts.interactive ??\n (process.env.NOIR_NON_INTERACTIVE === undefined || process.env.NOIR_NON_INTERACTIVE === '');\n\n /** Write a compiled skill file through the SAME conflict seam as\n * `regenerate`: read existing; if differs, consult the resolver (when\n * interactive + wired); record the conflict for the structured report. */\n const writeWithConflict = async (\n absDest: string,\n relDest: string,\n content: string,\n isReference: boolean,\n ): Promise<void> => {\n let existing: string | undefined;\n try {\n existing = await readFile(absDest, 'utf8');\n } catch {\n existing = undefined;\n }\n if (existing !== undefined && existing !== content) {\n const MODE = 'skill' as const;\n let resolution: SkillConflictResolution;\n const remembered = memory.get(MODE);\n if (remembered !== undefined) {\n resolution = remembered;\n } else if (interactive && opts.onConflict !== undefined) {\n const ctx: SkillConflictContext = {\n relPath: relDest,\n existing,\n proposed: content,\n mode: MODE,\n };\n const ret = await opts.onConflict(ctx);\n const unwrapped: SkillConflictResolution = typeof ret === 'string' ? ret : ret.resolution;\n if (typeof ret !== 'string' && ret.applyToAll === true) {\n memory.set(MODE, unwrapped);\n }\n resolution = unwrapped;\n } else {\n resolution = policy === 'preserve' ? 'preserve' : 'replace';\n }\n conflicts.push({\n path: relDest,\n mode: MODE,\n similarity: similarity(existing, content),\n existingSha: sha256Hex12(existing),\n proposedSha: sha256Hex12(content),\n resolution,\n });\n if (resolution === 'cancel') {\n throw new Error(`skill emit cancelled by user at ${relDest}`);\n }\n if (resolution === 'preserve') {\n // Skip this file; the user's bytes stand. Do not count as a reference.\n return;\n }\n if (resolution === 'rename') {\n // Move the user's file aside (unique suffix), then write the compiled bytes.\n await renameAside(absDest, '.local');\n } else if (resolution === 'duplicate') {\n // Write the compiled bytes to <absDest>.noir; keep the user's file.\n await writeFile(await uniqueAside(absDest, '.noir'), content, 'utf8');\n return;\n }\n // 'replace' → fall through and overwrite.\n }\n await writeFile(absDest, content, 'utf8');\n if (isReference) references++;\n };\n\n // Cursor skills land FLAT under targetDir (`.cursor/rules/<name>.mdc`) —\n // Cursor's rule loader scans `.cursor/rules/*.mdc` and does NOT recurse into\n // per-name subdirs. The verbatim branch (claude/agents-md/gemini/opencode)\n // keeps the canonical nested layout (`<name>/SKILL.md` + `<name>/references/`).\n const flat = target === 'cursor';\n for (const s of builtins) {\n const compiled = compileSkill(s, target);\n for (const f of compiled.files) {\n const dest = flat ? join(targetDir, ...f.path) : join(targetDir, s.name, ...f.path);\n await mkdir(dirname(dest), { recursive: true });\n const rel = flat ? f.path.join('/') : [s.name, ...f.path].join('/');\n await writeWithConflict(dest, rel, f.content, f.path[0] !== 'SKILL.md');\n }\n emitted.push(s.name);\n }\n if (emitIntegrations) {\n for (const i of integrations) {\n const compiled = compileIntegration(i, target);\n for (const f of compiled.files) {\n const dest = flat ? join(targetDir, ...f.path) : join(targetDir, i.name, ...f.path);\n await mkdir(dirname(dest), { recursive: true });\n const rel = flat ? f.path.join('/') : [i.name, ...f.path].join('/');\n await writeWithConflict(dest, rel, f.content, f.path[0] !== 'SKILL.md');\n }\n // `hostMcp` is NOT written per-skill — it is surfaced to the host adapter\n // (via discoverAll/compileIntegration) to merge into the host's single\n // MCP config (e.g. `.mcp.json`). Wiring lives in cli/daemon.\n emitted.push(i.name);\n integrationNames.push(i.name);\n }\n }\n\n // --- Prune stale `noir-*` entries from the managed namespace ----------\n // Idempotent hygiene: a previous Noir version may have shipped a builtin that\n // was since renamed/removed (e.g. `noir-old-thing`). Each `noir sync`\n // re-writes the CURRENT pack but a stale entry would otherwise linger forever.\n // After emit, scan `targetDir` for `noir-`-prefixed entries NOT in the emitted\n // set and remove them. ONLY the `noir-` namespace — user skills without the\n // prefix are NEVER touched (they are not Noir's to manage).\n //\n // `assertNotUserOwned` guard: a `noir-*` entry whose content does NOT\n // match the canonical Noir-emitted shape (SKILL.md with `name: noir-…`\n // frontmatter, OR a `.mdc` with the cursor frontmatter) is treated as\n // USER-AUTHORED and LEFT ALONE. A user hand-rolling `noir-myown/SKILL.md`\n // must never be silently deleted by a sync.\n //\n // Shape awareness: the nested layout (claude/agents-md/gemini/opencode)\n // writes one `noir-<name>/` DIR per skill → prune stale DIRS. The cursor flat\n // layout writes one `noir-<name>.mdc` FILE per skill → prune stale .mdc FILES.\n // Cursor ALSO clears legacy `noir-<name>/` dirs (nesting residue) so an\n // upgrade from nested→flat does not leave orphans under `.cursor/rules/`.\n const keep = new Set(emitted);\n const pruned: string[] = [];\n const preservedUserOwned: string[] = [];\n let dirEntries: import('node:fs').Dirent[] = [];\n try {\n dirEntries = await readdir(targetDir, { withFileTypes: true });\n } catch {\n dirEntries = []; // targetDir vanished between the mkdir above and now — nothing to prune.\n }\n for (const ent of dirEntries) {\n if (!ent.name.startsWith('noir-')) continue;\n if (flat) {\n // Cursor flat layout — prune stale FILES (`noir-<name>.mdc`) + legacy\n // nested DIRS (`noir-<name>/`) from a sync.\n if (ent.isFile()) {\n const mdcMatch = ent.name.match(/^(noir-[a-z0-9]+(?:-[a-z0-9]+)*)\\.mdc$/);\n const skillName = mdcMatch?.[1];\n if (skillName && keep.has(skillName)) continue;\n // `assertNotUserOwned`: a `.mdc` without canonical cursor\n // frontmatter (globs + alwaysApply) was hand-rolled by the user; skip.\n if (await isUserOwnedMdc(join(targetDir, ent.name))) {\n preservedUserOwned.push(ent.name);\n continue;\n }\n } else if (ent.isDirectory()) {\n // Legacy cursor nested dir — clear unconditionally (the flat\n // layout has NO `noir-{name}/` dirs under .cursor/rules/; any such dir\n // is stale by definition, regardless of name overlap with the current\n // pack — the fresh `.mdc` file is what's kept, not the dir).\n // Guard: skip if NONE of the `.mdc` files inside look Noir-emitted\n // (canonical cursor frontmatter) — a user hand-rolling a noir-X/ dir\n // with arbitrary content is preserved.\n if (await isUserOwnedCursorLegacyDir(join(targetDir, ent.name))) {\n preservedUserOwned.push(ent.name);\n continue;\n }\n } else {\n continue;\n }\n } else {\n // Nested layout — prune stale DIRS only (the canonical shape).\n if (!ent.isDirectory()) continue;\n if (keep.has(ent.name)) continue;\n // `assertNotUserOwned`: a `noir-*/` dir whose SKILL.md does not\n // carry canonical Noir frontmatter (`name: noir-…`) is user-authored.\n if (await isUserOwnedSkillDir(join(targetDir, ent.name))) {\n preservedUserOwned.push(ent.name);\n continue;\n }\n }\n // Best-effort removal: a failure to remove a stale entry must NOT fail the\n // emit (the fresh skills are already on disk and valid). Surface it via the\n // summary so a caller can warn; the next sync will try again.\n try {\n await rm(join(targetDir, ent.name), { recursive: true, force: true });\n pruned.push(ent.name);\n } catch {\n // Swallow — stale-entry pruning is hygienic, not correctness-critical.\n }\n }\n\n return {\n dir: targetDir,\n emitted,\n references,\n integrations: integrationNames,\n pruned,\n ...(preservedUserOwned.length > 0 ? { preservedUserOwned } : {}),\n ...(conflicts.length > 0 ? { conflicts } : {}),\n };\n}\n\n/** `assertNotUserOwned` for the nested layout: a `noir-{name}/` dir is\n * Noir-managed iff it contains a SKILL.md whose YAML frontmatter carries a\n * `name:` matching `noir-<dir>`. Missing SKILL.md, missing frontmatter, or a\n * non-matching name ⇒ user-authored (left alone). Best-effort: any IO error\n * ⇒ treat as user-owned (the safe default — never silently delete). */\nasync function isUserOwnedSkillDir(dirAbs: string): Promise<boolean> {\n try {\n const skillMd = await readFile(join(dirAbs, 'SKILL.md'), 'utf8');\n const m = skillMd.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!m) return true; // no frontmatter at all → user-authored\n const fm = parseYaml(m[1] ?? '') as { name?: unknown };\n const dirName = basename(dirAbs);\n return typeof fm?.name !== 'string' || fm.name !== dirName;\n } catch {\n return true; // ENOENT (no SKILL.md) or parse error → user-authored (safe)\n }\n}\n\n/** `assertNotUserOwned` for the cursor flat layout: a `noir-*.mdc` file\n * is Noir-managed iff its YAML frontmatter carries `alwaysApply:` (the cursor\n * shape the compiler always emits). Missing frontmatter / no `alwaysApply` ⇒\n * user-authored. */\nasync function isUserOwnedMdc(fileAbs: string): Promise<boolean> {\n try {\n const md = await readFile(fileAbs, 'utf8');\n const m = md.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!m) return true;\n const fm = parseYaml(m[1] ?? '') as { alwaysApply?: unknown };\n return fm?.alwaysApply === undefined;\n } catch {\n return true;\n }\n}\n\n/** `assertNotUserOwned` for the cursor flat-layout's legacy-nested-dir\n * case: a Noir sync left `noir-{name}/noir-{name}.mdc` behind. The dir\n * is Noir-managed iff at least one `.mdc` inside carries canonical cursor\n * frontmatter (`alwaysApply:`). Absent / hand-rolled content ⇒ user-authored.\n * Empty dir ⇒ treat as user-owned (safe — never silently delete). */\nasync function isUserOwnedCursorLegacyDir(dirAbs: string): Promise<boolean> {\n try {\n const entries = await readdir(dirAbs);\n if (entries.length === 0) return true;\n let sawCanonical = false;\n for (const name of entries) {\n if (!name.endsWith('.mdc')) continue;\n if (!(await isUserOwnedMdc(join(dirAbs, name)))) {\n sawCanonical = true;\n break;\n }\n }\n return !sawCanonical;\n } catch {\n return true; // ENOENT or unreadable → safe default (preserve)\n }\n}\n\n/** sha256 hex, first 12 chars (matches @noir-ai/create's sha256Hex12). */\nfunction sha256Hex12(s: string): string {\n return createHash('sha256').update(s, 'utf8').digest('hex').slice(0, 12);\n}\n\n/** LCS similarity in [0,1]. Local copy (skills has no create dependency)\n * so the structured report's `similarity` matches the engine's algorithm. */\nfunction similarity(a: string, b: string): number {\n if (a === b) return 1;\n const A = a.split('\\n');\n const B = b.split('\\n');\n if (A.length === 0 && B.length === 0) return 1;\n const n = A.length;\n const m = B.length;\n const dp: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));\n for (let i = n - 1; i >= 0; i--) {\n const dpi = dp[i] ?? [];\n const dpi1 = dp[i + 1] ?? [];\n const ai = A[i] ?? '';\n for (let j = m - 1; j >= 0; j--) {\n const bj = B[j] ?? '';\n dpi[j] = ai === bj ? (dpi1[j + 1] ?? 0) + 1 : Math.max(dpi1[j] ?? 0, dpi[j + 1] ?? 0);\n }\n }\n const lcs = dp[0]?.[0] ?? 0;\n return n + m === 0 ? 1 : (2 * lcs) / (n + m);\n}\n\n/** Pick a fresh `<abs><suffix>` aside path that does NOT exist (mirrors\n * @noir-ai/create's `uniqueAside`). Used by the `rename`/`duplicate` resolutions\n * so the user's bytes never get silently clobbered. */\nasync function renameAside(abs: string, suffix: string): Promise<void> {\n const { rename } = await import('node:fs/promises');\n const dest = await uniqueAsideAbs(abs, suffix);\n await rename(abs, dest);\n}\n\nasync function uniqueAside(abs: string, suffix: string): Promise<string> {\n return uniqueAsideAbs(abs, suffix);\n}\n\nasync function uniqueAsideAbs(abs: string, suffix: string): Promise<string> {\n const { existsSync } = await import('node:fs');\n let candidate = `${abs}${suffix}`;\n for (let n = 1; existsSync(candidate); n++) candidate = `${abs}${suffix}.${n}`;\n return candidate;\n}\n\n// Convenience for callers/tests that already hold raw markdown (unused by emit path;\n// kept so adapters/tests can validate a single in-memory skill without a dir).\nexport { discoverAll, discoverBuiltin };\n","import { readdirSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseFrontmatter } from './compiler.js';\nimport { parseIntegration } from './integrations-schema.js';\nimport type { BuiltinSkill, IntegrationDeclaration, IntegrationSkill } from './types.js';\n\n// Package root = ONE level up from THIS module's directory. Works in every layout\n// because `builtin/` is always a sibling of the dir this code runs from:\n// - vitest: .../packages/skills/src/discover.ts -> HERE = src -> PKG_ROOT = package root\n// - built: .../packages/skills/dist/index.js -> HERE = dist -> PKG_ROOT = package root\n// (tsup bundles everything into dist/index.js; `dirname` still yields `dist`.)\nconst HERE = dirname(fileURLToPath(import.meta.url)); // .../src OR .../dist\nconst PKG_ROOT = dirname(HERE); // .../packages/skills\nexport const BUILTIN_DIR = join(PKG_ROOT, 'builtin');\nexport const INTEGRATIONS_DIR = join(PKG_ROOT, 'integrations');\n\n/** Read a `<name>/SKILL.md` + its optional `references/` directory. Shared by\n * builtin + integration discovery (both share the SKILL.md + references shape;\n * the difference is the `integration.json` declaration). */\nfunction readSkillMd(dir: string): Pick<BuiltinSkill, 'skillMd' | 'frontmatter' | 'references'> {\n const skillMd = readFileSync(join(dir, 'SKILL.md'), 'utf8');\n const frontmatter = parseFrontmatter(skillMd);\n const refsDir = join(dir, 'references');\n let references: BuiltinSkill['references'] = [];\n try {\n references = readdirSync(refsDir)\n .filter((f) => f.endsWith('.md'))\n .sort()\n .map((f) => ({ name: f, content: readFileSync(join(refsDir, f), 'utf8') }));\n } catch {\n references = []; // no references/ dir is fine\n }\n return { skillMd, frontmatter, references };\n}\n\nexport function discoverBuiltin(builtinDir: string = BUILTIN_DIR): BuiltinSkill[] {\n const dirs = readdirSync(builtinDir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name.startsWith('noir-'))\n .map((e) => e.name)\n .sort();\n return dirs.map((name) => {\n const dir = join(builtinDir, name);\n return { name, dir, ...readSkillMd(dir) };\n });\n}\n\n/**\n * Discover every shipped integration under `integrations/<name>/` (sibling of\n * `builtin/`). Each entry is a builtin-shaped skill (validated `SKILL.md` +\n * optional references) PLUS a parsed + validated `integration.json`\n * declaration. The dir is resolved relative to the package root by default —\n * when `integrationsDir` is overridden (tests), only that dir is scanned.\n *\n * Throws an aggregate error if any integration is malformed (missing\n * `SKILL.md`, bad frontmatter, or an `integration.json` that fails the Zod\n * schema) — fail-fast, atomic-ish, mirroring the builtin compiler's posture.\n */\nexport function discoverIntegrations(\n integrationsDir: string = INTEGRATIONS_DIR,\n): IntegrationSkill[] {\n let entries: string[];\n try {\n entries = readdirSync(integrationsDir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && e.name.startsWith('noir-'))\n .map((e) => e.name)\n .sort();\n } catch {\n // No integrations/ dir (or unreadable) = no integrations shipped here.\n return [];\n }\n return entries.map((name) => {\n const dir = join(integrationsDir, name);\n const declarationRaw = JSON.parse(\n readFileSync(join(dir, 'integration.json'), 'utf8'),\n ) as unknown;\n let declaration: IntegrationDeclaration;\n try {\n declaration = parseIntegration(declarationRaw);\n } catch (err) {\n // ZodError → wrap with the integration's dir for an actionable message.\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Invalid integration.json in ${dir}: ${msg}`);\n }\n if (declaration.name !== name) {\n throw new Error(\n `Integration dir \"${name}\" must equal declaration name \"${declaration.name}\"`,\n );\n }\n return { name, dir, ...readSkillMd(dir), declaration, declarationRaw };\n });\n}\n\n/**\n * Discover the full shipped pack — builtins + integrations — in one call. Used\n * by `emitSkillsToDir` so `noir init`/`sync` emit BOTH (slice X goal). The\n * `integrationsDir` defaults to the sibling of `builtinDir` so a test fixture\n * that overrides only `builtinDir` cleanly resolves integrations to a\n * non-existent path (returns `[]`) instead of accidentally picking up the\n * shipped integrations.\n */\nexport function discoverAll(opts: { builtinDir?: string; integrationsDir?: string } = {}): {\n builtins: BuiltinSkill[];\n integrations: IntegrationSkill[];\n} {\n const builtinDir = opts.builtinDir ?? BUILTIN_DIR;\n const integrationsDir = opts.integrationsDir ?? join(dirname(builtinDir), 'integrations');\n return {\n builtins: discoverBuiltin(builtinDir),\n integrations: discoverIntegrations(integrationsDir),\n };\n}\n","// `integration.json` schema. Each integration ships a declaration\n// alongside its `SKILL.md` so the compiler can validate the auth/runtime/SDD\n// contract uniformly and so the host adapter knows whether to widen emission\n// (host MCP config) or stay skill-only.\n//\n// Doctrine (slice-x spec §architecture + v1x §4.4): skill-only by default;\n// `gated-write-proxy` for stateless writes routed through a Noir MCP tool\n// `mcp-stdio` for a Noir-spawned stateful runtime (deferred, gated on\n// keychain); `external-mcp` for a config-only pointer at a first-party/community\n// MCP server (emits a host `.mcp.json` server entry, no Noir runtime).\n//\n// Zod v4 (`z.record(keyType, valueType)` is key-first). The schema is additive\n// and defaults to the safest tier — every optional field degrades to \"skill\n// only, manual paste, no host MCP wiring\" — so a minimal `{ \"name\":\n// \"noir-x\" }` declaration still parses + behaves as a pure playbook.\n\nimport * as z from 'zod';\n\n/** Auth shape. Locked: `env-var` only until keychain lands (Q4b — refuse OAuth,\n * never silently lower the security bar). `fallback:'manual-paste'` keeps the\n * no-token path honest (the playbook tells the user to paste a value); `'none'`\n * is for integrations that genuinely do not need a token (read-only public\n * endpoints). */\nexport const IntegrationAuthSchema = z.object({\n type: z.literal('env-var'),\n tokenEnv: z.string().min(1),\n fallback: z.enum(['manual-paste', 'none']).default('manual-paste'),\n});\n\n/** SDD two-way binding. `intakeFrom` declares the external artifact kind the\n * `noir-intake` skill pulls from; `writeBack` enumerates the fields\n * `noir-wrap`/`noir-document` push back at session end. Strings (not enums)\n * for `writeBack` so a per-integration vocabulary stays expressible without\n * churning the schema. */\nexport const IntegrationSddSchema = z\n .object({\n intakeFrom: z.enum(['task', 'issue', 'none']).optional(),\n writeBack: z.array(z.string()).default([]),\n })\n .default({ writeBack: [] });\n\n/** Host MCP declaration. Only meaningful when `runtime ∈ {mcp-stdio,\n * external-mcp}`; `null` for `none`/`gated-write-proxy` (ClickUp — writes go\n * through Noir's own MCP tool, never the host's). */\nexport const IntegrationMcpSchema = z\n .object({\n command: z.string().min(1),\n transport: z.enum(['stdio', 'http']),\n args: z.array(z.string()).optional(),\n url: z.string().optional(),\n env: z.record(z.string(), z.string()).optional(),\n })\n .nullable()\n .default(null);\n\n/** Full `integration.json` declaration. `name` enforces the `noir-` prefix\n * shared with builtins so the unified pack reads consistently. */\nexport const IntegrationDeclarationSchema = z.object({\n name: z.string().regex(/^noir-[a-z0-9]+(?:-[a-z0-9]+)*$/, 'must match noir-<kebab>'),\n auth: IntegrationAuthSchema,\n runtime: z.enum(['none', 'gated-write-proxy', 'mcp-stdio', 'external-mcp']),\n sdd: IntegrationSddSchema,\n mcp: IntegrationMcpSchema,\n});\n\nexport type IntegrationDeclaration = z.infer<typeof IntegrationDeclarationSchema>;\n\n/** Coerce + validate an `integration.json` payload. Throws a ZodError on a\n * malformed declaration — the caller (discover) wraps it with the\n * integration's dir for an actionable message. */\nexport function parseIntegration(json: unknown): IntegrationDeclaration {\n return IntegrationDeclarationSchema.parse(json);\n}\n\n/** Non-throwing variant for the hygiene tests + lint helpers. */\nexport function validateIntegration(\n obj: unknown,\n): { ok: true; value: IntegrationDeclaration } | { ok: false; errors: string[] } {\n const parsed = IntegrationDeclarationSchema.safeParse(obj);\n if (parsed.success) return { ok: true, value: parsed.data };\n return { ok: false, errors: parsed.error.issues.map((i: { message: string }) => i.message) };\n}\n\n/** Runtime → host-MCP emission predicate. The compiler widens emission (makes\n * the host MCP block available to the adapter) ONLY for these tiers. The\n * adapter ultimately decides per-host what to render — for Claude, `external-\n * mcp` becomes a `.mcp.json` server entry; `mcp-stdio` registers through the\n * existing `noir mcp serve --stdio` entry so no NEW server entry is emitted. */\nexport function runtimeEmitsHostMcp(runtime: IntegrationDeclaration['runtime']): boolean {\n return runtime === 'mcp-stdio' || runtime === 'external-mcp';\n}\n","// Tokens that must NOT survive a port (predecessor plugin internals + Superpowers\n// rhetoric). Shared by the compiler's lint helper and the hygiene tests (T5).\nexport const FORBIDDEN_RESIDUE: readonly string[] = [\n 'workflow/<task', // predecessor state file\n 'noir-workflow.mode', // predecessor mode flag\n 'noir-workflow', // predecessor plugin name (as a plugin/path reference)\n '@uiigateway', // predecessor Angular specifics\n // NOTE: 'ClickUp'/'clickup' were forbidden during the predecessor-port era (the\n // ClickUp REST precedent lived in the deleted noir-workflow plugin). ClickUp is\n // now reintroduced as a first-class Noir integration (skills/integrations/noir-clickup),\n // so the token is no longer residue — it is the integration's legitimate subject.\n '<EXTREMELY-IMPORTANT', // Superpowers rhetoric\n 'SUBAGENT-STOP', // Superpowers rhetoric\n 'plugins/noir-workflow', // predecessor path\n];\n"],"mappings":"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","names":["dirname","join","join","dirname"]}
@@ -174,4 +174,4 @@ The skill NEVER follows a URL found inside a response field (no chasing `url`, `
174
174
  | `list_id` (default) | `integrations.clickup.listId` (optional; flows 3 + 5 require a list id — use the task's `list.id` otherwise). |
175
175
  | `space_id` | `integrations.clickup.spaceId` (optional; informational). |
176
176
 
177
- These keys live in the additive `integrations` block on the Noir config (X-T2). They are all optional — degraded by default; a no-token workspace still reads (manual paste) + plans (dry-run preview).
177
+ These keys live in the additive `integrations` block on the Noir config. They are all optional — degraded by default; a no-token workspace still reads (manual paste) + plans (dry-run preview).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noir-ai/skills",
3
- "version": "1.3.0-beta.6",
3
+ "version": "1.4.0",
4
4
  "description": "Noir skills — the builtin noir-* skill pack, the integrations pack (ClickUp first), and a copy-and-validate compiler.",
5
5
  "license": "MIT",
6
6
  "author": "agaaaptr",
@@ -23,7 +23,7 @@
23
23
  "pack"
24
24
  ],
25
25
  "engines": {
26
- "node": ">=20"
26
+ "node": ">=22"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "yaml": "^2.5.0",
49
49
  "zod": "^4.2.0",
50
- "@noir-ai/core": "1.3.0-beta.6"
50
+ "@noir-ai/core": "1.4.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^26.1.1"