total-recall-brain 3.41.4 → 3.41.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/bin/total-recall.mjs +3 -2
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/{index-CRzjQLu7.js → index-WT3gs-x6.js} +24 -24
  4. package/frontend/dist/index.html +1 -1
  5. package/package.json +2 -2
  6. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  7. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  8. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  9. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  10. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
  11. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
  12. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
  13. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +1 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3165 -87
  15. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
  17. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +32 -12
  19. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +294 -46
  20. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
  21. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -2
  22. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +190 -9
  23. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  26. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  27. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  30. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  31. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
  32. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  33. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3087 -71
  34. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  35. package/src/cli/compact.mjs +108 -0
  36. package/src/cli/context.mjs +52 -23
  37. package/src/cli/forget.mjs +2 -0
  38. package/src/cli/release.mjs +386 -0
  39. package/src/core/cli-commands.mjs +2 -0
  40. package/src/core/context-compactor.mjs +143 -0
  41. package/src/core/preflight-gate.mjs +2 -2
  42. package/src/core/preflight-receipt.mjs +1 -1
  43. package/src/core/surface.mjs +1 -1
  44. package/src/server/invoke-durability.mjs +39 -0
  45. package/src/server/invoke-write-child.mjs +25 -0
  46. package/src/server/routes/invoke.mjs +7 -0
@@ -24,6 +24,6 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@gregiteen/ssss-cli": "0.14.3"
27
+ "@gregiteen/ssss-cli": "0.14.4"
28
28
  }
29
29
  }
@@ -0,0 +1,108 @@
1
+ import { parseLayerFlag } from './agent-dir.mjs';
2
+ import { loadContextNodes } from './context.mjs';
3
+ import { curatedRules, selectRules } from '../core/context-policy.mjs';
4
+ import { compactRules } from '../core/context-compactor.mjs';
5
+ import { cosineSimilarity, getEmbedding, loadEmbeddingsIndex } from '../core/embeddings.mjs';
6
+ import { legacyRuleContributions } from '../core/surface.mjs';
7
+
8
+ const DEFAULT_BUDGET = 4000;
9
+ const EMBED_TIMEOUT_MS = 8000;
10
+
11
+ const USAGE = `Usage: total-recall compact "task" [--budget 4000] [--action edit,test] [--expand slug,slug] [--format text|json] [--project|--global]
12
+ Fits the applicable rules into --budget estimated tokens, ranked against the task. Rules keep their own words: full text, a leading excerpt, or a slug under Referenced. Read any of them in full with --expand. The output is partial by design; \`total-recall context\` prints the complete capsule.`;
13
+
14
+ function parseArgs(args) {
15
+ const options = { budget: DEFAULT_BUDGET, actions: [], expand: [], format: 'text' };
16
+ for (let i = 1; i < args.length; i++) {
17
+ const flag = args[i];
18
+ const value = args[++i];
19
+ if (flag === '--budget') options.budget = Number(value);
20
+ else if (flag === '--action') options.actions = value?.split(',').filter(Boolean) || [];
21
+ else if (flag === '--expand') options.expand = value?.split(',').filter(Boolean) || [];
22
+ else if (flag === '--format' && ['json', 'text'].includes(value)) options.format = value;
23
+ else throw new Error(`Unknown compact option: ${flag}`);
24
+ }
25
+ if (!Number.isFinite(options.budget) || options.budget < 1) throw new Error('--budget must be a positive number of tokens');
26
+ return options;
27
+ }
28
+
29
+ function loadVectors(derivedDirs) {
30
+ const vectors = {};
31
+ for (const dir of derivedDirs) {
32
+ try {
33
+ for (const [slug, entry] of Object.entries(loadEmbeddingsIndex(dir))) vectors[slug] ??= entry.embedding;
34
+ } catch { /* a brain without an index simply contributes no vectors */ }
35
+ }
36
+ return vectors;
37
+ }
38
+
39
+ async function embedTask(task) {
40
+ let timer;
41
+ const timeout = new Promise(resolve => { timer = setTimeout(() => resolve(null), EMBED_TIMEOUT_MS); });
42
+ try {
43
+ return await Promise.race([getEmbedding(task), timeout]);
44
+ } catch {
45
+ return null;
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+
51
+ function lexicalScore(terms, text) {
52
+ if (!terms.length) return 0;
53
+ const haystack = text.toLowerCase();
54
+ return terms.filter(term => haystack.includes(term)).length / terms.length;
55
+ }
56
+
57
+ /** Rank the applicable rules against the task and fit them into `budget`. Shared by `compact` and `context`. */
58
+ export async function compactNodes({ task, actions = [], budget, expand = [], complete = false, nodes, skillsDir, derivedDirs, header }) {
59
+ const rules = selectRules(curatedRules(nodes), { actions, projectRoot: process.cwd() });
60
+ const vectors = loadVectors(derivedDirs);
61
+ const taskVector = await embedTask(task);
62
+ const terms = String(task).toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) || [];
63
+ const semantic = Boolean(taskVector);
64
+
65
+ const score = (slug, text) => semantic && vectors[slug] ? cosineSimilarity(taskVector, vectors[slug]) : lexicalScore(terms, text);
66
+ const items = rules.map(node => {
67
+ const body = (node._directive || node.body || node.content || '').trim();
68
+ return {
69
+ slug: node.slug, body, priority: node.priority, importance: node.importance,
70
+ text: node._directive ? `[${node.slug}] ${node._directive}` : `## ${node.title || node.slug} [${node.slug}]\n\n${body}`,
71
+ vector: vectors[node.slug], score: score(node.slug, `${node.title || ''} ${body}`),
72
+ };
73
+ });
74
+ for (const { id, text } of legacyRuleContributions(skillsDir)) {
75
+ items.push({ slug: id, body: text, text, score: lexicalScore(terms, text) });
76
+ }
77
+
78
+ const ranking = semantic ? 'semantic' : 'lexical';
79
+ return { ranking, ...compactRules(items, { budget, expand, complete, header: (counts, tokens) => header(counts, tokens, ranking) }) };
80
+ }
81
+
82
+ export default async function compact(args = []) {
83
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
84
+ console.log(USAGE);
85
+ return;
86
+ }
87
+ const { layer, remainingArgs } = parseLayerFlag(args);
88
+ const task = remainingArgs[0];
89
+ const { budget, actions, expand, format } = parseArgs(remainingArgs);
90
+ const { nodes, skillsDir, derivedDirs } = loadContextNodes(layer);
91
+ const header = (counts, tokens, ranking) => [
92
+ `compacted:true tokens:${tokens} budget:${budget} rules:${counts.rules} full:${counts.full} summarized:${counts.summarized} referenced:${counts.referenced} duplicate:${counts.duplicate} omitted:${counts.omitted} ranking:${ranking}`,
93
+ 'PARTIAL: read any rule in full with --expand <slug>; total-recall context prints the complete capsule.',
94
+ ].join('\n');
95
+
96
+ let result;
97
+ try {
98
+ result = await compactNodes({ task, actions, budget, expand, nodes, skillsDir, derivedDirs, header });
99
+ } catch (error) {
100
+ console.log(error.requiredBudget ? `compacted:false reason:budget-too-small budget:${budget} required_budget:${error.requiredBudget}` : `compacted:false reason:${error.message}`);
101
+ process.exitCode = 2;
102
+ return;
103
+ }
104
+ const { context, counts, tokens, tiers, ranking } = result;
105
+ console.log(format === 'json'
106
+ ? JSON.stringify({ compacted: true, ranking, budget, tokens, counts, tiers, context })
107
+ : context);
108
+ }
@@ -2,15 +2,61 @@ import path from 'node:path';
2
2
  import { getBothBrains, parseLayerFlag } from './agent-dir.mjs';
