@nusoft/nuos-build-catalogue 0.19.0 → 0.19.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.
@@ -274,8 +274,52 @@ export async function cmdInstallProtocols(prompt, options = {}) {
274
274
  prompt.print('');
275
275
  prompt.print(`Refreshing swarm agent definitions (.claude/agents/):`);
276
276
  await installAgents(cwd, (msg) => prompt.print(msg));
277
+ // Quick non-interactive probe of the local-inference stack (WU 135).
278
+ // `install-protocols` is the natural upgrade path for existing
279
+ // projects, so we surface the LLM status here too — but as a status
280
+ // report rather than the full install/pull flow (which is what
281
+ // `setup-llm` is for). This keeps install-protocols fast and
282
+ // script-safe while making the LLM state visible without the user
283
+ // needing to know about a separate command.
284
+ prompt.print('');
285
+ prompt.print('Checking local semantic search (Ollama + qwen3-embedding:0.6b):');
286
+ await reportLlmStatus((msg) => prompt.print(` ${msg}`));
277
287
  return { output: '', exitCode: 0 };
278
288
  }
289
+ /**
290
+ * Quick probe + status print for the LLM stack. Non-interactive: never
291
+ * prompts, never installs, never pulls. The full install/pull flow
292
+ * lives in `setup-llm`; this is the "what's the current state?" report.
293
+ *
294
+ * Times out after ~1.5s when Ollama isn't running so the command stays
295
+ * snappy on machines that haven't set up local inference yet.
296
+ */
297
+ async function reportLlmStatus(log) {
298
+ const { narrowPlatform } = await import('../setup/types.js');
299
+ const { detectOllamaApi, detectModelPresent } = await import('../setup/ollama-detect.js');
300
+ const { DEFAULT_EMBEDDING_MODEL } = await import('../setup/run-llm-setup.js');
301
+ const platform = narrowPlatform(process.platform);
302
+ const apiHost = process.env.NUOS_CATALOGUE_OLLAMA_HOST ?? 'http://localhost:11434';
303
+ const modelId = process.env.NUOS_CATALOGUE_OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
304
+ const api = await detectOllamaApi(apiHost);
305
+ if (!api.reachable) {
306
+ log(`✗ Ollama is not running at ${apiHost}`);
307
+ log(' Run `nuos-catalogue setup-llm` for guided install + pull.');
308
+ return;
309
+ }
310
+ log(`✓ Ollama is running at ${apiHost}`);
311
+ const model = await detectModelPresent(apiHost, modelId);
312
+ if (!model.present) {
313
+ log(`✗ ${modelId} is not pulled`);
314
+ log(' Run `nuos-catalogue setup-llm` to download it (~600 MB).');
315
+ return;
316
+ }
317
+ log(`✓ ${modelId} is pulled (~600 MB)`);
318
+ log(`Semantic search is ready. Try \`nuos-catalogue search "your query"\` after the first index.`);
319
+ // Suppress the unused-variable warning while keeping platform available
320
+ // for future per-OS hints (e.g. "Ollama runs in the menu bar on macOS").
321
+ void platform;
322
+ }
279
323
  // ---------------------------------------------------------------------------
280
324
  // installHooks — copy bundled hook sources into the consumer + activate them
