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,161 @@
1
+ {
2
+ "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
+ "id": "git",
4
+ "name": "Git",
5
+ "version": "2.0.1",
6
+ "description": "The whole git CLI through Total Recall: sign-in from the auth store, guarded commits and pushes, undo, fleet state, and the intent behind every change.",
7
+ "author": "Total Recall",
8
+ "license": "MIT",
9
+ "use_cases": [
10
+ "software-development"
11
+ ],
12
+ "gate": {
13
+ "tasks": ["Run any git command", "Commit or push with identity, secret and rule checks", "Sign git in through the auth store", "Undo a destructive git command", "Check repository state across the mesh", "Keep the git CLI current on every node"],
14
+ "owned_commands": [
15
+ { "pattern": "(^|[;&|(]\\s*)git(\\s+-C\\s+\\S+)?\\s+[a-z]", "use": "total-recall git <same git command and arguments>" }
16
+ ]
17
+ },
18
+ "tasks": [
19
+ { "intent": "Check and update the git CLI", "schedule": "17 6 * * *", "command": "cli update --scheduled", "placement": "per-node" }
20
+ ],
21
+ "cli": {
22
+ "answers_help": true,
23
+ "command": "git",
24
+ "handler": "./cli.mjs",
25
+ "subcommands": [
26
+ {
27
+ "name": "audit",
28
+ "description": "Summarise branch, worktree, upstream and stash state (--json for machine output)",
29
+ "effect": "read"
30
+ },
31
+ {
32
+ "name": "diff",
33
+ "description": "Read a stat or patch for unstaged, staged or base-relative changes",
34
+ "effect": "read",
35
+ "args": [
36
+ { "name": "patch", "flag": "--patch", "type": "boolean", "label": "Show patch" },
37
+ { "name": "cached", "flag": "--cached", "type": "boolean", "label": "Use staged changes" },
38
+ { "name": "base", "flag": "--base", "type": "string", "label": "Base ref" }
39
+ ]
40
+ },
41
+ { "name": "commit", "description": "Check identity, conflicts, HEAD, secrets and repository rules, then run git's own commit with your arguments", "effect": "write", "timeout_ms": 600000, "args": [
42
+ { "name": "args", "type": "string", "label": "git commit arguments", "positional": true }
43
+ ] },
44
+ { "name": "push", "description": "Check outgoing commits for secrets and rules, turn --force into --force-with-lease, then run git's own push", "effect": "write", "timeout_ms": 600000, "args": [
45
+ { "name": "args", "type": "string", "label": "git push arguments", "positional": true }
46
+ ] },
47
+ { "name": "credential-helper", "description": "Git credential helper: give git the mapped auth-store credential (get); store and erase do nothing", "effect": "read" },
48
+ { "name": "auth", "description": "Sign-in: status (live check, no secret shown), map <host> <KEY>, setup (helper entries for this machine)", "effect": "write", "args": [
49
+ { "name": "action", "type": "string", "label": "status | map | setup", "positional": true }
50
+ ] },
51
+ { "name": "cli", "description": "Git CLI metadata (version, path, install source, upstream, release notes); --all-nodes for the mesh; cli update updates git through its installer", "effect": "write", "timeout_ms": 600000 },
52
+ { "name": "undo", "description": "Restore refs, index and files from the snapshot taken before a destructive git command; --list shows snapshots", "effect": "destructive", "args": [
53
+ { "name": "event", "type": "string", "label": "Snapshot", "positional": true },
54
+ { "name": "list", "flag": "--list", "type": "boolean", "label": "List snapshots" }
55
+ ] },
56
+ { "name": "fleet", "description": "Dirty, unpushed, behind, diverged, stale stashes and abandoned worktrees across registered repositories; --all-nodes for the mesh", "effect": "read", "timeout_ms": 600000 },
57
+ { "name": "where", "description": "Machines and checkouts that contain a commit", "effect": "read", "args": [
58
+ { "name": "commit", "type": "string", "label": "Commit", "required": true, "positional": true }
59
+ ] },
60
+ { "name": "why", "description": "Blame one line plus the session, tracker item, decision and rules recorded when it was committed", "effect": "read", "args": [
61
+ { "name": "location", "type": "string", "label": "file:line", "required": true, "positional": true }
62
+ ] },
63
+ { "name": "group", "description": "Group a dirty tree into commits by the agent session that wrote each change; --apply commits the groups", "effect": "write" },
64
+ { "name": "message", "description": "Draft a commit message from the staged diff, the active tracker and its action log", "effect": "read" },
65
+ { "name": "bisect-agent", "description": "Find the first bad commit with the repository's quality gate as the test", "effect": "write", "timeout_ms": 600000, "args": [
66
+ { "name": "good", "type": "string", "label": "Good commit", "required": true, "positional": true },
67
+ { "name": "bad", "type": "string", "label": "Bad commit", "required": true, "positional": true }
68
+ ] },
69
+ { "name": "resolve", "description": "Propose a resolution for each conflicted file from both sides' recorded intent; --apply writes it", "effect": "write" },
70
+ { "name": "handoff", "description": "Pack a branch, its tracker and related memory into one SSSS bundle for another agent or machine", "effect": "write", "args": [
71
+ { "name": "branch", "type": "string", "label": "Branch", "required": true, "positional": true },
72
+ { "name": "out", "flag": "--out", "type": "string", "label": "Output file" }
73
+ ] },
74
+ { "name": "worktree-list", "description": "Read registered worktrees for the current repository", "effect": "read" },
75
+ { "name": "worktree-create", "description": "Create a detached worktree at a specified path and base ref", "effect": "write", "args": [
76
+ { "name": "path", "type": "string", "label": "Worktree path", "required": true, "positional": true },
77
+ { "name": "base", "flag": "--base", "type": "string", "label": "Base ref" }
78
+ ] },
79
+ { "name": "worktree-remove", "description": "Remove an owned linked worktree, with explicit force for dirty content", "effect": "destructive", "args": [
80
+ { "name": "path", "type": "string", "label": "Worktree path", "required": true, "positional": true },
81
+ { "name": "force", "flag": "--force", "type": "boolean", "label": "Allow removal of dirty content" }
82
+ ] },
83
+ { "name": "worktree", "description": "Compatibility form: worktree list, create or remove", "effect": "destructive" },
84
+ {
85
+ "name": "identity",
86
+ "description": "Show who commits here and which identities the history publishes; exits 1 on a missing or machine-derived identity (--json for machine output)",
87
+ "effect": "read"
88
+ }
89
+ ]
90
+ },
91
+ "card_face": {
92
+ "skin": "copper-obsidian",
93
+ "theme": "copper",
94
+ "headline": "Worktree Hygiene & Repository Watch",
95
+ "primary_color": "#b45309",
96
+ "secondary_color": "#0c0a09",
97
+ "accent_color": "#f59e0b",
98
+ "touch_layout": "comfortable",
99
+ "interactive": true,
100
+ "quick_actions": [
101
+ {
102
+ "subcommand": "audit",
103
+ "label": "AUDIT"
104
+ },
105
+ {
106
+ "subcommand": "diff",
107
+ "label": "DIFFSTAT"
108
+ }
109
+ ]
110
+ },
111
+ "ui": {
112
+ "design_tokens": "ui/DESIGN.md",
113
+ "elements": [
114
+ {
115
+ "id": "card",
116
+ "kind": "card",
117
+ "tag": "tr-git-card",
118
+ "module": "ui/card.js",
119
+ "description": "Git Hygiene Repository Hygiene Card Face"
120
+ }
121
+ ]
122
+ },
123
+ "auth": {
124
+ "plugin": "auth",
125
+ "methods": [
126
+ "passkey",
127
+ "google-oauth2"
128
+ ],
129
+ "session_verification": true
130
+ },
131
+ "deploy": {
132
+ "targets": [
133
+ "ssss-app",
134
+ "nextjs",
135
+ "react",
136
+ "flask"
137
+ ],
138
+ "required_ssss_version": ">=0.9.3",
139
+ "access_grants": [
140
+ "auth:session:verify",
141
+ "ssss:vault:read",
142
+ "ssss:vault:write",
143
+ "ssss:events:append"
144
+ ]
145
+ },
146
+ "branding": {
147
+ "name": "Git Hygiene",
148
+ "color": "#b45309",
149
+ "accent": "#f59e0b",
150
+ "badge": "Repository Hygiene",
151
+ "font": "space-grotesk"
152
+ },
153
+ "status": {
154
+ "command": "audit",
155
+ "refresh_seconds": 60
156
+ },
157
+ "distribution": {
158
+ "tier": "official",
159
+ "license": "open"
160
+ }
161
+ }
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { createRequire } from 'node:module';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ export function git(ctx, args, options) {
10
+ const result = ctx.git(args, options);
11
+ if ((result.status ?? result.code) !== 0 || result.error) throw new Error(`Git ${args[0]} failed`);
12
+ return String(result.stdout ?? '');
13
+ }
14
+ export function tr(ctx, args, options = {}) {
15
+ return spawnSync(ctx.env.GIT_PLUGIN_TR_BIN || 'total-recall', args, {
16
+ cwd: ctx.cwd, env: ctx.env, encoding: 'utf8', shell: false,
17
+ timeout: 30000, maxBuffer: 16 * 1024 * 1024, ...options,
18
+ });
19
+ }
20
+ export function trJSON(ctx, args) {
21
+ const result = tr(ctx, args);
22
+ if (result.error || result.status !== 0) throw new Error(`Total Recall ${args[0]} failed`);
23
+ try { return JSON.parse(result.stdout); } catch { throw new Error(`Total Recall ${args[0]} returned invalid JSON`); }
24
+ }
25
+ export function repository(ctx) {
26
+ return git(ctx, ['rev-parse', '--path-format=absolute', '--git-common-dir']).trim();
27
+ }
28
+ export async function ledger(ctx, kind = 'commits', required = false) {
29
+ const dir = ctx.env.TR_PLUGIN_STATE_DIR;
30
+ if (!dir) {
31
+ if (required) throw new Error('TR_PLUGIN_STATE_DIR must be supplied by the Total Recall host');
32
+ return null;
33
+ }
34
+ if (!path.isAbsolute(dir)) throw new Error('TR_PLUGIN_STATE_DIR must be absolute');
35
+ let modulePath;
36
+ const root = ctx.env.TR_PACKAGE_ROOT;
37
+ if (root) {
38
+ if (!path.isAbsolute(root) || JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') throw new Error('Invalid Total Recall state host');
39
+ modulePath = path.join(root, 'src/core/plugin-state-store.mjs');
40
+ } else modulePath = require.resolve('total-recall-brain/src/core/plugin-state-store.mjs');
41
+ const { PluginStateStore } = await import(pathToFileURL(modulePath).href);
42
+ const key = crypto.createHash('sha256').update(repository(ctx)).digest('hex');
43
+ return new PluginStateStore(path.join(dir, `${kind}-${key}.jsonl`), 'git');
44
+ }
45
+ export function session(env) { return env.CLAUDE_SESSION_ID || env.CODEX_SESSION_ID || env.TR_SESSION_ID || null; }
46
+ export function trailers(message) {
47
+ // Trailers must be in the final paragraph. Body prose is not attribution.
48
+ const block = message.trim().split(/\n\s*\n/).at(-1) || '';
49
+ const result = {};
50
+ let key;
51
+ for (const line of block.split('\n')) {
52
+ const match = /^([\w-]+):\s*(.*)$/.exec(line);
53
+ if (match) { key = match[1].toLowerCase(); (result[key] ||= []).push(match[2]); }
54
+ else if (/^\s+/.test(line) && key) result[key][result[key].length - 1] += ` ${line.trim()}`;
55
+ else return {};
56
+ }
57
+ return result;
58
+ }
59
+ export function intent(message) {
60
+ const rows = trailers(message);
61
+ return {
62
+ session: rows['agent-session']?.[0] || rows['session-id']?.[0] || rows.session?.[0] || null,
63
+ trackerItems: [...new Set(message.match(/\b(?:[A-Z][A-Z0-9_]*-[A-Z]*\d[\w.-]*|P\d+\.\d+|R\d+)\b/g) || [])],
64
+ decisions: [...(rows.decision || []), ...message.split('\n').filter(row => /^Decision\s*:/i.test(row)).map(row => row.replace(/^Decision\s*:\s*/i, ''))].filter((row, i, all) => all.indexOf(row) === i),
65
+ rules: rows.rules || rows.rule || [], trailers: rows,
66
+ };
67
+ }
68
+ export async function commitContext(ctx, sha) {
69
+ const message = git(ctx, ['show', '-s', '--format=%B', sha]);
70
+ const store = await ledger(ctx);
71
+ const record = store?.read([]).findLast(row => row.type === 'git.commit_context' && row.sha === sha) || null;
72
+ return { sha, message: message.trimEnd(), recorded: record, trailers: trailers(message), ...intent(message),
73
+ ...(record ? { session: record.session, trackerItems: record.trackerItems, decisions: record.decisions, rules: record.rules } : {}) };
74
+ }
75
+ export function output(ctx, value, text) { console.log(ctx.json ? JSON.stringify(value, null, 2) : text); }
76
+ export function operation(summary, usage, run) {
77
+ return { summary, usage, async run(args, ctx) {
78
+ try { return await run(args, ctx); }
79
+ catch (error) { if (ctx.json) console.log(JSON.stringify({ ok: false, error: error.message })); else console.error(error.message); return 1; }
80
+ } };
81
+ }
82
+ export function activePlan(ctx) {
83
+ const root = git(ctx, ['rev-parse', '--show-toplevel']).trim();
84
+ const base = path.join(root, 'docs/projects/in-progress');
85
+ if (!fs.existsSync(base)) return { tracker: null, plan: null, items: [], actionLog: '', reason: 'No active plan' };
86
+ const dirs = fs.readdirSync(base, { withFileTypes: true }).filter(row => row.isDirectory());
87
+ const trackers = dirs.flatMap(dir => fs.readdirSync(path.join(base, dir.name)).filter(name => name.endsWith('_PROJECT_TRACKER.md')).map(name => path.join(base, dir.name, name)));
88
+ if (trackers.length !== 1) throw new Error('Expected one active project tracker');
89
+ const tracker = trackers[0];
90
+ const plan = tracker.replace('_PROJECT_TRACKER.md', '_DEVELOPMENT_PLAN.md');
91
+ const body = fs.readFileSync(tracker, 'utf8');
92
+ const planBody = fs.existsSync(plan) ? fs.readFileSync(plan, 'utf8') : '';
93
+ return { tracker, plan, body, planBody, items: body.split('\n').filter(row => /^- \[[ xX]\]/.test(row)), actionLog: body.split(/^## Action Log\s*$/m)[1]?.trim() || '' };
94
+ }
@@ -0,0 +1,50 @@
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
+ import { vi } from 'vitest';
7
+
8
+ export function fixture() {
9
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'git-phase3-spec-'));
10
+ const repo = path.join(tmp, 'repo'); fs.mkdirSync(repo);
11
+ const cwd = fs.realpathSync(repo);
12
+ const env = { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_AUTHOR_NAME: 'Test Agent', GIT_AUTHOR_EMAIL: 'agent@example.com', GIT_COMMITTER_NAME: 'Test Agent', GIT_COMMITTER_EMAIL: 'agent@example.com', TR_PLUGIN_STATE_DIR: path.join(tmp, 'state'), TR_PACKAGE_ROOT: fileURLToPath(new URL('../../node_modules/total-recall-brain', import.meta.url)), CODEX_SESSION_ID: 'session-one' };
13
+ delete env.CLAUDE_SESSION_ID;
14
+ delete env.GIT_INDEX_FILE;
15
+ // Specs run this checkout's CLI, never whatever version happens to be installed.
16
+ const CLI = new URL('../../cli.mjs', import.meta.url).href;
17
+ const RUN = `import(${JSON.stringify(CLI)}).then(m => m.run(['node', 'total-recall', 'git', ...process.argv.slice(1)])).then(() => process.exit(process.exitCode ?? 0))`;
18
+ const git = (args, options = {}) => spawnSync(process.execPath, ['--input-type=module', '-e', RUN, ...args], { cwd, env, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...options });
19
+ // Fixture setup uses plain git: setup is not the code under test, so no guard or record runs for it.
20
+ const raw = (args, options = {}) => spawnSync('git', args, { cwd, env, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...options });
21
+ const g = (...args) => { const r = raw(args); if (r.status !== 0) throw new Error(`Fixture git ${args[0]}: ${r.stderr}`); return r.stdout; };
22
+ g('init', '-q', '-b', 'main');
23
+ g('config', '--local', 'user.name', 'Test Agent');
24
+ g('config', '--local', 'user.email', 'agent@example.com');
25
+ g('config', '--local', 'commit.gpgsign', 'false');
26
+ const write = (name, content) => { fs.mkdirSync(path.dirname(path.join(cwd, name)), { recursive: true }); fs.writeFileSync(path.join(cwd, name), content); };
27
+ const commit = (message = 'Initial', file = 'a.txt', body = 'one\ntwo\nthree\nfour\nfive\n') => { write(file, body); g('add', '--', file); g('commit', '-qm', message); return g('rev-parse', 'HEAD').trim(); };
28
+ const responses = path.join(tmp, 'responses.json');
29
+ const calls = path.join(tmp, 'calls.jsonl');
30
+ const bin = path.join(tmp, 'total-recall-stub');
31
+ fs.writeFileSync(responses, '{}');
32
+ fs.writeFileSync(bin, `#!/usr/bin/env node
33
+ const fs=require('node:fs'),cp=require('node:child_process');
34
+ const args=process.argv.slice(2);fs.appendFileSync(${JSON.stringify(calls)},JSON.stringify(args)+'\\n');
35
+ const map=JSON.parse(fs.readFileSync(${JSON.stringify(responses)},'utf8'));
36
+ if(args[0]==='git'){const r=cp.spawnSync(process.execPath,['--input-type=module','-e',${JSON.stringify(RUN)},...args.slice(1)],{env:process.env,encoding:'utf8',input:fs.readFileSync(0)});process.stdout.write(r.stdout||'');process.stderr.write(r.stderr||'');process.exitCode=r.status??1;}
37
+ else if(args[0]==='auth'&&args[2]==='list'&&!map['auth secret']){process.stdout.write(JSON.stringify({keys:[]}));}
38
+ else if(args[0]==='auth'&&args[2]==='scan'&&!map['auth secret']){fs.readFileSync(0);process.stdout.write(JSON.stringify({ok:true,leak:false,keys:[]}));}
39
+ else if(args[0]==='code-quality'&&map.gateFile){const ok=fs.readFileSync(map.gateFile,'utf8').includes('good');process.stdout.write(JSON.stringify({ok}));process.exitCode=ok?0:1;}
40
+ else{const r=map[args.slice(0,2).join(' ')]||map[args[0]]||{status:0,body:{}};process.stdout.write(typeof r.body==='string'?r.body:JSON.stringify(r.body));process.exitCode=r.status??0;}
41
+ `, { mode: 0o755 });
42
+ env.GIT_PLUGIN_TR_BIN = bin;
43
+ const set = map => fs.writeFileSync(responses, JSON.stringify(map));
44
+ const readCalls = () => fs.existsSync(calls) ? fs.readFileSync(calls, 'utf8').trim().split('\n').filter(Boolean).map(row => JSON.parse(row)) : [];
45
+ const out = vi.spyOn(console, 'log').mockImplementation(() => {});
46
+ vi.spyOn(console, 'error').mockImplementation(() => {});
47
+ const ctx = { cwd, env, json: true, git };
48
+ const result = () => JSON.parse(out.mock.calls.at(-1)[0]);
49
+ return { tmp, cwd, env, ctx, g, git, write, commit, set, readCalls, result, cleanup() { vi.restoreAllMocks(); fs.rmSync(tmp, { recursive: true, force: true }); } };
50
+ }
@@ -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
+ }