3
3
  import { getNodes } from '../core/vault-cache.mjs';
4
4
  import { mergeGlobalRuleNodes, legacyRuleContributions } from '../core/surface.mjs';
5
- import { compileContext, capsuleResponse, renderCapsule } from '../core/context-compiler.mjs';
5
+ import { compileContext, capsuleResponse, renderCapsule, PREFLIGHT_NOTICE } from '../core/context-compiler.mjs';
6
6
  import { isBrainEnabled } from '../core/brain-registry.mjs';
7
7
  import { surfaceInputsHash } from '../core/command-surface.mjs';
8
8
  import { readConfigValue } from './config.mjs';
9
9
  import { recordCapsule } from '../core/context-telemetry.mjs';
10
10
 
11
+ /** About 28 KB: harnesses save output over about 30 KB to a file instead of showing it, which agents then cannot read (audit C-005, F-019). */
12
+ export const CAPSULE_LIMIT = 7000;
13
+ const JSON_ESCAPE_SHARE = 0.97;
14
+ const JSON_WRAPPER_TOKENS = 128;
15
+
16
+ /** The merged rule nodes a capsule draws from, for the selected layer. */
17
+ export function loadContextNodes(layer) {
18
+ const brains = getBothBrains();
19
+ const own = layer !== 'global' && brains.project && isBrainEnabled(brains.project.brainDir) ? brains.project.brainDir : null;
20
+ const global = layer !== 'project' && brains.global && isBrainEnabled(brains.global.brainDir) ? brains.global.brainDir : null;
21
+ if (!own && !global) throw new Error('No enabled brain in selected scope');
22
+ const nodes = own ? mergeGlobalRuleNodes(getNodes(path.join(own, 'memory-vault')), global ? getNodes(path.join(global, 'memory-vault')) : []) :
23
+ getNodes(path.join(global, 'memory-vault')).map(n => ({ ...n, _layer: 'global' }));
24
+ return { nodes, skillsDir: path.dirname(own || global),
25
+ derivedDirs: [own, global].filter(Boolean).map(dir => path.join(dir, 'memory-derived')) };
26
+ }
27
+
28
+ /** Compact the over-budget capsule, or explain the budget it needs. `fullRequired` is what the uncompacted capsule would take. */
29
+ async function printCompacted({ query, actions, budget, format, version, fullRequired, nodes, skillsDir, derivedDirs }) {
30
+ const { compactNodes } = await import('./compact.mjs');
31
+ // JSON escaping and its wrapper count against the budget too.
32
+ const target = format === 'json' ? Math.floor(budget * JSON_ESCAPE_SHARE) - JSON_WRAPPER_TOKENS : budget;
33
+ const header = (counts, tokens, ranking) => [
34
+ `ready:true compacted:true tokens:${tokens} budget:${budget} version:${version} rules:${counts.rules} full:${counts.full} summarized:${counts.summarized} referenced:${counts.referenced} duplicate:${counts.duplicate} ranking:${ranking}`,
35
+ PREFLIGHT_NOTICE,
36
+ `COMPACTED: every rule below still applies. Read a referenced rule with: total-recall compact "<task>" --expand <slug>`,
37
+ ].join('\n');
38
+ let result;
39
+ try {
40
+ result = await compactNodes({ task: query, actions, budget: Math.max(target, 1), complete: true, nodes, skillsDir, derivedDirs, header });
41
+ } catch (error) {
42
+ if (!error.requiredBudget) throw error;
43
+ const floor = format === 'json' ? Math.ceil((error.requiredBudget + JSON_WRAPPER_TOKENS) / JSON_ESCAPE_SHARE) : error.requiredBudget;
44
+ const requiredBudget = Math.min(fullRequired, floor);
45
+ console.log(format === 'json'
46
+ ? JSON.stringify({ ready: false, reason: 'budget-overflow', context: '', stats: { version, budget, required_budget: requiredBudget, token_measurement: 'estimated_chars_divided_by_four' } })
47
+ : `ready:false reason:budget-overflow budget:${budget} required_budget:${requiredBudget} version:${version}\nThis budget cannot hold every rule even compacted. Retry with --budget ${requiredBudget}; do not act until ready:true.`);
48
+ process.exitCode = 2;
49
+ return;
50
+ }
51
+ console.log(format === 'json'
52
+ ? JSON.stringify({ ready: true, compacted: true, ranking: result.ranking, context: result.context, counts: result.counts,
53
+ stats: { version, budget, total_tokens: result.tokens, token_measurement: 'estimated_chars_divided_by_four' } })
54
+ : result.context);
55
+ }
56
+
11
57
  export default async function context(args = []) {
12
58
  if (!args.length || args.includes('--help')) {
13
- console.log('Usage: total-recall context "task" [--action edit,test,publish] [--budget 4000] [--format text|json] [--knowledge] [--debug] [--project|--global]\nLocal rules only by default. Unknown applicability is required. Overflow exits 2; do not act until the complete output fits. --knowledge adds supporting documents; --debug exposes inventories within the same budget. Refresh on task/action/project or policy changes.');
59
+ console.log(`Usage: total-recall context "task" [--action edit,test,publish] [--budget ${CAPSULE_LIMIT}] [--format text|json] [--knowledge] [--debug] [--project|--global]\nLocal rules only by default. Unknown applicability is required. Over budget, the capsule compacts automatically: absolute rules keep at least a summary and every other rule keeps at least its slug. Configured budgets are capped at ${CAPSULE_LIMIT}; pass a larger --budget to print the full capsule. Exit 2 means the budget cannot hold every rule; do not act until ready:true. --knowledge adds supporting documents; --debug exposes inventories within the same budget. Refresh on task/action/project or policy changes.`);
14
60
  return;
15
61
  }
16
62
  const { layer, remainingArgs } = parseLayerFlag(args);
@@ -24,7 +70,7 @@ export default async function context(args = []) {
24
70
  }
25
71
  return undefined;
26
72
  })();
