@hybridlabor-api/aos 4.4.0 → 4.4.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.
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ // aos-hook-version: 2
2
3
  /**
3
4
  * memB ambient memory hook for Claude Code (UserPromptSubmit).
4
5
  *
6
+ * The version line above is what /aos-setup's doctor reads to tell a current
7
+ * hook from a stale one left behind by an older install. Bump it whenever this
8
+ * file changes in a way a machine must actually pick up, and bump the expected
9
+ * value in aos-doctor.mjs in the same commit.
10
+ *
5
11
  * Reads ~/.MemBDB/memb.db directly through node:sqlite and injects the most
6
12
  * relevant memories as ephemeral additionalContext — nothing is written to
7
13
  * disk and nothing lands in git. Fails open: any error exits 0 silently, so a
package/installer.js CHANGED
@@ -508,10 +508,30 @@ function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes
508
508
 
509
509
  const diskHash = computeFileHash(targetPath);
510
510
 
511
- // Case: foreign – no manifest entry AND disk hash unknown to this payload.
511
+ // Case: unrecognised content at a path this payload owns.
512
+ //
513
+ // This used to skip, on the reading that unknown content means a file the
514
+ // installer did not place. But resolveFileConflict only ever sees paths the
515
+ // payload is actively writing, so the path IS ours -- and the commonest
516
+ // cause of unknown content is simply an older release of our own file,
517
+ // installed before manifest tracking existed and therefore never adopted.
518
+ // Skipping froze those permanently: v4.4.0 still found skills on disk
519
+ // carrying pre-reorg ~/bdb-dev paths and a stale `category:`, untouched
520
+ // across every update since, because bootstrap adoption only catches a file
521
+ // that is byte-identical to the current payload.
522
+ //
523
+ // So update it, and keep the protection where it belongs -- in the backup,
524
+ // not in refusing to write. A genuine local edit is recoverable from .bak
525
+ // rather than silently outvoting the shipped version forever.
512
526
  if (!manifestEntry && !knownSourceHashes.has(diskHash)) {
513
- log.warn(`[manifest] Foreign file at ${targetPath} — left untouched.`);
514
- return 'skipped';
527
+ const bakPath = `${targetPath}.bak`;
528
+ try { fs.copyFileSync(targetPath, bakPath); } catch (e) {
529
+ log.warn(`[manifest] Could not create backup ${bakPath}: ${e.message}`);
530
+ }
531
+ fs.copyFileSync(sourcePath, targetPath);
532
+ manifest[targetPath] = { path: targetPath, sha256: sourceHash, version: pkg.version, installedAt: new Date().toISOString() };
533
+ log.warn(`[manifest] Unrecognised ${path.basename(targetPath)} backed up to ${bakPath}, shipped version written.`);
534
+ return 'bak';
515
535
  }
516
536
 
517
537
  // Case: first-run bootstrap – disk hash matches source hash but no manifest entry.
@@ -2717,7 +2737,7 @@ function injectHarnessRules() {
2717
2737
  log.step(`Copied ${dir} to ${targetPath}`);
2718
2738
  }
2719
2739
  });
2720
- mergeBdbSettingsHooks(path.join(homeDir, '.claude', 'settings.json'));
2740
+ installGlobalHooks();
2721
2741
  }, '');
2722
2742
 
2723
2743
  installStep('sync global .agents/', () => {
@@ -2731,6 +2751,22 @@ function injectHarnessRules() {
2731
2751
  }
2732
2752
  }
2733
2753
 
2754
+ // Deliver the hook scripts to ~/.claude/hooks/ and wire them in settings.json.
2755
+ // Both the full install and Quick Update funnel through here. Quick Update used
2756
+ // to do neither: it refreshes skills and submodules, but hooks are harness
2757
+ // plumbing rather than skills, so an existing install updating to a version
2758
+ // that adds or changes a hook silently never received it -- which is exactly
2759
+ // what happened to memb-inject.mjs in v4.4.0, on the machines that needed it
2760
+ // most (a fresh install got it; every machine that already had AOS did not).
2761
+ function installGlobalHooks() {
2762
+ const hooksSrc = path.join(srcDir, '.claude', 'hooks');
2763
+ if (fs.existsSync(hooksSrc)) {
2764
+ copyDirRecursiveSync(hooksSrc, path.join(homeDir, '.claude', 'hooks'));
2765
+ log.step(`Installed hooks to ${path.join(homeDir, '.claude', 'hooks')}`);
2766
+ }
2767
+ mergeBdbSettingsHooks(path.join(homeDir, '.claude', 'settings.json'));
2768
+ }
2769
+
2734
2770
  // Merge the BDB hooks -- the two gates plus the memB ambient-memory hook --
2735
2771
  // into a Claude Code settings.json without clobbering
2736
2772
  // user-owned keys (v3.13 audit BLOCKER-3: the harness-dir copy used to
@@ -3298,6 +3334,10 @@ async function runQuickUpdate(installState) {
3298
3334
  syncSkillsToGlobalHarnesses(excludeSkills);
3299
3335
  s.stop('Skills refreshed');
3300
3336
 
3337
+ installStep('refresh gate + memory hooks', () => {
3338
+ installGlobalHooks();
3339
+ }, 'Hooks keep whatever version this machine already had.');
3340
+
3301
3341
  // OpenWiki setup only ever ran on a brand-new install (main()'s fresh-install
3302
3342
  // branch below) -- an existing install running Quick Update never got offered
3303
3343
  // it and never had its daemon schedule refreshed. promptCredentials() already
@@ -3728,6 +3768,7 @@ if (require.main === module) {
3728
3768
  // Exported for tests -- requiring installer.js must not launch the TUI.
3729
3769
  module.exports = {
3730
3770
  mergeBdbSettingsHooks,
3771
+ installGlobalHooks,
3731
3772
  installProjectHarness,
3732
3773
  mirrorMcpServersTo,
3733
3774
  // Manifest store (exported for verification tests)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridlabor-api/aos",
3
- "version": "4.4.0",
3
+ "version": "4.4.1",
4
4
  "description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
5
5
  "main": "installer.js",
6
6
  "bin": {
@@ -123,9 +123,11 @@ injects the relevant memories as context. It fails open: any error exits `0`
123
123
  and the prompt proceeds untouched.
124
124
 
125
125
  Since v4.4.0 the installer ships `memb-inject.mjs` into `~/.claude/hooks/` and
126
- wires it as a `UserPromptSubmit` hook, so on a current install both doctor rows
127
- pass on their own. A machine where they fail is running an older AOS — the fix
128
- is the installer, not a manual copy:
126
+ wires it as a `UserPromptSubmit` hook. **v4.4.0's Quick Update did not** — it
127
+ refreshes skills and submodules, and hooks are harness plumbing rather than
128
+ skills, so a machine that already had AOS updated to 4.4.0 without ever
129
+ receiving the hook. Fixed in v4.4.1; a machine that took that update needs one
130
+ more run:
129
131
 
130
132
  ```bash
131
133
  npx -y @hybridlabor-api/aos@latest
@@ -135,6 +137,13 @@ The wiring is merged into `~/.claude/settings.json`, never written over it:
135
137
  user keys and foreign hook entries survive, and a re-run replaces the BDB entry
136
138
  rather than adding a second copy.
137
139
 
140
+ The hook carries an `aos-hook-version:` line on its second line, and the doctor
141
+ compares it against what the installed release expects. That is why the row can
142
+ read *stale* rather than simply passing: a hook file that exists and is wired
143
+ can still be an old copy carrying a bug this version fixed. When you change the
144
+ hook in a way machines must pick up, bump that line and the expected value in
145
+ `aos-doctor.mjs` in the same commit — the test suite fails if the two disagree.
146
+
138
147
  Unlike the two gate hooks, this one stays `$HOME`-anchored even in a project
139
148
  harness — the memB store is machine-global, and pointing it at
140
149
  `$CLAUDE_PROJECT_DIR` would make it fail on every prompt in any project the
@@ -139,10 +139,35 @@ function checkAos() {
139
139
  // ---------------------------------------------------------------- hooks
140
140
  function checkHooks() {
141
141
  const hooksDir = h('.claude', 'hooks');
142
+
143
+ // Existence is not enough: an older install leaves a stale copy behind, and
144
+ // the row would go green over a hook carrying a bug this version fixed.
145
+ // Hooks that carry an `aos-hook-version:` line are checked against what this
146
+ // release expects; the ones that do not are existence-only.
147
+ const EXPECTED_VERSION = { 'memb-inject.mjs': 2 };
148
+ const versionOf = (text) => {
149
+ const m = /^\/\/\s*aos-hook-version:\s*(\d+)/m.exec(text);
150
+ return m ? Number(m[1]) : null;
151
+ };
152
+
142
153
  for (const file of ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs']) {
143
154
  const p = path.join(hooksDir, file);
144
- add('hooks', file, existsSync(p), existsSync(p) ? p.replace(HOME, '~') : 'missing',
145
- 'npx -y @hybridlabor-api/aos@latest (the installer ships and wires all three).');
155
+ if (!existsSync(p)) {
156
+ add('hooks', file, false, 'missing',
157
+ 'npx -y @hybridlabor-api/aos@latest (the installer ships and wires all three).');
158
+ continue;
159
+ }
160
+ const want = EXPECTED_VERSION[file];
161
+ if (!want) {
162
+ add('hooks', file, true, tilde(p), '');
163
+ continue;
164
+ }
165
+ const got = versionOf(readFileSync(p, 'utf8'));
166
+ add('hooks', file, got === want,
167
+ got === want ? `${tilde(p)} (v${got})`
168
+ : got === null ? `${tilde(p)} — predates version stamping, so it is older than v${want}`
169
+ : `${tilde(p)} — v${got}, this release ships v${want}`,
170
+ 'npx -y @hybridlabor-api/aos@latest — Quick Update refreshes hooks from v4.4.1 on; an older installer never replaced them.');
146
171
  }
147
172
 
148
173
  const settings = readJson(h('.claude', 'settings.json'));