@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.
- package/dist/cli.js +52 -39
- package/dist/commands/init.js +119 -42
- package/dist/commands/plan.d.ts +12 -0
- package/dist/commands/plan.js +83 -0
- package/dist/commands/write.js +16 -5
- package/dist/embedder/ollama.d.ts +14 -8
- package/dist/embedder/ollama.js +15 -9
- package/dist/path-resolution.d.ts +68 -0
- package/dist/path-resolution.js +147 -0
- package/dist/runtime/ac-parse.js +10 -6
- package/dist/runtime/markdown-edit.d.ts +5 -0
- package/dist/runtime/markdown-edit.js +13 -6
- package/dist/runtime/mis-adapter.js +7 -2
- package/package.json +2 -2
- package/templates/hooks/install-hooks.sh +44 -0
- package/templates/hooks/post-commit +96 -0
- package/templates/hooks/pre-commit +162 -0
- package/templates/protocols/end-of-session.md +101 -13
- package/templates/protocols/persona-new.md +64 -30
- package/templates/protocols/plan-orientation.md +122 -0
- package/templates/protocols/start-of-session.md +52 -13
- package/templates/protocols/wu-new.md +75 -50
- package/templates/starter-kit/docs/build/GLOSSARY.md +115 -0
- package/templates/starter-kit/docs/build/STATE.md +30 -16
- package/templates/starter-kit/docs/build/WELCOME.md +79 -0
- package/templates/starter-kit/docs/build/architecture/_index.md +39 -0
- package/templates/starter-kit/docs/build/architecture/module-template.md +47 -0
- package/templates/starter-kit/docs/build/contracts/_index.md +39 -0
- package/templates/starter-kit/docs/build/contracts/contract-template.md +64 -0
- package/templates/starter-kit/docs/build/decisions/_index.md +21 -17
- package/templates/starter-kit/docs/build/design-system/_index.md +57 -0
- package/templates/starter-kit/docs/build/design-system/accessibility.md +77 -0
- package/templates/starter-kit/docs/build/design-system/components/_index.md +29 -0
- package/templates/starter-kit/docs/build/design-system/components/_template.md +60 -0
- package/templates/starter-kit/docs/build/design-system/patterns/_index.md +37 -0
- package/templates/starter-kit/docs/build/design-system/patterns/_template.md +57 -0
- package/templates/starter-kit/docs/build/design-system/tokens-colour.md +52 -0
- package/templates/starter-kit/docs/build/design-system/tokens-motion.md +42 -0
- package/templates/starter-kit/docs/build/design-system/tokens-radius-elevation.md +34 -0
- package/templates/starter-kit/docs/build/design-system/tokens-spacing.md +48 -0
- package/templates/starter-kit/docs/build/design-system/tokens-typography.md +46 -0
- package/templates/starter-kit/docs/build/design-system/voice.md +53 -0
- package/templates/starter-kit/docs/build/maps/01-template.md +15 -112
- package/templates/starter-kit/docs/build/maps/02-template.md +52 -0
- package/templates/starter-kit/docs/build/maps/03-template.md +46 -0
- package/templates/starter-kit/docs/build/maps/99-template-power-user-operational-plan.md +126 -0
- package/templates/starter-kit/docs/build/maps/_index.md +17 -52
- package/templates/starter-kit/docs/build/open-questions/_index.md +27 -13
- package/templates/starter-kit/docs/build/personas/_index.md +26 -60
- package/templates/starter-kit/docs/build/risks/_index.md +20 -13
- package/templates/starter-kit/docs/build/sessions/_index.md +18 -16
- package/templates/starter-kit/docs/build/ui-ux/_index.md +48 -0
- package/templates/starter-kit/docs/build/ui-ux/surface-template.md +72 -0
- package/templates/starter-kit/docs/build/work-units/001-template-simple.md +43 -0
- package/templates/starter-kit/docs/build/work-units/_index.md +18 -20
- package/templates/starter-kit/methodfile.json +19 -8
- /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
|
-
|
|
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 =
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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
|
|
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 =
|
|
136
|
-
const workflowsPath =
|
|
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
|
|
173
|
-
const
|
|
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 =
|
|
304
|
-
const workflowsPath =
|
|
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
|
|
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
|
|
410
|
-
NUOS_CATALOGUE_WORKFLOWS
|
|
411
|
-
NUOS_CATALOGUE_ROOT
|
|
412
|
-
NUOS_CATALOGUE_INDEX_DIR
|
|
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':
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
50
|
-
'end-of-session': '
|
|
51
|
-
'wu-new': '
|
|
52
|
-
'persona-new': '
|
|
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
|
|
194
|
-
prompt.print('
|
|
195
|
-
prompt.print(
|
|
196
|
-
prompt.print(
|
|
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('
|
|
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
|
|
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
|
-
|
|
381
|
+
**Start here** if you're new:
|
|
297
382
|
|
|
298
|
-
|
|
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
|
-
###
|
|
386
|
+
### Three commands
|
|
301
387
|
|
|
302
|
-
|
|
388
|
+
That's it. Everything else is automatic.
|
|
303
389
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
395
|
+
(\`init\` runs once at the start; you've already done that.)
|
|
314
396
|
|
|
315
|
-
|
|
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
|
-
|
|
399
|
+
### The principle that makes it work
|
|
327
400
|
|
|
328
|
-
|
|
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
|
|
335
|
-
- Never
|
|
336
|
-
- Never
|
|
337
|
-
|
|
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
|
+
}
|
package/dist/commands/write.js
CHANGED
|
@@ -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 <
|
|
98
|
-
return {
|
|
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:
|
|
124
|
+
criterionIndex: zeroBasedIndex,
|
|
114
125
|
evidence: args.evidence,
|
|
115
126
|
},
|
|
116
127
|
};
|
|
117
|
-
return await driveLifecycle(runtime, 'work_unit.tick_acceptance_criterion', capture, handle, `
|
|
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:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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 {
|
package/dist/embedder/ollama.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ollama embedder — local inference, no network egress.
|
|
3
3
|
*
|
|
4
|
-
* Default model: qwen3-embedding:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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:
|
|
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
|