27
- let actions = [], budget = configuredBudget ?? 4000, format = 'text', includeKnowledge = false, debug = false;
73
+ let actions = [], budget = Math.min(configuredBudget ?? CAPSULE_LIMIT, CAPSULE_LIMIT), format = 'text', includeKnowledge = false, debug = false;
28
74
  for (let i = 1; i < remainingArgs.length; i++) {
29
75
  const flag = remainingArgs[i];
30
76
  if (flag === '--knowledge') { includeKnowledge = true; continue; }
@@ -35,14 +81,8 @@ export default async function context(args = []) {
35
81
  else if (flag === '--format' && ['json', 'text'].includes(value)) format = value;
36
82
  else throw new Error(`Unknown context option: ${flag}`);
37
83
  }
38
- const brains = getBothBrains();
39
- const own = layer !== 'global' && brains.project && isBrainEnabled(brains.project.brainDir) ? brains.project.brainDir : null;
40
- const global = layer !== 'project' && brains.global && isBrainEnabled(brains.global.brainDir) ? brains.global.brainDir : null;
41
- if (!own && !global) throw new Error('No enabled brain in selected scope');
42
- const nodes = own ? mergeGlobalRuleNodes(getNodes(path.join(own, 'memory-vault')), global ? getNodes(path.join(global, 'memory-vault')) : []) :
43
- getNodes(path.join(global, 'memory-vault')).map(n => ({ ...n, _layer: 'global' }));
44
- const skillsDir = path.dirname(own || global);
45
- const derivedDir = own ? path.join(own, 'memory-derived') : (global ? path.join(global, 'memory-derived') : null);
84
+ const { nodes, skillsDir, derivedDirs } = loadContextNodes(layer);
85
+ const derivedDir = derivedDirs[0];
46
86
  const result = await compileContext({ query, actions, nodes, includeKnowledge, budget: { total: budget }, projectRoot: process.cwd(),
47
87
  derivedDir, contributions: legacyRuleContributions(skillsDir), versionInputs: surfaceInputsHash({ skillsDir }) });
48
88
  if (debug && format === 'text') throw new Error('--debug requires --format json');
@@ -55,18 +95,7 @@ export default async function context(args = []) {
55
95
  derivedDir, action: actions, query, consumer: 'cli',
56
96
  });
