@nusoft/nuos-build-catalogue 0.20.1 → 0.22.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 +39 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/install-claude-hooks.d.ts +31 -0
- package/dist/commands/install-claude-hooks.js +149 -0
- package/dist/commands/wu-active.d.ts +45 -0
- package/dist/commands/wu-active.js +83 -0
- package/package.json +2 -2
- package/templates/claude-hooks/check-implementation-write.sh +167 -0
- package/templates/protocols/build-wu.md +16 -11
- package/templates/protocols/plan-orientation.md +41 -4
- package/templates/protocols/wu-new.md +2 -0
- package/templates/starter-kit/methodfile.json +10 -0
package/dist/cli.js
CHANGED
|
@@ -159,6 +159,27 @@ async function cmdRegisterDispatch(command, positional, flags) {
|
|
|
159
159
|
process.exit(2);
|
|
160
160
|
}
|
|
161
161
|
const action = positional[0];
|
|
162
|
+
// WU 136 — `wu start` / `wu end` / `wu current` are file-only commands
|
|
163
|
+
// (manage the active-WU marker for the PreToolUse hook). They do NOT
|
|
164
|
+
// need the workflow store, so handle them BEFORE the store is opened —
|
|
165
|
+
// this also keeps them fast and avoids requiring a fully-migrated
|
|
166
|
+
// catalogue to declare an active WU.
|
|
167
|
+
if (command === 'wu' && (action === 'start' || action === 'end' || action === 'current')) {
|
|
168
|
+
const { cmdWuStart, cmdWuEnd, cmdWuCurrent } = await import('./commands/wu-active.js');
|
|
169
|
+
let result;
|
|
170
|
+
if (action === 'start') {
|
|
171
|
+
result = cmdWuStart(positional[1], { cwd: process.cwd() });
|
|
172
|
+
}
|
|
173
|
+
else if (action === 'end') {
|
|
174
|
+
result = cmdWuEnd({ cwd: process.cwd() });
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
result = cmdWuCurrent({ cwd: process.cwd() });
|
|
178
|
+
}
|
|
179
|
+
console.log(result.output);
|
|
180
|
+
process.exit(result.exitCode);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
162
183
|
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
163
184
|
const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
|
|
164
185
|
const store = await openWorkflowStore(workflowsPath);
|
|
@@ -368,6 +389,8 @@ Usage:
|
|
|
368
389
|
(run the LLM-setup phase outside 'init': detect Ollama, offer to install where reliable, pull qwen3-embedding:0.6b with a progress bar. Idempotent — safe to re-run)
|
|
369
390
|
nuos-catalogue install-protocols
|
|
370
391
|
(refresh .claude/commands/<protocols> from this CLI's bundled canonical bodies)
|
|
392
|
+
nuos-catalogue install-hooks
|
|
393
|
+
(WU 136 — install the Claude PreToolUse hook that gates sibling-repo writes on a declared active WU; idempotent)
|
|
371
394
|
|
|
372
395
|
nuos-catalogue index [--force] [--dry-run] [--catalogue=<dir>]
|
|
373
396
|
nuos-catalogue search "<query>" [--kind=<file_kind>] [--status=<s>] [--limit=N] [--json]
|
|
@@ -381,6 +404,12 @@ Usage:
|
|
|
381
404
|
nuos-catalogue wu advance <handle> --to=<status> [--reason="..."]
|
|
382
405
|
nuos-catalogue wu tick <handle> --index=N --evidence="..."
|
|
383
406
|
(--index is 1-based: --index=1 ticks the first AC)
|
|
407
|
+
nuos-catalogue wu start <handle>
|
|
408
|
+
(WU 136 — declare this WU as the active one for sibling-repo writes; required by the install-hooks gate)
|
|
409
|
+
nuos-catalogue wu end
|
|
410
|
+
(clear the active-WU marker)
|
|
411
|
+
nuos-catalogue wu current
|
|
412
|
+
(print the active WU handle, or "(none)")
|
|
384
413
|
nuos-catalogue decision list [--status=<s>] [--limit=N] [--json]
|
|
385
414
|
nuos-catalogue decision show <handle> [--json]
|
|
386
415
|
nuos-catalogue decision create (interactive)
|
|
@@ -490,6 +519,16 @@ async function main() {
|
|
|
490
519
|
}
|
|
491
520
|
break;
|
|
492
521
|
}
|
|
522
|
+
case 'install-hooks': {
|
|
523
|
+
// WU 136 — install the Claude PreToolUse hook that gates
|
|
524
|
+
// sibling-repo writes on a declared active WU.
|
|
525
|
+
const { cmdInstallClaudeHooks } = await import('./commands/install-claude-hooks.js');
|
|
526
|
+
const result = cmdInstallClaudeHooks({ cwd: process.cwd() });
|
|
527
|
+
if (result.output)
|
|
528
|
+
console.log(result.output);
|
|
529
|
+
process.exit(result.exitCode);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
493
532
|
case 'migrate':
|
|
494
533
|
await cmdMigrate(args.flags);
|
|
495
534
|
break;
|
package/dist/commands/init.js
CHANGED
|
@@ -51,7 +51,7 @@ const PROTOCOL_DESCRIPTIONS = {
|
|
|
51
51
|
'end-of-session': 'Capture what happened, update state, write session log, commit',
|
|
52
52
|
'wu-new': 'File a new work unit through a guided plain-English conversation',
|
|
53
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',
|
|
54
|
+
'plan-orientation': 'Phase A of planning — project description, tech stack, personas, the horizon map',
|
|
55
55
|
'build-wu': 'Orchestrate a swarm of agents to build one work unit end-to-end',
|
|
56
56
|
};
|
|
57
57
|
const TOOLS = {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `install-hooks` — copy the package's Claude Code PreToolUse hook
|
|
3
|
+
* into the consumer's `.claude/hooks/`, wire it into the consumer's
|
|
4
|
+
* `.claude/settings.json`, and add the active-WU marker file to
|
|
5
|
+
* `.gitignore` so the per-session marker doesn't pollute commits.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent. Safe to re-run after package upgrades — the hook script
|
|
8
|
+
* is overwritten, the settings entry is added only if missing, and the
|
|
9
|
+
* gitignore line is appended only if not present.
|
|
10
|
+
*
|
|
11
|
+
* @module commands/install-claude-hooks
|
|
12
|
+
*/
|
|
13
|
+
export interface CommandResult {
|
|
14
|
+
output: string;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
}
|
|
17
|
+
export interface InstallClaudeHooksOptions {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Override the path the templates are resolved from (testing only). */
|
|
20
|
+
templatesDir?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Idempotently install the Claude PreToolUse hook into the project at
|
|
24
|
+
* `cwd`. Returns a CommandResult describing what was done.
|
|
25
|
+
*/
|
|
26
|
+
export declare function cmdInstallClaudeHooks(opts?: InstallClaudeHooksOptions): CommandResult;
|
|
27
|
+
export declare function addPreToolUseHook(settings: Record<string, unknown>, matcher: string, command: string): {
|
|
28
|
+
value: Record<string, unknown>;
|
|
29
|
+
changed: boolean;
|
|
30
|
+
};
|
|
31
|
+
export declare function ensureGitignoreEntry(path: string, line: string): boolean;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `install-hooks` — copy the package's Claude Code PreToolUse hook
|
|
3
|
+
* into the consumer's `.claude/hooks/`, wire it into the consumer's
|
|
4
|
+
* `.claude/settings.json`, and add the active-WU marker file to
|
|
5
|
+
* `.gitignore` so the per-session marker doesn't pollute commits.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent. Safe to re-run after package upgrades — the hook script
|
|
8
|
+
* is overwritten, the settings entry is added only if missing, and the
|
|
9
|
+
* gitignore line is appended only if not present.
|
|
10
|
+
*
|
|
11
|
+
* @module commands/install-claude-hooks
|
|
12
|
+
*/
|
|
13
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
const HOOK_FILENAME = "check-implementation-write.sh";
|
|
17
|
+
const SETTINGS_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
18
|
+
const SETTINGS_COMMAND = `bash .claude/hooks/${HOOK_FILENAME}`;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the path to the bundled `templates/claude-hooks/` directory.
|
|
21
|
+
* When running from `dist/` (the published package) the templates dir
|
|
22
|
+
* sits at `../templates/claude-hooks/` relative to the compiled JS.
|
|
23
|
+
* When running from source (tsx during development) it's at
|
|
24
|
+
* `../../templates/claude-hooks/` relative to this file.
|
|
25
|
+
*/
|
|
26
|
+
function resolveTemplatesDir() {
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
// Look at sibling-of-parent first (compiled dist/commands/ → dist/../templates/),
|
|
29
|
+
// then grandparent-of-parent (src/commands/ → repo-root/templates/).
|
|
30
|
+
const candidates = [
|
|
31
|
+
resolve(here, "..", "..", "templates", "claude-hooks"),
|
|
32
|
+
resolve(here, "..", "templates", "claude-hooks"),
|
|
33
|
+
];
|
|
34
|
+
for (const c of candidates) {
|
|
35
|
+
if (existsSync(c))
|
|
36
|
+
return c;
|
|
37
|
+
}
|
|
38
|
+
// Last resort: return the first candidate so the error message names
|
|
39
|
+
// a real-ish path.
|
|
40
|
+
return candidates[0];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Idempotently install the Claude PreToolUse hook into the project at
|
|
44
|
+
* `cwd`. Returns a CommandResult describing what was done.
|
|
45
|
+
*/
|
|
46
|
+
export function cmdInstallClaudeHooks(opts = {}) {
|
|
47
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
48
|
+
const templatesDir = opts.templatesDir ?? resolveTemplatesDir();
|
|
49
|
+
const srcHook = join(templatesDir, HOOK_FILENAME);
|
|
50
|
+
if (!existsSync(srcHook)) {
|
|
51
|
+
return {
|
|
52
|
+
output: `✖ nuos: hook template not found at ${srcHook}\n The package may be installed incorrectly. Try reinstalling.`,
|
|
53
|
+
exitCode: 1,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const lines = [];
|
|
57
|
+
// 1. Copy the hook script into .claude/hooks/.
|
|
58
|
+
const hooksDir = join(cwd, ".claude", "hooks");
|
|
59
|
+
if (!existsSync(hooksDir))
|
|
60
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
61
|
+
const destHook = join(hooksDir, HOOK_FILENAME);
|
|
62
|
+
copyFileSync(srcHook, destHook);
|
|
63
|
+
// Mark executable. bash is invoked explicitly via the matcher's
|
|
64
|
+
// `command`, but the exec bit helps when the hook is invoked
|
|
65
|
+
// directly during debugging.
|
|
66
|
+
try {
|
|
67
|
+
chmodSync(destHook, 0o755);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// chmod is best-effort on filesystems that don't support it.
|
|
71
|
+
}
|
|
72
|
+
lines.push(` ✓ installed hook → .claude/hooks/${HOOK_FILENAME}`);
|
|
73
|
+
// 2. Merge into .claude/settings.json. We add a PreToolUse matcher
|
|
74
|
+
// entry only if no entry with the same command already exists.
|
|
75
|
+
const settingsPath = join(cwd, ".claude", "settings.json");
|
|
76
|
+
const settings = readJsonOrEmpty(settingsPath);
|
|
77
|
+
const updated = addPreToolUseHook(settings, SETTINGS_MATCHER, SETTINGS_COMMAND);
|
|
78
|
+
if (updated.changed) {
|
|
79
|
+
writeFileSync(settingsPath, JSON.stringify(updated.value, null, 2) + "\n", "utf8");
|
|
80
|
+
lines.push(" ✓ updated .claude/settings.json (PreToolUse entry added)");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push(" · .claude/settings.json already has the PreToolUse entry");
|
|
84
|
+
}
|
|
85
|
+
// 3. Add the active-WU marker to .gitignore.
|
|
86
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
87
|
+
const addedGitignore = ensureGitignoreEntry(gitignorePath, ".nuos-catalogue/active-wu");
|
|
88
|
+
if (addedGitignore) {
|
|
89
|
+
lines.push(" ✓ added .nuos-catalogue/active-wu to .gitignore");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(" · .nuos-catalogue/active-wu already in .gitignore");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
output: [
|
|
96
|
+
"nuos: Claude Code hooks installed.",
|
|
97
|
+
...lines,
|
|
98
|
+
"",
|
|
99
|
+
"Next: declare an active work unit before substantive sibling-repo work:",
|
|
100
|
+
" nuos-catalogue wu start <handle>",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
exitCode: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── helpers ─────────────────────────────────────────────────────────────
|
|
106
|
+
function readJsonOrEmpty(path) {
|
|
107
|
+
if (!existsSync(path))
|
|
108
|
+
return {};
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function addPreToolUseHook(settings, matcher, command) {
|
|
117
|
+
// Defensive copy so callers can't accidentally see in-place mutation.
|
|
118
|
+
const next = { ...settings };
|
|
119
|
+
const hooksField = next.hooks ?? {};
|
|
120
|
+
const preToolUse = hooksField.PreToolUse ?? [];
|
|
121
|
+
// Already present? Check by command string (any matcher).
|
|
122
|
+
for (const entry of preToolUse) {
|
|
123
|
+
for (const h of entry.hooks ?? []) {
|
|
124
|
+
if (h.command === command) {
|
|
125
|
+
return { value: next, changed: false };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const newEntry = {
|
|
130
|
+
matcher,
|
|
131
|
+
hooks: [{ type: "command", command }],
|
|
132
|
+
};
|
|
133
|
+
const newPreToolUse = [...preToolUse, newEntry];
|
|
134
|
+
const newHooks = { ...hooksField, PreToolUse: newPreToolUse };
|
|
135
|
+
next.hooks = newHooks;
|
|
136
|
+
return { value: next, changed: true };
|
|
137
|
+
}
|
|
138
|
+
export function ensureGitignoreEntry(path, line) {
|
|
139
|
+
let body = "";
|
|
140
|
+
if (existsSync(path)) {
|
|
141
|
+
body = readFileSync(path, "utf8");
|
|
142
|
+
}
|
|
143
|
+
const lines = body.split("\n").map((l) => l.trim());
|
|
144
|
+
if (lines.includes(line))
|
|
145
|
+
return false;
|
|
146
|
+
const newBody = body.endsWith("\n") || body.length === 0 ? body + line + "\n" : body + "\n" + line + "\n";
|
|
147
|
+
writeFileSync(path, newBody, "utf8");
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wu start` / `wu end` / `wu current` — manage the active-WU marker
|
|
3
|
+
* consumed by the Claude PreToolUse hook (WU 136).
|
|
4
|
+
*
|
|
5
|
+
* The marker is a single-line file at `.nuos-catalogue/active-wu`
|
|
6
|
+
* containing the handle of the WU currently being implemented. The hook
|
|
7
|
+
* reads it to decide whether a sibling-repo write is allowed.
|
|
8
|
+
*
|
|
9
|
+
* These commands are intentionally minimal — file read / write / unlink
|
|
10
|
+
* with friendly stdout. No workflow-store interaction, no validation of
|
|
11
|
+
* whether the handle resolves to a real WU. Validation could be added
|
|
12
|
+
* later if the unverified-handle pattern becomes a problem in practice;
|
|
13
|
+
* today's risk is low because the operator types the handle themselves.
|
|
14
|
+
*
|
|
15
|
+
* @module commands/wu-active
|
|
16
|
+
*/
|
|
17
|
+
export interface CommandResult {
|
|
18
|
+
output: string;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
}
|
|
21
|
+
export interface WuActiveOptions {
|
|
22
|
+
/** Project root. Defaults to process.cwd(). */
|
|
23
|
+
cwd?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Compute the path to the active-WU marker for a given project root. */
|
|
26
|
+
export declare function activeWuMarkerPath(cwd: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* `wu start <handle>` — write the handle into the marker. Creates the
|
|
29
|
+
* `.nuos-catalogue/` directory if it doesn't yet exist (idempotent).
|
|
30
|
+
*
|
|
31
|
+
* Overwrites any existing marker without ceremony — the operator's
|
|
32
|
+
* `start` is authoritative. If they want to know the current value
|
|
33
|
+
* before overwriting, they use `wu current`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cmdWuStart(handle: string | undefined, opts?: WuActiveOptions): CommandResult;
|
|
36
|
+
/**
|
|
37
|
+
* `wu end` — remove the marker. Succeeds silently if no marker is
|
|
38
|
+
* present (idempotent: stopping a stopped state is fine).
|
|
39
|
+
*/
|
|
40
|
+
export declare function cmdWuEnd(opts?: WuActiveOptions): CommandResult;
|
|
41
|
+
/**
|
|
42
|
+
* `wu current` — print the current active WU handle or `(none)`.
|
|
43
|
+
* Always exits 0; absence is not an error.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cmdWuCurrent(opts?: WuActiveOptions): CommandResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wu start` / `wu end` / `wu current` — manage the active-WU marker
|
|
3
|
+
* consumed by the Claude PreToolUse hook (WU 136).
|
|
4
|
+
*
|
|
5
|
+
* The marker is a single-line file at `.nuos-catalogue/active-wu`
|
|
6
|
+
* containing the handle of the WU currently being implemented. The hook
|
|
7
|
+
* reads it to decide whether a sibling-repo write is allowed.
|
|
8
|
+
*
|
|
9
|
+
* These commands are intentionally minimal — file read / write / unlink
|
|
10
|
+
* with friendly stdout. No workflow-store interaction, no validation of
|
|
11
|
+
* whether the handle resolves to a real WU. Validation could be added
|
|
12
|
+
* later if the unverified-handle pattern becomes a problem in practice;
|
|
13
|
+
* today's risk is low because the operator types the handle themselves.
|
|
14
|
+
*
|
|
15
|
+
* @module commands/wu-active
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
/** Compute the path to the active-WU marker for a given project root. */
|
|
20
|
+
export function activeWuMarkerPath(cwd) {
|
|
21
|
+
return join(cwd, ".nuos-catalogue", "active-wu");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `wu start <handle>` — write the handle into the marker. Creates the
|
|
25
|
+
* `.nuos-catalogue/` directory if it doesn't yet exist (idempotent).
|
|
26
|
+
*
|
|
27
|
+
* Overwrites any existing marker without ceremony — the operator's
|
|
28
|
+
* `start` is authoritative. If they want to know the current value
|
|
29
|
+
* before overwriting, they use `wu current`.
|
|
30
|
+
*/
|
|
31
|
+
export function cmdWuStart(handle, opts = {}) {
|
|
32
|
+
if (!handle || handle.trim().length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
output: "Usage: nuos-catalogue wu start <handle>",
|
|
35
|
+
exitCode: 2,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
39
|
+
const marker = activeWuMarkerPath(cwd);
|
|
40
|
+
const dir = dirname(marker);
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
writeFileSync(marker, handle.trim() + "\n", "utf8");
|
|
44
|
+
return {
|
|
45
|
+
output: `nuos: active work unit set to "${handle.trim()}"`,
|
|
46
|
+
exitCode: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `wu end` — remove the marker. Succeeds silently if no marker is
|
|
51
|
+
* present (idempotent: stopping a stopped state is fine).
|
|
52
|
+
*/
|
|
53
|
+
export function cmdWuEnd(opts = {}) {
|
|
54
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
55
|
+
const marker = activeWuMarkerPath(cwd);
|
|
56
|
+
if (existsSync(marker)) {
|
|
57
|
+
const prev = readFileSync(marker, "utf8").trim();
|
|
58
|
+
unlinkSync(marker);
|
|
59
|
+
return {
|
|
60
|
+
output: `nuos: cleared active work unit (was "${prev}")`,
|
|
61
|
+
exitCode: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
output: "nuos: no active work unit to clear",
|
|
66
|
+
exitCode: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `wu current` — print the current active WU handle or `(none)`.
|
|
71
|
+
* Always exits 0; absence is not an error.
|
|
72
|
+
*/
|
|
73
|
+
export function cmdWuCurrent(opts = {}) {
|
|
74
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
75
|
+
const marker = activeWuMarkerPath(cwd);
|
|
76
|
+
if (existsSync(marker)) {
|
|
77
|
+
const handle = readFileSync(marker, "utf8").trim();
|
|
78
|
+
if (handle.length > 0) {
|
|
79
|
+
return { output: handle, exitCode: 0 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { output: "(none)", exitCode: 0 };
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nusoft/nuos-build-catalogue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "NuOS build-catalogue tooling: semantic search (WU 110) + migration runner that lifts markdown artefacts into JSON-backed workflow records (WU 111, Phase G).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
20
20
|
"prepublishOnly": "npm run build",
|
|
21
21
|
"verify-storage": "tsx scripts/verify-persistence.ts",
|
|
22
|
-
"test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts",
|
|
22
|
+
"test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts tests/wu-active.test.ts tests/install-claude-hooks.test.ts",
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
24
|
"index": "tsx src/cli.ts index",
|
|
25
25
|
"search": "tsx src/cli.ts search"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# NuOS Build Method — Claude Code PreToolUse hook (WU 136).
|
|
4
|
+
#
|
|
5
|
+
# Blocks Edit / Write / MultiEdit / NotebookEdit tool calls when:
|
|
6
|
+
# (a) the target file is OUTSIDE the catalogue project root, AND
|
|
7
|
+
# (b) no active work unit has been declared via `nuos-catalogue wu start <handle>`.
|
|
8
|
+
#
|
|
9
|
+
# Rationale
|
|
10
|
+
# ─────────
|
|
11
|
+
# The existing git pre-commit hook (templates/hooks/pre-commit) catches
|
|
12
|
+
# catalogue-side drift on commit. It does not see writes to sibling
|
|
13
|
+
# implementation repos (sensight/, nuvector/, …) — those are different
|
|
14
|
+
# git roots. So an agent can ship hours of substantive implementation
|
|
15
|
+
# work across many sibling-repo files before any catalogue trace gets
|
|
16
|
+
# recorded. This hook closes that gap at the earliest possible moment:
|
|
17
|
+
# the file-write itself.
|
|
18
|
+
#
|
|
19
|
+
# This is a soft-gate. The block is honest about what it is and how to
|
|
20
|
+
# release it (declare the active WU). The catalogue stays in charge of
|
|
21
|
+
# the project's discipline; the operator can override locally if a
|
|
22
|
+
# one-off write is genuinely needed (see "Manual override" below).
|
|
23
|
+
#
|
|
24
|
+
# Behaviour
|
|
25
|
+
# ─────────
|
|
26
|
+
# 1. Read the tool-call JSON on stdin (Claude Code PreToolUse contract).
|
|
27
|
+
# 2. Extract `tool_input.file_path` or `tool_input.notebook_path`.
|
|
28
|
+
# If neither is present (parse failure), exit 0 — never block on
|
|
29
|
+
# ambiguous input.
|
|
30
|
+
# 3. Determine the catalogue project root via $CLAUDE_PROJECT_DIR, falling
|
|
31
|
+
# back to `git rev-parse --show-toplevel` from cwd. If neither works,
|
|
32
|
+
# exit 0 (degrade-safe).
|
|
33
|
+
# 4. Classify the target path:
|
|
34
|
+
# — Inside the project root: ALLOW. Editing the catalogue itself
|
|
35
|
+
# (work units, decisions, indexes, hooks, scripts) is the
|
|
36
|
+
# catalogue trace — no WU declaration required.
|
|
37
|
+
# — Outside the project root: this is sibling-repo implementation
|
|
38
|
+
# work and requires a declared active WU.
|
|
39
|
+
# 5. For sibling-repo paths, check for an active-WU marker at
|
|
40
|
+
# `$PROJECT_ROOT/.nuos-catalogue/active-wu`.
|
|
41
|
+
# — Marker present (non-empty file): ALLOW. Log the touch with the
|
|
42
|
+
# declared WU handle so the audit trail names the work.
|
|
43
|
+
# — Marker absent or empty: BLOCK with exit 2 and a stderr message
|
|
44
|
+
# telling the operator what was blocked, what's missing, and the
|
|
45
|
+
# two commands to recover.
|
|
46
|
+
#
|
|
47
|
+
# Manual override
|
|
48
|
+
# ───────────────
|
|
49
|
+
# If a write to a sibling repo is genuinely catalogue-orthogonal (e.g.
|
|
50
|
+
# adjusting a personal dotfile, applying a hotfix unrelated to project
|
|
51
|
+
# work), the operator can either:
|
|
52
|
+
# (a) declare a temporary "ad-hoc" WU: `nuos-catalogue wu start adhoc`
|
|
53
|
+
# then `nuos-catalogue wu end` when done. The audit log records
|
|
54
|
+
# the touches under "adhoc" — visible at end-of-session.
|
|
55
|
+
# (b) set NUOS_SKIP_IMPLEMENTATION_GATE=1 in the environment for that
|
|
56
|
+
# single tool call. The block is bypassed and a STRONG warning is
|
|
57
|
+
# emitted to stderr. The bypass is logged.
|
|
58
|
+
#
|
|
59
|
+
# Bypass log lives at $PROJECT_ROOT/.nuos-enforcement.log alongside the
|
|
60
|
+
# catalogue-write hook's audit trail.
|
|
61
|
+
#
|
|
62
|
+
# Exit codes
|
|
63
|
+
# ──────────
|
|
64
|
+
# 0 — allow (or degrade-safe)
|
|
65
|
+
# 2 — block (Claude Code surfaces stderr to the model)
|
|
66
|
+
|
|
67
|
+
set -uo pipefail
|
|
68
|
+
|
|
69
|
+
# ── Inputs ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
72
|
+
|
|
73
|
+
# Project root: prefer the Claude-provided env var, fall back to git.
|
|
74
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
75
|
+
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
76
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
77
|
+
fi
|
|
78
|
+
if [[ -z "$PROJECT_ROOT" ]]; then exit 0; fi
|
|
79
|
+
|
|
80
|
+
LOG="$PROJECT_ROOT/.nuos-enforcement.log"
|
|
81
|
+
|
|
82
|
+
# Extract the target file path. Edit/Write use `file_path`; NotebookEdit
|
|
83
|
+
# uses `notebook_path`. MultiEdit also uses `file_path` (single root
|
|
84
|
+
# file for the batch).
|
|
85
|
+
FILE=$(printf '%s' "$INPUT" \
|
|
86
|
+
| grep -oE '"(file_path|notebook_path)"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
|
87
|
+
| head -1 \
|
|
88
|
+
| sed -E 's/.*"([^"]+)"$/\1/')
|
|
89
|
+
|
|
90
|
+
# Parse failure: degrade safe (never block on ambiguous input).
|
|
91
|
+
if [[ -z "${FILE:-}" ]]; then exit 0; fi
|
|
92
|
+
|
|
93
|
+
# ── Classify the target path ────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
# Normalise: if the path is relative, treat it as relative to cwd. The
|
|
96
|
+
# tool always passes absolute paths in practice, but we guard regardless.
|
|
97
|
+
case "$FILE" in
|
|
98
|
+
/*) ABSOLUTE_FILE="$FILE" ;;
|
|
99
|
+
*) ABSOLUTE_FILE="$(pwd)/$FILE" ;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
# Trailing-slash-tolerant prefix match.
|
|
103
|
+
case "$ABSOLUTE_FILE/" in
|
|
104
|
+
"$PROJECT_ROOT"/*) IS_INTERNAL=1 ;;
|
|
105
|
+
*) IS_INTERNAL=0 ;;
|
|
106
|
+
esac
|
|
107
|
+
|
|
108
|
+
log_event() {
|
|
109
|
+
printf '%s | %s | %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$1" "$2" >> "$LOG" 2>/dev/null || true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ── Internal write: always allowed ──────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
if [[ "$IS_INTERNAL" == "1" ]]; then
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ── Sibling-repo write: requires an active WU declaration ──────────────
|
|
119
|
+
|
|
120
|
+
# Manual override (escape hatch). Logged loudly.
|
|
121
|
+
if [[ "${NUOS_SKIP_IMPLEMENTATION_GATE:-}" == "1" ]]; then
|
|
122
|
+
log_event "implementation-gate-bypassed" "$ABSOLUTE_FILE"
|
|
123
|
+
printf '⚠ nuos: NUOS_SKIP_IMPLEMENTATION_GATE=1 — sibling-repo write allowed without a WU declaration.\n' >&2
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
MARKER="$PROJECT_ROOT/.nuos-catalogue/active-wu"
|
|
128
|
+
ACTIVE_WU=""
|
|
129
|
+
if [[ -f "$MARKER" ]]; then
|
|
130
|
+
ACTIVE_WU="$(head -n 1 "$MARKER" 2>/dev/null | tr -d '[:space:]')"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [[ -n "$ACTIVE_WU" ]]; then
|
|
134
|
+
log_event "implementation-write-allowed[$ACTIVE_WU]" "$ABSOLUTE_FILE"
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Block. Stderr is surfaced to the model.
|
|
139
|
+
log_event "implementation-write-blocked" "$ABSOLUTE_FILE"
|
|
140
|
+
cat >&2 <<EOF
|
|
141
|
+
✖ nuos: implementation write blocked (WU 136 gate).
|
|
142
|
+
|
|
143
|
+
Target file: $ABSOLUTE_FILE
|
|
144
|
+
Reason: This path is OUTSIDE the catalogue project root
|
|
145
|
+
($PROJECT_ROOT)
|
|
146
|
+
and no active work unit has been declared.
|
|
147
|
+
|
|
148
|
+
Substantive implementation work in a sibling repo must trace to a
|
|
149
|
+
catalogued work unit. Choose one of:
|
|
150
|
+
|
|
151
|
+
1. Declare an existing WU as active for this session:
|
|
152
|
+
nuos-catalogue wu start <handle> e.g. wu start 136
|
|
153
|
+
|
|
154
|
+
2. File a new WU first, then declare it active:
|
|
155
|
+
nuos-catalogue wu create
|
|
156
|
+
nuos-catalogue wu start <new-handle>
|
|
157
|
+
|
|
158
|
+
3. When done, clear the marker:
|
|
159
|
+
nuos-catalogue wu end
|
|
160
|
+
|
|
161
|
+
Genuinely catalogue-orthogonal write? Set
|
|
162
|
+
NUOS_SKIP_IMPLEMENTATION_GATE=1 to bypass for one call (logged to
|
|
163
|
+
.nuos-enforcement.log for the audit trail).
|
|
164
|
+
|
|
165
|
+
EOF
|
|
166
|
+
|
|
167
|
+
exit 2
|
|
@@ -19,6 +19,7 @@ Also read:
|
|
|
19
19
|
- The contracts it touches (`docs/build/contracts/`)
|
|
20
20
|
- The architecture files for any modules involved (`docs/build/architecture/`)
|
|
21
21
|
- The relevant design-system pieces if the work unit ships a UI surface
|
|
22
|
+
- `methodfile.json`'s `techStack` section — if `techStack.defined` is `true`, extract the fields now; you'll inject them into every agent prompt in Step 4
|
|
22
23
|
- Run `nuos-catalogue search "<work unit title or outcome>"` to find related prior work
|
|
23
24
|
|
|
24
25
|
Before spawning any agents, search the cross-agent memory for relevant prior findings:
|
|
@@ -61,6 +62,20 @@ Skip steps when context allows — implementation-only WUs skip the architect; b
|
|
|
61
62
|
|
|
62
63
|
Use Claude Code's **Task tool**. Each spawn names the agent (`subagent_type`), the model (from `methodfile.json`'s `swarm.models` block — usually leave as default), and the precise input.
|
|
63
64
|
|
|
65
|
+
**Technical context injection:** If `techStack.defined` is `true` in `methodfile.json`, every agent spawn prompt must open with a "Technical context" block:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
**Technical context (from methodfile.json):**
|
|
69
|
+
- Languages: [languages]
|
|
70
|
+
- Frontend: [frontend]
|
|
71
|
+
- Backend: [backend]
|
|
72
|
+
- Database: [database]
|
|
73
|
+
- Deployment: [deployment]
|
|
74
|
+
- External services: [externalServices]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Omit `null` fields. If `techStack.defined` is `false` or the section is absent, note it in the swarm audit entry and suggest the operator define the stack (`/plan-orientation` or edit `methodfile.json` directly) before the next swarm run — agents generating code without a known stack default to generic patterns that often need rework.
|
|
78
|
+
|
|
64
79
|
**Spawn in parallel where possible.** If two agents can work independently (e.g. tester writing tests while reviewer reads design), spawn them in the same message. Sequential when an agent's output is the next agent's input (architect → coder).
|
|
65
80
|
|
|
66
81
|
For each spawn:
|
|
@@ -141,7 +156,7 @@ Every decision made by any agent during the swarm MUST land in the catalogue bef
|
|
|
141
156
|
|
|
142
157
|
## Cost guidance
|
|
143
158
|
|
|
144
|
-
A typical full-feature swarm spawning architect (Opus, ~30 min) + coder (Sonnet, ~1 hr) + tester (Sonnet, ~30 min) + reviewer (Sonnet, ~15 min)
|
|
159
|
+
A typical full-feature swarm spawning architect (Opus, ~30 min) + coder (Sonnet, ~1 hr) + tester (Sonnet, ~30 min) + reviewer (Sonnet, ~15 min) consumes substantially less of the operator's coding-tool plan budget than running the same work as a continuous Opus conversation. The 80/20 split — heavy reasoning for design and debugging only, lighter models for implementation and verification — is the lever. If a single work unit's swarm is consuming an unusual share of the day's plan budget, surface that to the operator before continuing; the WU is probably bigger than scoped.
|
|
145
160
|
|
|
146
161
|
---
|
|
147
162
|
|
|
@@ -158,16 +173,6 @@ If the reviewer returns REQUEST CHANGES, re-spawn the coder ONCE to address the
|
|
|
158
173
|
|
|
159
174
|
Don't loop indefinitely. A third reviewer rejection is a signal — the work unit's design, contract, or acceptance criteria need clarification, not more code.
|
|
160
175
|
|
|
161
|
-
### Cost ceiling per work unit
|
|
162
|
-
|
|
163
|
-
If the estimated cost (per the swarm audit) is exceeding **£10** for a single work unit:
|
|
164
|
-
|
|
165
|
-
- STOP the swarm
|
|
166
|
-
- Surface the cost trajectory to the operator
|
|
167
|
-
- Recommend either splitting the work unit into smaller pieces, or accepting the higher cost with their explicit go-ahead
|
|
168
|
-
|
|
169
|
-
This is a soft ceiling — the operator can authorise more. The point is to make cost visible before it accumulates invisibly.
|
|
170
|
-
|
|
171
176
|
### Time ceiling per agent
|
|
172
177
|
|
|
173
178
|
If a single agent's run is taking substantially longer than its rough budget (architect >1 hr, coder >2 hrs, tester >1 hr, reviewer >30 min):
|
|
@@ -45,7 +45,43 @@ Listen. Don't interrupt. When they're done, summarise back in 2-3 sentences in y
|
|
|
45
45
|
|
|
46
46
|
When the description is settled, **write it into `STATE.md`'s "What is currently in flight" section** — replacing the placeholder. Keep their voice; don't make it sound corporate. Show them the file path and confirm it's saved.
|
|
47
47
|
|
|
48
|
-
## Step 3 —
|
|
48
|
+
## Step 3 — Tech stack (5 min)
|
|
49
|
+
|
|
50
|
+
Now ask what they're building it with:
|
|
51
|
+
|
|
52
|
+
> "Before we meet the people your project is for — quickly, what are you building it with? Language, framework, database, where it'll run. If you know already, brilliant. If you haven't decided, just say so and we'll note it as an open question."
|
|
53
|
+
|
|
54
|
+
Listen and capture what they give you. Common patterns:
|
|
55
|
+
- *"Next.js, PostgreSQL, deployed on Vercel"* → frontend + database + deployment all filled
|
|
56
|
+
- *"React Native with Firebase"* → frontend + database/backend filled
|
|
57
|
+
- *"Not sure yet"* → set `defined: false`, file a Q-NNN
|
|
58
|
+
|
|
59
|
+
**Write the result to `methodfile.json` now**, under the `techStack` section:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"techStack": {
|
|
64
|
+
"defined": true,
|
|
65
|
+
"languages": ["TypeScript"],
|
|
66
|
+
"frontend": "Next.js 15 (App Router)",
|
|
67
|
+
"backend": "Next.js API Routes / Server Actions",
|
|
68
|
+
"database": "PostgreSQL (Supabase)",
|
|
69
|
+
"deployment": "Vercel",
|
|
70
|
+
"externalServices": ["Stripe"],
|
|
71
|
+
"notes": null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Fill in what you know; set unknown fields to `null`. If nothing is settled, set `defined: false`, leave all fields null, and file a Q-NNN open question: *"Tech stack not yet decided — revisit before Phase B."*
|
|
77
|
+
|
|
78
|
+
Show the operator the updated `methodfile.json` and confirm it saved. Tell them:
|
|
79
|
+
|
|
80
|
+
> *"This means every agent we spawn later will know what it's building against — language, framework, where it runs. Just a few fields, but it prevents a lot of wrong output later."*
|
|
81
|
+
|
|
82
|
+
**Drift discipline:** partial information is fine and still valuable. An operator who says *"definitely Next.js, not sure about the database yet"* should have `frontend: "Next.js"`, `database: null`, `defined: true`. Partial is better than undefined.
|
|
83
|
+
|
|
84
|
+
## Step 4 — One persona, then one or two more (15-20 min)
|
|
49
85
|
|
|
50
86
|
Tell the operator what's coming:
|
|
51
87
|
|
|
@@ -59,7 +95,7 @@ When P001 is filed, surface it and ask:
|
|
|
59
95
|
|
|
60
96
|
If yes, run `/persona-new` again. Aim for **1-3 total** — more than 3 in Phase A usually means the project is overscoped; file the rest as open questions and revisit later.
|
|
61
97
|
|
|
62
|
-
## Step
|
|
98
|
+
## Step 5 — Map 1: The Horizon (8-10 min)
|
|
63
99
|
|
|
64
100
|
When the personas are filed, transition:
|
|
65
101
|
|
|
@@ -75,7 +111,7 @@ Use the template at `docs/build/maps/01-template.md`. Walk through its sections
|
|
|
75
111
|
|
|
76
112
|
Write the map to `docs/build/maps/01-the-horizon.md`. Show them the file path and confirm.
|
|
77
113
|
|
|
78
|
-
## Step
|
|
114
|
+
## Step 6 — Open questions (2 min)
|
|
79
115
|
|
|
80
116
|
Pass over the conversation looking for anything the operator wasn't sure about. For each:
|
|
81
117
|
|
|
@@ -84,7 +120,7 @@ Pass over the conversation looking for anything the operator wasn't sure about.
|
|
|
84
120
|
|
|
85
121
|
> "I noticed a few things you weren't sure about yet — [list]. I've filed them as open questions so we'll come back to them. Two of them affect Phase B (Architecture), so we'll definitely hit them next session."
|
|
86
122
|
|
|
87
|
-
## Step
|
|
123
|
+
## Step 7 — Close (2 min)
|
|
88
124
|
|
|
89
125
|
Update STATE.md:
|
|
90
126
|
|
|
@@ -96,6 +132,7 @@ Then tell the operator what they now have:
|
|
|
96
132
|
|
|
97
133
|
> "You've got your first catalogue substrate:
|
|
98
134
|
>
|
|
135
|
+
> - **Tech stack** defined in `methodfile.json` — (or flagged as an open question if not yet settled)
|
|
99
136
|
> - **[N] personas** in `docs/build/personas/` — these anchor every later decision
|
|
100
137
|
> - **Map 1** at `docs/build/maps/01-the-horizon.md` — the whole-project picture
|
|
101
138
|
> - **[N] open questions** in `docs/build/open-questions/` — these are what we'll resolve as planning continues
|
|
@@ -42,6 +42,8 @@ For infrastructure work, persona / trigger / walkthrough are marked `N/A — inf
|
|
|
42
42
|
|
|
43
43
|
## Step 4 — File the work unit
|
|
44
44
|
|
|
45
|
+
Before writing the file, check `methodfile.json`'s `techStack.defined`. If it's `false` or the field is absent, tell the operator: *"I notice the tech stack isn't defined yet — that normally happens during Phase A planning. Want to define it now, or shall I file an open question?"* Either way, continue filing the work unit. If `defined` is `true`, the acceptance criteria may reference the stack where relevant (e.g. *"renders correctly with Next.js App Router SSR"*).
|
|
46
|
+
|
|
45
47
|
1. **Number it.** Scan `docs/build/work-units/` and `docs/build/work-units/done/` for the highest existing 3-digit prefix; new number is max + 1.
|
|
46
48
|
2. **Slugify the title.** Lowercase; dashes for spaces; no special characters; cap at 60 chars.
|
|
47
49
|
3. **Write the file** at `docs/build/work-units/NNN-slug.md`. Use `001-template-simple.md` for the simple shape, `001-template-full.md` for the full shape.
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
"tagline": "{{PROJECT_TAGLINE}}",
|
|
10
10
|
"domain": "{{PROJECT_DOMAIN}}"
|
|
11
11
|
},
|
|
12
|
+
"techStack": {
|
|
13
|
+
"defined": false,
|
|
14
|
+
"languages": [],
|
|
15
|
+
"frontend": null,
|
|
16
|
+
"backend": null,
|
|
17
|
+
"database": null,
|
|
18
|
+
"deployment": null,
|
|
19
|
+
"externalServices": [],
|
|
20
|
+
"notes": null
|
|
21
|
+
},
|
|
12
22
|
"catalogue": {
|
|
13
23
|
"root": "docs/build/",
|
|
14
24
|
"registers": {
|