create-ai-memory 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ram Christopher Baarde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ <div align="center">
2
+
3
+ # ai-memory
4
+
5
+ **Persistent, agent-agnostic session memory for AI coding CLIs. One Markdown vault, any agent.**
6
+
7
+ Give Claude Code, Codex, Gemini, Cursor, and opencode a shared second brain that
8
+ survives across sessions: durable profile, per-project context, and last-session carryover,
9
+ injected at launch. The vault is the source of truth; the chat is disposable.
10
+
11
+ No daemon, no database, no API key. Just zsh and Markdown files you can read.
12
+
13
+ ![zsh](https://img.shields.io/badge/shell-zsh-89e051)
14
+ ![tests](https://img.shields.io/badge/tests-35%20passing-brightgreen)
15
+ ![license](https://img.shields.io/badge/license-MIT-blue)
16
+ ![PRs](https://img.shields.io/badge/PRs-welcome-orange)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## The problem
23
+
24
+ Every AI coding session starts from zero.
25
+
26
+ Claude Code, Codex, Gemini CLI. The moment a session ends, the agent forgets what
27
+ was built, the decisions that were made, the PRD, every architectural and codebase
28
+ choice. The context that mattered most evaporates with the chat thread, and the
29
+ next run begins blind.
30
+
31
+ You pay the same tax on every run:
32
+
33
+ - **Agent amnesia.** Accumulated project knowledge vanishes when the thread closes.
34
+ - **Lost decisions.** Why a choice was made is nowhere; the next run re-litigates it.
35
+ - **Reload overhead.** You re-explain the stack, constraints, and conventions from scratch.
36
+ - **No continuity.** Nothing carries "where I left off" into the next session.
37
+
38
+ Switching agents makes it worse. Each CLI is its own island with its own memory,
39
+ or none. Knowledge earned in Claude doesn't reach Codex.
40
+
41
+ ## How it works
42
+
43
+ Keep the memory outside the chat, in plain Markdown on disk, and inject it into
44
+ whichever agent you launch. Because it's just files, the vault doubles as an
45
+ [Obsidian](https://obsidian.md) folder with graph view, backlinks, and search;
46
+ nothing here requires Obsidian.
47
+
48
+ Memory sits in three layers, each injected at the right scope:
49
+
50
+ | Layer | Lives in | Injected | Holds |
51
+ |---|---|---|---|
52
+ | **Global** | `_Global_Profile.md`, `_Standards.md` | every session, every project | who you are, your rules, coding standards, commit policy |
53
+ | **Project** | `_projects/<repo>.md` | sessions in that repo | purpose, architecture, constraints, active decisions |
54
+ | **Session** | `_session_logs/<repo>/<timestamp>.md` | next session as carryover | what changed, blockers, next steps |
55
+
56
+ ```
57
+ $ claude-start
58
+
59
+ ├─ resolve project from the current git repo
60
+ ├─ create a fresh session log from the template
61
+ ├─ gather: Global Profile + Standards (who you are)
62
+ │ Project note (this repo)
63
+ │ latest prior session log (where you left off)
64
+ ├─ ask which session skills to enable (optional)
65
+ └─ launch the agent with all of it as the first prompt
66
+
67
+
68
+ …you work…
69
+
70
+
71
+ on exit, a hook writes an auto session log
72
+ (branch, commits made, uncommitted changes)
73
+ → the NEXT session inherits it as carryover
74
+ ```
75
+
76
+ One environment variable, `AI_MEM_ROOT`, points at the vault, so the system moves
77
+ between machines by pointing at the same folder.
78
+
79
+ ## A session, start to finish
80
+
81
+ ```console
82
+ $ cd ~/code/checkout-api
83
+ $ claude-start
84
+ Use terse output this session? [y/N] n
85
+
86
+ # Claude launches pre-loaded with:
87
+ # • your profile + coding standards + commit policy (global)
88
+ # • checkout-api: purpose, architecture, decisions (project)
89
+ # • "Next: wire the refund webhook" (last session's carryover)
90
+
91
+ … you build the refund webhook, make a few commits …
92
+
93
+ $ ai-note "refund webhook live; still need idempotency keys" # jot mid-session
94
+
95
+ # On exit, a hook stamps the session log with the branch, the commits you made,
96
+ # and anything uncommitted. Tomorrow's claude-start picks up exactly there.
97
+ ```
98
+
99
+ No copy-pasting context. No re-explaining the stack. No "where were we."
100
+
101
+ ## Quickstart
102
+
103
+ ```sh
104
+ npm create ai-memory@latest # copies the tool in and runs the setup, no git clone
105
+ exec zsh
106
+ ```
107
+
108
+ Then, from inside any git repo:
109
+
110
+ ```sh
111
+ claude-start # or codex-start / gemini-start / cursor-start / opencode-start
112
+ ```
113
+
114
+ The agent opens already knowing your standards, this project, and where you left
115
+ off last time.
116
+
117
+ ## Table of contents
118
+
119
+ <table>
120
+ <tr>
121
+ <td valign="top" width="33%">
122
+
123
+ **Overview**
124
+
125
+ - [The problem](#the-problem)
126
+ - [How it works](#how-it-works)
127
+ - [A session, start to finish](#a-session-start-to-finish)
128
+
129
+ **Getting started**
130
+
131
+ - [Quickstart](#quickstart)
132
+ - [Install](#install)
133
+ - [Commands](#commands)
134
+
135
+ </td>
136
+ <td valign="top" width="33%">
137
+
138
+ **Reference**
139
+
140
+ - [Session skills](#session-skills-optional)
141
+ - [Vault layout](#vault-layout)
142
+ - [Adding a new agent](#adding-a-new-agent)
143
+ - [Optional integrations](#optional-integrations)
144
+ - [Configuration](#configuration)
145
+
146
+ </td>
147
+ <td valign="top" width="33%">
148
+
149
+ **Project**
150
+
151
+ - [Tests](#tests)
152
+ - [Design notes](#design-notes)
153
+ - [Roadmap](#roadmap)
154
+ - [License](#license)
155
+
156
+ </td>
157
+ </tr>
158
+ </table>
159
+
160
+ ## Install
161
+
162
+ Pick whichever fits how you manage your shell. All paths end at the same place.
163
+
164
+ **npm** (no git clone; the tool is bundled in the package):
165
+
166
+ ```sh
167
+ npm create ai-memory@latest # into ~/ai-memory, then runs the setup
168
+ npx create-ai-memory ~/code/ai-memory # or a directory you choose
169
+ ```
170
+
171
+ **zsh plugin manager:**
172
+
173
+ ```zsh
174
+ # zinit
175
+ zinit light rambaarde/create-ai-memory
176
+
177
+ # antidote (in your plugins file)
178
+ rambaarde/create-ai-memory
179
+
180
+ # oh-my-zsh: clone into custom/plugins, then add create-ai-memory to plugins=(...)
181
+ ```
182
+
183
+ Plugin-manager installs only source the module. That's fine: the vault
184
+ auto-scaffolds from the shipped templates on first use, so `install.sh` is
185
+ optional. Set `AI_MEM_ROOT` in `~/.zshrc` first if you don't want the default
186
+ `~/.ai-memory/_Ai_Memory`.
187
+
188
+ **Clone and run** (the source of truth all paths reuse):
189
+
190
+ ```sh
191
+ git clone https://github.com/rambaarde/create-ai-memory.git ~/ai-memory
192
+ ~/ai-memory/install.sh
193
+ ```
194
+
195
+ > **zsh only.** The module uses `print -r`, `${(s:|:)}`, and `select`. A bash
196
+ > port is welcome as a PR; see [Roadmap](#roadmap).
197
+
198
+ ## Commands
199
+
200
+ | Command | What it does |
201
+ |---|---|
202
+ | `claude-start` · `codex-start` · `gemini-start` · `cursor-start` · `opencode-start` | Launch an agent with full vault context and the session-skill picker |
203
+ | `ai-start [project]` | Prepare the session (project note and fresh log) without launching an agent |
204
+ | `ai-context [project]` | Print the vault context block for the current repo, and arm the git commit guard |
205
+ | `ai-note <text>` | Append a timestamped note to today's session log while you work |
206
+
207
+ Project is auto-resolved from the current git repo; pass a name to override.
208
+
209
+ ## Session skills (optional)
210
+
211
+ You define your own per-session skills; ai-memory ships none. At launch it asks
212
+ y/n for each skill you registered, then injects the chosen instruction blocks into
213
+ the agent (and for Cursor, writes them as a managed always-apply rule). Register
214
+ nothing and every session is plain.
215
+
216
+ Add skills in `~/.zshrc` before sourcing the module. Each entry maps a `key` to
217
+ `prompt::instruction block`, and `AI_MEM_SKILL_ORDER` sets the ask order:
218
+
219
+ ```zsh
220
+ typeset -gA AI_MEM_SKILLS
221
+ AI_MEM_SKILLS[terse]='Use terse output this session?::Respond tersely; drop filler and hedging.'
222
+ AI_MEM_SKILLS[design]='Use strict UI design discipline?::Apply careful frontend/UI design review to any design work.'
223
+ AI_MEM_SKILL_ORDER=(terse design)
224
+
225
+ source "$HOME/ai-memory/shell/ai-mem.zsh"
226
+ ```
227
+
228
+ The injected block applies for the whole run, so a session's chosen skills persist
229
+ as instructions across every change the agent makes.
230
+
231
+ ## Vault layout
232
+
233
+ ```
234
+ $AI_MEM_ROOT/
235
+ _Global_Profile.md your cross-project rules (injected every session)
236
+ _Standards.md extra shared standards (injected every session)
237
+ _projects/
238
+ _project_template.md scaffold for new project notes
239
+ <repo>.md per-project durable context
240
+ _session_logs/
241
+ _session_template.md scaffold for new session logs
242
+ <repo>/
243
+ <repo>-<timestamp>.md one file per session
244
+ ```
245
+
246
+ Notes are created from templates on first use and never overwritten. Edit
247
+ `_Global_Profile.md` and `_Standards.md` to make them yours; the shipped versions
248
+ are sanitized placeholders.
249
+
250
+ ## Adding a new agent
251
+
252
+ Launchers aren't hardcoded. Each agent is one small adapter, and the
253
+ `<name>-start` function is generated for you. `claude`, `codex`, `gemini`,
254
+ `cursor`, and `opencode` ship built in. To add another, say `aider`:
255
+
256
+ 1. Define the adapter in `shell/adapters.zsh`. It receives `$1` memory prompt,
257
+ `$2` mode block, and `$3` onward extra args:
258
+ ```zsh
259
+ _ai_adapter_aider() {
260
+ local memory_prompt="$1"
261
+ aider --message "$memory_prompt"
262
+ }
263
+ ```
264
+ 2. Register it in `~/.zshrc` before sourcing, or edit the default:
265
+ ```zsh
266
+ export AI_MEM_AGENTS="claude codex gemini cursor opencode aider"
267
+ ```
268
+ 3. `aider-start` now exists. No core edits.
269
+
270
+ ## Optional integrations
271
+
272
+ ### Claude Code hooks
273
+ The files live in `hooks/claude/`. Record repo `HEAD` at session start, then on
274
+ exit write an auto block to the log with the branch, commits made this session, and
275
+ uncommitted changes, so the next session has real carryover instead of an empty
276
+ template. Merge `settings.snippet.json` into `~/.claude/settings.json`, replacing
277
+ `<AI_MEM_HOME>` with an absolute path. Both hooks no-op for plain `claude` runs;
278
+ they gate on `$AI_MEM_ACTIVE_SESSION_LOG`.
279
+
280
+ ### Git commit guard
281
+ The files live in `hooks/git/`:
282
+
283
+ - **`commit-msg`** requires Conventional Commits and a structured body, and that
284
+ `ai-context` was loaded in the committing shell, matching a per-repo token. An
285
+ agent can't commit without the vault context loaded.
286
+ - **`pre-push`** blocks direct pushes to `main` unless `ALLOW_PUSH_TO_MAIN=1`.
287
+
288
+ Enable per repo:
289
+
290
+ ```sh
291
+ cp ~/ai-memory/hooks/git/* <repo>/.githooks/
292
+ git -C <repo> config core.hooksPath .githooks
293
+ ```
294
+
295
+ ## Configuration
296
+
297
+ | Env var | Default | Purpose |
298
+ |---|---|---|
299
+ | `AI_MEM_ROOT` | `$HOME/.ai-memory/_Ai_Memory` | Vault root. Point at any folder, including an existing Obsidian vault |
300
+ | `AI_MEM_AGENTS` | `claude codex gemini cursor opencode` | Space-separated agents to generate `-start` functions for |
301
+ | `AI_MEM_SKILLS` / `AI_MEM_SKILL_ORDER` | empty | Your per-session skills (see above) |
302
+
303
+ ## Tests
304
+
305
+ Offline unit suite (throwaway vault + git repo, no network): path guarding,
306
+ project resolution, session prep, context prompt, skill picker, launcher
307
+ generation, adapter dispatch, the commit token, and `ai-note`.
308
+
309
+ ```sh
310
+ zsh tests/run.sh # offline unit tests
311
+ zsh tests/smoke.sh # live: launches each agent headlessly, checks it responds
312
+ ```
313
+
314
+ `smoke.sh` makes real API calls, so each CLI must be installed and authed
315
+ (opencode defaults to DeepSeek; set it up or pass
316
+ `AIMEM_SMOKE_OPENCODE_MODEL=provider/model`).
317
+
318
+ ## Design notes
319
+
320
+ - **The vault is the source of truth; the chat is disposable.** Durable state lives
321
+ in Markdown, not in any agent's memory.
322
+ - **Path-guarded writes.** Every file operation is checked to stay inside
323
+ `$AI_MEM_ROOT`, so an agent can't write outside the memory boundary.
324
+ - **Project = git repo.** Resolution prefers the repo you're standing in, so
325
+ `cd`-ing between projects in one shell never pins the wrong project.
326
+ - **Additive by default.** Templates fill in; notes are never clobbered.
327
+
328
+ ## Roadmap
329
+
330
+ - Bash port of the shell module.
331
+ - Publish `create-ai-memory` to npm.
332
+ - More agent adapters shipped by default: opencode, aider.
333
+ - Optional cross-project index and search over session logs.
334
+
335
+ PRs welcome. Persistent AI memory shouldn't be a personal hack; it should be
336
+ something the whole community can install.
337
+
338
+ ## License
339
+
340
+ MIT © Ram Christopher Baarde
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-ai-memory: set up ai-memory with no git clone.
4
+ *
5
+ * The tool's files are bundled inside this npm package, so this script just
6
+ * copies them into place and hands off to the shell installer, which owns the
7
+ * interactive setup (banner, prompts, vault scaffold, ~/.zshrc lines). No git,
8
+ * no network beyond the npm download itself.
9
+ *
10
+ * Usage:
11
+ * npm create ai-memory@latest # into ~/ai-memory
12
+ * npx create-ai-memory ~/code/ai-memory # into a directory you choose
13
+ */
14
+ 'use strict';
15
+
16
+ const { spawnSync } = require('node:child_process');
17
+ const { cpSync, existsSync, mkdirSync } = require('node:fs');
18
+ const { join, resolve } = require('node:path');
19
+ const { homedir } = require('node:os');
20
+
21
+ const pkgRoot = resolve(__dirname, '..'); // bundled tool
22
+ const dest = resolve(process.argv[2] || join(homedir(), 'ai-memory'));
23
+
24
+ // Files that make up the tool; copied verbatim from the package into dest.
25
+ const ITEMS = ['shell', 'hooks', 'vault-template', 'install.sh', 'create-ai-memory.plugin.zsh', 'LICENSE'];
26
+
27
+ console.log(`create-ai-memory: installing into ${dest}`);
28
+ mkdirSync(dest, { recursive: true });
29
+ for (const item of ITEMS) {
30
+ const src = join(pkgRoot, item);
31
+ if (existsSync(src)) cpSync(src, join(dest, item), { recursive: true });
32
+ }
33
+
34
+ // Hand off to the shell installer for the interactive setup.
35
+ const r = spawnSync('sh', [join(dest, 'install.sh')], { stdio: 'inherit' });
36
+ if (r.error) {
37
+ console.error(`create-ai-memory: could not run install.sh: ${r.error.message}`);
38
+ console.error(`Files are in ${dest}; run sh ${join(dest, 'install.sh')} yourself.`);
39
+ process.exit(1);
40
+ }
41
+ process.exit(r.status ?? 0);
@@ -0,0 +1,5 @@
1
+ # Entry point for zsh plugin managers (zinit, antidote, oh-my-zsh, antigen),
2
+ # which load a repo by sourcing a root-level *.plugin.zsh. It just sources the
3
+ # real module, resolved relative to this file so it works wherever the manager
4
+ # clones the repo.
5
+ source "${0:A:h}/shell/ai-mem.zsh"
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code SessionStart hook for the AI-memory vault.
3
+ #
4
+ # Records the repo HEAD at session start so the matching summary hook can show
5
+ # exactly which commits this session produced. No-ops unless the session was
6
+ # launched via claude-start (which exports AI_MEM_ACTIVE_SESSION_LOG), so plain
7
+ # `claude` runs are untouched.
8
+ set -euo pipefail
9
+
10
+ log="${AI_MEM_ACTIVE_SESSION_LOG:-}"
11
+ [ -n "$log" ] || exit 0
12
+
13
+ sha="$(git rev-parse HEAD 2>/dev/null || true)"
14
+ [ -n "$sha" ] || exit 0
15
+
16
+ printf '%s\n' "$sha" > "${log%.md}.startsha"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code Stop hook for the AI-memory vault.
3
+ #
4
+ # Idempotently writes a concrete "Auto Session Log" block (branch, commits made
5
+ # this session, and uncommitted changes) into the active session log, so the
6
+ # NEXT session has real carryover instead of an empty template. Runs on every
7
+ # Stop and rewrites a single block, so the log always reflects the latest state
8
+ # even if the session is later killed abruptly.
9
+ #
10
+ # No-ops unless the session was launched via claude-start (which exports
11
+ # AI_MEM_ACTIVE_SESSION_LOG).
12
+ set -euo pipefail
13
+
14
+ log="${AI_MEM_ACTIVE_SESSION_LOG:-}"
15
+ [ -n "$log" ] || exit 0
16
+ [ -f "$log" ] || exit 0
17
+
18
+ marker="## Auto Session Log"
19
+
20
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '(no git)')"
21
+ repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"
22
+
23
+ start_sha=""
24
+ [ -f "${log%.md}.startsha" ] && start_sha="$(cat "${log%.md}.startsha" 2>/dev/null || true)"
25
+
26
+ commits=""
27
+ if [ -n "$start_sha" ]; then
28
+ commits="$(git log --no-merges --format='- %h %s' "${start_sha}..HEAD" 2>/dev/null || true)"
29
+ fi
30
+ [ -n "$commits" ] || commits="- (no commits this session)"
31
+
32
+ changes="$(git status --short 2>/dev/null | sed 's/^/- /' || true)"
33
+ [ -n "$changes" ] || changes="- (working tree clean)"
34
+
35
+ stamp="$(date '+%Y-%m-%d %H:%M:%S')"
36
+
37
+ block="$(cat <<EOF
38
+ ${marker}
39
+ _Auto-generated ${stamp}. Edit the Session Outcome section above for durable notes._
40
+
41
+ * **Repo:** ${repo:-?}
42
+ * **Branch:** ${branch}
43
+ * **Commits this session:**
44
+ ${commits}
45
+ * **Uncommitted changes at last checkpoint:**
46
+ ${changes}
47
+ EOF
48
+ )"
49
+
50
+ # Strip any previous auto block (from marker to EOF), then append the fresh one.
51
+ tmp="$(mktemp)"
52
+ awk -v m="$marker" 'index($0, m)==1 { exit } { print }' "$log" > "$tmp"
53
+ printf '%s\n\n%s\n' "$(cat "$tmp")" "$block" > "$log"
54
+ rm -f "$tmp"
@@ -0,0 +1,19 @@
1
+ {
2
+ "_comment": "Merge into ~/.claude/settings.json. Replace <AI_MEM_HOME> with the absolute path to your ai-mem checkout (Claude Code does not expand env vars in hook commands). Both hooks no-op unless the session was launched via claude-start.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ { "type": "command", "command": "<AI_MEM_HOME>/hooks/claude/session-start.sh" }
8
+ ]
9
+ }
10
+ ],
11
+ "Stop": [
12
+ {
13
+ "hooks": [
14
+ { "type": "command", "command": "<AI_MEM_HOME>/hooks/claude/session-summary.sh" }
15
+ ]
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,178 @@
1
+ #!/bin/sh
2
+ # Enforce Conventional Commits with a detailed body and an active vault-context
3
+ # token for this repository.
4
+ # Git-generated merge, revert, fixup, and squash commits are allowed so common
5
+ # history maintenance workflows still work without manual overrides.
6
+ set -eu
7
+
8
+ commit_msg_file=${1:-}
9
+
10
+ if [ -z "$commit_msg_file" ] || [ ! -f "$commit_msg_file" ]; then
11
+ echo "commit-msg: expected a commit message file path" >&2
12
+ exit 1
13
+ fi
14
+
15
+ first_subject=''
16
+ body_line_count=0
17
+ body_word_count=0
18
+ state=need_separator
19
+ subject_skipped=0
20
+
21
+ is_footer_line() {
22
+ case $1 in
23
+ BREAKING\ CHANGE:*|Signed-off-by:*|Co-authored-by:*|Reviewed-by:*|Acked-by:*|Refs\ *|Fixes\ *|Closes\ *|Resolves\ *)
24
+ return 0
25
+ ;;
26
+ esac
27
+
28
+ return 1
29
+ }
30
+
31
+ require_vault_context() {
32
+ local repo_root project_name ready_dir ready_file ready_token
33
+
34
+ if [ -z "${AI_MEM_ROOT:-}" ]; then
35
+ echo "commit-msg: AI_MEM_ROOT is not set; run ai-context first" >&2
36
+ return 1
37
+ fi
38
+
39
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
40
+ project_name="$(basename "$repo_root")"
41
+ ready_dir="$AI_MEM_ROOT/_session_logs/.context-ready"
42
+ ready_file="$ready_dir/$project_name.token"
43
+
44
+ if [ ! -f "$ready_file" ]; then
45
+ echo "commit-msg: load the vault context first with ai-context" >&2
46
+ return 1
47
+ fi
48
+
49
+ if [ -z "${AI_MEM_CONTEXT_TOKEN:-}" ]; then
50
+ echo "commit-msg: this shell has not loaded the vault context; run ai-context first" >&2
51
+ return 1
52
+ fi
53
+
54
+ ready_token="$(sed -n '1p' "$ready_file")"
55
+
56
+ if [ "$ready_token" != "$AI_MEM_CONTEXT_TOKEN" ]; then
57
+ echo "commit-msg: this shell's vault context token does not match the active session; rerun ai-context" >&2
58
+ return 1
59
+ fi
60
+ }
61
+
62
+ while IFS= read -r line || [ -n "$line" ]; do
63
+ case $line in
64
+ '')
65
+ continue
66
+ ;;
67
+ '#'*)
68
+ continue
69
+ ;;
70
+ esac
71
+ first_subject=$line
72
+ break
73
+ done < "$commit_msg_file"
74
+
75
+ if [ -z "$first_subject" ]; then
76
+ echo "commit-msg: commit message is empty" >&2
77
+ exit 1
78
+ fi
79
+
80
+ case $first_subject in
81
+ Merge\ *|Revert\ *|fixup!\ *|squash!\ *)
82
+ exit 0
83
+ ;;
84
+ esac
85
+
86
+ if printf '%s\n' "$first_subject" | grep -Eq '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\([^()[:space:]][^()[:space:]]*\))?(!)?: [^[:space:]].*$'; then
87
+ :
88
+ else
89
+ cat >&2 <<'EOF'
90
+ Commit message rejected.
91
+
92
+ Use Conventional Commits:
93
+ type(scope): description
94
+
95
+ Use a structured body after a blank line:
96
+ - short summary paragraph
97
+ - optional Why / Cause paragraph
98
+ - `-` bullet concrete changes or defenses
99
+ - optional notes or follow-up
100
+
101
+ Examples:
102
+ feat(tmux): add session color sync
103
+ fix(zsh): avoid duplicate prompt initialization
104
+ docs(memory): clarify commit policy
105
+
106
+ Allowed types:
107
+ feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert
108
+
109
+ Git-generated Merge, Revert, fixup!, and squash! messages are allowed.
110
+ EOF
111
+
112
+ exit 1
113
+ fi
114
+
115
+ while IFS= read -r line || [ -n "$line" ]; do
116
+ case $line in
117
+ '#'*)
118
+ continue
119
+ ;;
120
+ esac
121
+
122
+ if [ "$subject_skipped" -eq 0 ]; then
123
+ case $line in
124
+ '')
125
+ continue
126
+ ;;
127
+ esac
128
+ subject_skipped=1
129
+ continue
130
+ fi
131
+
132
+ if [ "$state" = need_separator ]; then
133
+ if [ -z "$line" ]; then
134
+ state=body
135
+ continue
136
+ fi
137
+
138
+ echo "commit-msg: add a blank line after the subject, then a detailed body" >&2
139
+ exit 1
140
+ fi
141
+
142
+ if [ "$state" = footer ]; then
143
+ continue
144
+ fi
145
+
146
+ if [ -z "$line" ]; then
147
+ continue
148
+ fi
149
+
150
+ if is_footer_line "$line"; then
151
+ if [ "$body_line_count" -eq 0 ]; then
152
+ echo "commit-msg: add a detailed body before footers" >&2
153
+ exit 1
154
+ fi
155
+ state=footer
156
+ continue
157
+ fi
158
+
159
+ body_line_count=$((body_line_count + 1))
160
+ words=$(printf '%s\n' "$line" | awk '{print NF}')
161
+ body_word_count=$((body_word_count + words))
162
+ done < "$commit_msg_file"
163
+
164
+ if [ "$body_line_count" -eq 0 ]; then
165
+ echo "commit-msg: commit message needs a structured body, not just a title" >&2
166
+ exit 1
167
+ fi
168
+
169
+ if [ "$body_line_count" -lt 2 ] && [ "$body_word_count" -lt 20 ]; then
170
+ echo "commit-msg: body is too short; add a summary and - bullet changes or defenses" >&2
171
+ exit 1
172
+ fi
173
+
174
+ if ! require_vault_context; then
175
+ exit 1
176
+ fi
177
+
178
+ exit 0
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ # Block pushes that target the production branch unless an explicit override is
3
+ # set for an emergency release.
4
+ set -eu
5
+
6
+ allow_main_push=${ALLOW_PUSH_TO_MAIN:-}
7
+
8
+ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
9
+ case $remote_ref in
10
+ refs/heads/main)
11
+ if [ "$allow_main_push" != "1" ]; then
12
+ echo "pre-push: push to main blocked; use a topic branch or set ALLOW_PUSH_TO_MAIN=1 for an explicit override" >&2
13
+ exit 1
14
+ fi
15
+ ;;
16
+ esac
17
+ done
18
+
19
+ exit 0