57
97
  if (!response.ready && !debug) {
58
- // Admission failed: emitting bodies here makes each budget retry load the
59
- // same rules again. Keep the full set in the compiler/API, and admit it only
60
- // after the caller explicitly raises the budget or curates applicability.
61
- const requiredBudget = response.stats.total_tokens + 32;
62
- const diagnostic = { ready: false, reason: 'budget-overflow', context: '', stats: {
63
- version: response.stats.version, required_count: response.stats.required_count,
64
- budget, required_budget: requiredBudget,
65
- token_measurement: 'estimated_chars_divided_by_four',
66
- } };
67
- console.log(format === 'json' ? JSON.stringify(diagnostic) :
68
- `ready:false reason:budget-overflow budget:${budget} required_budget:${requiredBudget} required_rules:${response.stats.required_count} version:${response.stats.version}\nRetry with --budget ${requiredBudget}; do not act until ready:true.\nTo shorten the rules for good: total-recall rules audit, then rules draft/apply.`);
69
- process.exitCode = 2;
98
+ await printCompacted({ query, actions, budget, format, version: response.stats.version, fullRequired: response.stats.total_tokens + 32, nodes, skillsDir, derivedDirs });
70
99
  return;
71
100
  }
72
101
  console.log(renderCapsule(response, format));
@@ -33,6 +33,8 @@ function printHelp() {
33
33
  }
34
34
 
