@nusoft/nuos-build-catalogue 0.11.0 → 0.14.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 (57) hide show
  1. package/dist/cli.js +52 -39
  2. package/dist/commands/init.js +119 -42
  3. package/dist/commands/plan.d.ts +12 -0
  4. package/dist/commands/plan.js +83 -0
  5. package/dist/commands/write.js +16 -5
  6. package/dist/embedder/ollama.d.ts +14 -8
  7. package/dist/embedder/ollama.js +15 -9
  8. package/dist/path-resolution.d.ts +68 -0
  9. package/dist/path-resolution.js +147 -0
  10. package/dist/runtime/ac-parse.js +10 -6
  11. package/dist/runtime/markdown-edit.d.ts +5 -0
  12. package/dist/runtime/markdown-edit.js +13 -6
  13. package/dist/runtime/mis-adapter.js +7 -2
  14. package/package.json +2 -2
  15. package/templates/hooks/install-hooks.sh +44 -0
  16. package/templates/hooks/post-commit +96 -0
  17. package/templates/hooks/pre-commit +162 -0
  18. package/templates/protocols/end-of-session.md +101 -13
  19. package/templates/protocols/persona-new.md +64 -30
  20. package/templates/protocols/plan-orientation.md +122 -0
  21. package/templates/protocols/start-of-session.md +52 -13
  22. package/templates/protocols/wu-new.md +75 -50
  23. package/templates/starter-kit/docs/build/GLOSSARY.md +115 -0
  24. package/templates/starter-kit/docs/build/STATE.md +30 -16
  25. package/templates/starter-kit/docs/build/WELCOME.md +79 -0
  26. package/templates/starter-kit/docs/build/architecture/_index.md +39 -0
  27. package/templates/starter-kit/docs/build/architecture/module-template.md +47 -0
  28. package/templates/starter-kit/docs/build/contracts/_index.md +39 -0
  29. package/templates/starter-kit/docs/build/contracts/contract-template.md +64 -0
  30. package/templates/starter-kit/docs/build/decisions/_index.md +21 -17
  31. package/templates/starter-kit/docs/build/design-system/_index.md +57 -0
  32. package/templates/starter-kit/docs/build/design-system/accessibility.md +77 -0
  33. package/templates/starter-kit/docs/build/design-system/components/_index.md +29 -0
  34. package/templates/starter-kit/docs/build/design-system/components/_template.md +60 -0
  35. package/templates/starter-kit/docs/build/design-system/patterns/_index.md +37 -0
  36. package/templates/starter-kit/docs/build/design-system/patterns/_template.md +57 -0
  37. package/templates/starter-kit/docs/build/design-system/tokens-colour.md +52 -0
  38. package/templates/starter-kit/docs/build/design-system/tokens-motion.md +42 -0
  39. package/templates/starter-kit/docs/build/design-system/tokens-radius-elevation.md +34 -0
  40. package/templates/starter-kit/docs/build/design-system/tokens-spacing.md +48 -0
  41. package/templates/starter-kit/docs/build/design-system/tokens-typography.md +46 -0
  42. package/templates/starter-kit/docs/build/design-system/voice.md +53 -0
  43. package/templates/starter-kit/docs/build/maps/01-template.md +15 -112
  44. package/templates/starter-kit/docs/build/maps/02-template.md +52 -0
  45. package/templates/starter-kit/docs/build/maps/03-template.md +46 -0
  46. package/templates/starter-kit/docs/build/maps/99-template-power-user-operational-plan.md +126 -0
  47. package/templates/starter-kit/docs/build/maps/_index.md +17 -52
  48. package/templates/starter-kit/docs/build/open-questions/_index.md +27 -13
  49. package/templates/starter-kit/docs/build/personas/_index.md +26 -60
  50. package/templates/starter-kit/docs/build/risks/_index.md +20 -13
  51. package/templates/starter-kit/docs/build/sessions/_index.md +18 -16
  52. package/templates/starter-kit/docs/build/ui-ux/_index.md +48 -0
  53. package/templates/starter-kit/docs/build/ui-ux/surface-template.md +72 -0
  54. package/templates/starter-kit/docs/build/work-units/001-template-simple.md +43 -0
  55. package/templates/starter-kit/docs/build/work-units/_index.md +18 -20
  56. package/templates/starter-kit/methodfile.json +19 -8
  57. /package/templates/starter-kit/docs/build/work-units/{001-template.md → 001-template-full.md} +0 -0
package/dist/cli.js CHANGED
@@ -10,8 +10,6 @@
10
10
  * Implementation note — uses minimist-free arg parsing to keep deps lean.
11
11
  * If we need richer parsing later, swap in commander/yargs.
12
12
  */
13
- import path from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
13
  // Static imports — these don't pull in NuVector / NuFlow transitively.
16
14
  // init / migrate / regenerate / summary / list / show / install-protocols all
17
15
  // work without those native deps being installed.
@@ -21,28 +19,7 @@ import { listRegister, showRecord, commandToRegister, listAcrossRegisters, } fro
21
19
  import { runRegenerate } from './regenerate/check.js';
22
20
  import { openPrompt } from './commands/prompt.js';
23
21
  import { cmdInit, cmdInstallProtocols } from './commands/init.js';
