@nusoft/nuos-build-catalogue 0.12.0 → 0.14.1

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 (56) hide show
  1. package/dist/cli.js +54 -41
  2. package/dist/commands/init.d.ts +12 -2
  3. package/dist/commands/init.js +136 -74
  4. package/dist/commands/plan.d.ts +12 -0
  5. package/dist/commands/plan.js +83 -0
  6. package/dist/commands/write.js +16 -5
  7. package/dist/path-resolution.d.ts +68 -0
  8. package/dist/path-resolution.js +147 -0
  9. package/dist/runtime/ac-parse.js +10 -6
  10. package/dist/runtime/markdown-edit.d.ts +5 -0
  11. package/dist/runtime/markdown-edit.js +13 -6
  12. package/dist/runtime/mis-adapter.js +7 -2
  13. package/package.json +2 -2
  14. package/templates/hooks/install-hooks.sh +44 -0
  15. package/templates/hooks/post-commit +96 -0
  16. package/templates/hooks/pre-commit +162 -0
  17. package/templates/protocols/end-of-session.md +101 -13
  18. package/templates/protocols/persona-new.md +64 -30
  19. package/templates/protocols/plan-orientation.md +122 -0
  20. package/templates/protocols/start-of-session.md +52 -13
  21. package/templates/protocols/wu-new.md +75 -50
  22. package/templates/starter-kit/docs/build/GLOSSARY.md +115 -0
  23. package/templates/starter-kit/docs/build/STATE.md +30 -16
  24. package/templates/starter-kit/docs/build/WELCOME.md +79 -0
  25. package/templates/starter-kit/docs/build/architecture/_index.md +39 -0
  26. package/templates/starter-kit/docs/build/architecture/module-template.md +47 -0
  27. package/templates/starter-kit/docs/build/contracts/_index.md +39 -0
  28. package/templates/starter-kit/docs/build/contracts/contract-template.md +64 -0
  29. package/templates/starter-kit/docs/build/decisions/_index.md +21 -17
  30. package/templates/starter-kit/docs/build/design-system/_index.md +57 -0
  31. package/templates/starter-kit/docs/build/design-system/accessibility.md +77 -0
  32. package/templates/starter-kit/docs/build/design-system/components/_index.md +29 -0
  33. package/templates/starter-kit/docs/build/design-system/components/_template.md +60 -0
  34. package/templates/starter-kit/docs/build/design-system/patterns/_index.md +37 -0
  35. package/templates/starter-kit/docs/build/design-system/patterns/_template.md +57 -0
  36. package/templates/starter-kit/docs/build/design-system/tokens-colour.md +52 -0
  37. package/templates/starter-kit/docs/build/design-system/tokens-motion.md +42 -0
  38. package/templates/starter-kit/docs/build/design-system/tokens-radius-elevation.md +34 -0
  39. package/templates/starter-kit/docs/build/design-system/tokens-spacing.md +48 -0
  40. package/templates/starter-kit/docs/build/design-system/tokens-typography.md +46 -0
  41. package/templates/starter-kit/docs/build/design-system/voice.md +53 -0
  42. package/templates/starter-kit/docs/build/maps/01-template.md +15 -112
  43. package/templates/starter-kit/docs/build/maps/02-template.md +52 -0
  44. package/templates/starter-kit/docs/build/maps/03-template.md +46 -0
  45. package/templates/starter-kit/docs/build/maps/99-template-power-user-operational-plan.md +126 -0
  46. package/templates/starter-kit/docs/build/maps/_index.md +17 -52
  47. package/templates/starter-kit/docs/build/open-questions/_index.md +27 -13
  48. package/templates/starter-kit/docs/build/personas/_index.md +26 -60
  49. package/templates/starter-kit/docs/build/risks/_index.md +20 -13
  50. package/templates/starter-kit/docs/build/sessions/_index.md +18 -16
  51. package/templates/starter-kit/docs/build/ui-ux/_index.md +48 -0
  52. package/templates/starter-kit/docs/build/ui-ux/surface-template.md +72 -0
  53. package/templates/starter-kit/docs/build/work-units/001-template-simple.md +43 -0
  54. package/templates/starter-kit/docs/build/work-units/_index.md +18 -20
  55. package/templates/starter-kit/methodfile.json +19 -8
  56. /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'])) {
