total-recall-brain 3.43.3 → 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 (252) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +16 -9
  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 +3 -3
  9. package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
  10. package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
  11. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
  12. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
  13. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
  14. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
  15. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
  16. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
  17. package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
  18. package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
  19. package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
  20. package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
  21. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
  22. package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
  23. package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
  33. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  39. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  40. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  55. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
  56. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
  57. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  58. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
  59. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
  60. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  61. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  63. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
  64. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
  65. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  87. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
  88. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
  89. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
  116. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
  117. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
  118. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  119. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
  121. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
  122. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  129. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  130. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  131. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  133. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
  134. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  135. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  136. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  137. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  138. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
  139. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
  140. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  141. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  142. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  143. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
  144. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
  145. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  146. package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
  147. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
  148. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
  149. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
  150. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  151. package/src/cli/agent.mjs +1 -1
  152. package/src/cli/command.mjs +41 -2
  153. package/src/cli/connect.mjs +2 -29
  154. package/src/cli/context.mjs +1 -0
  155. package/src/cli/dream.mjs +93 -69
  156. package/src/cli/harness.mjs +4 -4
  157. package/src/cli/help.mjs +1 -1
  158. package/src/cli/ingest-okf.mjs +4 -4
  159. package/src/cli/init.mjs +0 -25
  160. package/src/cli/lessons.mjs +51 -0
  161. package/src/cli/memory-plugin.mjs +1 -1
  162. package/src/cli/plugin/copies.mjs +64 -14
  163. package/src/cli/plugin/create.mjs +6 -7
  164. package/src/cli/plugin/index.mjs +11 -7
  165. package/src/cli/plugin/install.mjs +10 -15
  166. package/src/cli/plugin/remove.mjs +1 -1
  167. package/src/cli/plugin/search.mjs +2 -2
  168. package/src/cli/preflight.mjs +19 -6
  169. package/src/cli/proposals.mjs +1 -1
  170. package/src/cli/recall.mjs +66 -23
  171. package/src/cli/release.mjs +4 -1
  172. package/src/cli/remember.mjs +3 -14
  173. package/src/cli/server.mjs +18 -0
  174. package/src/cli/startup.mjs +1 -0
  175. package/src/cli/task.mjs +9 -2
  176. package/src/cli/update.mjs +10 -3
  177. package/src/core/access-ledger.mjs +78 -0
  178. package/src/core/agent-manager.mjs +2 -2
  179. package/src/core/automatic-brain-updates.mjs +11 -6
  180. package/src/core/brain-locate.mjs +15 -0
  181. package/src/core/brain-updates.mjs +42 -12
  182. package/src/core/claude-hooks.mjs +3 -1
  183. package/src/core/cleanup-task.mjs +15 -0
  184. package/src/core/cli-commands.mjs +2 -0
  185. package/src/core/cli-maintenance.mjs +4 -1
  186. package/src/core/cli-semantic-index.mjs +23 -3
  187. package/src/core/context-compactor.mjs +7 -2
  188. package/src/core/context-compiler.mjs +26 -1
  189. package/src/core/context-policy.mjs +7 -3
  190. package/src/core/daemon-control.mjs +22 -0
  191. package/src/core/daemon-loop.mjs +39 -62
  192. package/src/core/default-plugins.mjs +1 -1
  193. package/src/core/dream.mjs +33 -105
  194. package/src/core/ensure-dependencies.mjs +148 -0
  195. package/src/core/fast-recall.mjs +2 -2
  196. package/src/core/field-evaluation.mjs +23 -0
  197. package/src/core/gate-mode.mjs +1 -1
  198. package/src/core/global-brain-updates.mjs +1 -1
  199. package/src/core/harness-hooks.mjs +14 -2
  200. package/src/core/lesson-hooks.mjs +74 -0
  201. package/src/core/lesson-memory.mjs +24 -0
  202. package/src/core/lesson-sessions.mjs +52 -0
  203. package/src/core/lessons.mjs +96 -0
  204. package/src/core/mesh-brain-updates.mjs +2 -1
  205. package/src/core/meta-harness.mjs +2 -2
  206. package/src/core/optimizer.mjs +66 -165
  207. package/src/core/plugin-contracts/card.mjs +3 -0
  208. package/src/core/plugin-loader.mjs +12 -3
  209. package/src/core/plugin-runtime.mjs +40 -1
  210. package/src/core/plugin-store.mjs +35 -23
  211. package/src/core/post-mortem.mjs +52 -36
  212. package/src/core/preflight-gate.mjs +23 -2
  213. package/src/core/proposal-applier.mjs +31 -2
  214. package/src/core/repo-sync.mjs +4 -6
  215. package/src/core/required-plugins.mjs +26 -4
  216. package/src/core/rule-length.mjs +23 -0
  217. package/src/core/runtime.mjs +1 -1
  218. package/src/core/scheduler.mjs +35 -43
  219. package/src/core/schema.mjs +1 -2
  220. package/src/core/search.mjs +4 -3
  221. package/src/core/server-restart.mjs +128 -7
  222. package/src/core/server-service.mjs +148 -0
  223. package/src/core/session-watcher.mjs +19 -1
  224. package/src/core/skill-gap.mjs +16 -0
  225. package/src/core/ssss-kernel-bridge.mjs +33 -6
  226. package/src/core/startup-health.mjs +6 -1
  227. package/src/core/surface.mjs +68 -57
  228. package/src/core/task-context.mjs +9 -8
  229. package/src/core/task-executors.mjs +54 -106
  230. package/src/core/validated-write.mjs +5 -0
  231. package/src/core/vault-files.mjs +8 -2
  232. package/src/core/vault.mjs +1 -1
  233. package/src/core/vector-field.mjs +134 -192
  234. package/src/server/index.mjs +3 -1
  235. package/src/server/invoke-core-child.mjs +24 -0
  236. package/src/server/invoke-core-operations.mjs +102 -0
  237. package/src/server/route-manifest.json +0 -56
  238. package/src/server/routes/brains.mjs +0 -18
  239. package/src/server/routes/context.mjs +0 -41
  240. package/src/server/routes/invoke.mjs +26 -1
  241. package/src/server/routes/ssss.mjs +0 -169
  242. package/src/server/routes/system.mjs +0 -15
  243. package/src/server/routes/tasks.mjs +0 -26
  244. package/src/server/routes/vault.mjs +2 -23
  245. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  246. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  247. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  248. package/src/core/append-log.mjs +0 -131
  249. package/src/core/plugin-tasks.mjs +0 -173
  250. package/templates/onboarding-interview.md +0 -82
  251. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  252. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ /** The host owns the state path and the SSSS implementation. */
