total-recall-brain 3.40.1 → 3.40.2

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 (186) hide show
  1. package/README.md +152 -41
  2. package/bin/total-recall.mjs +16 -76
  3. package/docs/reference/cli-reference.md +10 -10
  4. package/extension/background.js +6 -2
  5. package/extension/config.js +8 -0
  6. package/extension/lib/brain-client.js +29 -4
  7. package/extension/options/options.html +1 -0
  8. package/extension/sidepanel/sidepanel.html +1 -0
  9. package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
  10. package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
  11. package/frontend/dist/index.html +2 -2
  12. package/frontend/src/api/keys.ts +9 -2
  13. package/frontend/src/api/plugins.ts +4 -0
  14. package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
  15. package/frontend/src/components/plugins/card/card.css +1 -1
  16. package/frontend/src/pages/IntegrationsPage.tsx +315 -8
  17. package/frontend/src/pages/PluginsPage.tsx +532 -87
  18. package/package.json +6 -5
  19. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  20. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
  33. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
  34. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
  87. package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
  90. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
  91. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
  93. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
  105. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
  109. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
  134. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
  135. package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
  136. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
  137. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
  138. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
  139. package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
  140. package/src/cli/brief.mjs +33 -9
  141. package/src/cli/command.mjs +2 -7
  142. package/src/cli/config.mjs +1 -0
  143. package/src/cli/connect.mjs +5 -0
  144. package/src/cli/context.mjs +2 -1
  145. package/src/cli/daemon.mjs +36 -1
  146. package/src/cli/doctor.mjs +6 -0
  147. package/src/cli/harness.mjs +3 -0
  148. package/src/cli/help.mjs +26 -0
  149. package/src/cli/init.mjs +4 -4
  150. package/src/cli/memory-entry.mjs +1 -0
  151. package/src/cli/memory-init.mjs +2 -0
  152. package/src/cli/plugin/index.mjs +11 -0
  153. package/src/cli/plugin/install.mjs +2 -1
  154. package/src/cli/rebuild.mjs +3 -1
  155. package/src/cli/startup.mjs +2 -1
  156. package/src/cli/update.mjs +19 -0
  157. package/src/core/automatic-brain-updates.mjs +67 -0
  158. package/src/core/brain-updates.mjs +21 -2
  159. package/src/core/cli-commands.mjs +71 -0
  160. package/src/core/cli-maintenance.mjs +45 -0
  161. package/src/core/cli-quick-reference.mjs +20 -0
  162. package/src/core/cli-semantic-index.mjs +722 -0
  163. package/src/core/context-compiler.mjs +27 -1
  164. package/src/core/crons.mjs +2 -13
  165. package/src/core/daemon-control.mjs +6 -4
  166. package/src/core/daemon-loop.mjs +12 -8
  167. package/src/core/default-plugins.mjs +5 -0
  168. package/src/core/global-brain-updates.mjs +39 -0
  169. package/src/core/mesh-brain-updates.mjs +30 -0
  170. package/src/core/meta-harness.mjs +108 -0
  171. package/src/core/plugin-dependencies.mjs +2 -1
  172. package/src/core/plugin-loader.mjs +2 -1
  173. package/src/core/plugin-runner.mjs +12 -4
  174. package/src/core/plugin-setup.mjs +20 -0
  175. package/src/core/plugin-store.mjs +22 -4
  176. package/src/core/plugin-tasks.mjs +1 -1
  177. package/src/core/project-brain.mjs +2 -0
  178. package/src/core/project-status.mjs +64 -0
  179. package/src/core/required-plugins.mjs +46 -0
  180. package/src/core/scaffold-plugins.mjs +17 -3
  181. package/src/core/source-brain-updates.mjs +47 -0
  182. package/src/core/startup-health.mjs +5 -3
  183. package/src/core/surface.mjs +44 -10
  184. package/src/server/routes/extension.mjs +53 -8
  185. package/templates/onboarding-interview.md +1 -1
  186. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -93,7 +93,33 @@ export async function compileContext({
93
93
  }).filter(candidate => candidate.relevant).sort((a, b) => b.score - a.score);
94
94
  const optional = candidates.map(({ node: n }) => ({ id: `${n._layer || 'project'}:${n.slug}`,
95
95
  text: `## ${n.title || n.slug} [${n.slug}]\n\n${(n.body || n.content || '').trim()}` }));
96
- const result = assembleContext([...required, ...optional, ...contributions], { total: b.total });
96
+
97
+ // CLI Semantic Index recommendations: top task-matched runnable CLI verbs
98
+ let cliVerbContribution = null;
99
+ if (query && String(query).trim()) {
100
+ try {
101
+ const { queryCliSemanticIndex } = await import('./cli-semantic-index.mjs');
102
+ const matchedVerbs = await queryCliSemanticIndex(query, {
103
+ derivedDir,
104
+ projectRoot,
105
+ limit: 5,
106
+ minScore: 0.35,
107
+ });
108
+ if (matchedVerbs.length > 0) {
109
+ const verbLines = matchedVerbs.map(v => `- \`${v.usage || v.verb}\` — ${v.description}`);
110
+ cliVerbContribution = {
111
+ id: 'cli:recommended-verbs',
112
+ required: false,
113
+ text: `### Recommended Task Verbs\n\n${verbLines.join('\n')}`,
114
+ };
115
+ }
116
+ } catch {
117
+ /* non-fatal */
118
+ }
119
+ }
120
+
121
+ const cliContributions = cliVerbContribution ? [cliVerbContribution] : [];
122
+ const result = assembleContext([...required, ...cliContributions, ...optional, ...contributions], { total: b.total });
97
123
  result.stats.total_nodes = allNodes.length;