24
- // Dynamic imports below index / search / write commands / create commands
25
- // load NuVector or NuFlow transitively. Loading them at module-parse time
26
- // would crash on platforms where the NuVector native binary isn't resolved
27
- // (e.g. fresh npx installs before @nusoft/nuvector ships its platform-specific
28
- // binaries as optionalDependencies). Lazy-load so the lightweight commands
29
- // (init, migrate, etc.) work universally; the heavyweight commands degrade
30
- // gracefully when their deps are missing.
31
- const __filename = fileURLToPath(import.meta.url);
32
- const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
33
- // Defaults resolve in this order: env var > flag-supplied > package-relative
34
- // fallback. The package-relative fallback only makes sense when running
35
- // against the nuos catalogue as a sibling (the original WU 110 use case);
36
- // for any other consumer (Sensight, NuTutor, etc.) the env vars or the
37
- // per-command flags are the right path. CLAUDE.md guidance for adopters:
38
- // set NUOS_CATALOGUE_BUILD_ROOT and NUOS_CATALOGUE_WORKFLOWS in your
39
- // shell profile.
40
- const DEFAULT_CATALOGUE_ROOT = process.env.NUOS_CATALOGUE_ROOT ?? path.resolve(PACKAGE_ROOT, '../nuos/docs');
41
- const DEFAULT_BUILD_ROOT = process.env.NUOS_CATALOGUE_BUILD_ROOT ?? path.resolve(PACKAGE_ROOT, '../nuos/docs/build');
42
- const DEFAULT_INDEX_DIR = process.env.NUOS_CATALOGUE_INDEX_DIR ?? path.resolve(PACKAGE_ROOT, '.nuos-catalogue');
43
- const DEFAULT_INDEX_PATH = path.join(DEFAULT_INDEX_DIR, 'index.nv');
44
- const DEFAULT_HASH_PATH = path.join(DEFAULT_INDEX_DIR, 'hashes.json');
45
- const DEFAULT_WORKFLOWS_PATH = process.env.NUOS_CATALOGUE_WORKFLOWS ?? path.join(DEFAULT_INDEX_DIR, 'workflows.json');
22
+ import { resolveBuildRoot, resolveCatalogueRoot, resolveWorkflowsPath, resolveIndexPath, resolveHashPath, gitignoreCatalogueNote, } from './path-resolution.js';
46
23
  function parseArgs(argv) {
47
24
  const [command, ...rest] = argv;
48
25
  const positional = [];
@@ -64,9 +41,10 @@ function parseArgs(argv) {
64
41
  return { command: command ?? 'help', positional, flags };
65
42
  }
66
43
  async function cmdIndex(flags) {
67
- const catalogueRoot = String(flags['catalogue'] ?? DEFAULT_CATALOGUE_ROOT);
68
- const indexPath = String(flags['index'] ?? DEFAULT_INDEX_PATH);
69
- const hashPath = String(flags['hash-file'] ?? DEFAULT_HASH_PATH);
44
+ const catalogueRoot = resolveCatalogueRoot(flags['catalogue']);
45
+ const buildRoot = resolveBuildRoot(flags['build-root']);
46
+ const indexPath = resolveIndexPath(buildRoot, flags['index']);
47
+ const hashPath = resolveHashPath(buildRoot, flags['hash-file']);
70
48
  const { selectEmbedderFromEnv } = await import('./embedder/select.js');
71
49
  const { openStore } = await import('./store/open.js');
72
50
  const { runIndex } = await import('./indexer/upsert.js');
@@ -99,7 +77,8 @@ async function cmdSearch(positional, flags) {
99
77
  console.error('Usage: nuos-catalogue search "<query>" [--kind=...] [--status=...] [--limit=N] [--json]');
100
78
  process.exit(2);
101
79
  }
102
- const indexPath = String(flags['index'] ?? DEFAULT_INDEX_PATH);
80
+ const buildRoot = resolveBuildRoot(flags['build-root']);
81
+ const indexPath = resolveIndexPath(buildRoot, flags['index']);
103
82
  const { selectEmbedderFromEnv } = await import('./embedder/select.js');
104
83
  const { openStore } = await import('./store/open.js');
105
84
  const { runSearch } = await import('./search/query.js');
@@ -132,8 +111,8 @@ async function cmdSearch(positional, flags) {
132
111
  }
133
112
  }
134
113
  async function cmdMigrate(flags) {
135
- const buildRoot = String(flags['build-root'] ?? DEFAULT_BUILD_ROOT);
136
- const workflowsPath = String(flags['workflows'] ?? DEFAULT_WORKFLOWS_PATH);
114
+ const buildRoot = resolveBuildRoot(flags['build-root']);
115
+ const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
137
116
  const dryRun = Boolean(flags['dry-run']);
138
117
  console.log(`migrating ${buildRoot}`);
139
118
  console.log(` workflows file: ${workflowsPath}`);
@@ -161,6 +140,17 @@ async function cmdMigrate(flags) {
161
140
  console.log(' Resolve by renaming the conflicting files (e.g. give them distinct number prefixes) then re-run migrate.');
162
141
  }
163
142
  console.log(`(${(report.durationMs / 1000).toFixed(2)}s)`);
143
+ // Surface a gitignore hint if the project's .gitignore is missing the
144
+ // `.nuos-catalogue/` entry. Silent if .gitignore is absent or correct.
145
+ // (We do this after the success block so the report is the first thing
146
+ // the operator reads; the note follows.)
147
+ if (!dryRun) {
148
+ const note = gitignoreCatalogueNote(buildRoot);
149
+ if (note) {
150
+ console.log('');
151
+ console.log(note);
152
+ }
153
+ }
164
154
  }
165
155
  async function cmdRegisterDispatch(command, positional, flags) {
166
156
  const register = commandToRegister(command);
@@ -169,8 +159,8 @@ async function cmdRegisterDispatch(command, positional, flags) {
169
159
  process.exit(2);
170
160
  }
171
161
  const action = positional[0];
172
- const workflowsPath = String(flags['workflows'] ?? DEFAULT_WORKFLOWS_PATH);
173
- const buildRoot = String(flags['build-root'] ?? DEFAULT_BUILD_ROOT);
162
+ const buildRoot = resolveBuildRoot(flags['build-root']);
163
+ const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
174
164
  const store = await openWorkflowStore(workflowsPath);
175
165
  const asJson = Boolean(flags['json']);
176
166
  switch (action) {
@@ -300,8 +290,8 @@ async function cmdRegisterDispatch(command, positional, flags) {
300
290
  }
301
291
  }
302
292
  async function cmdRegenerate(flags) {
303
- const buildRoot = String(flags['build-root'] ?? DEFAULT_BUILD_ROOT);
304
- const workflowsPath = String(flags['workflows'] ?? DEFAULT_WORKFLOWS_PATH);
293
+ const buildRoot = resolveBuildRoot(flags['build-root']);
294
+ const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
305
295
  const write = Boolean(flags['write']);
306
296
  const showDiffs = Boolean(flags['diff']);
307
297
  const registerFilter = flags['register'] ? String(flags['register']) : undefined;
@@ -353,7 +343,8 @@ async function cmdRegenerate(flags) {
353
343
  process.exit(report.differs > 0 || report.missing > 0 ? 1 : 0);
354
344
  }
355
345
  async function cmdSummary(flags) {
356
- const workflowsPath = String(flags['workflows'] ?? DEFAULT_WORKFLOWS_PATH);
346
+ const buildRoot = resolveBuildRoot(flags['build-root']);
347
+ const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
357
348
  const store = await openWorkflowStore(workflowsPath);
358
349
  const { byRegister, total } = listAcrossRegisters(store);
359
350
  if (Boolean(flags['json'])) {
@@ -387,6 +378,7 @@ Usage:
387
378
  nuos-catalogue wu create (interactive — multi-step prompts)
388
379
  nuos-catalogue wu advance <handle> --to=<status> [--reason="..."]
389
380
  nuos-catalogue wu tick <handle> --index=N --evidence="..."
381
+ (--index is 1-based: --index=1 ticks the first AC)
390
382
  nuos-catalogue decision list [--status=<s>] [--limit=N] [--json]
391
383
  nuos-catalogue decision show <handle> [--json]
392
384
  nuos-catalogue decision create (interactive)
@@ -399,17 +391,25 @@ Usage:
399
391
  nuos-catalogue persona show <handle> [--json]
400
392
  nuos-catalogue persona create (interactive — seven dimensions + acid-test per D046)
401
393
 
394
+ nuos-catalogue plan status show planning progress across the 5-phase arc
395
+
402
396
  nuos-catalogue help
403
397
 
404
398
  Handles accepted: canonical (wu-111, D046, Q009, P001) or friendly
405
399
  (WU 111, 111, D45, Q9). Unambiguous integers ("111" under "wu show")
406
400
  are normalised to the canonical form.
407
401
 
402
+ Default locations: when --build-root / --workflows / --catalogue are
403
+ omitted and the matching env vars are unset, the CLI walks up from the
404
+ current working directory looking for a docs/build/ directory (the
405
+ same way git finds its repo root). Invoke from anywhere inside the
406
+ project. The workflow store lives at <project-root>/.nuos-catalogue/.
407
+
408
408
  Environment:
409
- NUOS_CATALOGUE_BUILD_ROOT default for --build-root (the catalogue's docs/build/ dir)
410
- NUOS_CATALOGUE_WORKFLOWS default for --workflows (the JSON workflow store path)
411
- NUOS_CATALOGUE_ROOT default for --catalogue (semantic-search index source)
412
- NUOS_CATALOGUE_INDEX_DIR default parent dir for index.nv + workflows.json
409
+ NUOS_CATALOGUE_BUILD_ROOT override for --build-root (the catalogue's docs/build/ dir)
410
+ NUOS_CATALOGUE_WORKFLOWS override for --workflows (the JSON workflow store path)
411
+ NUOS_CATALOGUE_ROOT override for --catalogue (semantic-search index source)
412
+ NUOS_CATALOGUE_INDEX_DIR override for parent dir of index.nv + workflows.json
413
413
  NUOS_CATALOGUE_EMBEDDER vertex | openai | stub (default: vertex)
414
414
  GOOGLE_CLOUD_PROJECT required for vertex
415
415
  GOOGLE_CLOUD_LOCATION optional (default: us-central1)
@@ -473,6 +473,19 @@ async function main() {
473
473
  case 'persona':
474
474
  await cmdRegisterDispatch(args.command, args.positional, args.flags);
475
475
  break;
476
+ case 'plan': {
477
+ const sub = args.positional[0];
478
+ if (sub === 'status') {
479
+ const { cmdPlanStatus } = await import('./commands/plan.js');
480
+ const code = await cmdPlanStatus({ cwd: process.cwd() });
481
+ if (code !== 0)
482
+ process.exit(code);
483
+ break;
484
+ }
485
+ console.error(`unknown plan subcommand: ${sub ?? '(none)'}`);
486
+ console.error('available: plan status');
487
+ process.exit(1);
488
+ }
476
489
  case 'help':
477
490
  case '--help':
478
491
  case '-h':
@@ -39,6 +39,7 @@ const PROTOCOL_FILES = [
39
39
  'end-of-session.md',
40
40
  'wu-new.md',
41
41
  'persona-new.md',
42
+ 'plan-orientation.md',
42
43
  ];
43
44
  /**
44
45
  * One-line descriptions used in the frontmatter of installed protocol
@@ -46,10 +47,11 @@ const PROTOCOL_FILES = [
46
47
  * read as an imperative summary.
47
48
  */
48
49
  const PROTOCOL_DESCRIPTIONS = {
49
- 'start-of-session': 'Read STATE, last session log, active WU; surface next action',
50
- 'end-of-session': 'Write session log, update STATE + indices, move WUs to done/, commit',
51
- 'wu-new': 'Create a new work unit with the six-field outcome shape (per D046)',
52
- 'persona-new': 'Create a new P-NNN persona with the seven dimensions and acid-test (per D046)',
50
+ 'start-of-session': 'Read where the project is and propose the next concrete action',
51
+ 'end-of-session': 'Capture what happened, update state, write session log, commit',
52
+ 'wu-new': 'File a new work unit through a guided plain-English conversation',
53
+ 'persona-new': 'File a new persona by walking the seven dimensions conversationally',
54
+ 'plan-orientation': 'Phase A of planning — project description, personas, the horizon map',
53
55
  };
54
56
  const TOOLS = {
55
57
  claude: {
@@ -165,6 +167,12 @@ export async function cmdInit(prompt, options = {}) {
165
167
  log_line(` · ${existed ? 'overwrote' : 'installed'} ${tool.destPath(slug)} (${tool.label})`);
166
168
  }
167
169
  }
170
+ // Step 3b: install the git hooks (pre-commit enforcement + post-commit
171
+ // auto-reindex). Two source files land in scripts/hooks/ so the consumer
172
+ // has the version-controlled source-of-truth; copies are pushed into
173
+ // .git/hooks/ so they fire immediately without the user re-running an
174
+ // installer.
175
+ await installHooks(cwd, log_line);
168
176
  // Step 4: CLAUDE.md
169
177
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
170
178
  const catalogueSection = renderCatalogueSection(name);
@@ -190,14 +198,24 @@ export async function cmdInit(prompt, options = {}) {
190
198
  prompt.print('');
191
199
  prompt.print(`✅ Catalogue initialised at ${path.join(cwd, 'docs/build')}`);
192
200
  prompt.print('');
193
- prompt.print('Next steps:');
194
- prompt.print(' 1. Set env vars in your shell profile so the CLI knows where this catalogue lives:');
195
- prompt.print(` export NUOS_CATALOGUE_BUILD_ROOT="${path.join(cwd, 'docs/build')}"`);
196
- prompt.print(` export NUOS_CATALOGUE_WORKFLOWS="${path.join(cwd, '.nuos-catalogue/workflows.json')}"`);
197
- prompt.print(' 2. Edit docs/build/STATE.md to describe the actual current state of this project.');
198
- prompt.print(' 3. File the first WU: `nuos-catalogue wu create`');
201
+ prompt.print('Next step:');
202
+ prompt.print(' Run `/start-of-session` in Claude Code (or OpenCode, or Codex CLI).');
203
+ prompt.print(' The AI will detect this is a brand-new catalogue and walk you');
204
+ prompt.print(' through a 5-phase planning arc:');
199
205
  prompt.print('');
200
- prompt.print('To refresh protocols only later (without re-running init): `nuos-catalogue install-protocols`');
206
+ prompt.print(' A. Orientation — project description, personas, the horizon (~30 min)');
207
+ prompt.print(' B. Architecture — major pieces and their contracts (~60-90 min)');
208
+ prompt.print(' C. UI/UX + Design — surfaces and the design system (~60-90 min)');
209
+ prompt.print(' D. Maps — phases of work and near-term plan (~45 min)');
210
+ prompt.print(' E. Initial work units — first 5-10 things to build (~60 min)');
211
+ prompt.print('');
212
+ prompt.print(' Each phase is its own session. Pause whenever you want.');
213
+ prompt.print('');
214
+ prompt.print('To read about the catalogue before starting:');
215
+ prompt.print(` ${path.join(cwd, 'docs/build/WELCOME.md')}`);
216
+ prompt.print(` ${path.join(cwd, 'docs/build/GLOSSARY.md')}`);
217
+ prompt.print('');
218
+ prompt.print('To refresh protocols and hooks later: `nuos-catalogue install-protocols`');
201
219
  return { output: '', exitCode: 0 };
202
220
  }
203
221
  export async function cmdInstallProtocols(prompt, options = {}) {
@@ -230,9 +248,76 @@ export async function cmdInstallProtocols(prompt, options = {}) {
230
248
  prompt.print(`Refreshing protocols (Claude Code / OpenCode / Codex CLI):`);
231
249
  for (const l of lines)
232
250
  prompt.print(l);
251
+ // Refresh hooks too — same idempotent shape.
252
+ prompt.print('');
253
+ prompt.print(`Refreshing git hooks (pre-commit enforcement, post-commit auto-reindex):`);
254
+ await installHooks(cwd, (msg) => prompt.print(msg));
233
255
  return { output: '', exitCode: 0 };
234
256
  }
235
257
  // ---------------------------------------------------------------------------
258
+ // installHooks — copy bundled hook sources into the consumer + activate them
259
+ // ---------------------------------------------------------------------------
260
+ /**
261
+ * Bundled hooks ship in templates/hooks/. Two source files (pre-commit,
262
+ * post-commit) land in the consumer's scripts/hooks/ so the maintainer
263
+ * has the version-controlled source. Copies also land in .git/hooks/ so
264
+ * they fire immediately. The bash install-hooks.sh script also lands in
265
+ * scripts/ so re-running it after a fresh clone reconstitutes .git/hooks/.
266
+ *
267
+ * Idempotent: byte-identical sources are skipped silently. Permissions
268
+ * are set executable on every install so a chmod isn't required.
269
+ */
270
+ async function installHooks(cwd, log_line) {
271
+ const hooksTemplatesRoot = path.join(TEMPLATES_ROOT, 'hooks');
272
+ if (!existsSync(hooksTemplatesRoot)) {
273
+ log_line(' · (hooks bundle not present in this CLI install — skipping)');
274
+ return;
275
+ }
276
+ // 1) Source-of-truth files in <cwd>/scripts/
277
+ const scriptsDir = path.join(cwd, 'scripts');
278
+ const scriptsHooksDir = path.join(scriptsDir, 'hooks');
279
+ await mkdir(scriptsHooksDir, { recursive: true });
280
+ const hookFiles = ['pre-commit', 'post-commit'];
281
+ for (const name of hookFiles) {
282
+ const src = path.join(hooksTemplatesRoot, name);
283
+ const dest = path.join(scriptsHooksDir, name);
284
+ await writeHookFile(src, dest, log_line, ` · `, `scripts/hooks/${name}`);
285
+ }
286
+ // install-hooks.sh — convenience bash installer; sits next to scripts/hooks/
287
+ const installerSrc = path.join(hooksTemplatesRoot, 'install-hooks.sh');
288
+ const installerDest = path.join(scriptsDir, 'install-hooks.sh');
289
+ await writeHookFile(installerSrc, installerDest, log_line, ` · `, `scripts/install-hooks.sh`);
290
+ // 2) Active copies in <cwd>/.git/hooks/ — only if .git/ exists. Some
291
+ // tests run init in a non-git directory; that's fine, skip the active
292
+ // install and let the user run install-hooks.sh later.
293
+ const gitHooksDir = path.join(cwd, '.git', 'hooks');
294
+ if (!existsSync(path.join(cwd, '.git'))) {
295
+ log_line(` · (no .git/ found at ${cwd} — skipping active hook install; run scripts/install-hooks.sh after \`git init\`)`);
296
+ return;
297
+ }
298
+ await mkdir(gitHooksDir, { recursive: true });
299
+ for (const name of hookFiles) {
300
+ const src = path.join(hooksTemplatesRoot, name);
301
+ const dest = path.join(gitHooksDir, name);
302
+ await writeHookFile(src, dest, log_line, ` · `, `.git/hooks/${name}`);
303
+ }
304
+ }
305
+ async function writeHookFile(src, dest, log_line, prefix, label) {
306
+ const srcContent = await readFile(src, 'utf8');
307
+ let action = 'created';
308
+ if (existsSync(dest)) {
309
+ const destContent = await readFile(dest, 'utf8');
310
+ action = destContent === srcContent ? 'unchanged' : 'updated';
311
+ }
312
+ if (action !== 'unchanged') {
313
+ await writeFile(dest, srcContent, 'utf8');
314
+ }
315
+ // chmod +x — required for git to actually run them
316
+ const { chmod } = await import('node:fs/promises');
317
+ await chmod(dest, 0o755);
318
+ log_line(`${prefix}${action} ${label}`);
319
+ }
320
+ // ---------------------------------------------------------------------------
236
321
  // Helpers
237
322
  // ---------------------------------------------------------------------------
238
323
  function substitute(content, subs) {
@@ -291,48 +376,40 @@ async function ensureGitignoreEntries(gitignorePath, log_line) {
291
376
  function renderCatalogueSection(projectName) {
292
377
  return `## Build catalogue (NuOS Build Method)
293
378
 
294
- This repo runs **the NuOS Build Method**. The catalogue lives at [docs/build/](docs/build/) and tracks work units, decisions, open questions, personas, sessions, and risks.
379
+ This project uses the **NuOS Build Method catalogue** at [docs/build/](docs/build/). It is the project's memory — who it's for, what's been built, what's been decided, what's still unknown, what's at risk. Eleven registers in plain Markdown. The catalogue stays current through two protocols that bookend every session.
295
380
 
296
- ### At the start of every session
381
+ **Start here** if you're new:
297
382
 
298
- Run \`/start-of-session\` (or follow [docs/build/START-OF-SESSION.md](docs/build/START-OF-SESSION.md)).
383
+ - [docs/build/WELCOME.md](docs/build/WELCOME.md) — what this catalogue is, in 5 minutes
384
+ - [docs/build/GLOSSARY.md](docs/build/GLOSSARY.md) — every term defined once
299
385
 
300
- ### At the end of every session
386
+ ### Three commands
301
387
 
302
- Run \`/end-of-session\`. **Without it, work is lost.**
388
+ That's it. Everything else is automatic.
303
389
 
304
- ### Daily use via the CLI
305
-
306
- Set these env vars in your shell profile so commands work without flags:
307
-
308
- \`\`\`bash
309
- export NUOS_CATALOGUE_BUILD_ROOT="$(pwd)/docs/build"
310
- export NUOS_CATALOGUE_WORKFLOWS="$(pwd)/.nuos-catalogue/workflows.json"
390
+ \`\`\`text
391
+ /start-of-session — every time you begin working
392
+ /end-of-session — every time you stop
311
393
  \`\`\`
312
394
 
313
- Then:
395
+ (\`init\` runs once at the start; you've already done that.)
314
396
 
315
- \`\`\`bash
316
- nuos-catalogue wu create # interactive — file a new WU
317
- nuos-catalogue wu list # what's in flight
318
- nuos-catalogue wu advance <handle> --to=in_progress
319
- nuos-catalogue wu tick <handle> --index=N --evidence="commit abc123"
320
- nuos-catalogue decision create
321
- nuos-catalogue question create
322
- nuos-catalogue regenerate # check store-vs-disk drift
323
- nuos-catalogue summary # totals by register
324
- \`\`\`
397
+ If this is a brand-new project, \`/start-of-session\` will detect the empty catalogue and walk you through 5 short planning phases (Orientation, Architecture, UI/UX + Design System, Maps, Initial Work Units) before any building starts. Each phase is its own session. Take them in order; pause whenever you need to.
325
398
 
326
- To refresh the protocol bodies later (after a CLI upgrade):
399
+ ### The principle that makes it work
327
400
 
328
- \`\`\`bash
329
- nuos-catalogue install-protocols
330
- \`\`\`
401
+ **Project memory never drifts from project reality.** Every decision made in conversation gets saved before the session ends. Every change to an existing piece flows through a protocol. The pre-commit hook blocks silent edits to accepted decisions; the post-commit hook auto-refreshes the search index after every commit. What the AI finds when you ask "what did we decide about X?" is always current.
331
402
 
332
403
  ### What never to do
333
404
 
334
- - Never make architectural decisions without recording them in \`docs/build/decisions/\`
335
- - Never start work outside the active work unit without recording why
336
- - Never skip end-of-session
337
- - Never modify a committed \`accepted\` decision file (use \`decision supersede\` instead)`;
405
+ - **Never close a session without \`/end-of-session\`.** Work that isn't written down is work that's lost.
406
+ - **Never edit an accepted decision file.** If something changes, file a new decision that supersedes the old one. The pre-commit hook will block silent edits.
407
+ - **Never make an architectural decision in conversation without filing it.** If you and the AI agree on "let's go with X", file it as a decision *before moving on*. Drift is the failure mode that makes the catalogue worthless.
408
+
409
+ ### If you need more
410
+
411
+ - All registers and their templates live under [docs/build/](docs/build/)
412
+ - The full CLI surface (creating work units / decisions / personas / questions / contracts / surfaces from the command line) is documented at [docs/build/WELCOME.md](docs/build/WELCOME.md)
413
+ - To refresh protocols and hooks later (after a CLI upgrade): \`npx @nusoft/nuos-build-catalogue install-protocols\`
414
+ `;
338
415
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `nuos-catalogue plan status` — read methodfile.json's planning tracker
3
+ * and print a 5-line summary of where the project is in the planning arc.
4
+ *
5
+ * Read-only; never mutates state. The actual phase advancement is done by
6
+ * the relevant plan-* protocol when it finishes its work and updates the
7
+ * methodfile + STATE.md as part of its end-of-session.
8
+ */
9
+ export interface PlanStatusOptions {
10
+ cwd?: string;
11
+ }
12
+ export declare function cmdPlanStatus(options?: PlanStatusOptions): Promise<number>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `nuos-catalogue plan status` — read methodfile.json's planning tracker
3
+ * and print a 5-line summary of where the project is in the planning arc.
4
+ *
5
+ * Read-only; never mutates state. The actual phase advancement is done by
6
+ * the relevant plan-* protocol when it finishes its work and updates the
7
+ * methodfile + STATE.md as part of its end-of-session.
8
+ */
9
+ import { readFile } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import path from 'node:path';
12
+ const PHASES = [
13
+ { key: 'phaseA_orientation', label: 'A. Orientation' },
14
+ { key: 'phaseB_architecture', label: 'B. Architecture & Contracts' },
15
+ { key: 'phaseC_uiUxDesignSystem', label: 'C. UI/UX + Design System' },
16
+ { key: 'phaseD_maps', label: 'D. Maps' },
17
+ { key: 'phaseE_initialWorkUnits', label: 'E. Initial Work Units' },
18
+ ];
19
+ function statusIcon(s) {
20
+ switch (s) {
21
+ case 'complete':
22
+ return '✅';
23
+ case 'in_progress':
24
+ return '🟡';
25
+ default:
26
+ return '🔵';
27
+ }
28
+ }
29
+ function statusLabel(s) {
30
+ switch (s) {
31
+ case 'complete':
32
+ return 'complete';
33
+ case 'in_progress':
34
+ return 'in progress';
35
+ default:
36
+ return 'not started';
37
+ }
38
+ }
39
+ export async function cmdPlanStatus(options = {}) {
40
+ const cwd = options.cwd ?? process.cwd();
41
+ const methodfilePath = path.join(cwd, 'methodfile.json');
42
+ if (!existsSync(methodfilePath)) {
43
+ console.error(`No methodfile.json found at ${cwd}.`);
44
+ console.error('Run `nuos-catalogue init` first to set up a catalogue.');
45
+ return 1;
46
+ }
47
+ let mf;
48
+ try {
49
+ mf = JSON.parse(await readFile(methodfilePath, 'utf8'));
50
+ }
51
+ catch (err) {
52
+ console.error(`Couldn't read methodfile.json: ${err.message}`);
53
+ return 1;
54
+ }
55
+ const planning = mf.planning ?? {};
56
+ console.log('');
57
+ console.log('Planning progress for this project:');
58
+ console.log('');
59
+ for (const { key, label } of PHASES) {
60
+ const status = planning[key];
61
+ console.log(` ${statusIcon(status)} ${label.padEnd(36)} ${statusLabel(status)}`);
62
+ }
63
+ console.log('');
64
+ const firstNotStarted = PHASES.find((p) => planning[p.key] !== 'complete');
65
+ if (!firstNotStarted) {
66
+ console.log('All five phases complete. The project is ready to build —');
67
+ console.log('use /start-of-session and /end-of-session for normal work from here.');
68
+ console.log('');
69
+ return 0;
70
+ }
71
+ if (planning[firstNotStarted.key] === 'in_progress') {
72
+ console.log(`Currently in: ${firstNotStarted.label} (in progress)`);
73
+ console.log('Resume by running /start-of-session — the AI reads the last session log');
74
+ console.log('and picks up at the right step.');
75
+ }
76
+ else {
77
+ console.log(`Next phase: ${firstNotStarted.label}`);
78
+ console.log('Begin by running /start-of-session — the AI detects the empty phase');
79
+ console.log('and routes to the right planning protocol.');
80
+ }
81
+ console.log('');
82
+ return 0;
83
+ }
@@ -86,16 +86,25 @@ function inferWorkflowStatus(record) {
86
86
  }
87
87
  // ---------------------------------------------------------------------------
88
88
  // wu tick <handle> --index=N --evidence="..."
89
+ //
90
+ // --index is **1-based** at the CLI boundary (so --index=1 ticks the first
91
+ // AC). Internally the pack workflow + ac-parse use 0-based indexing; the
92
+ // conversion happens here so the user-facing surface matches the
93
+ // human-readable summary text the mis-adapter writes into the markdown
94
+ // changelog ("Acceptance criterion 3 ticked: ...").
89
95
  // ---------------------------------------------------------------------------
90
96
  export async function cmdWuTick(store, runtime, args) {
91
97
  if (!args.handle) {
92
98
  return {
93
- output: 'Usage: nuos-catalogue wu tick <handle> --index=N --evidence="..."',
99
+ output: 'Usage: nuos-catalogue wu tick <handle> --index=N --evidence="..." (--index is 1-based)',
94
100
  exitCode: 2,
95
101
  };
96
102
  }
97
- if (typeof args.index !== 'number' || !Number.isInteger(args.index) || args.index < 0) {
98
- return { output: '--index=<non-negative integer> is required', exitCode: 2 };
103
+ if (typeof args.index !== 'number' || !Number.isInteger(args.index) || args.index < 1) {
104
+ return {
105
+ output: '--index=<positive integer> is required (1-based: --index=1 ticks the first AC)',
106
+ exitCode: 2,
107
+ };
99
108
  }
100
109
  if (!args.evidence || args.evidence.trim().length === 0) {
101
110
  return { output: '--evidence="..." is required (non-empty)', exitCode: 2 };
@@ -104,17 +113,19 @@ export async function cmdWuTick(store, runtime, args) {
104
113
  if (!store.has(handle)) {
105
114
  return { output: `no work_unit record for handle "${handle}"`, exitCode: 1 };
106
115
  }
116
+ // Convert 1-based CLI input to 0-based for the workflow + ac-parse layer.
117
+ const zeroBasedIndex = args.index - 1;
107
118
  const capture = {
108
119
  channel: 'typed_note',
109
120
  content: `tick AC #${args.index} on ${handle}`,
110
121
  subjects: [{ kind: 'work_unit', id: handle }],
111
122
  metadata: {
112
123
  targetHandle: handle,
113
- criterionIndex: args.index,
124
+ criterionIndex: zeroBasedIndex,
114
125
  evidence: args.evidence,
115
126
  },
116
127
  };
117
- return await driveLifecycle(runtime, 'work_unit.tick_acceptance_criterion', capture, handle, `index ${args.index}`);
128
+ return await driveLifecycle(runtime, 'work_unit.tick_acceptance_criterion', capture, handle, `AC ${args.index}`);
118
129
  }
119
130
  // ---------------------------------------------------------------------------
120
131
  // decision supersede <target> --by=<superseding> [--reason="..."]
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Ollama embedder — local inference, no network egress.
3
3
  *
4
- * Default model: qwen3-embedding:8b (4096 dims, 32k context). Config via
5
- * NUOS_CATALOGUE_OLLAMA_MODEL. Smaller variants (qwen3-embedding:4b,
6
- * qwen3-embedding:0.6b) work the same way; switching variants requires
7
- * a full reindex if the dimension changes.
4
+ * Default model: qwen3-embedding:0.6b (1024 dims). Picked as default
5
+ * because it runs on the broad majority of developer machines without
6
+ * meaningful CPU strain — the prior 8b default produced noticeable load
7
+ * on Apple Silicon during a catalogue reindex, and the build harness
8
+ * ships to projects whose maintainers won't necessarily have an
9
+ * M-series Mac. Higher-fidelity variants (qwen3-embedding:4b at 2560
10
+ * dims, qwen3-embedding:8b at 4096 dims) are available via
11
+ * NUOS_CATALOGUE_OLLAMA_MODEL when the user wants better recall and
12
+ * has the headroom. Switching variants requires a full reindex because
13
+ * dimensions change.
8
14
  *
9
15
  * Why local: keeps the catalogue's content (and any future workload that
10
16
  * uses the same Embedder interface) inside whatever boundary Ollama is
@@ -26,10 +32,10 @@
26
32
  * idle-timeout (the keep_alive: "1m" we sent) cleans up within a
27
33
  * minute.
28
34
  *
29
- * Sizing note — the 8b model at Q4_K_M is ~4.7GB on disk and benefits
30
- * from ~16GB of RAM. Apple Silicon Metal acceleration helps a lot. On
31
- * smaller boxes drop to qwen3-embedding:4b (better accuracy/RAM ratio)
32
- * or qwen3-embedding:0.6b (CPU-only friendly).
35
+ * Sizing note — the new 0.6b default is ~600MB on disk and runs
36
+ * comfortably on any modern laptop, including CPU-only. The 4b variant
37
+ * (~2.5GB) and 8b variant (~4.7GB, benefits from ~16GB RAM + Metal)
38
+ * are upgrades for users who want better recall and have the headroom.
33
39
  */
34
40
  import type { Embedder } from './types.js';
35
41
  export declare class OllamaEmbedder implements Embedder {
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Ollama embedder — local inference, no network egress.
3
3
  *
4
- * Default model: qwen3-embedding:8b (4096 dims, 32k context). Config via
5
- * NUOS_CATALOGUE_OLLAMA_MODEL. Smaller variants (qwen3-embedding:4b,
6
- * qwen3-embedding:0.6b) work the same way; switching variants requires
7
- * a full reindex if the dimension changes.
4
+ * Default model: qwen3-embedding:0.6b (1024 dims). Picked as default
5
+ * because it runs on the broad majority of developer machines without
6
+ * meaningful CPU strain — the prior 8b default produced noticeable load
7
+ * on Apple Silicon during a catalogue reindex, and the build harness
8
+ * ships to projects whose maintainers won't necessarily have an
9
+ * M-series Mac. Higher-fidelity variants (qwen3-embedding:4b at 2560
10
+ * dims, qwen3-embedding:8b at 4096 dims) are available via
11
+ * NUOS_CATALOGUE_OLLAMA_MODEL when the user wants better recall and
12
+ * has the headroom. Switching variants requires a full reindex because
13
+ * dimensions change.
8
14
  *
9
15
  * Why local: keeps the catalogue's content (and any future workload that
10
16
  * uses the same Embedder interface) inside whatever boundary Ollama is
@@ -26,12 +32,12 @@
26
32
  * idle-timeout (the keep_alive: "1m" we sent) cleans up within a
27
33
  * minute.
28
34
  *
29
- * Sizing note — the 8b model at Q4_K_M is ~4.7GB on disk and benefits
30
- * from ~16GB of RAM. Apple Silicon Metal acceleration helps a lot. On
31
- * smaller boxes drop to qwen3-embedding:4b (better accuracy/RAM ratio)
32
- * or qwen3-embedding:0.6b (CPU-only friendly).
35
+ * Sizing note — the new 0.6b default is ~600MB on disk and runs
36
+ * comfortably on any modern laptop, including CPU-only. The 4b variant
37
+ * (~2.5GB) and 8b variant (~4.7GB, benefits from ~16GB RAM + Metal)
38
+ * are upgrades for users who want better recall and have the headroom.
33
39
  */
34
- const DEFAULT_MODEL = 'qwen3-embedding:8b';
40
+ const DEFAULT_MODEL = 'qwen3-embedding:0.6b';
35
41
  const DEFAULT_HOST = 'http://localhost:11434';
36
42
  // Qwen3-Embedding produces Matryoshka representations 32–4096 dims.
37
43
  // We use the model default. A future tweak could truncate to e.g. 1024