total-recall-brain 3.44.0 → 3.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +13 -8
  4. package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
  5. package/frontend/dist/index.html +1 -1
  6. package/frontend/src/App.tsx +2 -2
  7. package/frontend/src/pages/PluginsPage.tsx +1 -1
  8. package/package.json +1 -1
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  15. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  16. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  17. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  20. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  21. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  26. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  27. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
  34. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  42. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  44. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
  45. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
  46. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  68. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
  69. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  74. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
  75. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
  76. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
  77. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
  78. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
  80. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  81. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
  82. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
  84. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
  85. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
  86. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  87. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
  88. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  122. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
  123. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  124. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  125. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  126. package/src/cli/agent.mjs +1 -1
  127. package/src/cli/command.mjs +41 -2
  128. package/src/cli/dream.mjs +93 -69
  129. package/src/cli/harness.mjs +4 -4
  130. package/src/cli/help.mjs +1 -1
  131. package/src/cli/init.mjs +0 -25
  132. package/src/cli/lessons.mjs +51 -0
  133. package/src/cli/memory-plugin.mjs +1 -1
  134. package/src/cli/plugin/copies.mjs +64 -14
  135. package/src/cli/plugin/create.mjs +6 -7
  136. package/src/cli/plugin/index.mjs +11 -7
  137. package/src/cli/plugin/install.mjs +10 -15
  138. package/src/cli/plugin/remove.mjs +1 -1
  139. package/src/cli/plugin/search.mjs +2 -2
  140. package/src/cli/preflight.mjs +19 -6
  141. package/src/cli/proposals.mjs +1 -1
  142. package/src/cli/recall.mjs +66 -23
  143. package/src/cli/remember.mjs +3 -14
  144. package/src/cli/server.mjs +18 -0
  145. package/src/cli/startup.mjs +1 -0
  146. package/src/cli/task.mjs +9 -2
  147. package/src/cli/update.mjs +6 -2
  148. package/src/core/access-ledger.mjs +78 -0
  149. package/src/core/agent-manager.mjs +2 -2
  150. package/src/core/brain-locate.mjs +15 -0
  151. package/src/core/brain-updates.mjs +31 -11
  152. package/src/core/claude-hooks.mjs +3 -1
  153. package/src/core/cleanup-task.mjs +15 -0
  154. package/src/core/cli-commands.mjs +2 -0
  155. package/src/core/cli-maintenance.mjs +4 -1
  156. package/src/core/cli-semantic-index.mjs +23 -3
  157. package/src/core/context-compiler.mjs +20 -0
  158. package/src/core/daemon-loop.mjs +39 -62
  159. package/src/core/default-plugins.mjs +1 -1
  160. package/src/core/dream.mjs +33 -105
  161. package/src/core/ensure-dependencies.mjs +148 -0
  162. package/src/core/fast-recall.mjs +2 -2
  163. package/src/core/field-evaluation.mjs +23 -0
  164. package/src/core/gate-mode.mjs +1 -1
  165. package/src/core/harness-hooks.mjs +14 -2
  166. package/src/core/lesson-hooks.mjs +74 -0
  167. package/src/core/lesson-memory.mjs +24 -0
  168. package/src/core/lesson-sessions.mjs +52 -0
  169. package/src/core/lessons.mjs +96 -0
  170. package/src/core/meta-harness.mjs +2 -2
  171. package/src/core/optimizer.mjs +66 -165
  172. package/src/core/plugin-contracts/card.mjs +3 -0
  173. package/src/core/plugin-loader.mjs +12 -3
  174. package/src/core/plugin-runtime.mjs +40 -1
  175. package/src/core/plugin-store.mjs +35 -23
  176. package/src/core/post-mortem.mjs +52 -36
  177. package/src/core/preflight-gate.mjs +23 -2
  178. package/src/core/proposal-applier.mjs +31 -2
  179. package/src/core/repo-sync.mjs +4 -6
  180. package/src/core/required-plugins.mjs +26 -4
  181. package/src/core/rule-length.mjs +23 -0
  182. package/src/core/runtime.mjs +1 -1
  183. package/src/core/scheduler.mjs +35 -43
  184. package/src/core/schema.mjs +1 -2
  185. package/src/core/search.mjs +4 -3
  186. package/src/core/server-service.mjs +148 -0
  187. package/src/core/session-watcher.mjs +19 -1
  188. package/src/core/skill-gap.mjs +16 -0
  189. package/src/core/ssss-kernel-bridge.mjs +32 -4
  190. package/src/core/startup-health.mjs +6 -1
  191. package/src/core/surface.mjs +65 -54
  192. package/src/core/task-executors.mjs +54 -106
  193. package/src/core/validated-write.mjs +1 -0
  194. package/src/core/vector-field.mjs +134 -192
  195. package/src/server/invoke-core-child.mjs +24 -0
  196. package/src/server/invoke-core-operations.mjs +102 -0
  197. package/src/server/route-manifest.json +0 -56
  198. package/src/server/routes/brains.mjs +0 -18
  199. package/src/server/routes/context.mjs +0 -41
  200. package/src/server/routes/invoke.mjs +26 -1
  201. package/src/server/routes/ssss.mjs +0 -169
  202. package/src/server/routes/system.mjs +0 -15
  203. package/src/server/routes/tasks.mjs +0 -26
  204. package/src/server/routes/vault.mjs +2 -23
  205. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  206. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  207. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  208. package/src/core/append-log.mjs +0 -131
  209. package/src/core/plugin-tasks.mjs +0 -173
  210. package/templates/onboarding-interview.md +0 -82
  211. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  212. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