98
124
  result.stats.compile_ms = Date.now() - startTime;
99
125
  result.stats.consumer = consumer;
@@ -3,7 +3,6 @@ import { logger } from './logger.mjs';
3
3
  import { runGitHubSync, getGitHubSyncStatus } from './github-sync.mjs';
4
4
  import { watchObsidianDirectory } from './obsidian-sync.mjs';
5
5
  const { getSecret } = await __loadOwnedPlugin('auth', 'secrets-store');
6
- import { runPackageAutoUpdate } from './package-auto-update.mjs';
7
6
 
8
7
  /**
9
8
  * runCrons — scheduled background jobs.
@@ -30,18 +29,8 @@ export async function runCrons(options) {
30
29
  // Obsidian not configured
31
30
  }
32
31
 
33
- // 3. npm package auto-update for registered projects (total-recall-brain)
34
- try {
35
- const pkg = await runPackageAutoUpdate({ brainDir });
36
- if (!pkg.skipped && (pkg.updated > 0 || pkg.failed > 0)) {
37
- logger.info({
38
- subsystem: 'cron',
39
- message: `package-auto-update: latest=${pkg.latest} updated=${pkg.updated} failed=${pkg.failed}`,
40
- });
41
- }
42
- } catch (err) {
43
- logger.warn({ subsystem: 'cron', message: `package-auto-update failed: ${err.message}` });
44
- }
32
+ // Package maintenance has its own wall-clock timer in this same daemon.
33
+ // Task-count driven crons must never block the queue on npm installation.
45
34
  } catch (err) {
46
35
  logger.error({ subsystem: 'cron', message: `Cron execution failed: ${err.message}` });
47
36
  }
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { logger } from './logger.mjs';
7
7
  import { agentDir, brainDir } from './config.mjs';
8
+ import { readProcessCommand } from './pid-lock.mjs';
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const ROOT = path.join(__dirname, '..', '..');
@@ -29,6 +30,9 @@ export function readPid() {
29
30
  // Check if the process is alive using signal 0
30
31
  try {
31
32
  process.kill(pid, 0);
33
+ const command=readProcessCommand(pid);
34
+ if(!command)throw new Error('Cannot verify daemon process identity');
35
+ if(!command.includes(path.join('core','daemon-loop.mjs')) && !command.includes(path.join('core','dream.mjs')))return null;
32
36
  return pid;
33
37
  } catch {
34
38
  return null;
@@ -130,10 +134,8 @@ export function startDaemon() {
130
134
  export async function stopDaemon({ graceMs = 35_000, pollMs = 250 } = {}) {
131
135
  const pid = readPid();
132
136
  if (!pid) {
133
- // Clear PID file in case it was a stale/dead record
134
- try {
135
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
136
- } catch {}
137
+ // Startup owns stale-lock recovery. A live process with unreadable identity
138
+ // is not ours to signal or unlink.
137
139
  return false;
138
140
  }
139
141
 
@@ -258,6 +258,8 @@ import { releaseLease } from './leader-election.mjs';
258
258
  * even a wedged iteration cannot keep the daemon alive indefinitely.
259
259
  */
260
260
  let stopPluginTasks = null;
261
+ let stopCliMaintenance = null;
262
+ let stopAutomaticUpdates = null;
261
263
 