281
325
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nusoft/nuos-build-catalogue",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
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": {
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # NuOS catalogue post-commit hook — auto-refresh the semantic-search index
4
+ # after every commit that touched docs/build/**.
5
+ #
6
+ # Why this hook:
7
+ # `nuos-catalogue search` only finds what's in the NuVector index. The
8
+ # index is hash-based and incremental, but it doesn't update itself —
9
+ # you have to run `nuos-catalogue index` after meaningful changes for
10
+ # new content to be searchable. Running it manually is too easy to
11
+ # forget; the discipline argument that gave us the pre-commit hook
12
+ # (index-drift, accepted-decision immutability) applies equally here.
13
+ #
14
+ # Behaviour:
15
+ # - Skip if the just-landed commit did NOT touch docs/build/** (most
16
+ # code commits don't need a reindex)
17
+ # - Skip if `nuos-catalogue` isn't resolvable (the CLI may not be
18
+ # installed yet on a fresh clone; this hook should never block)
19
+ # - Otherwise: run the index in the BACKGROUND so the user's terminal
20
+ # isn't held while we embed. All output goes to .nuos-enforcement.log.
21
+ #
22
+ # The hook respects two env vars:
23
+ # NUOS_CATALOGUE_INDEX_DIR default: <repo>/.nuos-catalogue
24
+ # NUOS_CATALOGUE_OLLAMA_MODEL passed through to the CLI
25
+ #
26
+ # This hook never blocks. If indexing fails (Ollama not running, model
27
+ # not pulled, dimension mismatch with existing index), the error goes
28
+ # to the log and the user can investigate. The commit itself already
29
+ # landed by the time post-commit fires.
30
+
31
+ set -uo pipefail
32
+
33
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
34
+ LOG="$REPO_ROOT/.nuos-enforcement.log"
35
+
36
+ dim() { printf '\033[2m%s\033[0m\n' "$*"; }
37
+ yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
38
+
39
+ # ---- Skip-paths --------------------------------------------------------
40
+
41
+ # Skip if this project doesn't have a catalogue at all.
42
+ if [[ ! -d "$REPO_ROOT/docs/build" ]]; then
43
+ exit 0
44
+ fi
45
+
46
+ # Skip if the just-landed commit didn't touch docs/build/**.
47
+ if ! git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -q '^docs/build/'; then
48
+ exit 0
49
+ fi
50
+
51
+ # Resolve the CLI: prefer globally-installed `nuos-catalogue`; fall back
52
+ # to `npx --yes` which fetches from npm on demand.
53
+ INDEX_CMD=""
54
+ if command -v nuos-catalogue >/dev/null 2>&1; then
55
+ INDEX_CMD="nuos-catalogue"
56
+ elif command -v npx >/dev/null 2>&1; then
57
+ INDEX_CMD="npx --yes @nusoft/nuos-build-catalogue"
58
+ else
59
+ yellow "[nuos:post-commit] neither nuos-catalogue nor npx found; skipping index refresh"
60
+ printf '%s | post-commit-skip | no CLI available\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$LOG"
61
+ exit 0
62
+ fi
63
+
64
+ # ---- Background index refresh ------------------------------------------
65
+
66
+ # Default the index dir to project-local if the user hasn't set one.
67
+ : "${NUOS_CATALOGUE_INDEX_DIR:=$REPO_ROOT/.nuos-catalogue}"
68
+ export NUOS_CATALOGUE_INDEX_DIR
69
+
70
+ # Detach from the terminal so the post-commit returns immediately. All
71
+ # output (stdout + stderr) goes to the enforcement log so the user can
72
+ # tail it if they want to see progress.
73
+ (
74
+ start=$(date +%s)
75
+ printf '%s | post-commit-index | start (model=%s, dir=%s)\n' \
76
+ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
77
+ "${NUOS_CATALOGUE_OLLAMA_MODEL:-default}" \
78
+ "$NUOS_CATALOGUE_INDEX_DIR" \
79
+ >> "$LOG"
80
+
81
+ if cd "$REPO_ROOT" && $INDEX_CMD index >> "$LOG" 2>&1; then
82
+ end=$(date +%s)
83
+ printf '%s | post-commit-index | done in %ss\n' \
84
+ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
85
+ $((end - start)) \
86
+ >> "$LOG"
87
+ else
88
+ printf '%s | post-commit-index | FAILED (see lines above in %s)\n' \
89
+ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
90
+ "$(basename "$LOG")" \
91
+ >> "$LOG"
92
+ fi
93
+ ) </dev/null >/dev/null 2>&1 &
94
+ disown 2>/dev/null || true
95
+
96
+ dim "[nuos:post-commit] index refresh started in background — see .nuos-enforcement.log"
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # NuOS catalogue pre-commit hook (WU 111 enforcement phase).
4
+ #
5
+ # Catches drift before commit. After WU 111's Phase J ship, the
6
+ # accepted-decision rule has flipped from warning → block (per the
7
+ # pack's narrower-than-originally-planned enforcement scope, recorded
8
+ # in WU 111's "Forward-compatibility commitments" section). Other
9
+ # rules described in WU 128's original aggressive list are NOT shipping
10
+ # — distinguishing tool-written from human-written content was deemed
11
+ # overengineering for a planning-artefact catalogue.
12
+ #
13
+ # Active rules:
14
+ # 1. index-drift detection — every WU/decision/open-question/risk file
15
+ # must have a matching row in its _index.md (and vice versa)
16
+ # 2. active-decision modification block — modifying a committed
17
+ # `accepted` decision file is BLOCKED (not just warned). The
18
+ # discipline is to write a superseding D-NNN+1 and link forward.
19
+ # To deliberately fix a typo or link in an accepted decision,
20
+ # use `git commit --no-verify` (CLAUDE.md prohibits this for
21
+ # substantive changes; reserve it for typo-only fixes).
22
+ #
23
+ # Sentinel-protected sections (e.g. STATE.md's `nuos:sentinel`) remain
24
+ # protected via the existing `.claude/hooks/check-catalogue-write.sh`
25
+ # Claude Code hook. That rule continues unchanged at WU 111 ship.
26
+ #
27
+ # Bypass: this hook respects --no-verify like any other. The CLAUDE.md
28
+ # policy explicitly prohibits --no-verify use for substantive changes;
29
+ # the technical block fires at the CI server-side check (a future WU).
30
+
31
+ set -uo pipefail
32
+
33
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
34
+ ENFORCEMENT_LOG="$REPO_ROOT/.nuos-enforcement.log"
35
+ EXIT_CODE=0
36
+
37
+ red() { printf '\033[31m%s\033[0m\n' "$*"; }
38
+ yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
39
+ green() { printf '\033[32m%s\033[0m\n' "$*"; }
40
+ dim() { printf '\033[2m%s\033[0m\n' "$*"; }
41
+
42
+ log_event() {
43
+ printf '%s | %s | %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$1" "$2" >> "$ENFORCEMENT_LOG"
44
+ }
45
+
46
+ # ---------- Generic index-drift checker ---------------------------------
47
+ #
48
+ # Args:
49
+ # $1 — kind label (for messages)
50
+ # $2 — directory (relative to repo root)
51
+ # $3 — _index.md path (relative to repo root)
52
+ # $4 — filename regex (matches IDs the directory contains)
53
+ # $5 — index-row ID regex (a Perl-compatible regex that anchors on the
54
+ # leftmost column of an index table row)
55
+ #
56
+ # The two regexes must extract the SAME set of IDs (e.g. "001", "030g",
57
+ # "D042") so set comparison works.
58
+
59
+ check_index_drift() {
60
+ local kind="$1" dir="$2" index="$3" file_regex="$4" row_regex="$5"
61
+
62
+ if [[ ! -d "$REPO_ROOT/$dir" ]]; then return 0; fi
63
+ if [[ ! -f "$REPO_ROOT/$index" ]]; then return 0; fi
64
+
65
+ # IDs extracted from filenames in the directory (top-level + one subdir
66
+ # like done/, resolved/, superseded/)
67
+ local ids_in_tree
68
+ ids_in_tree=$(cd "$REPO_ROOT/$dir" && {
69
+ find . -maxdepth 2 -type f -name "*.md" \
70
+ -not -name "_index.md" \
71
+ -not -name "*-template.md" 2>/dev/null \
72
+ | sed -nE "$file_regex" \
73
+ | sort -u
74
+ })
75
+
76
+ # IDs extracted from leftmost column of table rows in the index
77
+ local ids_in_index
78
+ ids_in_index=$(sed -nE "$row_regex" "$REPO_ROOT/$index" | sort -u)
79
+
80
+ local missing_from_index
81
+ missing_from_index=$(comm -23 <(printf '%s\n' "$ids_in_tree") <(printf '%s\n' "$ids_in_index"))
82
+
83
+ if [[ -n "$missing_from_index" ]]; then
84
+ red "✖ index-drift ($kind): on disk but missing from $index:"
85
+ while IFS= read -r id; do echo " — $id"; done <<< "$missing_from_index"
86
+ log_event "index-drift" "$kind missing from index: $(echo "$missing_from_index" | tr '\n' ',')"
87
+ EXIT_CODE=1
88
+ fi
89
+ }
90
+
91
+ dim "[nuos:pre-commit] index-drift check"
92
+
93
+ # Note: BSD sed (macOS default) is fussy about `|` as both delimiter and
94
+ # regex content. Using `#` as the s/// delimiter sidesteps the collision.
95
+
96
+ # Work units: filenames are NNN-slug.md or NNNa-slug.md (with optional letter).
97
+ # Index rows are `| NNN |` or `| NNNa |` in the leftmost column.
98
+ check_index_drift \
99
+ "work-units" \
100
+ "docs/build/work-units" \
101
+ "docs/build/work-units/_index.md" \
102
+ 's#^\./(done/)?([0-9]{3}[a-z]?)-[^/]*\.md$#\2#p' \
103
+ 's#^\| ([0-9]{3}[a-z]?) \|.*$#\1#p'
104
+
105
+ # Decisions: filenames are DNNN-slug.md.
106
+ # Index rows: `| DNNN |` or `| [DNNN](...) |`.
107
+ check_index_drift \
108
+ "decisions" \
109
+ "docs/build/decisions" \
110
+ "docs/build/decisions/_index.md" \
111
+ 's#^\./(done/|superseded/)?(D[0-9]{3})-[^/]*\.md$#\2#p' \
112
+ 's#^\| \[?(D[0-9]{3}).*$#\1#p'
113
+
114
+ # Open questions: filenames are QNNN-slug.md.
115
+ check_index_drift \
116
+ "open-questions" \
117
+ "docs/build/open-questions" \
118
+ "docs/build/open-questions/_index.md" \
119
+ 's#^\./(resolved/)?(Q[0-9]{3})-[^/]*\.md$#\2#p' \
120
+ 's#^\| \[?(Q[0-9]{3}).*$#\1#p'
121
+
122
+ # Risks: per current convention, individual risk files are inline in
123
+ # risks/_index.md (no per-risk .md files yet). Skip the check entirely
124
+ # until that pattern changes.
125
+ if compgen -G "$REPO_ROOT/docs/build/risks/R[0-9][0-9][0-9]-*.md" > /dev/null; then
126
+ check_index_drift \
127
+ "risks" \
128
+ "docs/build/risks" \
129
+ "docs/build/risks/_index.md" \
130
+ 's#^\./(R[0-9]{3})-[^/]*\.md$#\1#p' \
131
+ 's#^\| \[?(R[0-9]{3}).*$#\1#p'
132
+ fi
133
+
134
+ # ---------- Rule 2: active-decision modification block (WU 111 ship) ---
135
+
136
+ dim "[nuos:pre-commit] active-decision modification check"
137
+ modified_decisions=$(git diff --cached --name-only --diff-filter=M \
138
+ | grep -E '^docs/build/decisions/D[0-9]+.*\.md$' \
139
+ | grep -v '/superseded/' \
140
+ || true)
141
+
142
+ if [[ -n "$modified_decisions" ]]; then
143
+ red "✖ active-decision modification — BLOCKED (WU 111 enforcement):"
144
+ while IFS= read -r f; do echo " — $f"; done <<< "$modified_decisions"
145
+ red " Decisions are immutable once accepted. The discipline is to write a"
146
+ red " superseding D-NNN+1 and link forward. Use:"
147
+ red " nuos-catalogue decision supersede <target> --by=<new-D> --reason=\"...\""
148
+ red ""
149
+ red " If this edit is a non-substantive typo fix or link cleanup that does"
150
+ red " not change the decision's meaning, you may bypass this block with"
151
+ red " --no-verify. CLAUDE.md prohibits --no-verify for substantive changes."
152
+ log_event "active-decision-block" "$(echo "$modified_decisions" | tr '\n' ',')"
153
+ EXIT_CODE=1
154
+ fi
155
+
156
+ # ---------- Result ------------------------------------------------------
157
+
158
+ if [[ $EXIT_CODE -eq 0 ]]; then
159
+ green "[nuos:pre-commit] all rules pass (WU 111 enforcement)"
160
+ log_event "pre-commit-pass" "$(git diff --cached --name-only | wc -l | tr -d ' ') files"
161
+ fi
162
+ exit $EXIT_CODE
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Install the catalogue's git hooks into .git/hooks/.
4
+ #
5
+ # Why this script (not husky):
6
+ # The nuos repo is a markdown catalogue, not an npm package. Adding a
7
+ # package.json + node_modules just to install hooks would be infrastructure
8
+ # tax for what is otherwise a doc repo. A small bash installer copies the
9
+ # hooks from the version-controlled scripts/hooks/ into .git/hooks/.
10
+ #
11
+ # Usage:
12
+ # bash scripts/install-hooks.sh
13
+ #
14
+ # Re-run any time scripts/hooks/ changes; the installer is idempotent.
15
+
16
+ set -euo pipefail
17
+
18
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
19
+ SOURCE="$REPO_ROOT/scripts/hooks"
20
+ TARGET="$REPO_ROOT/.git/hooks"
21
+
22
+ if [[ ! -d "$SOURCE" ]]; then
23
+ echo "✖ scripts/hooks/ not found; nothing to install" >&2
24
+ exit 1
25
+ fi
26
+
27
+ mkdir -p "$TARGET"
28
+
29
+ installed=0
30
+ for hook in "$SOURCE"/*; do
31
+ name="$(basename "$hook")"
32
+ cp "$hook" "$TARGET/$name"
33
+ chmod +x "$TARGET/$name"
34
+ installed=$((installed + 1))
35
+ done
36
+
37
+ echo "✓ installed $installed hook(s) into .git/hooks/"
38
+ echo
39
+ echo "Active rules (WU 111 enforcement):"
40
+ echo " • index-drift detection (work-units, decisions, open-questions, risks)"
41
+ echo " • active-decision modification BLOCK (was warning under WU 128 light-touch)"
42
+ echo
43
+ echo "To verify the install: \`git hook list\` (git ≥2.36) or \`ls .git/hooks/\`"
44
+ echo "To uninstall: \`rm .git/hooks/pre-commit\`"