@@ -371,7 +362,7 @@ function cmdHelp() {
371
362
  console.log(`nuos-catalogue — NuOS build-catalogue tooling (WU 110, WU 111)
372
363
 
373
364
  Usage:
374
- nuos-catalogue init [--name=X --tagline="Y" --domain=Z --role=consumer --yes]
365
+ nuos-catalogue init [--name=X --tagline="Y" --role=consumer --interactive]
375
366
  (interactive bootstrap of docs/build/, methodfile.json, .claude/commands/<protocols>, CLAUDE.md, .gitignore overrides; refuses if docs/build/ already exists)
376
367
  nuos-catalogue install-protocols
377
368
  (refresh .claude/commands/<protocols> from this CLI's bundled canonical bodies)
@@ -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)
@@ -434,7 +434,7 @@ async function main() {
434
434
  tagline: args.flags['tagline'] ? String(args.flags['tagline']) : undefined,
435
435
  domain: args.flags['domain'] ? String(args.flags['domain']) : undefined,
436
436
  role: args.flags['role'] ? String(args.flags['role']) : undefined,
437
- nonInteractive: Boolean(args.flags['yes']),
437
+ interactive: Boolean(args.flags['interactive']),
438
438
  });
439
439
  if (result.output)
440
440
  console.log(result.output);
@@ -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':
@@ -30,12 +30,22 @@ import type { Prompt } from './prompt.js';
30
30
  export interface InitOptions {
31
31
  /** Target project root (default: cwd). */
32
32
  cwd?: string;
33
- /** Pre-supplied values; if any are missing, the prompt fills them in. */
33
+ /** Pre-supplied values; if omitted, sensible defaults are used. */
34
34
  name?: string;
35
35
  tagline?: string;
36
36
  domain?: string;
37
37
  role?: string;
38
- /** When true, runs all steps without prompts (uses defaults / supplied values). */
38
+ /**
39
+ * Opt into the prompt flow (project name, tagline, role, confirm).
40
+ * Default is non-interactive: scaffolds immediately with sensible
41
+ * defaults. The real configuration happens during Phase A of the
42
+ * planning arc, not at init time.
43
+ */
44
+ interactive?: boolean;
45
+ /**
46
+ * @deprecated kept only for backward compat with the older `--yes` flag;
47
+ * has no effect — init is always non-interactive unless `interactive` is set.
48
+ */
39
49
  nonInteractive?: boolean;
40
50
  }
41
51
  export interface InitResult {
@@ -30,7 +30,6 @@ import { mkdir, readFile, writeFile, readdir, access } from 'node:fs/promises';
30
30
  import { existsSync, constants } from 'node:fs';
31
31
  import path from 'node:path';
32
32
  import { fileURLToPath } from 'node:url';
33
- import { askUntilValid, validate } from './prompt.js';
34
33
  const __filename = fileURLToPath(import.meta.url);
35
34
  const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..', '..');
36
35
  const TEMPLATES_ROOT = path.resolve(PACKAGE_ROOT, 'templates');
@@ -39,6 +38,7 @@ const PROTOCOL_FILES = [
39
38
  'end-of-session.md',
40
39
  'wu-new.md',
41
40
  'persona-new.md',
41
+ 'plan-orientation.md',
42
42
  ];
43
43
  /**
44
44
  * One-line descriptions used in the frontmatter of installed protocol
@@ -46,10 +46,11 @@ const PROTOCOL_FILES = [
46
46
  * read as an imperative summary.
47
47
  */
48
48
  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)',
49
+ 'start-of-session': 'Read where the project is and propose the next concrete action',
50
+ 'end-of-session': 'Capture what happened, update state, write session log, commit',
51
+ 'wu-new': 'File a new work unit through a guided plain-English conversation',
52
+ 'persona-new': 'File a new persona by walking the seven dimensions conversationally',
53
+ 'plan-orientation': 'Phase A of planning — project description, personas, the horizon map',
53
54
  };
54
55
  const TOOLS = {
55
56
  claude: {
@@ -93,44 +94,41 @@ export async function cmdInit(prompt, options = {}) {
93
94
  };
94
95
  }
95
96
  // Gather inputs.
97
+ //
98
+ // Init is **zero-prompt by default**. The user wants `npx ... init` to
99
+ // just work — sensible defaults fill in everything, the scaffold goes
100
+ // down, and the real planning happens in /start-of-session (Phase A of
101
+ // the planning arc), where the AI walks the user through the project
102
+ // description, personas, and the horizon map IN CONTEXT.
103
+ //
104
+ // Old behavior (4 prompts: name, tagline, domain, role) wasn't load-
105
+ // bearing — tagline gets produced during Phase A; domain is a relic;
106
+ // role is a planning-time annotation rarely used downstream. Users who
107
+ // want the prompts back can pass --interactive.
96
108
  const projectDefault = path.basename(cwd);
97
- let name = options.name;
98
- let tagline = options.tagline;
99
- let domain = options.domain;
100
- let role = options.role;
101
- if (!options.nonInteractive) {
102
- prompt.print('Initialising a NuOS Build Method catalogue.');
109
+ let name = options.name ?? projectDefault;
110
+ let tagline = options.tagline ?? '';
111
+ let domain = options.domain ?? '';
112
+ let role = options.role ?? 'consumer';
113
+ if (options.interactive) {
114
+ prompt.print('Setting up the catalogue.');
103
115
  prompt.print('');
104
- if (!name) {
105
- name = await askUntilValid(prompt, `Project name [${projectDefault}]: `, (v) => (v.trim().length === 0 ? null : null) // empty allowed; default applied below
106
- );
107
- if (!name.trim())
108
- name = projectDefault;
116
+ if (!options.name) {
117
+ const answer = (await prompt.ask(`Project name [${projectDefault}]: `)).trim();
118
+ if (answer)
119
+ name = answer;
109
120
  }
110
- if (!tagline) {
111
- tagline = await askUntilValid(prompt, 'One-sentence tagline (what this project is): ', (v) => validate.nonEmpty(v, 'tagline'));
121
+ if (!options.tagline) {
122
+ tagline = (await prompt.ask('One-line description (or empty Phase A will fill it in): ')).trim();
112
123
  }
113
- if (!domain) {
114
- domain = (await prompt.ask('Domain (e.g. example.com; empty for none): ')).trim() || 'n/a';
115
- }
116
- if (!role) {
124
+ if (!options.role) {
117
125
  role = await prompt.askChoice('Project role?', ['consumer', 'standalone', 'harness']);
118
126
  }
119
- const confirm = await prompt.confirm(`About to create docs/build/, methodfile.json, .claude/commands/<protocols>, append to CLAUDE.md, update .gitignore, and run first migrate. Proceed?`, true);
127
+ const confirm = await prompt.confirm(`Create docs/build/, install protocols + hooks, set up the catalogue. Proceed?`, true);
120
128
  if (!confirm) {
121
- return { output: 'init cancelled by operator.', exitCode: 1 };
129
+ return { output: 'init cancelled.', exitCode: 1 };
122
130
  }
123
131
  }
124
- else {
125
- if (!name)
126
- name = projectDefault;
127
- if (!tagline)
128
- tagline = '';
129
- if (!domain)
130
- domain = 'n/a';
131
- if (!role)
132
- role = 'consumer';
133
- }
134
132
  const subs = {
135
133
  '{{PROJECT_NAME}}': name,
136
134
  '{{PROJECT_TAGLINE}}': tagline,
@@ -138,10 +136,15 @@ export async function cmdInit(prompt, options = {}) {
138
136
  '{{PROJECT_ROLE}}': role,
139
137
  '{{TODAY}}': today,
140
138
  };
139
+ // Per-step "· created X" messages are kept as an in-memory audit trail
140
+ // but NOT printed by default. The end-user-facing output is just a
141
+ // single "Done. Type /start-of-session" at the close. Pass `interactive`
142
+ // to see the per-step lines (useful for debugging).
141
143
  const log = [];
142
144
  const log_line = (msg) => {
143
145
  log.push(msg);
144
- prompt.print(msg);
146
+ if (options.interactive)
147
+ prompt.print(msg);
145
148
  };
146
149
  // Step 1: docs/build/ scaffold from starter-kit
147
150
  log_line(' · creating docs/build/ from bundled starter-kit');
@@ -165,6 +168,12 @@ export async function cmdInit(prompt, options = {}) {
165
168
  log_line(` · ${existed ? 'overwrote' : 'installed'} ${tool.destPath(slug)} (${tool.label})`);
166
169
  }
167
170
  }
171
+ // Step 3b: install the git hooks (pre-commit enforcement + post-commit
172
+ // auto-reindex). Two source files land in scripts/hooks/ so the consumer
173
+ // has the version-controlled source-of-truth; copies are pushed into
174
+ // .git/hooks/ so they fire immediately without the user re-running an
175
+ // installer.
176
+ await installHooks(cwd, log_line);
168
177
  // Step 4: CLAUDE.md
169
178
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
170
179
  const catalogueSection = renderCatalogueSection(name);
@@ -188,16 +197,10 @@ export async function cmdInit(prompt, options = {}) {
188
197
  const gitignorePath = path.join(cwd, '.gitignore');
189
198
  await ensureGitignoreEntries(gitignorePath, log_line);
190
199
  prompt.print('');
191
- prompt.print(`✅ Catalogue initialised at ${path.join(cwd, 'docs/build')}`);
200
+ prompt.print('✅ Done.');
192
201
  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`');
202
+ prompt.print('Now type /start-of-session into Claude Code to begin.');
199
203
  prompt.print('');
200
- prompt.print('To refresh protocols only later (without re-running init): `nuos-catalogue install-protocols`');
201
204
  return { output: '', exitCode: 0 };
202
205
  }
203
206
  export async function cmdInstallProtocols(prompt, options = {}) {
@@ -230,9 +233,76 @@ export async function cmdInstallProtocols(prompt, options = {}) {
230
233
  prompt.print(`Refreshing protocols (Claude Code / OpenCode / Codex CLI):`);
231
234
  for (const l of lines)
232
235
  prompt.print(l);
236
+ // Refresh hooks too — same idempotent shape.
237
+ prompt.print('');
238
+ prompt.print(`Refreshing git hooks (pre-commit enforcement, post-commit auto-reindex):`);
239
+ await installHooks(cwd, (msg) => prompt.print(msg));
233
240
  return { output: '', exitCode: 0 };
234
241
  }
235
242
  // ---------------------------------------------------------------------------
243
+ // installHooks — copy bundled hook sources into the consumer + activate them
244
+ // ---------------------------------------------------------------------------
245
+ /**
246
+ * Bundled hooks ship in templates/hooks/. Two source files (pre-commit,
247
+ * post-commit) land in the consumer's scripts/hooks/ so the maintainer
248
+ * has the version-controlled source. Copies also land in .git/hooks/ so
249
+ * they fire immediately. The bash install-hooks.sh script also lands in
250
+ * scripts/ so re-running it after a fresh clone reconstitutes .git/hooks/.
251
+ *
252
+ * Idempotent: byte-identical sources are skipped silently. Permissions
253
+ * are set executable on every install so a chmod isn't required.
254
+ */
255
+ async function installHooks(cwd, log_line) {
256
+ const hooksTemplatesRoot = path.join(TEMPLATES_ROOT, 'hooks');
257
+ if (!existsSync(hooksTemplatesRoot)) {
258
+ log_line(' · (hooks bundle not present in this CLI install — skipping)');
259
+ return;
260
+ }
261
+ // 1) Source-of-truth files in <cwd>/scripts/
262
+ const scriptsDir = path.join(cwd, 'scripts');
263
+ const scriptsHooksDir = path.join(scriptsDir, 'hooks');
264
+ await mkdir(scriptsHooksDir, { recursive: true });
265
+ const hookFiles = ['pre-commit', 'post-commit'];
266
+ for (const name of hookFiles) {
267
+ const src = path.join(hooksTemplatesRoot, name);
268
+ const dest = path.join(scriptsHooksDir, name);
269
+ await writeHookFile(src, dest, log_line, ` · `, `scripts/hooks/${name}`);
270
+ }
271
+ // install-hooks.sh — convenience bash installer; sits next to scripts/hooks/
272
+ const installerSrc = path.join(hooksTemplatesRoot, 'install-hooks.sh');
273
+ const installerDest = path.join(scriptsDir, 'install-hooks.sh');
274
+ await writeHookFile(installerSrc, installerDest, log_line, ` · `, `scripts/install-hooks.sh`);
275
+ // 2) Active copies in <cwd>/.git/hooks/ — only if .git/ exists. Some
276
+ // tests run init in a non-git directory; that's fine, skip the active
277
+ // install and let the user run install-hooks.sh later.
278
+ const gitHooksDir = path.join(cwd, '.git', 'hooks');
279
+ if (!existsSync(path.join(cwd, '.git'))) {
280
+ log_line(` · (no .git/ found at ${cwd} — skipping active hook install; run scripts/install-hooks.sh after \`git init\`)`);
281
+ return;
282
+ }
283
+ await mkdir(gitHooksDir, { recursive: true });
284
+ for (const name of hookFiles) {
285
+ const src = path.join(hooksTemplatesRoot, name);
286
+ const dest = path.join(gitHooksDir, name);
287
+ await writeHookFile(src, dest, log_line, ` · `, `.git/hooks/${name}`);
288
+ }
289
+ }
290
+ async function writeHookFile(src, dest, log_line, prefix, label) {
291
+ const srcContent = await readFile(src, 'utf8');
292
+ let action = 'created';
293
+ if (existsSync(dest)) {
294
+ const destContent = await readFile(dest, 'utf8');
295
+ action = destContent === srcContent ? 'unchanged' : 'updated';
296
+ }
297
+ if (action !== 'unchanged') {
298
+ await writeFile(dest, srcContent, 'utf8');
299
+ }
300
+ // chmod +x — required for git to actually run them
301
+ const { chmod } = await import('node:fs/promises');
302
+ await chmod(dest, 0o755);
303
+ log_line(`${prefix}${action} ${label}`);
304
+ }
305
+ // ---------------------------------------------------------------------------
236
306
  // Helpers
237
307
  // ---------------------------------------------------------------------------
238
308
  function substitute(content, subs) {
@@ -291,48 +361,40 @@ async function ensureGitignoreEntries(gitignorePath, log_line) {
291
361
  function renderCatalogueSection(projectName) {
292
362
  return `## Build catalogue (NuOS Build Method)
293
363
 
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.
364
+ 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
365
 
296
- ### At the start of every session
366
+ **Start here** if you're new:
297
367
 
298
- Run \`/start-of-session\` (or follow [docs/build/START-OF-SESSION.md](docs/build/START-OF-SESSION.md)).
368
+ - [docs/build/WELCOME.md](docs/build/WELCOME.md) — what this catalogue is, in 5 minutes
369
+ - [docs/build/GLOSSARY.md](docs/build/GLOSSARY.md) — every term defined once
299
370
 
300
- ### At the end of every session
371
+ ### Three commands
301
372
 
302
- Run \`/end-of-session\`. **Without it, work is lost.**
373
+ That's it. Everything else is automatic.
303
374
 
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"
375
+ \`\`\`text
376
+ /start-of-session — every time you begin working
377
+ /end-of-session — every time you stop
311
378
  \`\`\`
312
379
 
313
- Then:
380
+ (\`init\` runs once at the start; you've already done that.)
314
381
 
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
- \`\`\`
382
+ 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
383
 
326
- To refresh the protocol bodies later (after a CLI upgrade):
384
+ ### The principle that makes it work
327
385
 
328
- \`\`\`bash
329
- nuos-catalogue install-protocols
330
- \`\`\`
386
+ **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
387
 
332
388
  ### What never to do
333
389
 
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)`;
390
+ - **Never close a session without \`/end-of-session\`.** Work that isn't written down is work that's lost.
391
+ - **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.
392
+ - **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.
393
+
394
+ ### If you need more
395
+
396
+ - All registers and their templates live under [docs/build/](docs/build/)
397
+ - 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)
398
+ - To refresh protocols and hooks later (after a CLI upgrade): \`npx @nusoft/nuos-build-catalogue install-protocols\`
399
+ `;
338
400
  }
@@ -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
+ }