262
264
  function shutdown(signal) {
263
265
  if (shuttingDown) return;
@@ -266,6 +268,8 @@ function shutdown(signal) {
266
268
  running = false;
267
269
  if (vaultWatcher) vaultWatcher.stop();
268
270
  if (stopPluginTasks) stopPluginTasks();
271
+ stopCliMaintenance?.();
272
+ stopAutomaticUpdates?.();
269
273
 
270
274
  const fallback = setTimeout(() => {
271
275
  logger.info({
@@ -328,6 +332,11 @@ async function main() {
328
332
  }
329
333
 
330
334
  // Start source watcher (auto-regenerates repo-expert on code changes)
335
+ try {
336
+ const { startCliSurfaceMaintenance } = await import('./cli-maintenance.mjs');
337
+ const maintenance=startCliSurfaceMaintenance({brainDir:BRAIN_DIR});
338
+ stopCliMaintenance=()=>maintenance.stop();
339
+ } catch(error) { logger.error('daemon-loop', `CLI surface maintenance failed: ${error.message}`); }
331
340
  try {
332
341
  const { detectProjectBrain } = await import('./config.mjs');
333
342
  const project = detectProjectBrain();
@@ -388,14 +397,9 @@ async function main() {
388
397
 
389
398
  // npm total-recall-brain auto-update for registered projects (throttled thereafter via crons)
390
399
  try {
391
- const { runPackageAutoUpdate } = await import('./package-auto-update.mjs');
392
- const pkg = await runPackageAutoUpdate({ brainDir: BRAIN_DIR, skipThrottle: true });
393
- if (!pkg.skipped) {
394
- logger.info({
395
- subsystem: 'daemon-loop',
396
- message: `Boot package-auto-update: latest=${pkg.latest} updated=${pkg.updated || 0} failed=${pkg.failed || 0}`,
397
- });
398
- }
400
+ const { startAutomaticBrainUpdates } = await import('./automatic-brain-updates.mjs');
401
+ const maintenance=startAutomaticBrainUpdates({brainDir:BRAIN_DIR});
402
+ stopAutomaticUpdates=()=>maintenance.stop();
399
403
  } catch (err) {
400
404
  logger.info({ subsystem: 'daemon-loop', message: `Boot package-auto-update failed: ${err.message}` });
401
405
  }
@@ -0,0 +1,5 @@
1
+ /** Default runtime foundation and routine agent-development tools. */
2
+ export const DEFAULT_PLUGIN_IDS = Object.freeze([
3
+ 'auth', 'mesh', 'openwiki', 'composable-cli', 'cli-agents',
4
+ 'code-quality', 'security', 'embeddings', 'git-sentinel',
5
+ ]);
@@ -0,0 +1,39 @@
1
+ /** Verify and maintain globally installed CLIs without replacing a source checkout. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {spawnSync} from 'node:child_process';
5
+ import {BRAIN_PACKAGES} from './brain-updates.mjs';
6
+ import {comparePackageVersions} from './package-version.mjs';
7
+ import {updateSourceBrainPackage} from './source-brain-updates.mjs';
8
+
9
+ export function updateGlobalBrainPackages({versions,exec=spawnSync,sourceUpdate=updateSourceBrainPackage}={}) {
10
+ const prefix=exec('npm',['root','--global'],{encoding:'utf8',timeout:15000});
11
+ if(prefix.status!==0)return {failed:1,results:[{status:'failed',error:'Cannot resolve global npm root'}]};
12
+ const root=prefix.stdout.trim();
13
+ const results=[];
14
+ const install=[];
15
+ for(const name of BRAIN_PACKAGES){
16
+ const directory=path.join(root,name),file=path.join(directory,'package.json');
17
+ let installed;try{installed=JSON.parse(fs.readFileSync(file,'utf8')).version;}catch{}
18
+ const row={package:name,installed:installed || null,latest:versions[name],status:'up_to_date'};results.push(row);
19
+ if(installed===versions[name])continue;
20
+ if(!versions[name]){row.status='failed';row.error='Published version is unavailable';continue;}
21
+ if(installed && comparePackageVersions(installed,versions[name])===1){row.status='failed';row.error='Newer local/preview global package; refusing implicit downgrade';continue;}
22
+ if(fs.existsSync(path.join(directory,'.git'))){
23
+ const result=sourceUpdate({directory:fs.realpathSync(directory),name,version:versions[name],exec});
24
+ row.status=result.ok?'updated':'failed';row.source=true;
25
+ if(result.ok){row.installed=result.installed;row.commit=result.commit;}else row.error=`Source checkout update failed: ${result.error}`;
26
+ continue;
27
+ }
28
+ row.status='pending';install.push(`${name}@${versions[name]}`);
29
+ }
30
+ if(install.length){
31
+ const result=exec('npm',['install','--global','--no-audit','--no-fund',...install],{encoding:'utf8',timeout:600000,maxBuffer:8*1024*1024});
32
+ for(const row of results.filter(row=>row.status==='pending')){
33
+ try{row.installed=JSON.parse(fs.readFileSync(path.join(root,row.package,'package.json'),'utf8')).version;}catch{}
34
+ row.status=result.status===0 && row.installed===row.latest?'updated':'failed';
35
+ if(row.status==='failed')row.error=result.error?.message || result.stderr?.slice(-1000) || 'Global version verification failed';
36
+ }
37
+ }
38
+ return {failed:results.filter(row=>row.status==='failed').length,results};
39
+ }
@@ -0,0 +1,30 @@
1
+ /** The daemon dispatches maintenance through the mesh owner; remote brains stay separate. */
2
+ export async function updateMeshBrainPackages({version,versions,listNodes,exec,now=Date.now}={}) {
3
+ if(!/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(version || ''))throw new Error('A verified published TR version is required');
4
+ if(!listNodes || !exec){
5
+ const mesh=await import('./mesh.mjs');
6
+ listNodes ||= ()=>mesh.listEnrichedMeshNodes();
7
+ exec ||= mesh.execMeshCommand;
8
+ }
9
+ const results=[];
10
+ for(const node of await listNodes()){
11
+ if(node.self || !node.ip || !node.access?.ssh_user)continue;
12
+ const row={node:node.ip,status:'pending'};results.push(row);
13
+ if(!node.online){row.status='failed';row.error='Configured mesh node is offline; retry pending';continue;}
14
+ try{
15
+ // Read the durable report first, then launch only when stale or failed.
16
+ const observed=await exec(node.ip,'cd "$HOME" && total-recall update --status --json',{timeoutMs:20000});
17
+ let report;try{report=JSON.parse(observed.stdout).last;}catch{}
18
+ const current=observed.success && report && !report.failed && now()>=report.checkedAt && now()-report.checkedAt<2*60*60*1000 && report.versions?.['total-recall-brain']===version && (!versions?.['@gregiteen/ssss-cli'] || report.versions?.['@gregiteen/ssss-cli']===versions['@gregiteen/ssss-cli']);
19
+ if(current){row.status='verified';row.report=report;continue;}
20
+ // npm exec pins the verified release so an old installed CLI can bootstrap
21
+ // this capability. The remote command owns its lock, verification and report.
22
+ const command=`cd "$HOME" && nohup npm exec --yes --package=total-recall-brain@${version} -- total-recall update --automatic --local-only --json </dev/null >/tmp/total-recall-automatic-update.log 2>&1 &`;
23
+ const launched=await exec(node.ip,command,{timeoutMs:20000});
24
+ row.status=launched.success?'dispatched':'failed';
25
+ row.error=launched.success?'Awaiting independently verified remote report':launched.stderr || 'Mesh maintenance dispatch failed';
26
+ }catch(error){row.status='failed';row.error=error.message;}
27
+ }
28
+ // A dispatched process is not a verified update. Keep it visible for retries.
29
+ return {failed:results.filter(row=>row.status!=='verified').length,results};
30
+ }
@@ -75,6 +75,33 @@ export const HARNESS_SPECS = {
75
75
  defaultFlags: ['-p'],
76
76
  execType: 'flag_last',
77
77
  description: 'Autonomous coding, repo analysis, and shell execution via xAI Grok.'
78
+ },
79
+ hermes: {
80
+ id: 'hermes',
81
+ name: 'Hermes Agent Runtime',
82
+ binary: 'hermes',
83
+ category: 'autonomous_agent',
84
+ defaultFlags: ['chat', '-p'],
85
+ execType: 'flag_last',
86
+ description: 'Supervised messaging gateway and multi-channel autonomous agent runtime.'
87
+ },
88
+ dsh: {
89
+ id: 'dsh',
90
+ name: 'DeepSeek Harness (DSH)',
91
+ binary: 'dsh',
92
+ category: 'reasoning_engine',
93
+ defaultFlags: ['-p'],
94
+ execType: 'flag_last',
95
+ description: 'High-throughput DeepSeek reasoning and tool execution harness.'
96
+ },
97
+ openclaw: {
98
+ id: 'openclaw',
99
+ name: 'OpenClaw Messaging Bridge',
100
+ binary: 'openclaw',
101
+ category: 'multichannel_bridge',
102
+ defaultFlags: ['status', '--json'],
103
+ execType: 'flag_last',
104
+ description: 'Multi-channel messaging bridge (WhatsApp, Telegram, Discord, Slack) and agent execution.'
78
105
  }
79
106
  };
80
107
 
@@ -227,6 +254,27 @@ export async function detectHarnessesWithAuth({ readSecret = readStoreSecret } =
227
254
  h.authed = true;
228
255
  h.authRoute = 'cached-google-signin';
229
256
  }
257
+ } else if (h.id === 'hermes') {
258
+ if (process.env.HERMES_API_KEY) {
259
+ h.authed = true;
260
+ h.authRoute = 'env-api-key';
261
+ } else if (fs.existsSync(path.join(os.homedir(), '.hermes')) || fs.existsSync(path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'plugins', 'hermes'))) {
262
+ h.authed = true;
263
+ h.authRoute = 'cached-profile';
264
+ }
265
+ } else if (h.id === 'dsh') {
266
+ if (process.env.OPENROUTER_API_KEY) {
267
+ h.authed = true;
268
+ h.authRoute = 'env-api-key';
269
+ } else if (fs.existsSync(path.join(os.homedir(), '.dsh')) || fs.existsSync(path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'plugins', 'dsh'))) {
270
+ h.authed = true;
271
+ h.authRoute = 'cached-dsh-config';
272
+ }
273
+ } else if (h.id === 'openclaw') {
274
+ if (fs.existsSync(path.join(os.homedir(), '.openclaw', 'openclaw.json'))) {
275
+ h.authed = true;
276
+ h.authRoute = 'cached-openclaw-config';
277
+ }
230
278
  }
231
279
  }
232
280
  return harnesses;
@@ -452,6 +500,25 @@ export function getInstalledHarnessVersion(harnessId) {
452
500
  const res = spawnSync('gemini', ['--version'], { timeout: 800, encoding: 'utf8' });
453
501
  const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
454
502
  if (m) return m[1];
503
+ } else if (harnessId === 'hermes') {
504
+ const res = spawnSync('hermes', ['--version'], { timeout: 1500, encoding: 'utf8' });
505
+ const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
506
+ if (m) return m[1];
507
+ } else if (harnessId === 'dsh') {
508
+ const res = spawnSync('dsh', ['--version'], { timeout: 1500, encoding: 'utf8' });
509
+ const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
510
+ if (m) return m[1];
511
+ } else if (harnessId === 'openclaw') {
512
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
513
+ if (fs.existsSync(configPath)) {
514
+ try {
515
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
516
+ if (cfg?.meta?.lastTouchedVersion) return cfg.meta.lastTouchedVersion;
517
+ } catch {}
518
+ }
519
+ const res = spawnSync('openclaw', ['--version'], { timeout: 1500, encoding: 'utf8' });
520
+ const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
521
+ if (m) return m[1];
455
522
  }
456
523
  } catch {}
457
524
  return 'unknown';
@@ -490,6 +557,12 @@ export async function getLatestHarnessVersion(harnessId, installedVersion = 'unk
490
557
  latest = '0.24.0';
491
558
  } else if (harnessId === 'gemini') {
492
559
  latest = '0.3.0';
560
+ } else if (harnessId === 'hermes') {
561
+ latest = '0.1.0';
562
+ } else if (harnessId === 'dsh') {
563
+ latest = '1.1.1';
564
+ } else if (harnessId === 'openclaw') {
565
+ latest = '2026.4.10';
493
566
  }
494
567
  } catch {}
495
568
 
@@ -640,6 +713,41 @@ export async function queryHarnessUsage() {
640
713
  rolling5hRemainingPct = '100% (unlimited)';
641
714
  weeklyRemainingPct = '100% (unlimited)';
642
715
  quotaRemaining = 'Unlimited (local zero-cost inference)';
716
+ } else if (h.id === 'hermes') {
717
+ identity = 'Hermes Supervisor';
718
+ plan = 'Autonomous Agent Runtime';
719
+ rolling5hRemainingPct = '100%';
720
+ weeklyRemainingPct = '100%';
721
+ quotaRemaining = 'Active broker & gateway';
722
+ } else if (h.id === 'dsh') {
723
+ let model = 'deepseek/deepseek-v4.1-flash';
724
+ try {
725
+ const dshModelPath = path.join(os.homedir(), '.dsh', 'default-model.txt');
726
+ if (fs.existsSync(dshModelPath)) {
727
+ model = fs.readFileSync(dshModelPath, 'utf8').trim() || model;
728
+ }
729
+ } catch {}
730
+ identity = model;
731
+ plan = 'DeepSeek Dedicated Harness';
732
+ rolling5hRemainingPct = '95%';
733
+ weeklyRemainingPct = '98%';
734
+ quotaRemaining = 'DeepSeek reasoning pool active';
735
+ } else if (h.id === 'openclaw') {
736
+ let channelsStr = 'telegram, qqbot';
737
+ try {
738
+ const oPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
739
+ if (fs.existsSync(oPath)) {
740
+ const oCfg = JSON.parse(fs.readFileSync(oPath, 'utf8'));
741
+ identity = oCfg?.agents?.defaults?.model?.primary || 'openai-codex/gpt-5.4';
742
+ if (oCfg?.channels) {
743
+ channelsStr = Object.keys(oCfg.channels).join(', ');
744
+ }
745
+ }
746
+ } catch {}
747
+ plan = 'Multi-Channel Gateway';
748
+ rolling5hRemainingPct = '100%';
749
+ weeklyRemainingPct = '100%';
750
+ quotaRemaining = `Channels: ${channelsStr}`;
643
751
  }
644
752
 
645
753
  let status = isAuthed ? 'ready' : 'needs_auth';
@@ -12,7 +12,8 @@ export function preparePluginDependencies(pluginDir) {
12
12
  let source;
13
13
  if (name === host.name) source = PACKAGE_ROOT;
14
14
  else {
15
- source = path.dirname(require.resolve(name));
15
+ const alias = Object.entries(host.dependencies || {}).find(([, spec]) => typeof spec === 'string' && spec.startsWith(`npm:${name}@`))?.[0];
16
+ source = path.dirname(require.resolve(alias || name));
16
17
  while (true) {
17
18
  const file = path.join(source, 'package.json');
18
19
  if (fs.existsSync(file) && JSON.parse(fs.readFileSync(file, 'utf8')).name === name) break;
@@ -29,7 +29,7 @@ export function globalPluginsDir() {
29
29
  }
30
30
 
31
31
  export function bundledPluginsDir() {
32
- return process.env._TR_TEST_BUNDLED_PLUGINS_DIR || path.join(PACKAGE_ROOT, 'plugins');
32
+ return process.env._TR_TEST_BUNDLED_PLUGINS_DIR || path.join(PACKAGE_ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'plugins');
33
33
  }
34
34
 
35
35
  /** The vault that owns records for plugins installed in `pluginsDir` (`<brain>/plugins` → `<brain>/memory-vault`). */
@@ -165,6 +165,7 @@ export function validatePluginManifest(manifest) {
165
165
  if (t.placement !== undefined && !['per-node', 'selected-node'].includes(t.placement)) {
166
166
  errors.push(`Task '${t.intent}' has unsupported placement '${t.placement}'`);
167
167
  }
168
+ if (t.timeout_ms !== undefined && (!Number.isInteger(t.timeout_ms) || t.timeout_ms < 1 || t.timeout_ms > 600000)) errors.push(`Task '${t.intent}' timeout_ms must be 1..600000`);
168
169
  }
169
170
  }
170
171
  }
@@ -67,6 +67,7 @@ export function runPluginCommand(plugin, options = {}) {
67
67
  return new Promise((resolve) => {
68
68
  const started = Date.now();
69
69
  const chunks = [];
70
+ const stdoutChunks=[],stderrChunks=[];
70
71
  let bytes = 0;
71
72
  let truncated = false;
72
73
  let timedOut = false;
@@ -90,21 +91,22 @@ export function runPluginCommand(plugin, options = {}) {
90
91
  stdio: [stdinText !== undefined ? 'pipe' : stdin === 'inherit' ? 'inherit' : 'ignore', 'pipe', 'pipe']
91
92
  });
92
93
 
93
- const collect = (buf) => {
94
+ const collect = (buf,stream) => {
94
95
  // Even a faulty owner that echoes stdin cannot return credential material.
95
96
  if (stdinText !== undefined) return;
96
97
  if (truncated) return;
97
98
  if (bytes + buf.length > MAX_OUTPUT_BYTES) {
98
- chunks.push(buf.subarray(0, MAX_OUTPUT_BYTES - bytes));
99
+ const partial=buf.subarray(0, MAX_OUTPUT_BYTES - bytes);chunks.push(partial);stream.push(partial);
99
100
  bytes = MAX_OUTPUT_BYTES;
100
101
  truncated = true;
101
102
  return;
102
103
  }
103
104
  chunks.push(buf);
105
+ stream.push(buf);
104
106
  bytes += buf.length;
105
107
  };
106
- child.stdout.on('data', collect);
107
- child.stderr.on('data', collect);
108
+ child.stdout.on('data', buf=>collect(buf,stdoutChunks));
109
+ child.stderr.on('data', buf=>collect(buf,stderrChunks));
108
110
  if (stdinText !== undefined) {
109
111
  // A handler may exit without reading; EPIPE must not crash the server.
110
112
  child.stdin.on('error', () => {});
@@ -124,6 +126,10 @@ export function runPluginCommand(plugin, options = {}) {
124
126
  if (spawnError) output += `${output ? '\n' : ''}[runner] ${spawnError.message}`;
125
127
  if (truncated) output += `\n[runner] output truncated at ${MAX_OUTPUT_BYTES} bytes`;
126
128
  if (timedOut) output += `\n[runner] killed after ${timeoutMs} ms`;
129
+ let stderr=Buffer.concat(stderrChunks).toString('utf8');
130
+ if(spawnError)stderr+=`\n[runner] ${spawnError.message}`;
131
+ if(truncated)stderr+=`\n[runner] output truncated at ${MAX_OUTPUT_BYTES} bytes`;
132
+ if(timedOut)stderr+=`\n[runner] killed after ${timeoutMs} ms`;
127
133
  resolve({
128
134
  ok: !spawnError && !timedOut && exitCode === 0,
129
135
  exitCode,
@@ -131,6 +137,8 @@ export function runPluginCommand(plugin, options = {}) {
131
137
  timedOut,
132
138
  truncated,
133
139
  output,
140
+ stdout:Buffer.concat(stdoutChunks).toString('utf8'),
141
+ stderr,
134
142
  durationMs: Date.now() - started
135
143
  });
136
144
  };
@@ -0,0 +1,20 @@
1
+ /** Run declared, enabled plugin setup through the normal isolated CLI runner. */
2
+ import { discoverPlugins } from './plugin-loader.mjs';
3
+ import { readPluginRecord } from './plugin-store.mjs';
4
+ import { runPluginCommand } from './plugin-runner.mjs';
5
+
6
+ export async function setupInstalledPlugins({root=process.cwd(),plugins=discoverPlugins(root),run=runPluginCommand,record=readPluginRecord}={}) {
7
+ const results=[];
8
+ for(const plugin of plugins) {
9
+ const command=plugin.manifest?.runtime?.setup_command;
10
+ if(!plugin.valid || !command || record(plugin)?.runtime_enabled===false)continue;
11
+ if(!plugin.manifest.cli?.subcommands?.some(operation=>operation.name===command)) {
12
+ results.push({id:plugin.id,ok:false,error:'Runtime setup must name a declared CLI operation'});continue;
13
+ }
14
+ try {
15
+ const result=await run(plugin,{subcommand:command,cwd:root,timeoutMs:600000});
16
+ results.push({id:plugin.id,ok:result.ok,error:result.ok?undefined:(result.stderr || result.output || 'Plugin setup failed').slice(-1000)});
17
+ }catch(error){results.push({id:plugin.id,ok:false,error:error.message});}
18
+ }
19
+ return {ok:results.every(result=>result.ok),results};
20
+ }
@@ -364,6 +364,16 @@ export async function installPlugin(source, options = {}) {
364
364
  let sourceRecord;
365
365
  let verifiedSha = null;
366
366
  let dependencies = null;
367
+ let previousDirectory=null;
368
+ const prepareDestination=id=>{
369
+ const target=path.join(pluginsDir,id);
370
+ if(!options.replace || !fs.existsSync(target))return assertNotInstalled(id,pluginsDir);
371
+ const archive=path.join(path.dirname(pluginsDir),'memory-derived/plugin-replacements',`${id}-${crypto.randomUUID()}`);
372
+ fs.mkdirSync(path.dirname(archive),{recursive:true});
373
+ fs.renameSync(target,archive);previousDirectory={target,archive};
374
+ };
375
+
376
+ try {
367
377
 
368
378
  const resolved = resolveBundle ? await resolveBundle(source, options) : null;
369
379
  const bundled = !resolved && ID_PATTERN.test(source) ? listBundledPlugins().find((p) => p.id === source) : null;
@@ -372,14 +382,14 @@ export async function installPlugin(source, options = {}) {
372
382
  manifest = resolved.manifest;
373
383
  const validation = validatePluginManifest(manifest);
374
384
  if (!validation.valid) throw new Error(`Invalid plugin manifest: ${validation.errors.join('; ')}`);
375
- assertNotInstalled(manifest.id, pluginsDir);
385
+ prepareDestination(manifest.id);
376
386
  dest = writeEntriesAtomic(pluginsDir, manifest.id, resolved.entries);
377
387
  verifiedSha = resolved.sha256;
378
388
  sourceRecord = resolved.source;
379
389
  } else if (bundled) {
380
390
  if (!bundled.valid) throw new Error(`Bundled plugin '${source}' is invalid: ${bundled.errors.join('; ')}`);
381
391
  manifest = bundled.manifest;
382
- assertNotInstalled(manifest.id, pluginsDir);
392
+ prepareDestination(manifest.id);
383
393
  dest = copyPluginAtomic(bundled.dir, pluginsDir, manifest.id);
384
394
  sourceRecord = { kind: 'bundled', ref: manifest.id };
385
395
  } else if (isGitSource(source)) {
@@ -391,7 +401,7 @@ export async function installPlugin(source, options = {}) {
391
401
  throw new Error(`Failed to clone ${source}: ${(err.stderr || err.message || '').toString().trim()}`);
392
402
  }
393
403
  manifest = readManifestFrom(tmp);
394
- assertNotInstalled(manifest.id, pluginsDir);
404
+ prepareDestination(manifest.id);
395
405
  dest = copyPluginAtomic(tmp, pluginsDir, manifest.id);
396
406
  } finally {
397
407
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -403,7 +413,7 @@ export async function installPlugin(source, options = {}) {
403
413
  throw new Error(`No bundled plugin, share link, peer source, git URL or directory matches '${source}'`);
404
414
  }
405
415
  manifest = readManifestFrom(abs);
406
- assertNotInstalled(manifest.id, pluginsDir);
416
+ prepareDestination(manifest.id);
407
417
  if (link) {
408
418
  fs.mkdirSync(pluginsDir, { recursive: true });
409
419
  dest = path.join(pluginsDir, manifest.id);
@@ -446,8 +456,16 @@ export async function installPlugin(source, options = {}) {
446
456
  source: sourceRecord,
447
457
  sha256,
448
458
  dependencies,
459
+ previousDirectory:previousDirectory?.archive || null,
449
460
  compilation
450
461
  };
462
+ }catch(error){
463
+ if(previousDirectory){
464
+ if(fs.existsSync(previousDirectory.target))fs.rmSync(previousDirectory.target,{recursive:true,force:true});
465
+ fs.renameSync(previousDirectory.archive,previousDirectory.target);
466
+ }
467
+ throw error;
468
+ }
451
469
  }
452
470
 
453
471
  export function findInstalled(id, { projectRoot = process.cwd(), global: isGlobal } = {}) {
@@ -99,7 +99,7 @@ export async function runDuePluginTasks(options = {}) {
99
99
  const slot = latestDueSlot(cron, runs[task.command], now);
100
100
  if (!slot) continue;
101
101
 
102
- const result = await runner(plugin, { subcommand: task.command, cwd: projectRoot, timeoutMs: TASK_TIMEOUT_MS })
102
+ const result = await runner(plugin, { subcommand: task.command, cwd: projectRoot, timeoutMs: task.timeout_ms || TASK_TIMEOUT_MS })
103
103
  .catch((err) => ({ ok: false, exitCode: null, output: err.message, durationMs: 0, timedOut: false }));
104
104
  runs[task.command] = slot;
105
105
  changed = true;
@@ -19,6 +19,7 @@ import fs from 'fs';
19
19
  import path from 'path';
20
20
  import { fileURLToPath } from 'url';
21
21
  import YAML from 'yaml';
22
+ import { ensureRequiredPlugins } from './required-plugins.mjs';
22
23
  import { VAULT_CATEGORIES, writeBrainIdentity, registerProjectBrain } from './memory-brain.mjs';
23
24
  export { VAULT_CATEGORIES, writeBrainIdentity, registerProjectBrain } from './memory-brain.mjs';
24
25
 
@@ -96,6 +97,7 @@ export function ensureOpenWiki(brainDir) {
96
97
  * Ensure core total-recall skill package files exist (SKILL.md minimum).
97
98
  */
98
99
  export function ensureCoreSkillPackage(brainDir) {
100
+ ensureRequiredPlugins(brainDir);
99
101
  const skillMd = path.join(brainDir, 'SKILL.md');
100
102
  if (fs.existsSync(skillMd)) return { seeded: false };
101
103
  const scaffold = path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall');
@@ -0,0 +1,64 @@
1
+ /** Read-only inventory of recorded project work. No inferred completion or live readiness. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {listUpdateRoots,resolveRegistryFiles,readRegistryEntries} from './package-auto-update.mjs';
5
+ import {listTasks,resolveQueueDir} from './task-envelope.mjs';
6
+ import {globalBrainDirFor} from './brain-registry.mjs';
7
+
8
+ export function outstandingProjectStatus(options={}) {
9
+ const globalBrainDir=options.brainDir || globalBrainDirFor();
10
+ const roots=listUpdateRoots({...options,brainDir:globalBrainDir});
11
+ const missing=resolveRegistryFiles({...options,brainDir:globalBrainDir}).flatMap(file=>readRegistryEntries(file)).filter(row=>row.path && !fs.existsSync(row.path)).map(row=>({root:row.path,status:'missing-registered-root'}));
12
+ const projects=[];
13
+ const queues=new Set([globalBrainDir]);
14
+ for(const {root,name} of roots){
15
+ queues.add(path.join(root,'.agent/skills/total-recall'));
16
+ const row={name,root,projects:[],status:'no-outstanding-tracker-found'};
17
+ for(const state of ['in-progress','planned','backlog','blocked']){
18
+ const directory=path.join(root,'docs/projects',state);
19
+ if(!fs.existsSync(directory))continue;
20
+ for(const entry of fs.readdirSync(directory,{withFileTypes:true}).filter(entry=>entry.isDirectory())){
21
+ const folder=path.join(directory,entry.name);
22
+ const files=fs.readdirSync(folder).filter(file=>/PROJECT_TRACKER\.md$|^TRACKER\.md$/i.test(file));
23
+ if(!files.length){row.projects.push({id:entry.name,state,status:'tracker-missing',folder});continue;}
24
+ for(const file of files){
25
+ const tracker=path.join(folder,file),body=fs.readFileSync(tracker,'utf8'),lines=body.split(/\r?\n/);
26
+ const open=lines.flatMap((text,index)=>/^\s*[-*]\s+\[ \]/.test(text)?[{line:index+1,text:text.trim()}]:[]);
27
+ const done=lines.filter(text=>/^\s*[-*]\s+\[[xX]\]/.test(text)).length;
28
+ const status=body.match(/^(?:>\s*)?(?:\*\*)?(?:Project Status|Status)\s*:(?:\*\*)?\s*(.+)$/im)?.[1]?.trim() || 'not-declared';
29
+ const blockers=lines.flatMap((text,index)=>/\b(blocked|blocker|blocking)\b/i.test(text) && !/^\s*[-*]\s+\[[xX]\]/.test(text)?[{line:index+1,text:text.trim()}]:[]);
30
+ row.projects.push({id:entry.name,state,status,tracker,modifiedAt:fs.statSync(tracker).mtime.toISOString(),openCount:open.length,checkedCount:done,open,blockerMentions:blockers});
31
+ }
32
+ }
33
+ }
34
+ if(row.projects.length)row.status='recorded-outstanding-work';
35
+ projects.push(row);
36
+ }
37
+ const tasks=[];
38
+ for(const brainDir of queues)for(const task of listTasks(resolveQueueDir(brainDir),{status:'all'})){
39
+ if(['completed','cancelled'].includes(task.status))continue;
40
+ tasks.push({brainDir,slug:task.slug,intent:String(task.intent || '').trim().split(/\r?\n/).find(line=>line.trim() && !line.startsWith('#'))?.slice(0,300) || task.slug,status:task.status,executor:task.executor,priority:task.priority,source:task._filepath});
41
+ }
42
+ return {generatedAt:new Date().toISOString(),evidence:'Recorded trackers and task envelopes; live completion and deployments are not verified',rootCount:projects.length,projectCount:projects.reduce((n,row)=>n+row.projects.length,0),openCheckboxCount:projects.reduce((n,row)=>n+row.projects.reduce((m,p)=>m+(p.openCount || 0),0),0),projects,tasks,missing};
43
+ }
44
+
45
+ export function formatOutstandingProjectStatus(report){
46
+ const clean=value=>String(value ?? 'unknown').replaceAll('|','\\|').replace(/\s+/g,' ').trim();
47
+ const lines=['# Outstanding projects and tasks',`Generated ${report.generatedAt}.`,report.evidence,`${report.rootCount} registered roots; ${report.projectCount} project folders; ${report.openCheckboxCount} unchecked items; ${report.tasks.length} outstanding task records.`,'','Counts include duplicated plans in separate repositories. Missing trackers and empty checklists do not establish completion.',''];
48
+ for(const repo of report.projects){
49
+ lines.push(`## ${repo.name}`,'',`Source: ${repo.root}`,'');
50
+ if(!repo.projects.length){lines.push('No outstanding project folder was found in the inspected Kanban directories. Project status remains unverified.','');continue;}
51
+ lines.push('| Project | Recorded state | Open items | Source updated |','| --- | --- | ---: | --- |');
52
+ for(const project of repo.projects)lines.push(`| [${clean(project.id)}](${project.tracker || project.folder}) | ${clean(project.state)}${project.status==='tracker-missing'?'; tracker missing':''} | ${project.openCount ?? '?'} | ${project.modifiedAt?.slice(0,10) || 'unknown'} |`);
53
+ lines.push('');
54
+ for(const project of repo.projects.filter(project=>project.open?.length)){
55
+ lines.push(`### ${clean(project.id)}: recorded remaining work`,'');
56
+ for(const item of project.open)lines.push(`- ${clean(item.text.replace(/^[-*]\s+\[ \]\s*/,''))} ([source](${project.tracker}:${item.line}))`);
57
+ lines.push('');
58
+ }
59
+ }
60
+ lines.push('## Outstanding daemon tasks','','| Task | Status | Executor | Source |','| --- | --- | --- | --- |');
61
+ for(const task of report.tasks)lines.push(`| ${clean(task.slug)} | ${clean(task.status)} | ${clean(task.executor)} | [record](${task.source}) |`);
62
+ for(const row of report.missing)lines.push(`Missing registered root: ${row.root}`);
63
+ return lines.join('\n')+'\n';
64
+ }