35
35
  export default async function forget(args) {
36
+ // Checked first: a destructive command must never act on a request for help.
37
+ if (args.includes('--help') || args.includes('-h')) { printHelp(); return; }
36
38
  const { layer: explicitLayer, remainingArgs } = parseLayerFlag(args);
37
39
 
38
40
  // Check for --project-all
@@ -0,0 +1,386 @@
1
+ /**
2
+ * total-recall release — publish a package, propagate it, and verify every consumer.
3
+ *
4
+ * The missing capability this fills: publishing to npm and verifying that the
5
+ * published artifact actually reached every registered consumer were manual
6
+ * steps outside any TR command. A release that is only on npm is not released,
7
+ * and a release whose consumers were never verified is a claim, not a fact.
8
+ *
9
+ * Usage:
10
+ * npx total-recall release plan <pkg> [--version X.Y.Z]
11
+ * npx total-recall release publish <pkg> [--version X.Y.Z] [--otp N]
12
+ * npx total-recall release propagate [--apply]
13
+ * npx total-recall release verify <pkg> [--version X.Y.Z]
14
+ * npx total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N]
15
+ *
16
+ * Never publishes without --apply. Never trusts the publish command's own
17
+ * report: verification reads the registry and each consumer's resolved
18
+ * installation independently.
19
+ */
20
+
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { spawnSync } from 'node:child_process';
24
+ import { BRAIN_PACKAGES, updateAllBrains } from '../core/brain-updates.mjs';
25
+ import { listUpdateRoots, rootsFromEnv } from '../core/package-auto-update.mjs';
26
+
27
+ const DEFAULT_PACKAGES = BRAIN_PACKAGES;
28
+
29
+ /** @param {string[]} args */
30
+ export function parseArgs(args) {
31
+ const opts = {
32
+ action: null,
33
+ packages: [],
34
+ version: null,
35
+ apply: false,
36
+ otp: null,
37
+ json: false,
38
+ help: false,
39
+ root: null,
40
+ timeoutMs: 20 * 60 * 1000,
41
+ };
42
+ const positional = [];
43
+ for (let i = 0; i < args.length; i++) {
44
+ const a = args[i];
45
+ if (a === '--help' || a === '-h') opts.help = true;
46
+ else if (a === '--apply') opts.apply = true;
47
+ else if (a === '--json') opts.json = true;
48
+ else if (a === '--version' && args[i + 1]) opts.version = args[++i];
49
+ else if (a === '--otp' && args[i + 1]) opts.otp = args[++i];
50
+ else if (a === '--root' && args[i + 1]) opts.root = args[++i];
51
+ else if (a === '--timeout' && args[i + 1]) opts.timeoutMs = Number(args[++i]);
52
+ else if (!a.startsWith('-')) positional.push(a);
53
+ }
54
+ opts.action = positional.shift() || null;
55
+ opts.packages = positional;
56
+ return opts;
57
+ }
58
+
59
+ const ACTIONS = ['plan', 'publish', 'propagate', 'verify', 'all'];
60
+
61
+ function printHelp() {
62
+ console.log(`
63
+ total-recall release — publish, propagate and verify a package release
64
+
65
+ Usage:
66
+ total-recall release plan <pkg> [--version X.Y.Z]
67
+ total-recall release publish <pkg> [--version X.Y.Z] [--otp N] --apply
68
+ total-recall release propagate [--apply] [--json]
69
+ total-recall release verify <pkg> [--version X.Y.Z] [--json]
70
+ total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N] [--json]
71
+
72
+ Actions:
73
+ plan Resolve the release version and report gate/consumer readiness. No writes.
74
+ publish Run the package's prepublish gate, then npm publish. Requires --apply.
75
+ propagate Pin the published version across every registered consumer (update --all-brains).
76
+ verify Independently confirm the registry artifact and each consumer's resolved install.
77
+ all plan → publish → propagate → verify, stopping at the first failure.
78
+
79
+ Rules enforced here:
80
+ • Publishing requires --apply; plan/verify never write.
81
+ • Verification never trusts the publish or propagate report: it re-reads the
82
+ npm registry and each consumer's own resolved package.json.
83
+ • A version that is only on npm is reported as incomplete, not as released.
84
+
85
+ Options:
86
+ --apply Perform writes (required for publish/propagate).
87
+ --version X.Y.Z Expect or assert a specific version (defaults to the source package.json).
88
+ --otp N npm one-time password, passed through to npm publish.
89
+ --root PATH Source repo for the package (defaults to $PWD).
90
+ --json Machine-readable output.
91
+ `);
92
+ }
93
+
94
+ /** Read a package's source metadata without assuming a monorepo layout. */
95
+ export function readSourcePackage(root, pkg) {
96
+ const candidates = [path.join(root, 'package.json')];
97
+ // A workspace source may live one level down (packages/<dir>/package.json).
98
+ for (const dir of ['packages', 'apps']) {
99
+ const base = path.join(root, dir);
100
+ if (!fs.existsSync(base)) continue;
101
+ for (const entry of fs.readdirSync(base)) {
102
+ candidates.push(path.join(base, entry, 'package.json'));
103
+ }
104
+ }
105
+ for (const file of candidates) {
106
+ if (!fs.existsSync(file)) continue;
107
+ try {
108
+ const pkgJson = JSON.parse(fs.readFileSync(file, 'utf8'));
109
+ if (pkgJson.name === pkg) return { file, dir: path.dirname(file), pkgJson };
110
+ } catch {
111
+ /* ignore malformed candidates rather than failing the whole release */
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** npm registry read. This is the independent source of truth for "published". */
118
+ export function publishedVersion(pkg, { exec = spawnSync } = {}) {
119
+ const res = exec('npm', ['view', pkg, 'version'], { encoding: 'utf8', timeout: 60000 });
120
+ if (res.status !== 0) return { ok: false, version: null, error: (res.stderr || '').trim() || 'npm view failed' };
121
+ return { ok: true, version: (res.stdout || '').trim(), error: null };
122
+ }
123
+
124
+ /** Wait for a just-published version to become readable from the registry. */
125
+ export async function awaitPublished(pkg, version, { attempts = 8, delayMs = 20000, exec = spawnSync, sleep } = {}) {
126
+ const pause = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
127
+ let last = null;
128
+ for (let i = 0; i < attempts; i++) {
129
+ const read = publishedVersion(pkg, { exec });
130
+ last = read;
131
+ if (read.ok && read.version === version) return { ok: true, version: read.version, attempts: i + 1 };
132
+ if (i < attempts - 1) await pause(delayMs);
133
+ }
134
+ return { ok: false, version: last?.version ?? null, error: last?.error ?? `registry still reports ${last?.version}`, attempts };
135
+ }
136
+
137
+ /**
138
+ * Every consumer root TR knows about: the project registry plus TR_SYNC_REPOS.
139
+ * Used by verify so it never depends on the propagate step's own bookkeeping.
140
+ */
141
+ export function consumerRoots(opts = {}) {
142
+ // Explicit roots are authoritative: passing them means "verify exactly these",
143
+ // which is what makes this callable from tests and from --root. Without them,
144
+ // fall back to the whole registered fleet plus TR_SYNC_REPOS.
145
+ const explicit = (opts.roots || []).filter(Boolean);
146
+ const source = explicit.length
147
+ ? explicit
148
+ : [...listUpdateRoots(opts).map((row) => (typeof row === 'string' ? row : row.root)), ...rootsFromEnv()];
149
+ const seen = new Set();
150
+ const out = [];
151
+ for (const root of source) {
152
+ if (!root) continue;
153
+ const resolved = path.resolve(root);
154
+ if (seen.has(resolved)) continue;
155
+ seen.add(resolved);
156
+ out.push(resolved);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /** Read the version a consumer actually resolves for a package (declared + installed). */
162
+ export function consumerState(root, pkg, version = null) {
163
+ const declaredFiles = [];
164
+ const pkgFile = path.join(root, 'package.json');
165
+ let declared = null;
166
+ if (fs.existsSync(pkgFile)) {
167
+ try {
168
+ const j = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
169
+ const all = { ...(j.dependencies || {}), ...(j.devDependencies || {}), ...(j.optionalDependencies || {}) };
170
+ declared = all[pkg] ?? null;
171
+ if (declared) declaredFiles.push(pkgFile);
172
+ } catch {
173
+ /* unreadable package.json is reported as unknown, not as satisfied */
174
+ }
175
+ }
176
+ if (!declared) return { root, declared: null, installed: null, satisfied: false, reason: 'not-declared' };
177
+
178
+ // Resolve the version a spec pins. Handles plain versions ("0.14.4"),
179
+ // npm: aliases ("npm:@scope/pkg@0.14.4") and ranges. A range is satisfied by
180
+ // the installed version rather than by string equality.
181
+ const spec = String(declared).trim();
182
+ let want = null;
183
+ let range = false;
184
+ if (spec.startsWith('npm:')) {
185
+ const afterAt = spec.slice(4);
186
+ const idx = afterAt.lastIndexOf('@');
187
+ want = idx > 0 ? afterAt.slice(idx + 1) : null;
188
+ } else if (/^[~^]|^[<>]=?|\|\||\s-\s|\*|x$/i.test(spec)) {
189
+ range = true;
190
+ } else {
191
+ want = spec;
192
+ }
193
+
194
+ const installedCandidates = [
195
+ path.join(root, 'node_modules', pkg, 'package.json'),
196
+ // npm: aliases install under the alias directory, which is what .bin shims run.
197
+ ...aliasDirs(root, pkg),
198
+ ];
199
+ let installed = null;
200
+ for (const file of installedCandidates) {
201
+ if (!fs.existsSync(file)) continue;
202
+ try {
203
+ installed = JSON.parse(fs.readFileSync(file, 'utf8')).version ?? null;
204
+ if (installed) break;
205
+ } catch {
206
+ /* keep looking */
207
+ }
208
+ }
209
+ const satisfied = range ? Boolean(installed) && installed === version : Boolean(want && installed === want);
210
+ return {
211
+ root,
212
+ declared,
213
+ want,
214
+ range,
215
+ installed,
216
+ satisfied,
217
+ reason: !installed ? 'not-installed' : satisfied ? 'ok' : 'installed-mismatch',
218
+ };
219
+ }
220
+
221
+ /** Alias install dirs for a package, e.g. @ssss/cli -> node_modules/@ssss/cli. */
222
+ function aliasDirs(root, pkg) {
223
+ const dirs = [];
224
+ const nm = path.join(root, 'node_modules');
225
+ if (!fs.existsSync(nm)) return dirs;
226
+ // Known alias pair for the SSSS CLI: the alias dir is what .bin/ssss executes.
227
+ if (pkg === '@gregiteen/ssss-cli') {
228
+ dirs.push(path.join(nm, '@ssss', 'cli', 'package.json'));
229
+ }
230
+ return dirs;
231
+ }
232
+
233
+ export function verifyConsumers(pkg, version, opts = {}) {
234
+ const roots = consumerRoots(opts);
235
+ const states = roots.map((root) => consumerState(root, pkg, version));
236
+ const relevant = states.filter((s) => s.declared);
237
+ const failing = relevant.filter((s) => !s.satisfied);
238
+ return {
239
+ pkg,
240
+ version,
241
+ roots_considered: roots.length,
242
+ consumers: relevant.length,
243
+ failing: failing.length,
244
+ failures: failing.map((s) => ({ root: s.root, declared: s.declared, installed: s.installed, reason: s.reason })),
245
+ };
246
+ }
247
+
248
+ function runPrepublish(dir, pkgJson, { exec = spawnSync, timeoutMs } = {}) {
249
+ if (!pkgJson?.scripts?.prepublishOnly) return { ok: true, skipped: true };
250
+ const manager = fs.existsSync(path.join(dir, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm';
251
+ const res = exec(manager, ['run', 'prepublishOnly'], { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
252
+ return { ok: res.status === 0, skipped: false, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
253
+ }
254
+
255
+ function doPublish(dir, { otp, exec = spawnSync, timeoutMs } = {}) {
256
+ const args = ['publish'];
257
+ if (otp) args.push('--otp', otp);
258
+ const res = exec('npm', args, { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
259
+ return { ok: res.status === 0, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
260
+ }
261
+
262
+ /**
263
+ * @param {string[]} argv full process argv (bin convention)
264
+ */
265
+ export async function run(argv = process.argv, deps = {}) {
266
+ const opts = parseArgs(argv.slice(3));
267
+ const log = opts.json ? () => {} : (...a) => console.log(...a);
268
+ const emit = (obj) => { if (opts.json) console.log(JSON.stringify(obj, null, 2)); };
269
+
270
+ if (opts.help || !opts.action) {
271
+ printHelp();
272
+ if (!opts.action && !opts.help) process.exitCode = 1;
273
+ return;
274
+ }
275
+ if (!ACTIONS.includes(opts.action)) {
276
+ console.error(`Unknown release action: ${opts.action}. Expected one of: ${ACTIONS.join(', ')}`);
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+
281
+ const report = { action: opts.action, packages: {}, ok: false, steps: [] };
282
+ const fail = (message) => {
283
+ report.error = message;
284
+ report.ok = false;
285
+ if (opts.json) emit(report); else console.error(`\n ✗ ${message}\n`);
286
+ process.exitCode = 1;
287
+ };
288
+
289
+ // propagate and verify act on the whole fleet and need no package argument.
290
+ if (opts.action === 'propagate') {
291
+ const dryRun = !opts.apply;
292
+ log(`\n Propagating ${DEFAULT_PACKAGES.join(' + ')} across every registered consumer${dryRun ? ' (plan)' : ''}...`);
293
+ const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
294
+ report.steps.push({ step: 'propagate', dryRun, summary });
295
+ report.ok = (summary.failed || 0) === 0;
296
+ if (opts.json) { emit(report); return; }
297
+ log(`\n roots: ${summary.root_count ?? '—'} consumers: ${summary.consumer_count ?? '—'} failed: ${summary.failed ?? 0}`);
298
+ if (dryRun) log(' Run with --apply to install.\n');
299
+ if (!report.ok) process.exitCode = 1;
300
+ return;
301
+ }
302
+
303
+ const packages = opts.packages.length ? opts.packages : DEFAULT_PACKAGES;
304
+ if (!packages.length) return fail('No package given.');
305
+
306
+ for (const pkg of packages) {
307
+ const root = opts.root || process.cwd();
308
+ const source = readSourcePackage(root, pkg);
309
+ const entry = { pkg, root, source: source?.file ?? null };
310
+ report.packages[pkg] = entry;
311
+
312
+ if (opts.action === 'verify') {
313
+ const version = opts.version || source?.pkgJson?.version;
314
+ if (!version) return fail(`${pkg}: no --version and no source package.json found under ${root}`);
315
+ entry.version = version;
316
+ const reg = await awaitPublished(pkg, version, { attempts: 1, delayMs: 0 });
317
+ entry.registry = reg;
318
+ const consumers = verifyConsumers(pkg, version, { roots: opts.root ? [opts.root] : [] });
319
+ entry.consumers = consumers;
320
+ if (!reg.ok) return fail(`${pkg}@${version} is not readable from the npm registry (${reg.error}).`);
321
+ if (consumers.failing > 0) {
322
+ return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers not satisfied. First: ${consumers.failures[0].root} (${consumers.failures[0].installed ?? 'not installed'}).`);
323
+ }
324
+ log(` ✓ ${pkg}@${version} verified: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
325
+ continue;
326
+ }
327
+
328
+ if (!source) return fail(`${pkg}: source package.json not found under ${root}. Pass --root <repo>.`);
329
+ const version = opts.version || source.pkgJson.version;
330
+ entry.version = version;
331
+ if (opts.version && source.pkgJson.version !== opts.version) {
332
+ return fail(`${pkg}: source is ${source.pkgJson.version} but --version ${opts.version} was requested. Bump the source first.`);
333
+ }
334
+
335
+ if (opts.action === 'plan') {
336
+ const reg = publishedVersion(pkg);
337
+ entry.registry = reg;
338
+ entry.gates = source.pkgJson.scripts?.prepublishOnly ? 'present' : 'absent';
339
+ entry.alreadyPublished = reg.ok && reg.version === version;
340
+ log(` ${pkg}: source ${version}, registry ${reg.version ?? '—'}${entry.alreadyPublished ? ' (already published)' : ''}, prepublish gate ${entry.gates}`);
341
+ continue;
342
+ }
343
+
344
+ if (opts.action === 'publish' || opts.action === 'all') {
345
+ const reg = publishedVersion(pkg);
346
+ entry.registry_before = reg;
347
+ if (reg.ok && reg.version === version) {
348
+ log(` · ${pkg}@${version} already published — skipping publish`);
349
+ entry.published = 'already';
350
+ } else {
351
+ if (!opts.apply) return fail(`publish requires --apply (would publish ${pkg}@${version}).`);
352
+ const gate = runPrepublish(source.dir, source.pkgJson, { timeoutMs: opts.timeoutMs });
353
+ entry.gate = gate.ok ? 'passed' : 'failed';
354
+ report.steps.push({ step: 'prepublish', pkg, ok: gate.ok, skipped: gate.skipped });
355
+ if (!gate.ok) return fail(`${pkg}: prepublishOnly gate failed.\n${gate.output}`);
356
+ const pub = doPublish(source.dir, { otp: opts.otp, timeoutMs: opts.timeoutMs });
357
+ entry.publish_output = pub.output;
358
+ if (!pub.ok) return fail(`${pkg}: npm publish failed.\n${pub.output}`);
359
+ const confirmed = await (deps.awaitPublished || awaitPublished)(pkg, version, deps);
360
+ entry.registry_after = confirmed;
361
+ if (!confirmed.ok) return fail(`${pkg}@${version} published but the registry still reports ${confirmed.version}. Treat as NOT released.`);
362
+ entry.published = 'yes';
363
+ log(` ✓ ${pkg}@${version} published and confirmed on the registry`);
364
+ }
365
+ if (opts.action === 'publish') continue;
366
+ }
367
+
368
+ if (opts.action === 'all') {
369
+ const dryRun = !opts.apply;
370
+ const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
371
+ report.steps.push({ step: 'propagate', pkg, summary });
372
+ if ((summary.failed || 0) > 0) return fail(`${pkg}: propagation reported ${summary.failed} failure(s).`);
373
+ const consumers = verifyConsumers(pkg, version, {});
374
+ entry.consumers = consumers;
375
+ if (consumers.failing > 0) {
376
+ return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers still not satisfied after propagation.`);
377
+ }
378
+ log(` ✓ ${pkg}@${version}: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
379
+ }
380
+ }
381
+
382
+ report.ok = true;
383
+ if (opts.json) emit(report);
384
+ else if (opts.action === 'plan') log('\n Plan only — nothing published or changed.\n');
385
+ else log('');
386
+ }
@@ -14,6 +14,7 @@ export const CLI_COMMANDS = {
14
14
  restore: 'restore.mjs',
15
15
  upgrade: 'upgrade.mjs',
16
16
  update: 'update.mjs',
17
+ release: 'release.mjs',
17
18
  friction: 'friction.mjs',
18
19
  chat: 'chat.mjs',
19
20
  status: 'status.mjs',
@@ -22,6 +23,7 @@ export const CLI_COMMANDS = {
22
23
  edit: 'edit.mjs',
23
24
  recall: 'recall.mjs',
24
25
  context: 'context.mjs',
26
+ compact: 'compact.mjs',
25
27
  preflight: 'preflight.mjs',
26
28
  'tr-preflight': 'preflight.mjs',
27
29
  'context-stats': 'context-stats.mjs',