10
+ export async function stateStore(ctx, name) {
11
+ const dir = ctx.env.TR_PLUGIN_STATE_DIR;
12
+ if (!dir || !path.isAbsolute(dir)) throw new Error('TR_PLUGIN_STATE_DIR must be supplied by the Total Recall host');
13
+ const root = ctx.env.TR_PACKAGE_ROOT;
14
+ let modulePath;
15
+ if (root) {
16
+ if (!path.isAbsolute(root) || JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') {
17
+ throw new Error('Invalid Total Recall state host');
18
+ }
19
+ modulePath = path.join(root, 'src/core/plugin-state-store.mjs');
20
+ } else modulePath = require.resolve('total-recall-brain/src/core/plugin-state-store.mjs');
21
+ const { PluginStateStore } = await import(pathToFileURL(modulePath).href);
22
+ return new PluginStateStore(path.join(dir, name), 'git');
23
+ }
24
+
25
+ /** Never forward service output or errors. Auth output can contain a secret. */
26
+ export function tr(args, ctx) {
27
+ return spawnSync(ctx.env.GIT_PLUGIN_TR_BIN || 'total-recall', args, {
28
+ cwd: ctx.cwd, env: ctx.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
29
+ timeout: 30000, maxBuffer: 16 * 1024 * 1024, shell: false,
30
+ });
31
+ }
32
+
33
+ export function jsonResult(result, reason) {
34
+ if (result.status !== 0 || result.error || result.signal) throw new Error(reason);
35
+ try { return JSON.parse(result.stdout); } catch { throw new Error(reason); }
36
+ }
37
+
38
+ export function emit(ctx, value, text) {
39
+ console.log(ctx.json ? JSON.stringify(value, null, 2) : text);
40
+ }
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export function fixture() {
8
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-plugin-spec-'));
9
+ const bin = path.join(dir, 'bin');
10
+ fs.mkdirSync(bin);
11
+ const calls = path.join(dir, 'calls.jsonl');
12
+ const writeBin = (name, source) => {
13
+ const file = path.join(bin, name);
14
+ fs.writeFileSync(file, `#!${process.execPath}\n${source}`, { mode: 0o700 });
15
+ return file;
16
+ };
17
+ const tr = writeBin('tr-stub', `
18
+ import fs from 'node:fs';
19
+ const args = process.argv.slice(2);
20
+ fs.appendFileSync(process.env.SPEC_CALLS, JSON.stringify(args) + '\\n');
21
+ if (args[0] === 'auth' && args[2] === 'get') {
22
+ if (process.env.SPEC_AUTH_FAIL) { console.error('fixture-sensitive-diagnostic'); process.exitCode = 1; }
23
+ else console.log(JSON.stringify({ found: !process.env.SPEC_AUTH_MISSING, value: process.env.SPEC_TOKEN || 'fixture-token' }));
24
+ } else if (args[0] === 'auth' && args[2] === 'list') {
25
+ console.log(JSON.stringify({ keys: [{ key: 'github_token' }, { key: 'GITLAB_TOKEN' }] }));
26
+ } else if (args[0] === 'mesh' && args[1] === 'nodes') {
27
+ console.log(JSON.stringify({ nodes: [{ name: 'alpha' }, { name: 'beta' }] }));
28
+ } else if (args[0] === 'mesh' && args[1] === 'exec') {
29
+ if (process.env.SPEC_NODE_FAIL && args[2] === 'beta') process.exitCode = 1;
30
+ else console.log(JSON.stringify({ version: '2.52.0', hostname: args[2], versions_behind: 0 }));
31
+ } else if (args[0] !== 'alerts') process.exitCode = 1;
32
+ `);
33
+ const env = { ...process.env, HOME: dir, GIT_CONFIG_GLOBAL: path.join(dir, 'gitconfig'),
34
+ GIT_CONFIG_NOSYSTEM: '1', TR_PLUGIN_STATE_DIR: path.join(dir, 'state'),
35
+ TR_PACKAGE_ROOT: fileURLToPath(new URL('../../node_modules/total-recall-brain', import.meta.url)),
36
+ GIT_PLUGIN_TR_BIN: tr, SPEC_CALLS: calls, PATH: `${bin}${path.delimiter}${process.env.PATH}`,
37
+ GIT_AUTHOR_NAME: 'Fixture Person', GIT_AUTHOR_EMAIL: 'fixture@example.com',
38
+ GIT_COMMITTER_NAME: 'Fixture Person', GIT_COMMITTER_EMAIL: 'fixture@example.com',
39
+ };
40
+ for (const key of Object.keys(env)) if (/^GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+)$/.test(key)) delete env[key];
41
+ return { dir, bin, env, writeBin,
42
+ calls: () => fs.existsSync(calls) ? fs.readFileSync(calls, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : [],
43
+ cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
44
+ };
45
+ }
46
+
47
+ export function realContext(f) {
48
+ return { cwd: f.dir, env: f.env, json: false,
49
+ git: (args, options = {}) => spawnSync('git', args, { cwd: f.dir, env: f.env, encoding: 'utf8', ...options }),
50
+ };
51
+ }
@@ -0,0 +1,195 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ export function helpOnly(args) {
10
+ const values = new Set(['-C', '-c', '-m', '-F', '-o', '--message', '--file', '--author', '--date',
11
+ '--reuse-message', '--reedit-message', '--fixup', '--squash', '--trailer', '--cleanup',
12
+ '--repo', '--receive-pack', '--exec', '--push-option', '--reason', '--git-dir', '--work-tree', '--namespace', '--config-env', '--pathspec-from-file']);
13
+ for (let i = 0; i < args.length; i += 1) {
14
+ if (args[i] === '--') return false;
15
+ if (values.has(args[i])) { i += 1; continue; }
16
+ if (args[i] === '--help' || args[i] === '-h') return true;
17
+ if (/^-[aneqvsiop]*[mFCc]$/.test(args[i])) i += 1;
18
+ }
19
+ return false;
20
+ }
21
+
22
+ /** Keep global git options attached to every inspection of the command. */
23
+ export function invocation(args, ctx) {
24
+ let i = 0;
25
+ while (i < args.length && args[i].startsWith('-')) {
26
+ if (['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env'].includes(args[i])) i += 2;
27
+ else i += 1;
28
+ }
29
+ const prefix = args.slice(0, i);
30
+ return { command: args[i], args: args.slice(i + 1), prefix,
31
+ ctx: { ...ctx, git: (tail, options) => ctx.git([...prefix, ...tail], options) } };
32
+ }
33
+
34
+ export function gitText(ctx, args, options) {
35
+ const row = ctx.git(args, options);
36
+ if (row.error || (row.status ?? row.code) !== 0) throw new Error(`Git ${args[0]} failed`);
37
+ return Buffer.isBuffer(row.stdout) ? row.stdout.toString('utf8') : String(row.stdout ?? '');
38
+ }
39
+
40
+ /** Do not forward child diagnostics: an auth failure can contain a credential. */
41
+ export function tr(ctx, args) {
42
+ const result = spawnSync(ctx.env?.GIT_PLUGIN_TR_BIN || 'total-recall', args, {
43
+ cwd: ctx.cwd, env: ctx.env, encoding: 'utf8', shell: false,
44
+ timeout: 30000, maxBuffer: 64 * 1024 * 1024,
45
+ });
46
+ if (result.error || result.status !== 0) throw new Error(`Total Recall ${args.slice(0, 3).join(' ')} failed`);
47
+ return result.stdout;
48
+ }
49
+ export function trJson(ctx, args) {
50
+ try { return JSON.parse(tr(ctx, args)); }
51
+ catch { throw new Error(`Total Recall ${args.slice(0, 3).join(' ')} returned no valid result`); }
52
+ }
53
+
54
+ export async function stateStore(ctx, name = 'events') {
55
+ const dir = ctx.env?.TR_PLUGIN_STATE_DIR;
56
+ if (!dir || !path.isAbsolute(dir)) throw new Error('TR_PLUGIN_STATE_DIR must be supplied by the Total Recall host');
57
+ const root = ctx.env?.TR_PACKAGE_ROOT;
58
+ let modulePath;
59
+ if (root) {
60
+ if (!path.isAbsolute(root) || JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') {
61
+ throw new Error('Invalid Total Recall state host');
62
+ }
63
+ modulePath = path.join(root, 'src/core/plugin-state-store.mjs');
64
+ } else modulePath = require.resolve('total-recall-brain/src/core/plugin-state-store.mjs');
65
+ const { PluginStateStore } = await import(pathToFileURL(modulePath).href);
66
+ return new PluginStateStore(path.join(dir, `${name}.jsonl`), 'git');
67
+ }
68
+
69
+ export function repoId(ctx) {
70
+ return gitText(ctx, ['rev-parse', '--path-format=absolute', '--git-common-dir']).trim();
71
+ }
72
+
73
+ /** Inspect the index git would commit. Never change the user's index here. */
74
+ export function prospectiveCommit(ctx, args) {
75
+ const paths = [];
76
+ const pathFlags = [];
77
+ const valueOptions = new Set(['--message', '--file', '--author', '--date', '--reuse-message', '--reedit-message', '--fixup', '--squash', '--trailer', '--cleanup']);
78
+ let all = false, include = false, only = false;
79
+ for (let i = 0; i < args.length; i += 1) {
80
+ const arg = args[i];
81
+ if (arg === '--') { paths.push(...args.slice(i + 1)); break; }
82
+ if (['--interactive', '--patch'].includes(arg)) throw new Error('Stage interactive changes before commit so the guards can inspect them');
83
+ if (arg === '--all') { all = true; continue; }
84
+ if (arg === '--include') { include = true; continue; }
85
+ if (arg === '--only') { only = true; continue; }
86
+ if (arg === '--pathspec-from-file') { pathFlags.push(arg, args[++i]); continue; }
87
+ if (arg.startsWith('--pathspec-from-file=') || arg === '--pathspec-file-nul') { pathFlags.push(arg); continue; }
88
+ if (valueOptions.has(arg)) { i += 1; continue; }
89
+ if (arg.startsWith('--')) continue;
90
+ if (arg.startsWith('-')) {
91
+ for (let j = 1; j < arg.length; j += 1) {
92
+ const flag = arg[j];
93
+ if ('mFCc'.includes(flag)) { if (j === arg.length - 1) i += 1; break; }
94
+ if (flag === 'a') all = true;
95
+ if (flag === 'i') include = true;
96
+ if (flag === 'o') only = true;
97
+ if (flag === 'p') throw new Error('Stage interactive changes before commit so the guards can inspect them');
98
+ // -S/-u take an optional attached value. It is not a flag cluster.
99
+ if ('Su'.includes(flag)) break;
100
+ }
101
+ } else paths.push(arg);
102
+ }
103
+ if (!all && !only && !include && !paths.length && !pathFlags.length) return { ctx, cleanup() {} };
104
+ if (pathFlags.some(a => a === '--pathspec-from-file=-' || a === '-')) throw new Error('Stage stdin pathspecs before commit');
105
+ const dir = mkdtempSync(path.join(os.tmpdir(), 'git-prospective-index-'));
106
+ const env = { ...ctx.env, GIT_INDEX_FILE: path.join(dir, 'index') };
107
+ const local = { ...ctx, git: (a, options = {}) => ctx.git(a, { ...options, env }) };
108
+ const cleanup = () => rmSync(dir, { recursive: true, force: true });
109
+ try {
110
+ const index = gitText(ctx, ['rev-parse', '--path-format=absolute', '--git-path', 'index']).trim();
111
+ if ((paths.length || pathFlags.length || only) && !include) {
112
+ const head = ctx.git(['rev-parse', '--verify', 'HEAD']);
113
+ gitText(local, ['read-tree', ...(head.status === 0 ? ['HEAD'] : ['--empty'])]);
114
+ } else if (existsSync(index)) copyFileSync(index, env.GIT_INDEX_FILE);
115
+ else gitText(local, ['read-tree', '--empty']);
116
+ if (all) gitText(local, ['add', '-u']);
117
+ if (paths.length || pathFlags.length) gitText(local, ['add', '--all', ...pathFlags, '--', ...paths]);
118
+ return { ctx: local, cleanup };
119
+ } catch (error) { cleanup(); throw error; }
120
+ }
121
+
122
+ /** Scan every local branch and tag that the selected remote does not contain.
123
+ * A missing remote object is not an exclusion. This can over-scan, never under-scan.
124
+ */
125
+ /**
126
+ * The commits this push would publish: exactly the refs git itself would update
127
+ * (`git push --dry-run --porcelain` with the same arguments), minus what the remote already has.
128
+ * Local branches the push does not send are never counted.
129
+ */
130
+ export function outgoingCommits(ctx, args = []) {
131
+ const dry = ctx.git(['push', '--dry-run', '--porcelain', ...args.filter(a => a !== '--porcelain' && a !== '--dry-run')]);
132
+ if (dry.error || ![0, 1].includes(dry.status)) return allUnpushedCommits(ctx, args);
133
+ const tips = [];
134
+ let remote = null;
135
+ for (const line of String(dry.stdout || '').split('\n')) {
136
+ if (line.startsWith('To ')) { remote = line.slice(3).trim(); continue; }
137
+ const m = line.match(/^([ +\-*=!])\t([^:\t]*):([^\t]+)\t/);
138
+ if (!m || m[1] === '=' || m[1] === '-' || m[1] === '!' || !m[2]) continue;
139
+ const resolved = ctx.git(['rev-parse', '--verify', '--end-of-options', `${m[2]}^{commit}`]);
140
+ if (resolved.status === 0) tips.push(resolved.stdout.trim());
141
+ }
142
+ if (!tips.length) return [];
143
+ const advertised = remote ? ctx.git(['ls-remote', '--', remote]) : { status: 1, stdout: '' };
144
+ const excluded = [...new Set(String(advertised.stdout || '').split('\n').map(line => line.split(/\s+/)[0]).filter(oid => /^[a-f0-9]{40,64}$/.test(oid)))]
145
+ .filter(oid => ctx.git(['cat-file', '-e', `${oid}^{commit}`]).status === 0);
146
+ return gitText(ctx, ['rev-list', ...new Set(tips), ...(excluded.length ? ['--not', ...excluded] : []), '--']).trim().split('\n').filter(Boolean);
147
+ }
148
+
149
+ /** Every unpushed local commit: the conservative superset used when git cannot say what a push sends. */
150
+ function allUnpushedCommits(ctx, args = []) {
151
+ const takesValue = new Set(['--repo', '--receive-pack', '--exec', '--push-option', '-o']);
152
+ const positional = [];
153
+ let remoteOption;
154
+ for (let i = 0; i < args.length; i += 1) {
155
+ const a = args[i];
156
+ if (takesValue.has(a)) { if (a === '--repo') remoteOption = args[i + 1]; i += 1; }
157
+ else if (a.startsWith('--repo=')) remoteOption = a.slice(7);
158
+ else if (a === '--') positional.push(...args.slice(i + 1));
159
+ else if (!a.startsWith('-')) positional.push(a);
160
+ if (a === '--') break;
161
+ }
162
+ const branch = ctx.git(['symbolic-ref', '--short', '-q', 'HEAD']);
163
+ const config = key => {
164
+ const result = ctx.git(['config', '--get', key]);
165
+ return result.status === 0 ? result.stdout.trim() : '';
166
+ };
167
+ const remote = remoteOption || positional[0] ||
168
+ config(`branch.${branch.stdout?.trim()}.pushRemote`) || config('remote.pushDefault') ||
169
+ config(`branch.${branch.stdout?.trim()}.remote`) || 'origin';
170
+ const advertised = gitText(ctx, ['ls-remote', '--', remote]);
171
+ const excluded = [...new Set(advertised.split('\n').map(line => line.split(/\s+/)[0]).filter(oid => /^[a-f0-9]{40,64}$/.test(oid)))].filter(oid =>
172
+ ctx.git(['cat-file', '-e', `${oid}^{commit}`]).status === 0);
173
+ const tips = gitText(ctx, ['for-each-ref', '--format=%(objectname)', 'refs/heads', 'refs/tags']).trim().split('\n').filter(Boolean);
174
+ const head = ctx.git(['rev-parse', '--verify', 'HEAD']);
175
+ if (head.status === 0) tips.push(head.stdout.trim());
176
+ // Explicit source expressions may name commits outside branches and tags.
177
+ for (const spec of positional.slice(remoteOption ? 0 : 1)) {
178
+ const source = spec.replace(/^\+/, '').split(':')[0];
179
+ if (!source || source.includes('*')) continue;
180
+ const result = ctx.git(['rev-parse', '--verify', '--end-of-options', `${source}^{commit}`]);
181
+ if (result.status === 0) tips.push(result.stdout.trim());
182
+ }
183
+ if (!tips.length) return [];
184
+ return gitText(ctx, ['rev-list', ...new Set(tips), ...(excluded.length ? ['--not', ...excluded] : []), '--']).trim().split('\n').filter(Boolean);
185
+ }
186
+
187
+ export function output(ctx, value, text) {
188
+ if (ctx.json) console.log(JSON.stringify(value, null, 2));
189
+ else console.log(text);
190
+ }
191
+ export function failure(ctx, error) {
192
+ if (ctx.json) console.log(JSON.stringify({ ok: false, error: error.message }));
193
+ else console.error(error.message);
194
+ return 1;
195
+ }
@@ -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
+ };