@noir-ai/create 1.16.0 → 1.17.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/dist/index.d.ts CHANGED
@@ -238,8 +238,14 @@ interface WriteOutcome {
238
238
  *
239
239
  * Parent directories are NOT created here — the orchestrator does that once
240
240
  * for the whole manifest so a missing dir is a single, attributable failure
241
- * rather than N silent ones inside the writer. */
242
- declare function regenerate(absPath: string, content: string): WriteOutcome;
241
+ * rather than N silent ones inside the writer.
242
+ *
243
+ * `mode` applies to the temp file at creation and rides the rename onto the
244
+ * target, so an entry executed directly (the SessionStart hook runner) lands
245
+ * with its exec bit on a first write and a rewrite alike. As with core's
246
+ * `atomicWriteFile`, the requested bits are an upper bound (the process umask
247
+ * can narrow them) and Windows ignores them (ACL-based permissions). */
248
+ declare function regenerate(absPath: string, content: string, mode?: number): WriteOutcome;
243
249
  /** Re-emit a managed region, delegating to core's `writeManagedRegion`.
244
250
  * `regionText` MUST already include the begin/end markers (matches the shape
245
251
  * `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`
package/dist/index.js CHANGED
@@ -328,6 +328,16 @@ function buildHostArtifacts(adapter, ctx) {
328
328
  }
329
329
  ]
330
330
  }
331
+ ],
332
+ SubagentStart: [
333
+ {
334
+ hooks: [
335
+ {
336
+ type: "command",
337
+ command: `"${ctx.root}/.noir/hooks/noir-session-start.mjs"`
338
+ }
339
+ ]
340
+ }
331
341
  ]
332
342
  }
333
343
  };
@@ -337,11 +347,12 @@ function buildHostArtifacts(adapter, ctx) {
337
347
  host,
338
348
  content: JSON.stringify(hookEntry, null, 2),
339
349
  dedupSubstring: HOOK_DEDUP,
340
- description: "SessionStart hook entry (user-owned, written once)"
350
+ description: "SessionStart + SubagentStart hook entry (user-owned, written once)"
341
351
  });
342
352
  entries.push({
343
353
  path: ".noir/hooks/noir-session-start.mjs",
344
354
  mode: "regenerate",
355
+ fileMode: 493,
345
356
  host,
346
357
  content: SESSION_START_HOOK_SCRIPT,
347
358
  description: "SessionStart hook runner (Noir-owned, re-emitted)"
@@ -354,13 +365,23 @@ function buildHostArtifacts(adapter, ctx) {
354
365
  template: "router.md.tmpl",
355
366
  description: "skill router contract (co-owned managed block)"
356
367
  });
368
+ if (rulesEnabled) {
369
+ entries.push({
370
+ path: ".noir/rules/anti-slop.md",
371
+ mode: "regenerate",
372
+ host,
373
+ template: "anti-slop.md.tmpl",
374
+ description: "always-on anti-slop + laziness ruleset (hook-injected, not @imported)"
375
+ });
376
+ }
357
377
  }
358
378
  return entries;
359
379
  }
360
380
  var SESSION_START_HOOK_SCRIPT = `#!/usr/bin/env node
361
- // Noir SessionStart hook \u2014 injects the skill router contract at session start.
362
- // The mutable contract lives in .noir/router.md (a Noir managed block, so user
363
- // edits outside the markers survive noir sync). This script is a pure runner.
381
+ // Noir SessionStart hook \u2014 injects the skill router contract + anti-slop ruleset
382
+ // at session start. The mutable contract lives in .noir/router.md (a Noir
383
+ // managed block, so user edits outside the markers survive noir sync) and the
384
+ // ruleset in .noir/rules/anti-slop.md (regenerated). This script is a pure runner.
364
385
  import { readFileSync, existsSync } from 'node:fs';
365
386
  import { join, dirname } from 'node:path';
366
387
  import { fileURLToPath } from 'node:url';
@@ -368,16 +389,23 @@ import { fileURLToPath } from 'node:url';
368
389
  const HERE = dirname(fileURLToPath(import.meta.url));
369
390
  const ROOT = join(HERE, '..', '..');
370
391
  const ROUTER = join(ROOT, '.noir', 'router.md');
392
+ const RULES = join(ROOT, '.noir', 'rules', 'anti-slop.md');
393
+
394
+ function readIfPresent(path) {
395
+ if (!existsSync(path)) return '';
396
+ return readFileSync(path, 'utf8').trim();
397
+ }
371
398
 
372
399
  function main() {
373
- if (!existsSync(ROUTER)) {
374
- // No router contract (project not initialized / router removed) \u2014 silent.
400
+ const parts = [readIfPresent(ROUTER), readIfPresent(RULES)].filter((part) => part !== '');
401
+ if (parts.length === 0) {
402
+ // Neither contract present (project not initialized / both removed) \u2014 silent.
375
403
  process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
376
404
  process.exit(0);
377
405
  }
378
- const contract = readFileSync(ROUTER, 'utf8').trim();
406
+ const context = parts.join('\\n\\n');
379
407
  process.stdout.write(
380
- JSON.stringify({ hookSpecificOutput: { additionalContext: contract } }),
408
+ JSON.stringify({ hookSpecificOutput: { additionalContext: context } }),
381
409
  );
382
410
  }
383
411
 
@@ -801,7 +829,7 @@ import {
801
829
  import { basename, dirname as dirname3, join as join4 } from "path";
802
830
  import { atomicWriteFile, stripManagedBlock, writeManagedRegion } from "@noir-ai/core";
803
831
  import { ensureOwnerOnly } from "@noir-ai/core";
804
- function regenerate(absPath, content) {
832
+ function regenerate(absPath, content, mode) {
805
833
  const dir = dirname3(absPath);
806
834
  const tmp = join4(
807
835
  dir,
@@ -809,7 +837,7 @@ function regenerate(absPath, content) {
809
837
  );
810
838
  let fd;
811
839
  try {
812
- fd = openSync(tmp, "w");
840
+ fd = openSync(tmp, "w", mode);
813
841
  writeSync(fd, content, 0, "utf8");
814
842
  closeSync(fd);
815
843
  fd = void 0;
@@ -1230,7 +1258,15 @@ function applyWithConflict(ours, theirs, path) {
1230
1258
  }
1231
1259
 
1232
1260
  // src/scaffold.ts
1233
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
1261
+ import {
1262
+ chmodSync,
1263
+ existsSync as existsSync6,
1264
+ mkdirSync as mkdirSync3,
1265
+ readFileSync as readFileSync7,
1266
+ renameSync as renameSync2,
1267
+ rmSync as rmSync2,
1268
+ statSync
1269
+ } from "fs";
1234
1270
  import { basename as basename2, dirname as dirname5, join as join8 } from "path";
1235
1271
  import {
1236
1272
  createProjectId,
@@ -1679,7 +1715,8 @@ async function scaffold(opts) {
1679
1715
  if (entry.mode === "regenerate") {
1680
1716
  const out = await writeWithConflict(abs, entry.path, body, opts, {
1681
1717
  memory: conflictMemory,
1682
- record: recordConflict
1718
+ record: recordConflict,
1719
+ mode: entry.fileMode
1683
1720
  });
1684
1721
  written.push(...out.written);
1685
1722
  skipped.push(...out.skipped);
@@ -1894,10 +1931,14 @@ async function writeWithConflict(abs, relPath, proposed, opts, internals) {
1894
1931
  existing = void 0;
1895
1932
  }
1896
1933
  if (existing === void 0) {
1897
- regenerate(abs, proposed);
1934
+ regenerate(abs, proposed, internals.mode);
1898
1935
  return { written: [relPath], skipped: [], identical: [] };
1899
1936
  }
1900
1937
  if (existing === proposed) {
1938
+ if (internals.mode !== void 0 && (statSync(abs).mode & 511) !== internals.mode) {
1939
+ chmodSync(abs, internals.mode);
1940
+ return { written: [relPath], skipped: [], identical: [] };
1941
+ }
1901
1942
  return { written: [], skipped: [], identical: [relPath] };
1902
1943
  }
1903
1944
  const MODE = internals.artifactClass ?? "regenerate";
@@ -1919,21 +1960,21 @@ async function writeWithConflict(abs, relPath, proposed, opts, internals) {
1919
1960
  internals.record(relPath, MODE, existing, proposed, resolution, sim);
1920
1961
  switch (resolution) {
1921
1962
  case "replace":
1922
- regenerate(abs, proposed);
1963
+ regenerate(abs, proposed, internals.mode);
1923
1964
  return { written: [relPath], skipped: [], identical: [] };
1924
1965
  case "merge": {
1925
- regenerate(abs, proposed);
1966
+ regenerate(abs, proposed, internals.mode);
1926
1967
  return { written: [relPath], skipped: [], identical: [] };
1927
1968
  }
1928
1969
  case "rename": {
1929
1970
  const aside = uniqueAside(abs, relPath, ".local");
1930
1971
  renameSync2(abs, aside.abs);
1931
- regenerate(abs, proposed);
1972
+ regenerate(abs, proposed, internals.mode);
1932
1973
  return { written: [relPath], skipped: [aside.rel], identical: [] };
1933
1974
  }
1934
1975
  case "duplicate": {
1935
1976
  const aside = uniqueAside(abs, relPath, ".noir");
1936
- regenerate(aside.abs, proposed);
1977
+ regenerate(aside.abs, proposed, internals.mode);
1937
1978
  return { written: [aside.rel], skipped: [relPath], identical: [] };
1938
1979
  }
1939
1980
  case "preserve":