@@ -0,0 +1,201 @@
1
+ import { readFileSync, existsSync, unlinkSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { emit, jsonResult, stateStore, tr } from '../lib/runtime.mjs';
6
+
7
+ export const HELPER = '!total-recall git credential-helper';
8
+ const DEFAULT_MAP = { 'github.com': 'github_token' };
9
+
10
+ function hostName(value) {
11
+ if (typeof value !== 'string' || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]+)?$/i.test(value)) {
12
+ throw new Error('Supply a host name with an optional port');
13
+ }
14
+ return value.toLowerCase();
15
+ }
16
+
17
+ async function mappings(ctx) {
18
+ if (!ctx.env.TR_PLUGIN_STATE_DIR) return { ...DEFAULT_MAP };
19
+ const store = await stateStore(ctx, 'auth.json');
20
+ const saved = store.read({ hosts: {} });
21
+ return { ...DEFAULT_MAP, ...saved.hosts };
22
+ }
23
+
24
+ /** Append config to the caller's environment. No credential enters argv or env. */
25
+ export async function authGitEnv(ctx) {
26
+ const env = { ...ctx.env };
27
+ let index = Number(env.GIT_CONFIG_COUNT || 0);
28
+ if (!Number.isSafeInteger(index) || index < 0 || index > 10000) throw new Error('Invalid GIT_CONFIG_COUNT');
29
+ for (const host of Object.keys(await mappings(ctx))) {
30
+ hostName(host);
31
+ for (const value of ['', HELPER]) {
32
+ env[`GIT_CONFIG_KEY_${index}`] = `credential.https://${host}.helper`;
33
+ env[`GIT_CONFIG_VALUE_${index}`] = value;
34
+ index += 1;
35
+ }
36
+ }
37
+ env.GIT_CONFIG_COUNT = String(index);
38
+ return env;
39
+ }
40
+
41
+ // The current dispatcher creates a fresh ctx for after(). Restore only our config keys.
42
+ const pending = new WeakMap();
43
+ export const guards = [{
44
+ name: 'auth-store',
45
+ async before(_args, ctx) {
46
+ try {
47
+ const next = await authGitEnv(ctx);
48
+ const previous = new Map();
49
+ for (const [key, value] of Object.entries(next)) {
50
+ if (key === 'GIT_CONFIG_COUNT' || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(key)) {
51
+ previous.set(key, ctx.env[key]);
52
+ ctx.env[key] = value;
53
+ }
54
+ }
55
+ const stack = pending.get(ctx.env) || [];
56
+ stack.push(previous);
57
+ pending.set(ctx.env, stack);
58
+ } catch { return { refuse: 'Credential helper configuration is unavailable' }; }
59
+ },
60
+ after(_args, ctx) {
61
+ for (const [key, value] of pending.get(ctx.env)?.pop() || []) {
62
+ if (value === undefined) delete ctx.env[key]; else ctx.env[key] = value;
63
+ }
64
+ },
65
+ }];
66
+
67
+ async function credential(args, ctx) {
68
+ // Unsupported actions are silent, as the Git protocol requires.
69
+ if (args.length !== 1 || args[0] !== 'get') return 0;
70
+ try {
71
+ const input = readFileSync(0, 'utf8');
72
+ if (input.length > 65536) throw new Error('Invalid credential request');
73
+ const attributes = {};
74
+ for (const line of input.split(/\r?\n/)) {
75
+ if (!line) break;
76
+ const at = line.indexOf('=');
77
+ if (at > 0) attributes[line.slice(0, at)] = line.slice(at + 1);
78
+ }
79
+ if (attributes.protocol !== 'https' || !attributes.host) return 0;
80
+ const hosts = await mappings(ctx);
81
+ const host = hostName(attributes.host);
82
+ if (!Object.hasOwn(hosts, host)) return 0;
83
+ const key = hosts[host];
84
+ const result = jsonResult(tr(['auth', 'secret', 'get', key, '--json'], ctx), 'Auth store is unavailable');
85
+ if (!result.found || typeof result.value !== 'string' || !result.value || /[\r\n\0]/.test(result.value)) {
86
+ throw new Error('Auth key is unavailable');
87
+ }
88
+ if (ctx.env.GIT_PLUGIN_AUTH_PROBE_ID) {
89
+ const probes = await stateStore(ctx, 'auth-probes.jsonl');
90
+ probes.append({ probe: ctx.env.GIT_PLUGIN_AUTH_PROBE_ID, host, key, timestamp: new Date().toISOString() });
91
+ }
92
+ // This is the only credential output. Git reads this pipe.
93
+ process.stdout.write(`username=x-access-token\npassword=${result.value}\nquit=true\n\n`);
94
+ return 0;
95
+ } catch {
96
+ process.stdout.write('quit=true\n\n');
97
+ console.error('Git credential is unavailable from the auth store');
98
+ return 1;
99
+ }
100
+ }
101
+
102
+ async function remoteCheck(ctx) {
103
+ const remote = ctx.env.GIT_PLUGIN_AUTH_REMOTE || ctx.git(['remote', 'get-url', 'origin']).stdout?.trim();
104
+ let url;
105
+ try { url = new URL(remote); } catch { throw new Error('Set an HTTPS origin for the private repository to verify'); }
106
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
107
+ throw new Error('The verification remote must be HTTPS without credentials');
108
+ }
109
+ const hosts = await mappings(ctx);
110
+ const key = Object.hasOwn(hosts, url.host.toLowerCase()) ? hosts[url.host.toLowerCase()] : null;
111
+ if (!key) throw new Error('No auth key is mapped to the verification host');
112
+ const listed = jsonResult(tr(['auth', 'secret', 'list', '--json'], ctx), 'Cannot read auth key metadata');
113
+ if (!Array.isArray(listed.keys) || !listed.keys.some(row => row.key === key || row.name === key)) {
114
+ throw new Error('The mapped auth key is not present');
115
+ }
116
+ const probe = randomUUID();
117
+ const env = { ...await authGitEnv(ctx), GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never', GIT_PLUGIN_AUTH_PROBE_ID: probe };
118
+ const live = ctx.git(['ls-remote', '--', url.href], { env, timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] });
119
+ const probes = await stateStore(ctx, 'auth-probes.jsonl');
120
+ const credentialUsed = probes.readTail(1000).some(row => row.probe === probe && row.host === url.host && row.key === key);
121
+ const remoteRead = live.status === 0 && !live.error && !live.signal;
122
+ return { host: url.host, key, key_present: true, remote_read: remoteRead, credential_used: credentialUsed, verified: remoteRead && credentialUsed };
123
+ }
124
+
125
+ async function setup(ctx) {
126
+ const config = 'credential.https://github.com.helper';
127
+ const previous = ctx.git(['config', '--global', '--get-all', config]);
128
+ if (![0, 1].includes(previous.status)) throw new Error('Cannot read global credential helpers');
129
+ const old = previous.status === 0 ? previous.stdout.trimEnd().split('\n') : [];
130
+ const set = args => {
131
+ if (ctx.git(['config', '--global', ...args]).status !== 0) throw new Error('Cannot write global credential helpers');
132
+ };
133
+ try {
134
+ set(['--replace-all', config, '']);
135
+ set(['--add', config, HELPER]);
136
+ const result = await remoteCheck(ctx);
137
+ if (!result.verified) throw new Error('Auth-store sign-in was not verified; plaintext credentials were kept. Use a private HTTPS origin');
138
+ if (result.host !== 'github.com') throw new Error('Setup must verify an HTTPS github.com private repository');
139
+ // Remove only gh helper values. Keep unrelated host and global helpers.
140
+ const helpers = ctx.git(['config', '--global', '--get-regexp', String.raw`^credential\..*helper$`]);
141
+ if (![0, 1].includes(helpers.status)) throw new Error('Cannot read legacy helper entries');
142
+ for (const line of (helpers.stdout || '').split('\n')) {
143
+ const match = line.match(/^(\S+)\s+(.+)$/);
144
+ if (match && /(?:^|[\s/!])gh\s+auth\s+git-credential(?:\s|$)/.test(match[2])) {
145
+ set(['--fixed-value', '--unset-all', match[1], match[2]]);
146
+ }
147
+ }
148
+ const file = path.join(ctx.env.HOME || os.homedir(), '.git-credentials');
149
+ if (existsSync(file)) {
150
+ console.error(`Removing verified plaintext credential file: ${file}`);
151
+ unlinkSync(file);
152
+ }
153
+ return { ...result, helper_wired: true, setup: true };
154
+ } catch (error) {
155
+ const cleared = ctx.git(['config', '--global', '--unset-all', config]);
156
+ let restored = [0, 5].includes(cleared.status);
157
+ for (const value of old) restored = ctx.git(['config', '--global', '--add', config, value]).status === 0 && restored;
158
+ if (!restored) throw new Error('Setup failed; global credential helper restoration failed');
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ export const operations = {
164
+ 'credential-helper': {
165
+ summary: 'Read a Git credential from the auth store',
166
+ usage: 'credential-helper get|store|erase',
167
+ run: credential,
168
+ },
169
+ auth: {
170
+ summary: 'Verify sign-in or configure the auth-store helper',
171
+ usage: 'auth status | auth map <host> <KEY> | auth setup',
172
+ async run(args, ctx) {
173
+ try {
174
+ const [action = 'status', host, key] = args;
175
+ if (action === 'map' && args.length === 3) {
176
+ const normalized = hostName(host);
177
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key)) throw new Error('Supply an auth key name');
178
+ const store = await stateStore(ctx, 'auth.json');
179
+ const saved = store.read({ hosts: {} });
180
+ store.write({ ...saved, hosts: { ...saved.hosts, [normalized]: key } });
181
+ emit(ctx, { host: normalized, key }, `Mapped ${normalized} to ${key}`);
182
+ return 0;
183
+ }
184
+ if (args.length > 1 || !['status', 'setup'].includes(action)) throw new Error('Use auth status, auth map <host> <KEY>, or auth setup');
185
+ const result = action === 'setup' ? await setup(ctx) : await remoteCheck(ctx);
186
+ if (action === 'status') {
187
+ const row = ctx.git(['config', '--get-all', `credential.https://${result.host}.helper`]);
188
+ const helpers = row.status === 0 ? row.stdout.trimEnd().split('\n') : [];
189
+ result.helper_wired = helpers.at(-1) === HELPER && helpers.includes('');
190
+ }
191
+ emit(ctx, result, `${result.host}: auth key ${result.key} is present; live remote read ${result.remote_read ? 'passed' : 'failed'}; auth-store sign-in ${result.verified ? 'verified' : 'not verified (use a private HTTPS origin)'}`);
192
+ return result.verified ? 0 : 1;
193
+ } catch (error) {
194
+ // All errors here are fixed messages. Never forward subprocess output.
195
+ const reason = error.message.startsWith('Plugin state') || error.message.startsWith('SSSS') ? 'Plugin state is unavailable' : error.message;
196
+ emit(ctx, { ok: false, error: reason }, `Auth failed: ${reason}`);
197
+ return 1;
198
+ }
199
+ },
200
+ },
201
+ };
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { git, tr, operation, output } from '../lib/context.mjs';
4
+
5
+ // Git treats 1..127 (except 125) as a bad revision. Abort on runner failures.
6
+ const RUNNER = `const {spawnSync}=require('node:child_process');
7
+ const r=spawnSync(process.env.GIT_PLUGIN_TR_BIN||'total-recall',['code-quality','check','--json'],{encoding:'utf8',shell:false,maxBuffer:16777216});
8
+ let data;try{data=JSON.parse(r.stdout)}catch{}
9
+ const valid=data&&typeof data.ok==='boolean';
10
+ process.exitCode=r.error||!valid||![0,1,125].includes(r.status)?128:r.status===125?125:r.status===0&&data.ok?0:1;`;
11
+
12
+ export const operations = {
13
+ 'bisect-agent': operation('Find the first bad commit with the repository quality gate', 'bisect-agent <good> <bad> [--json]', async (args, ctx) => {
14
+ if (args.length !== 2 || args.some(arg => arg.startsWith('-'))) throw new Error('Supply good and bad revisions');
15
+ if (git(ctx, ['status', '--porcelain=v1', '-z']).length) throw new Error('Bisect requires a clean tree');
16
+ const bisectStart = git(ctx, ['rev-parse', '--git-path', 'BISECT_START']).trim();
17
+ if (fs.existsSync(path.resolve(ctx.cwd, bisectStart))) throw new Error('A bisect session is already active');
18
+ const refs = args.map(ref => git(ctx, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]).trim());
19
+ if (refs[0] === refs[1]) throw new Error('Good and bad revisions must differ');
20
+ git(ctx, ['merge-base', '--is-ancestor', ...refs]);
21
+ // Probe the contract before changing HEAD. A failing gate is a valid probe.
22
+ const probe = tr(ctx, ['code-quality', 'check', '--json']);
23
+ let report;
24
+ try { report = JSON.parse(probe.stdout); } catch { /* invalid below */ }
25
+ if (probe.error || ![0, 1].includes(probe.status) || typeof report?.ok !== 'boolean') throw new Error('Repository code-quality check is unavailable or returned an invalid report');
26
+ let started = false;
27
+ try {
28
+ git(ctx, ['bisect', 'start', refs[1], refs[0]]);
29
+ started = true;
30
+ const result = ctx.git(['bisect', 'run', process.execPath, '-e', RUNNER], { env: ctx.env, timeout: 30 * 60 * 1000 });
31
+ if ((result.status ?? result.code) !== 0 || result.error) throw new Error('Bisect did not identify one bad commit. The gate failed, aborted or skipped revisions.');
32
+ const firstBad = git(ctx, ['rev-parse', '--verify', 'refs/bisect/bad']).trim();
33
+ const message = git(ctx, ['show', '-s', '--format=%s', firstBad]).trim();
34
+ output(ctx, { ok: true, firstBad, message, gate: 'total-recall code-quality check --json' }, `First bad commit: ${firstBad}\n${message}`);
35
+ return 0;
36
+ } finally {
37
+ if (started) git(ctx, ['bisect', 'reset']);
38
+ }
39
+ }),
40
+ };
@@ -0,0 +1,159 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { emit, jsonResult, stateStore, tr } from '../lib/runtime.mjs';
6
+ import { authGitEnv } from './auth.mjs';
7
+
8
+ const UPSTREAM = 'https://github.com/git/git';
9
+ const DAY = 24 * 60 * 60 * 1000;
10
+ const compare = (a, b) => {
11
+ const left = a.split('.').map(Number), right = b.split('.').map(Number);
12
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] - right[i];
13
+ return 0;
14
+ };
15
+
16
+ export function executable(name, ctx) {
17
+ for (const dir of (ctx.env.PATH || '').split(path.delimiter)) {
18
+ const file = path.resolve(ctx.cwd, dir, name);
19
+ try { accessSync(file, constants.X_OK); if (statSync(file).isFile()) return file; } catch { /* try next PATH entry */ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function tool(name, args, ctx, timeout = 15000) {
25
+ const file = executable(name, ctx);
26
+ if (!file) return { status: null };
27
+ return spawnSync(file, args, { cwd: ctx.cwd, env: ctx.env, encoding: 'utf8',
28
+ stdio: ['ignore', 'pipe', 'pipe'], timeout, maxBuffer: 16 * 1024 * 1024, shell: false });
29
+ }
30
+
31
+ function installSource(file, build, ctx) {
32
+ if (/[/\\](?:Cellar|Homebrew|linuxbrew)[/\\]/i.test(file)) return 'brew';
33
+ const brew = tool('brew', ['--prefix'], ctx);
34
+ if (brew.status === 0 && brew.stdout?.trim()) {
35
+ try { if (file.startsWith(`${realpathSync(brew.stdout.trim())}${path.sep}`)) return 'brew'; } catch { /* invalid prefix */ }
36
+ }
37
+ const apt = tool('dpkg-query', ['-S', file], ctx);
38
+ if (apt.status === 0 && /^git(?::[^:\s]+)?:\s/m.test(apt.stdout || '')) return 'apt';
39
+ if (file === '/usr/bin/git' && /Apple Git/i.test(build)) return 'apple-clt';
40
+ return 'other';
41
+ }
42
+
43
+ async function upstream(ctx, force) {
44
+ const store = await stateStore(ctx, 'upstream.json');
45
+ const cached = store.read(null);
46
+ const age = Date.now() - Date.parse(cached?.checked_at);
47
+ if (!force && age >= 0 && age < DAY && Array.isArray(cached?.versions) && cached.versions.length &&
48
+ cached.versions.every(v => /^\d+\.\d+\.\d+$/.test(v))) return { ...cached, cached: true, stale: false };
49
+ const result = ctx.git(['ls-remote', '--tags', '--refs', UPSTREAM], {
50
+ env: { ...await authGitEnv(ctx), GIT_TERMINAL_PROMPT: '0' }, timeout: 30000,
51
+ });
52
+ const versions = [...new Set((result.stdout || '').split('\n').flatMap(line => {
53
+ const match = line.match(/^[0-9a-f]+\s+refs\/tags\/v(\d+\.\d+\.\d+)$/);
54
+ return match ? [match[1]] : [];
55
+ }))].sort(compare);
56
+ if (result.status !== 0 || result.error || !versions.length) {
57
+ return { versions: [], checked_at: cached?.checked_at ?? null, cached: false, stale: true, error: 'Cannot read stable upstream Git releases' };
58
+ }
59
+ const value = { versions, checked_at: new Date().toISOString() };
60
+ store.write(value);
61
+ return { ...value, cached: false, stale: false };
62
+ }
63
+
64
+ export async function collectCli(ctx, { force = false } = {}) {
65
+ const command = executable('git', ctx);
66
+ if (!command) throw new Error('Git executable was not found in PATH');
67
+ const resolved = realpathSync(command);
68
+ const versionRow = ctx.git(['--version']);
69
+ const build = ctx.git(['version', '--build-options']);
70
+ const version = versionRow.status === 0 && versionRow.stdout?.match(/^git version (\d+\.\d+\.\d+)(?:\D.*)?\s*$/)?.[1];
71
+ if (!version || build.status !== 0) throw new Error('Cannot read the Git version and build options');
72
+ const releases = await upstream(ctx, force);
73
+ const latest = releases.versions.at(-1) ?? null;
74
+ const hostname = os.hostname();
75
+ const report = {
76
+ version, resolved_path: resolved, install_source: installSource(resolved, versionRow.stdout, ctx),
77
+ build_options: build.stdout.trimEnd(), latest_upstream: latest,
78
+ release_notes_url: latest ? `${UPSTREAM}/blob/master/Documentation/RelNotes/${latest}.adoc` : null,
79
+ versions_behind: latest ? releases.versions.filter(v => compare(v, version) > 0).length : null,
80
+ checked_at: new Date().toISOString(), hostname,
81
+ upstream_checked_at: releases.checked_at, upstream_cached: releases.cached, upstream_stale: releases.stale,
82
+ ...(releases.error ? { error: releases.error } : {}),
83
+ };
84
+ const id = Buffer.from(hostname).toString('hex');
85
+ const store = await stateStore(ctx, `cli-${id}.json`);
86
+ store.read(null);
87
+ store.write(report);
88
+ const events = await stateStore(ctx, 'cli-events.jsonl');
89
+ events.append({ type: 'git.cli_checked', timestamp: report.checked_at, ...report });
90
+ return report;
91
+ }
92
+
93
+ async function allNodes(ctx) {
94
+ const inventory = jsonResult(tr(['mesh', 'nodes', '--json'], ctx), 'Cannot read mesh nodes');
95
+ if (!Array.isArray(inventory.nodes) || !inventory.nodes.length) throw new Error('Mesh returned no nodes');
96
+ const nodes = [];
97
+ for (const node of inventory.nodes) {
98
+ const name = node.name || node.hostname || node.id;
99
+ if (typeof name !== 'string' || !name || name.startsWith('-')) {
100
+ nodes.push({ ok: false, error: 'Mesh node has no usable name' }); continue;
101
+ }
102
+ try {
103
+ const result = jsonResult(tr(['mesh', 'exec', name, 'total-recall git cli --json'], ctx), 'Node CLI check failed');
104
+ if (!result.version || !result.hostname || result.error || result.upstream_stale) throw new Error('Node CLI metadata is incomplete');
105
+ nodes.push({ node: name, ok: true, ...result });
106
+ } catch { nodes.push({ node: name, ok: false, error: 'Node CLI check failed' }); }
107
+ }
108
+ return { ok: nodes.every(node => node.ok), nodes };
109
+ }
110
+
111
+ async function update(ctx) {
112
+ const before = await collectCli(ctx, { force: true });
113
+ if (before.error) throw new Error(before.error);
114
+ if (before.versions_behind === 0) return { ok: true, updated: false, cli: before };
115
+ let result;
116
+ switch (before.install_source) {
117
+ case 'brew': result = tool('brew', ['upgrade', 'git'], ctx, 15 * 60 * 1000); break;
118
+ case 'apt': result = tool('apt-get', ['install', '--only-upgrade', '-y', 'git'], ctx, 15 * 60 * 1000); break;
119
+ case 'apple-clt':
120
+ if (!executable('brew', ctx)) throw new Error('Apple Command Line Tools Git needs Homebrew; no installer is available');
121
+ result = tool('brew', ['install', 'git'], ctx, 15 * 60 * 1000); break;
122
+ default: throw new Error('Git install source is other; no supported updater is available');
123
+ }
124
+ if (result.status !== 0 || result.error || result.signal) throw new Error('The Git installer failed; check installer permissions and configured sources');
125
+ const after = await collectCli(ctx);
126
+ if (after.error || after.versions_behind !== 0) throw new Error('Git is still behind upstream; the configured source or PATH has no current Git');
127
+ return { ok: true, updated: after.version !== before.version, previous_version: before.version, cli: after };
128
+ }
129
+
130
+ export const operations = {
131
+ cli: {
132
+ summary: 'Record Git CLI metadata or update Git through its installer',
133
+ usage: 'cli [--json] [--all-nodes] | cli update [--scheduled]',
134
+ async run(args, ctx) {
135
+ const json = ctx.json || args.includes('--json');
136
+ ctx = { ...ctx, json };
137
+ const rest = args.filter(a => a !== '--json');
138
+ const isUpdate = rest[0] === 'update';
139
+ try {
140
+ let report;
141
+ if (!rest.length) report = await collectCli(ctx);
142
+ else if (rest.length === 1 && rest[0] === '--all-nodes') report = await allNodes(ctx);
143
+ else if (isUpdate && (rest.length === 1 || (rest.length === 2 && rest[1] === '--scheduled'))) report = await update(ctx);
144
+ else throw new Error('Use cli [--all-nodes] or cli update [--scheduled]');
145
+ emit(ctx, report, JSON.stringify(report, null, 2));
146
+ return report.error || report.ok === false ? 1 : 0;
147
+ } catch (error) {
148
+ const reason = error.message;
149
+ emit(ctx, { ok: false, error: reason, checked_at: new Date().toISOString(), hostname: os.hostname() }, `Git CLI check failed: ${reason}`);
150
+ if (isUpdate) {
151
+ // Fixed message only. Installer and service output can contain private data.
152
+ const alert = tr(['alerts', 'send', '--title', 'Git update failed', '--message', 'Git remains out of date. Run total-recall git cli --json for details.'], ctx);
153
+ if (alert.status !== 0) console.error('The operator alert could not be delivered');
154
+ }
155
+ return 1;
156
+ }
157
+ },
158
+ },
159
+ };
@@ -0,0 +1,30 @@
1
+ import { git, tr, ledger, intent, session } from '../lib/context.mjs';
2
+
3
+ export const guards = [{
4
+ name: 'commit-memory',
5
+ before() {},
6
+ async after(args, ctx, status) {
7
+ if (args[0] !== 'commit' || status !== 0 || args.some(arg => ['--dry-run', '--short', '--porcelain', '--long'].includes(arg))) return;
8
+ const sha = git(ctx, ['rev-parse', '--verify', 'HEAD']).trim();
9
+ const store = await ledger(ctx, 'commits', true);
10
+ if (store.read([]).some(row => row.sha === sha)) return;
11
+ const message = git(ctx, ['show', '-s', '--format=%B', sha]).trimEnd();
12
+ const files = git(ctx, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-m', '-z', sha]).split('\0').filter(Boolean);
13
+ const record = { type: 'git.commit_context', sha, message, ...intent(message), files: [...new Set(files)], timestamp: new Date().toISOString() };
14
+ record.session = session(ctx.env) || record.session;
15
+ // Save the commit before any optional provider call.
16
+ store.append(record, { idempotencyKey: `commit-${sha}` });
17
+ const memory = record.decisions.length ? tr(ctx, ['remember', 'decision', `${record.decisions.join('\n')}\nCommit: ${sha}`, '--project', '--slug', `git-decision-${sha}`]) : null;
18
+ const available = tr(ctx, ['openwiki', '--help']);
19
+ let wiki;
20
+ if (available.status !== 0 || available.error) wiki = { status: 'skipped', reason: 'OpenWiki is unavailable' };
21
+ else {
22
+ // The current OpenWiki CLI accepts a refresh request, but has no queue verb.
23
+ const refresh = tr(ctx, ['openwiki', 'refresh', '--repo', ctx.cwd, '--apply', '--json']);
24
+ wiki = { status: refresh.status === 0 && !refresh.error ? 'requested' : 'failed', command: 'openwiki refresh' };
25
+ }
26
+ store.append({ type: 'git.commit_memory', sha, timestamp: new Date().toISOString(), memory: memory === null ? 'skipped: no named decision' : memory.status === 0 && !memory.error ? 'recorded' : 'failed', openwiki: wiki });
27
+ if (memory && (memory.status !== 0 || memory.error)) console.error('Commit saved. Repository decision memory failed.');
28
+ if (wiki.status === 'failed') console.error('Commit saved. OpenWiki refresh failed.');
29
+ },
30
+ }];
@@ -0,0 +1,46 @@
1
+ import { authGitEnv } from './auth.mjs';
2
+
3
+ const machineEmail = /@(?:localhost|[^@\s]+\.(?:local|lan|home|internal))$|@[^@.\s]+$/i;
4
+
5
+ export function checkCommit(ctx) {
6
+ for (const key of ['user.name', 'user.email']) {
7
+ const row = ctx.git(['config', '--get', key]);
8
+ if (row.status !== 0 || !row.stdout?.trim()) return `${key} is not configured or cannot be read`;
9
+ if (key === 'user.email' && machineEmail.test(row.stdout.trim())) return 'user.email names a machine';
10
+ }
11
+ // Check the effective identity too. Environment variables can override config.
12
+ for (const role of ['AUTHOR', 'COMMITTER']) {
13
+ const identity = ctx.git(['var', `GIT_${role}_IDENT`]);
14
+ if (identity.status !== 0) return `Cannot read the ${role.toLowerCase()} identity`;
15
+ const match = identity.stdout?.match(/^(.+) <([^<>]+)> \d+ [+-]\d+\s*$/);
16
+ if (!match || !match[1].trim()) return `Missing ${role.toLowerCase()} identity`;
17
+ if (machineEmail.test(match[2])) return `${role.toLowerCase()} email names a machine`;
18
+ }
19
+ const head = ctx.git(['symbolic-ref', '-q', 'HEAD']);
20
+ if (head.status === 1) return 'Refusing to commit on a detached HEAD';
21
+ if (head.status !== 0) return 'Cannot read HEAD';
22
+ const conflicts = ctx.git(['ls-files', '--unmerged', '-z']);
23
+ if (conflicts.status !== 0) return 'Cannot read unresolved conflicts';
24
+ if (conflicts.stdout) return 'Refusing to commit with unresolved conflicts';
25
+ }
26
+
27
+ export const operations = {
28
+ commit: {
29
+ summary: 'Check identity, conflicts and HEAD, then run git commit',
30
+ usage: 'commit [git commit arguments]',
31
+ async run(args, ctx) {
32
+ try {
33
+ const reason = checkCommit(ctx);
34
+ if (reason) { console.error(`Commit refused: ${reason}`); return 1; }
35
+ const env = await authGitEnv(ctx);
36
+ // Keep the editor, stdin, hooks and Git output attached to the terminal.
37
+ const result = ctx.git(['commit', ...args], { env, stdio: 'inherit' });
38
+ if (result.error || result.signal) return 1;
39
+ return result.status ?? 1;
40
+ } catch {
41
+ console.error('Commit refused: credential helper configuration is unavailable');
42
+ return 1;
43
+ }
44
+ },
45
+ },
46
+ };
@@ -0,0 +1,156 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { failure, gitText, output, trJson } from './_support.mjs';
5
+
6
+ const STALE_MS = 30 * 24 * 60 * 60 * 1000;
7
+
8
+ export function registeredRepositories(ctx) {
9
+ let data;
10
+ try { data = trJson(ctx, ['brain', 'list', '--json']); } catch { /* Older brain list has no JSON mode. */ }
11
+ const normalize = value => {
12
+ const rows = Array.isArray(value) ? value : value?.projects || value?.brains || value?.repositories;
13
+ if (!Array.isArray(rows)) return null;
14
+ const paths = rows.map(row => typeof row === 'string' ? row : row?.path || row?.root || row?.projectPath);
15
+ if (paths.some(p => typeof p !== 'string' || !path.isAbsolute(p))) throw new Error('Project registry contains an invalid repository path');
16
+ return [...new Set(paths)];
17
+ };
18
+ const rows = normalize(data);
19
+ if (rows) return rows;
20
+ const file = ctx.env?.GIT_PLUGIN_REGISTRY_FILE || path.join(ctx.env?.TR_GLOBAL_BRAIN || ctx.env?.TR_SECRETS_BRAIN || path.join(os.homedir(), '.agent', 'skills', 'total-recall'), 'config', 'project-registry.json');
21
+ const fallback = normalize(JSON.parse(readFileSync(file, 'utf8')));
22
+ if (!fallback) throw new Error('Project registry has no repository list');
23
+ return fallback;
24
+ }
25
+
26
+ export function inspectRepository(directory, ctx, now = Date.now()) {
27
+ const local = { ...ctx, git: (args, options) => ctx.git(['-C', directory, ...args], options) };
28
+ const errors = [];
29
+ const report = { path: directory, dirty: null, unpushed: null, behind: null, diverged: null,
30
+ staleStashes: null, abandonedWorktrees: null, upstream: null, errors };
31
+ const get = args => {
32
+ try { return gitText(local, args); } catch { errors.push(args[0]); return null; }
33
+ };
34
+ if (get(['rev-parse', '--is-inside-work-tree'])?.trim() !== 'true') return { ...report, error: 'Repository is unavailable' };
35
+ const status = get(['status', '--porcelain=v1', '-z']);
36
+ if (status !== null) report.dirty = status.length > 0;
37
+ const upstream = local.git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
38
+ if (upstream.status === 0) {
39
+ report.upstream = upstream.stdout.trim();
40
+ const counts = get(['rev-list', '--left-right', '--count', '@{upstream}...HEAD'])?.trim().match(/^(\d+)\s+(\d+)$/);
41
+ if (counts) { report.behind = Number(counts[1]); report.unpushed = Number(counts[2]); report.diverged = report.behind > 0 && report.unpushed > 0; }
42
+ } else {
43
+ // No upstream is unknown sync state, not zero unpushed commits.
44
+ report.upstreamStatus = 'unavailable';
45
+ if (!/no upstream configured|no such branch|does not point to a branch|unknown revision/i.test(upstream.stderr || '')) errors.push('upstream');
46
+ }
47
+ const stashes = get(['stash', 'list', '--format=%H%x00%ct%x00%gs']);
48
+ if (stashes !== null) report.staleStashes = stashes.split('\n').filter(Boolean).flatMap(row => {
49
+ const [commit, timestamp, message] = row.split('\0');
50
+ return Number(timestamp) * 1000 < now - STALE_MS ? [{ commit, timestamp: Number(timestamp), message }] : [];
51
+ });
52
+ const trees = get(['worktree', 'list', '--porcelain', '-z']);
53
+ if (trees !== null) report.abandonedWorktrees = trees.split('\0\0').filter(Boolean).slice(1).flatMap(record => {
54
+ const fields = Object.fromEntries(record.split('\0').filter(Boolean).map(row => {
55
+ const i = row.indexOf(' '); return i < 0 ? [row, true] : [row.slice(0, i), row.slice(i + 1)];
56
+ }));
57
+ if (fields.locked) return [];
58
+ if (fields.prunable || !existsSync(fields.worktree)) return [{ path: fields.worktree, reason: 'missing-or-prunable' }];
59
+ const treeCtx = { ...ctx, git: (a, options) => ctx.git(['-C', fields.worktree, ...a], options) };
60
+ const state = treeCtx.git(['status', '--porcelain=v1', '-z']);
61
+ const last = treeCtx.git(['log', '-1', '--format=%ct']);
62
+ if (state.status !== 0 || last.status !== 0) { errors.push('worktree-inspection'); return []; }
63
+ return !state.stdout && Number(last.stdout.trim()) * 1000 < now - STALE_MS
64
+ ? [{ path: fields.worktree, reason: 'clean-with-old-head', candidate: true }] : [];
65
+ });
66
+ return report;
67
+ }
68
+
69
+ function containingCheckouts(directory, commit, ctx) {
70
+ const local = { ...ctx, git: (args, options) => ctx.git(['-C', directory, ...args], options) };
71
+ const oid = gitText(local, ['rev-parse', '--verify', '--end-of-options', `${commit}^{commit}`]).trim();
72
+ const refs = gitText(local, ['for-each-ref', `--contains=${oid}`, '--format=%(refname)', 'refs/heads', 'refs/remotes', 'refs/tags']).trim().split('\n').filter(Boolean);
73
+ const records = gitText(local, ['worktree', 'list', '--porcelain', '-z']).split('\0\0').filter(Boolean);
74
+ const checkouts = [];
75
+ for (const record of records) {
76
+ const file = record.split('\0').find(row => row.startsWith('worktree '))?.slice(9);
77
+ if (!file) continue;
78
+ const contains = ctx.git(['-C', file, 'merge-base', '--is-ancestor', oid, 'HEAD']);
79
+ if (contains.status === 0) checkouts.push(file);
80
+ else if (contains.status !== 1) throw new Error('Cannot inspect a registered checkout');
81
+ }
82
+ return { path: directory, commit: oid, objectPresent: true, refs, checkouts };
83
+ }
84
+
85
+ function localReport(ctx, commit) {
86
+ const errors = [];
87
+ const repositories = [];
88
+ for (const directory of registeredRepositories(ctx)) {
89
+ try {
90
+ if (commit) {
91
+ const contains = ctx.git(['-C', directory, 'cat-file', '-e', `${commit}^{commit}`]);
92
+ if (contains.status !== 0) {
93
+ if (ctx.git(['-C', directory, 'rev-parse', '--git-dir']).status !== 0) errors.push({ path: directory, error: 'Repository is unavailable' });
94
+ continue;
95
+ }
96
+ repositories.push(containingCheckouts(directory, commit, ctx));
97
+ } else {
98
+ const report = inspectRepository(directory, ctx);
99
+ repositories.push(report);
100
+ if (report.error || report.errors.length) errors.push({ path: directory, error: report.error || report.errors.join(', ') });
101
+ }
102
+ } catch { errors.push({ path: directory, error: 'Repository inspection failed' }); }
103
+ }
104
+ return { node: os.hostname(), repositories, errors, observedAt: new Date().toISOString(), syncSource: 'local refs; no fetch' };
105
+ }
106
+
107
+ function remoteReports(ctx, commit) {
108
+ const inventory = trJson(ctx, ['mesh', 'nodes', '--json']);
109
+ const rows = Array.isArray(inventory) ? inventory : inventory.nodes;
110
+ if (!Array.isArray(rows)) throw new Error('Mesh returned no node list');
111
+ const reports = [];
112
+ const seen = new Set();
113
+ for (const node of rows) {
114
+ const name = typeof node === 'string' ? node : node.hostname || node.name || node.id;
115
+ if (!name || seen.has(name) || name === os.hostname() || node.self || node.isLocal || node.isCurrentNode) continue;
116
+ seen.add(name);
117
+ try {
118
+ if (node.online === false || node.status === 'offline') throw new Error('Node is offline');
119
+ // Mesh joins words for remote execution. Quote the whole fixed command once.
120
+ const quote = word => `'${word.replaceAll("'", "'\\''")}'`;
121
+ const namespace = 'git';
122
+ const command = ['total-recall', namespace, commit ? 'where' : 'fleet', ...(commit ? [commit, '--local'] : []), '--json'].map(quote).join(' ');
123
+ const response = trJson(ctx, ['mesh', 'exec', name, command, '--json']);
124
+ if (response.exitCode !== 0 || typeof response.stdout !== 'string') throw new Error('Remote command failed');
125
+ const parsed = JSON.parse(response.stdout);
126
+ if (!Array.isArray(parsed.nodes) || typeof parsed.ok !== 'boolean') throw new Error('Remote report is invalid');
127
+ reports.push(...parsed.nodes);
128
+ } catch { reports.push({ node: name, repositories: [], errors: [{ error: 'Node is unavailable or returned no valid report' }] }); }
129
+ }
130
+ return reports;
131
+ }
132
+
133
+ async function run(args, ctx, where = false) {
134
+ try {
135
+ const flags = new Set(where ? ['--local', '--all-nodes'] : ['--all-nodes']);
136
+ const words = args.filter(a => !a.startsWith('-'));
137
+ if (args.some(a => a.startsWith('-') && !flags.has(a)) || words.length !== (where ? 1 : 0)) throw new Error(where ? 'Use where <commit> [--local]' : 'Use fleet [--all-nodes]');
138
+ const commit = words[0];
139
+ if (where && !/^[a-f0-9]{4,64}$/i.test(commit)) throw new Error('Supply a commit object id (4 to 64 hexadecimal characters)');
140
+ const nodes = [localReport(ctx, commit)];
141
+ if (args.includes('--all-nodes') || where && !args.includes('--local')) nodes.push(...remoteReports(ctx, commit));
142
+ const ok = nodes.every(node => !node.errors?.length);
143
+ const result = { ok, ...(where ? { commit } : {}), nodes };
144
+ const lines = nodes.flatMap(node => [node.node, ...node.repositories.map(repo => where
145
+ ? ` ${repo.path}: ${repo.checkouts.length} checkouts; ${repo.refs.length} refs`
146
+ : ` ${repo.path}: dirty=${repo.dirty ?? 'unknown'} unpushed=${repo.unpushed ?? 'unknown'} behind=${repo.behind ?? 'unknown'} diverged=${repo.diverged ?? 'unknown'} stale-stashes=${repo.staleStashes?.length ?? 'unknown'} abandoned-worktrees=${repo.abandonedWorktrees?.length ?? 'unknown'}`),
147
+ ...(node.errors || []).map(e => ` unavailable: ${e.path || node.node}`)]);
148
+ output(ctx, result, lines.join('\n'));
149
+ return ok ? 0 : 1;
150
+ } catch (error) { return failure(ctx, error); }
151
+ }
152
+
153
+ export const operations = {
154
+ fleet: { summary: 'Report repository state on this node or every mesh node', usage: 'fleet [--all-nodes] [--json]', run: (args, ctx) => run(args, ctx) },
155
+ where: { summary: 'Find machines and checkouts that contain a commit', usage: 'where <commit> [--local] [--json]', run: (args, ctx) => run(args, ctx, true) },
156
+ };