cortexhawk 3.1.1 → 3.2.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/CHANGELOG.md CHANGED
@@ -3,12 +3,32 @@
3
3
  All notable changes to CortexHawk are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/)
5
5
 
6
- ## [3.1.1] - 2026-02-15
6
+ ## [3.2.0] - 2026-02-15
7
+
8
+ ### Added
9
+ - `/commit` command: lightweight conventional commit + push without review or PR — use `/ship` for full workflow, `/commit` for quick iterations
10
+ - Install auto-detects existing PR/commit templates; generates CortexHawk defaults (`.github/PULL_REQUEST_TEMPLATE.md`, `.gitmessage`) if missing — agents (`git-manager`, `/ship`, `/commit`) read templates at runtime
11
+ - `--version` / `-v` flag: displays CortexHawk version
12
+ - `scripts/refresh-context.sh`: regenerates `docs/.context/_shared.md` mid-session — `/task` and `/backlog` auto-refresh after modifying backlog
7
13
 
8
14
  ### Fixed
9
15
  - `branch-guard` / `commit-guard` hooks: JSON parsing via `jq` with regex fallback — fixes false "hook error" on commands containing quotes or HEREDOC
10
16
  - `commit-guard`: multi-format commit message extraction (HEREDOC, single/double quotes)
11
17
  - `file-guard`: match on basename only — `*secret*`/`*credentials*` no longer false-positive on legitimate files (e.g., `oauth_service.py`); `.env.example`/`.env.sample`/`.env.template` whitelisted; `docker-compose*.yml` unblocked
18
+ - **Security**: eliminate shell injection in all Python HEREDOC/inline scripts — `$hooks_json`, file paths, and user input no longer interpolated into Python source; uses `sys.argv`/`sys.stdin` instead. Hook names validated against path traversal, hook paths shell-escaped via `shlex.quote()`
19
+ - Portable `sed -i` via `sed_inplace()` helper — fixes `sed` failures on macOS (BSD) across snapshot, hook toggle, and init wizard
20
+ - Argument validation for `--target`, `--profile`, and `--restore` flags — prevents cryptic shell errors when value is missing
21
+ - `update_gitignore()` now ensures essential entries (`.env`, `node_modules/`, `dist/`, etc.) are present — previously only added `.claude/` to an existing `.gitignore`
22
+ - Warning when python3 is not found — `generate_hooks_config()` previously failed silently, now alerts user that static fallback is used
23
+ - `.env` parser now strips single quotes, inline comments, and trailing whitespace — previously only stripped double quotes
24
+ - `--init` wizard now supports "All" and "Auto-detect" targets — removed false `--target all/auto` + `--init` guards, scope step auto-selects local for multi-target
25
+ - Snapshot no longer reads stale `/tmp/cortexhawk-custom-*.json` from previous runs — uses `$PROFILE_FILE` from current session only
26
+ - `copy_skills()` now warns when a skill from the profile doesn't exist in source — previously silently skipped
27
+ - Removed `local` keyword used outside function in `--target auto` dispatcher — fixes portability issues
28
+ - `.gitignore` no longer duplicates `# CortexHawk` header with `--target all` — groups all target dirs under single header
29
+ - `SKILL_COUNT` no longer shows 1 when no skills detected — fixed `wc -l` on empty string in `autodetect-profile.sh`
30
+ - `_shared.md` now includes generation timestamp for staleness awareness
31
+ - `settings.json` now merges on reinstall instead of skipping — new hooks regenerated from compose.yml, new permissions added via union (user customizations preserved). `--update` also merges new permissions
12
32
 
13
33
  ## [3.1.0] - 2026-02-14
14
34
 
package/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ Open-source development toolkit for Claude Code — optimized agents, skills, co
6
6
 
7
7
  ```
8
8
  agents/ — 20 specialized AI agents
9
- commands/ — 32 slash commands
9
+ commands/ — 33 slash commands
10
10
  scripts/ — Validation and post-install audit scripts
11
11
  skills/ — 36 domain-specific knowledge modules
12
12
  hooks/ — 9 lifecycle hooks
@@ -49,7 +49,7 @@ Custom agents in `.cortexhawk-agents/` at project root. Each `.md` file uses `ex
49
49
 
50
50
  ## Commands
51
51
 
52
- `/plan` `/build` `/test` `/review` `/ship` `/debug` `/scan` `/check` `/refactor` `/research` `/doc` `/bootstrap` `/tdd` `/optimize` `/migrate` `/monitor` `/api-gen` `/changelog` `/journal` `/brainstorm` `/simplify` `/deploy` `/export` `/backlog` `/pulse` `/map` `/learn` `/chain` `/task` `/ci` `/context` `/upgrade`
52
+ `/plan` `/build` `/test` `/review` `/ship` `/commit` `/debug` `/scan` `/check` `/refactor` `/research` `/doc` `/bootstrap` `/tdd` `/optimize` `/migrate` `/monitor` `/api-gen` `/changelog` `/journal` `/brainstorm` `/simplify` `/deploy` `/export` `/backlog` `/pulse` `/map` `/learn` `/chain` `/task` `/ci` `/context` `/upgrade`
53
53
 
54
54
  ## Skills
55
55
 
package/README.md CHANGED
@@ -2,22 +2,35 @@
2
2
 
3
3
  [![GitHub stars](https://img.shields.io/github/stars/Spechawk94/CortexHawk?style=flat-square)](https://github.com/Spechawk94/CortexHawk/stargazers)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
5
- [![Version](https://img.shields.io/badge/version-3.1.0-green.svg?style=flat-square)](CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/version-3.2.0-green.svg?style=flat-square)](CHANGELOG.md)
6
6
  [![npm](https://img.shields.io/npm/v/cortexhawk?style=flat-square&color=red)](https://www.npmjs.com/package/cortexhawk)
7
7
  [![Skills](https://img.shields.io/badge/skills-36%20built--in%20%7C%2087k%2B%20via%20SkillsMP-orange.svg?style=flat-square)](https://skillsmp.com)
8
- [![Components](https://img.shields.io/badge/20%20agents%20%7C%2032%20commands%20%7C%209%20hooks%20%7C%207%20modes-purple.svg?style=flat-square)](#whats-inside)
8
+ [![Components](https://img.shields.io/badge/20%20agents%20%7C%2033%20commands%20%7C%209%20hooks%20%7C%207%20modes-purple.svg?style=flat-square)](#whats-inside)
9
9
 
10
10
  An open-source, community-driven development toolkit for Claude Code.
11
11
 
12
12
  CortexHawk provides a modular collection of optimized agents, skills, commands, hooks, and behavioral modes that transform Claude Code into a full-stack development team. Every prompt has been written for maximum efficiency — less token bloat, sharper instructions, better agent coordination.
13
13
 
14
- ### What's New in v3.1
14
+ ### What's New in v3.2
15
+
16
+ - **`/commit` command** — lightweight conventional commit + push without review or PR (use `/ship` for full workflow)
17
+ - **`--version` flag** — standard CLI version display
18
+ - **PR/commit templates** — auto-detected at install, generated if missing; agents read templates at runtime
19
+ - **Settings.json merge** — reinstall and `--update` now merge new hooks + permissions instead of skipping
20
+ - **Security hardening** — eliminated shell injection in all Python HEREDOC scripts, portable `sed -i`, input validation
21
+ - **`--init` wizard** — "Auto-detect" target option, improved multi-target support
22
+ - **15+ bug fixes** — see [CHANGELOG.md](CHANGELOG.md) for full details
23
+
24
+ <details>
25
+ <summary>v3.1 changes</summary>
15
26
 
16
27
  - **`npm install -g cortexhawk`** — available on npm, auto-resolves source from symlinked binary
17
28
  - **`cortexhawk` CLI wrapper** — clean subcommands (init, install, update, doctor, validate, search, snapshot, etc.) instead of `bash install.sh --flags`
18
29
  - **`--target auto`** — auto-detects installed CLIs (claude, kimi, codex) and installs for all found
19
30
  - **`cortexhawk validate`** — post-install diagnostic verifying skills/agents discovery per target
20
31
 
32
+ </details>
33
+
21
34
  <details>
22
35
  <summary>v3.0 changes</summary>
23
36
 
@@ -103,7 +116,7 @@ Specialized AI agents that coordinate together instead of working in silos.
103
116
  | `fullstack-developer` | Full-stack orchestration front+back |
104
117
  | `teacher` | Teaches concepts with 3 pedagogical levels (guided, mentor, professor) |
105
118
 
106
- ### Commands (32)
119
+ ### Commands (33)
107
120
 
108
121
  Slash commands for common workflows.
109
122
 
@@ -114,6 +127,7 @@ Slash commands for common workflows.
114
127
  | `/test` | Generate and run tests |
115
128
  | `/review` | Multi-agent code review |
116
129
  | `/ship` | Commit + PR pipeline |
130
+ | `/commit` | Lightweight commit + push (no review, no PR) |
117
131
  | `/debug` | Debug and fix issues |
118
132
  | `/scan` | Full security audit |
119
133
  | `/check` | Pre-commit quality gate (lint + test + scan + review → GO/NO-GO) |
@@ -313,7 +327,7 @@ Each target adapts components to the CLI's native format:
313
327
  | Component | Claude Code | Kimi CLI | Codex CLI |
314
328
  |---|---|---|---|
315
329
  | Agents (20) | `.claude/agents/*.md` | Skills (`/skill:agent-*`) + `AGENTS.md` | `AGENTS.md` |
316
- | Commands (32) | `.claude/commands/*.md` → `/plan` | Skills (`/skill:cmd-*`) | Skills (`$cmd-*`) |
330
+ | Commands (33) | `.claude/commands/*.md` → `/plan` | Skills (`/skill:cmd-*`) | Skills (`$cmd-*`) |
317
331
  | Skills (36) | `.claude/skills/` | `.kimi/skills/` (auto-discovered) | `.agents/skills/` |
318
332
  | Hooks (9) | `settings.json` (automatic) | Skills (`/skill:hook-*`, manual) | Dispatcher (partial) |
319
333
  | Modes (7) | `.claude/modes/` (native) | Skills (`/skill:modes/*`) | Skills (`$mode-*`) |
@@ -335,6 +349,7 @@ Each target adapts components to the CLI's native format:
335
349
  ./install.sh --update --dry-run # preview update delta
336
350
 
337
351
  # Diagnostics
352
+ ./install.sh --version # show CortexHawk version
338
353
  ./install.sh --doctor # check installation health
339
354
  ./install.sh --test-hooks # dry-run all hooks with synthetic inputs
340
355
  ./install.sh --stats # installation overview (version, counts)
@@ -49,6 +49,10 @@ Description: imperative mood, lowercase, no period, max 72 chars
49
49
  - Checklist: tests pass, no warnings, docs updated
50
50
  ```
51
51
 
52
+ ## Templates
53
+ - Before creating a PR, check for `.github/PULL_REQUEST_TEMPLATE.md` — if found, follow that format
54
+ - Before committing, check for `.gitmessage` — if found, follow that format for the commit message
55
+
52
56
  ## Rules
53
57
  - Atomic commits — one logical change per commit
54
58
  - Never force-push to shared branches
@@ -12,6 +12,7 @@ Activate the **project-manager** agent in backlog mode.
12
12
  3. Score: impact (H/M/L), effort (H/M/L), feasibility (H/M/L)
13
13
  4. Update `docs/backlog.md` — add new items, re-prioritize existing ones
14
14
  5. Mark items already implemented as done
15
+ 6. Run `bash scripts/refresh-context.sh` to update shared context
15
16
 
16
17
  Backlog format in `docs/backlog.md`:
17
18
 
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: commit
3
+ description: Conventional commit and push — lightweight alternative to /ship (no review, no PR).
4
+ ---
5
+
6
+ # /commit
7
+
8
+ Activate the **git-manager** agent. Commit: `$ARGUMENTS`
9
+
10
+ 1. Read `## Git Workflow` from CLAUDE.md if present — respect branching strategy, commit convention, and auto-push settings
11
+ 2. If `.gitmessage` exists, read it for commit message format guidance
12
+ 3. Review staged and unstaged changes, stage relevant files (never `git add -A`)
13
+ 4. Generate conventional commit message from changes — format: `type(scope): description`
14
+ 5. Commit with the generated message
15
+ 6. If auto-push is enabled, push to remote
16
+ 7. Show commit summary (hash, message, files changed)
17
+
18
+ ## Rules
19
+
20
+ - No review pass — use `/ship` for reviewed commits, `/commit` for quick iterations
21
+ - No PR creation — use `/ship` when a PR is needed
22
+ - Respect `.gitignore` — never stage `.env`, secrets, or debug artifacts
23
+ - If no changes to commit, report and stop
24
+ - If `$ARGUMENTS` is provided, use it as commit message context (not the literal message)
package/commands/ship.md CHANGED
@@ -11,7 +11,7 @@ Activate the **git-manager** agent, then the **reviewer** agent. Ship: `$ARGUMEN
11
11
  1. Stage changes and generate conventional commit message
12
12
  2. Run quick review pass — reviewer runs Pass 1 (Correctness) and Pass 2 (Security) only, reporting Critical findings exclusively
13
13
  3. If review passes, commit and push
14
- 4. Create PR with description, testing notes, and checklist
14
+ 4. Create PR if `.github/PULL_REQUEST_TEMPLATE.md` exists, follow that format; otherwise use: Summary, Changes, Test Plan, Checklist
15
15
  5. If review finds critical issues, report them and stop — don't ship broken code
16
16
 
17
17
  Format: `feat(scope): description` or `fix(scope): description`
package/commands/task.md CHANGED
@@ -16,6 +16,7 @@ Activate the **project-manager** agent as orchestrator. Execute backlog item `$A
16
16
  6. Update `CHANGELOG.md` with a one-line entry under the current version's `### Added` section
17
17
  7. If chain completes without critical blockers, execute `/ship`
18
18
  8. Mark item as `done` in backlog
19
+ 9. Run `bash scripts/refresh-context.sh` to update shared context
19
20
 
20
21
  ## Save Rules
21
22
 
@@ -44,14 +44,17 @@ if [ -d "docs/.context" ] && [ ! -L "docs/.context" ]; then
44
44
 
45
45
  echo "# Shared Context" > "$SHARED"
46
46
  echo "" >> "$SHARED"
47
- echo "_Auto-generated by session-start. Do not edit manually._" >> "$SHARED"
47
+ echo "_Auto-generated at $(date '+%Y-%m-%d %H:%M'). Snapshot from session start may be stale._" >> "$SHARED"
48
48
  echo "" >> "$SHARED"
49
49
 
50
50
  # Backlog summary
51
51
  if [ -f "docs/backlog.md" ]; then
52
- ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null || echo 0)
53
- DEFERRED=$(grep -c '| deferred |' docs/backlog.md 2>/dev/null || echo 0)
54
- DONE=$(grep -c '| done |' docs/backlog.md 2>/dev/null || echo 0)
52
+ ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null || true)
53
+ : "${ACTIVE:=0}"
54
+ DEFERRED=$(grep -c '| deferred |' docs/backlog.md 2>/dev/null || true)
55
+ : "${DEFERRED:=0}"
56
+ DONE=$(grep -c '| done |' docs/backlog.md 2>/dev/null || true)
57
+ : "${DONE:=0}"
55
58
  echo "## Backlog" >> "$SHARED"
56
59
  echo "- Active: $ACTIVE | Deferred: $DEFERRED | Done: $DONE" >> "$SHARED"
57
60
  # List active items
package/install.sh CHANGED
@@ -7,7 +7,14 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
7
  if [ -f ".env" ]; then
8
8
  while IFS='=' read -r key value; do
9
9
  [[ -z "$key" || "$key" == \#* ]] && continue
10
+ # Strip double quotes
10
11
  value="${value%\"}" && value="${value#\"}"
12
+ # Strip single quotes
13
+ value="${value%'}" && value="${value#'}"
14
+ # Strip inline comments (space + # marks start of comment)
15
+ value="$(printf '%s\n' "$value" | sed 's/[[:space:]]#.*$//')"
16
+ # Strip trailing whitespace
17
+ value="${value%"${value##*[! ]}"}"
11
18
  export "$key=$value" 2>/dev/null || true
12
19
  done < .env
13
20
  fi
@@ -20,6 +27,15 @@ get_version() {
20
27
  grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
21
28
  }
22
29
 
30
+ # Portable sed -i (GNU vs BSD)
31
+ sed_inplace() {
32
+ if sed --version 2>/dev/null | grep -q GNU; then
33
+ sed -i "$@"
34
+ else
35
+ sed -i '' "$@"
36
+ fi
37
+ }
38
+
23
39
  compute_checksum() {
24
40
  local file="$1"
25
41
  if command -v sha256sum >/dev/null 2>&1; then
@@ -106,6 +122,7 @@ print_usage() {
106
122
  echo " --dry-run Simulate install/update without writing files"
107
123
  echo " --test-hooks Dry-run all hooks with synthetic inputs"
108
124
  echo " --no-scan Skip post-install security audit"
125
+ echo " --version, -v Show CortexHawk version"
109
126
  echo " --help, -h Show this help"
110
127
  }
111
128
 
@@ -149,10 +166,18 @@ MAX_SNAPSHOTS=10
149
166
  while [ $# -gt 0 ]; do
150
167
  case "$1" in
151
168
  --target)
169
+ if [ -z "${2:-}" ]; then
170
+ echo "Error: --target requires a value (claude|kimi|codex|auto|all)"
171
+ exit 1
172
+ fi
152
173
  TARGET_CLI="$2"
153
174
  shift 2
154
175
  ;;
155
176
  --profile)
177
+ if [ -z "${2:-}" ]; then
178
+ echo "Error: --profile requires a value (fullstack|api|data)"
179
+ exit 1
180
+ fi
156
181
  PROFILE="$2"
157
182
  shift 2
158
183
  ;;
@@ -218,6 +243,10 @@ while [ $# -gt 0 ]; do
218
243
  ;;
219
244
  --restore)
220
245
  RESTORE_MODE=true
246
+ if [ -z "${2:-}" ]; then
247
+ echo "Error: --restore requires a snapshot path or --latest"
248
+ exit 1
249
+ fi
221
250
  SNAPSHOT_FILE="$2"
222
251
  if [ "$SNAPSHOT_FILE" = "--latest" ]; then
223
252
  if [ "$GLOBAL" = true ]; then
@@ -323,6 +352,10 @@ while [ $# -gt 0 ]; do
323
352
  fi
324
353
  shift 2
325
354
  ;;
355
+ --version|-v)
356
+ echo "CortexHawk $(get_version)"
357
+ exit 0
358
+ ;;
326
359
  --help|-h)
327
360
  print_usage
328
361
  exit 0
@@ -368,14 +401,6 @@ if [ "$RESTORE_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" =
368
401
  echo "Error: --restore cannot be combined with --init or --update"
369
402
  exit 1
370
403
  fi
371
- if [ "$TARGET_CLI" = "all" ] && [ "$INIT_MODE" = true ]; then
372
- echo "Error: --target all cannot be combined with --init (run --init per target instead)"
373
- exit 1
374
- fi
375
- if [ "$TARGET_CLI" = "auto" ] && [ "$INIT_MODE" = true ]; then
376
- echo "Error: --target auto cannot be combined with --init (run --init per target instead)"
377
- exit 1
378
- fi
379
404
  if [ -n "$ADD_SKILL_URL" ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
380
405
  echo "Error: --add-skill cannot be combined with --init, --update, --snapshot, or --restore"
381
406
  exit 1
@@ -486,6 +511,10 @@ copy_skills() {
486
511
  grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/' | while read -r skill; do
487
512
  local src="$SCRIPT_DIR/skills/$skill"
488
513
  local dest="$target_dir/skills/$skill"
514
+ if [ ! -d "$src" ]; then
515
+ yellow " Warning: skill '$skill' not found in source — skipping"
516
+ continue
517
+ fi
489
518
  mkdir -p "$(dirname "$dest")"
490
519
  cp -r "$src" "$dest" 2>/dev/null || true
491
520
  done
@@ -594,11 +623,34 @@ update_gitignore() {
594
623
  touch "$gitignore"
595
624
  fi
596
625
 
597
- # Auto-add target directory (always)
598
- if ! grep -qx "$target_dir/" "$gitignore" 2>/dev/null; then
599
- echo "" >> "$gitignore"
600
- echo "# CortexHawk" >> "$gitignore"
601
- echo "$target_dir/" >> "$gitignore"
626
+ # Ensure essential entries are present (disable glob to keep *.log literal)
627
+ local essentials="node_modules/ __pycache__/ .env .env.local *.log dist/ build/ .DS_Store"
628
+ local added_count=0
629
+ set -f
630
+ for entry in $essentials; do
631
+ if ! grep -qxF "$entry" "$gitignore" 2>/dev/null; then
632
+ echo "$entry" >> "$gitignore"
633
+ added_count=$((added_count + 1))
634
+ fi
635
+ done
636
+ set +f
637
+ if [ "$added_count" -gt 0 ]; then
638
+ green " Added $added_count essential entries to .gitignore (.env, node_modules/, etc.)"
639
+ fi
640
+
641
+ # Auto-add target directory (always), group under single header
642
+ if ! grep -qxF "$target_dir/" "$gitignore" 2>/dev/null; then
643
+ if grep -q "# CortexHawk" "$gitignore" 2>/dev/null; then
644
+ # Append under the first existing header only
645
+ local header_line
646
+ header_line=$(grep -n "# CortexHawk" "$gitignore" | head -1 | cut -d: -f1)
647
+ sed_inplace "${header_line}a\\
648
+ $target_dir/" "$gitignore"
649
+ else
650
+ echo "" >> "$gitignore"
651
+ echo "# CortexHawk" >> "$gitignore"
652
+ echo "$target_dir/" >> "$gitignore"
653
+ fi
602
654
  green " Added $target_dir/ to .gitignore"
603
655
  fi
604
656
 
@@ -619,6 +671,52 @@ update_gitignore() {
619
671
  fi
620
672
  }
621
673
 
674
+ # --- setup_templates() ---
675
+ # Detects existing PR/commit templates; generates CortexHawk defaults if missing
676
+ # Args: $1=project_root
677
+ setup_templates() {
678
+ local project_root="$1"
679
+
680
+ [ "$GLOBAL" = true ] && return
681
+
682
+ # PR template
683
+ local pr_template=""
684
+ for path in ".github/PULL_REQUEST_TEMPLATE.md" ".github/pull_request_template.md" "docs/pull_request_template.md"; do
685
+ if [ -f "$project_root/$path" ]; then
686
+ pr_template="$path"
687
+ break
688
+ fi
689
+ done
690
+ # Also check template directory
691
+ if [ -z "$pr_template" ] && [ -d "$project_root/.github/PULL_REQUEST_TEMPLATE" ]; then
692
+ pr_template=".github/PULL_REQUEST_TEMPLATE/"
693
+ fi
694
+
695
+ if [ -n "$pr_template" ]; then
696
+ green " PR template found: $pr_template"
697
+ else
698
+ mkdir -p "$project_root/.github"
699
+ cp "$SCRIPT_DIR/templates/github/PULL_REQUEST_TEMPLATE.md" "$project_root/.github/PULL_REQUEST_TEMPLATE.md"
700
+ green " PR template created: .github/PULL_REQUEST_TEMPLATE.md"
701
+ fi
702
+
703
+ # Commit template
704
+ local commit_template=""
705
+ for path in ".gitmessage" ".github/gitmessage" ".github/commit-template"; do
706
+ if [ -f "$project_root/$path" ]; then
707
+ commit_template="$path"
708
+ break
709
+ fi
710
+ done
711
+
712
+ if [ -n "$commit_template" ]; then
713
+ green " Commit template found: $commit_template"
714
+ else
715
+ cp "$SCRIPT_DIR/templates/github/gitmessage" "$project_root/.gitmessage"
716
+ green " Commit template created: .gitmessage"
717
+ fi
718
+ }
719
+
622
720
  # --- run_audit() ---
623
721
  run_audit() {
624
722
  local project_root="$1"
@@ -641,11 +739,12 @@ generate_hooks_config() {
641
739
  fi
642
740
 
643
741
  if ! command -v python3 >/dev/null 2>&1; then
742
+ yellow " Warning: python3 not found — hooks config will use static fallback" >&2
644
743
  return 1
645
744
  fi
646
745
 
647
- python3 << PYEOF
648
- import re, json, sys
746
+ python3 - "$compose_file" "$hooks_dir" << 'PYEOF'
747
+ import re, json, sys, shlex
649
748
 
650
749
  def parse_compose(path, hooks_dir):
651
750
  """Parse compose.yml and generate Claude Code hooks JSON."""
@@ -679,10 +778,13 @@ def parse_compose(path, hooks_dir):
679
778
  # Hook item
680
779
  elif line.strip().startswith('- '):
681
780
  hook_name = line.strip()[2:].strip()
781
+ if not re.match(r'^[a-zA-Z0-9_-]+$', hook_name):
782
+ print(f"Warning: skipping invalid hook name: {hook_name}", file=sys.stderr)
783
+ continue
682
784
  hook_path = f"{hooks_dir}/{hook_name}.sh"
683
785
 
684
786
  # Build command — hooks read stdin JSON (Claude Code protocol)
685
- cmd = f'bash {hook_path}'
787
+ cmd = f'bash {shlex.quote(hook_path)}'
686
788
 
687
789
  hook_entry = {"type": "command", "command": cmd}
688
790
 
@@ -708,7 +810,7 @@ def parse_compose(path, hooks_dir):
708
810
  return result
709
811
 
710
812
  try:
711
- result = parse_compose("$compose_file", "$hooks_dir")
813
+ result = parse_compose(sys.argv[1], sys.argv[2])
712
814
  print(json.dumps(result, indent=2))
713
815
  except Exception as e:
714
816
  print(f"Error: {e}", file=sys.stderr)
@@ -1176,30 +1278,40 @@ do_update() {
1176
1278
  # Make hooks executable
1177
1279
  chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
1178
1280
 
1179
- # 7b. Regenerate settings.json hooks section from compose.yml
1281
+ # 7b. Regenerate settings.json hooks + merge new permissions from compose.yml
1180
1282
  if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
1181
1283
  local hooks_json
1182
1284
  hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
1183
- if [ -n "$hooks_json" ]; then
1184
- python3 << PYEOF
1285
+ if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
1286
+ echo "$hooks_json" | python3 -c "
1185
1287
  import json, sys
1186
-
1187
- # Read current settings.json to preserve permissions
1288
+ hooks = json.load(sys.stdin)
1188
1289
  current = {}
1189
1290
  try:
1190
- with open('$TARGET/settings.json') as f:
1291
+ with open(sys.argv[1]) as f:
1191
1292
  current = json.load(f)
1192
1293
  except:
1193
1294
  pass
1194
-
1195
- # Merge: keep existing permissions, replace hooks
1196
- hooks = json.loads('''$hooks_json''')
1197
1295
  current['hooks'] = hooks
1198
-
1199
- with open('$TARGET/settings.json', 'w') as f:
1296
+ # Merge new permissions from source
1297
+ try:
1298
+ with open(sys.argv[2]) as f:
1299
+ src_perms = json.load(f).get('permissions', {})
1300
+ cur_perms = current.get('permissions', {})
1301
+ for key in ('allow', 'deny'):
1302
+ src_list = src_perms.get(key, [])
1303
+ cur_list = cur_perms.get(key, [])
1304
+ added = [p for p in src_list if p not in cur_list]
1305
+ if added:
1306
+ cur_list.extend(added)
1307
+ cur_perms[key] = cur_list
1308
+ current['permissions'] = cur_perms
1309
+ except:
1310
+ pass
1311
+ with open(sys.argv[1], 'w') as f:
1200
1312
  json.dump(current, f, indent=2)
1201
1313
  f.write('\n')
1202
- PYEOF
1314
+ " "$TARGET/settings.json" "$SCRIPT_DIR/settings.json"
1203
1315
  echo " Regenerated settings.json hooks from compose.yml"
1204
1316
  fi
1205
1317
  fi
@@ -1214,6 +1326,7 @@ PYEOF
1214
1326
  local target_dir_name=".${TARGET_CLI:-claude}"
1215
1327
  update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
1216
1328
  [ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
1329
+ setup_templates "$(dirname "$TARGET")"
1217
1330
 
1218
1331
  if [ ! -f "$TARGET/git-workflow.conf" ]; then
1219
1332
  GIT_BRANCHING="direct-main"
@@ -1313,12 +1426,10 @@ do_snapshot() {
1313
1426
  git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
1314
1427
  fi
1315
1428
 
1316
- # Read custom profile if applicable
1429
+ # Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
1317
1430
  local profile_def="null"
1318
- local custom_profile
1319
- custom_profile=$(ls /tmp/cortexhawk-custom-*.json 2>/dev/null | head -1)
1320
- if [ -n "$custom_profile" ] && [ -f "$custom_profile" ]; then
1321
- profile_def=$(cat "$custom_profile")
1431
+ if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
1432
+ profile_def=$(cat "$PROFILE_FILE")
1322
1433
  fi
1323
1434
 
1324
1435
  # Build files checksums from manifest
@@ -1369,7 +1480,7 @@ do_snapshot() {
1369
1480
  [ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
1370
1481
  [ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
1371
1482
  # Remove trailing comma from last entry
1372
- sed -i '$ s/,$//' "$snap_file"
1483
+ sed_inplace '$ s/,$//' "$snap_file"
1373
1484
  printf ' }\n' >> "$snap_file"
1374
1485
  printf '}\n' >> "$snap_file"
1375
1486
 
@@ -1875,15 +1986,15 @@ do_restore() {
1875
1986
  if command -v python3 >/dev/null 2>&1; then
1876
1987
  python3 -c "
1877
1988
  import json, sys
1878
- with open('$snap_file') as f:
1989
+ with open(sys.argv[1]) as f:
1879
1990
  snap = json.load(f)
1880
1991
  settings = snap.get('settings')
1881
1992
  if settings is not None:
1882
- with open('$TARGET/settings.json', 'w') as f:
1993
+ with open(sys.argv[2], 'w') as f:
1883
1994
  json.dump(settings, f, indent=2)
1884
1995
  f.write('\n')
1885
1996
  print(' Restored settings.json')
1886
- "
1997
+ " "$snap_file" "$TARGET/settings.json"
1887
1998
  else
1888
1999
  echo " Warning: python3 not found — settings.json not restored from snapshot"
1889
2000
  fi
@@ -1909,23 +2020,24 @@ if settings is not None:
1909
2020
  if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
1910
2021
  python3 -c "
1911
2022
  import json, base64, sys, os
1912
- with open('$snap_file') as f:
2023
+ snap_file, target_dir = sys.argv[1], sys.argv[2]
2024
+ with open(snap_file) as f:
1913
2025
  snap = json.load(f)
1914
2026
  contents = snap.get('file_contents', {})
1915
2027
  for filename, b64data in contents.items():
1916
2028
  try:
1917
2029
  data = base64.b64decode(b64data).decode('utf-8')
1918
2030
  if filename == 'CLAUDE.md':
1919
- target_path = os.path.dirname('$TARGET') + '/CLAUDE.md'
2031
+ target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
1920
2032
  else:
1921
- target_path = '$TARGET/' + filename
2033
+ target_path = target_dir + '/' + filename
1922
2034
  os.makedirs(os.path.dirname(target_path), exist_ok=True)
1923
2035
  with open(target_path, 'w') as f:
1924
2036
  f.write(data)
1925
2037
  print(f' Restored {filename} (from file_contents)')
1926
2038
  except Exception as e:
1927
2039
  print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
1928
- "
2040
+ " "$snap_file" "$TARGET"
1929
2041
  fi
1930
2042
 
1931
2043
  # Write new manifest
@@ -2312,14 +2424,14 @@ do_list_hooks() {
2312
2424
  printf " %-20s %-14s %-8s %s\n" "Name" "Event" "Status" "Description"
2313
2425
  printf " %-20s %-14s %-8s %s\n" "----" "-----" "------" "-----------"
2314
2426
  # Parse hooks.json with python3 for reliable JSON handling
2315
- python3 << PYEOF
2316
- import json
2317
- with open("$hooks_json") as f:
2427
+ python3 - "$hooks_json" "$compose_file" << 'PYEOF'
2428
+ import json, sys
2429
+ with open(sys.argv[1]) as f:
2318
2430
  data = json.load(f)
2319
2431
  # Read compose.yml to detect disabled hooks (commented out)
2320
2432
  disabled = set()
2321
2433
  try:
2322
- with open("$compose_file") as f:
2434
+ with open(sys.argv[2]) as f:
2323
2435
  for line in f:
2324
2436
  stripped = line.strip()
2325
2437
  if stripped.startswith("# - "):
@@ -2368,14 +2480,14 @@ do_toggle_hook() {
2368
2480
  echo "Hook '$hook_name' is already disabled"
2369
2481
  return 0
2370
2482
  fi
2371
- sed -i "s/^ - ${hook_name}$/ # - ${hook_name}/" "$compose_file"
2483
+ sed_inplace "s/^ - ${hook_name}$/ # - ${hook_name}/" "$compose_file"
2372
2484
  echo "Disabled hook: $hook_name"
2373
2485
  else
2374
2486
  if grep -q "^ - ${hook_name}$" "$compose_file"; then
2375
2487
  echo "Hook '$hook_name' is already enabled"
2376
2488
  return 0
2377
2489
  fi
2378
- sed -i "s/^ # - ${hook_name}$/ - ${hook_name}/" "$compose_file"
2490
+ sed_inplace "s/^ # - ${hook_name}$/ - ${hook_name}/" "$compose_file"
2379
2491
  echo "Enabled hook: $hook_name"
2380
2492
  fi
2381
2493
  # Regenerate settings.json if target exists
@@ -2383,17 +2495,18 @@ do_toggle_hook() {
2383
2495
  if [ -d "$target" ] && [ -f "$target/settings.json" ]; then
2384
2496
  local hooks_json
2385
2497
  hooks_json=$(generate_hooks_config "$compose_file" ".claude/hooks")
2386
- if [ -n "$hooks_json" ]; then
2387
- python3 << PYEOF
2388
- import json
2389
- with open('$target/settings.json') as f:
2498
+ if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
2499
+ echo "$hooks_json" | python3 -c "
2500
+ import json, sys
2501
+ hooks = json.load(sys.stdin)
2502
+ with open(sys.argv[1]) as f:
2390
2503
  settings = json.load(f)
2391
- settings['hooks'] = json.loads('''$hooks_json''')
2392
- with open('$target/settings.json', 'w') as f:
2504
+ settings['hooks'] = hooks
2505
+ with open(sys.argv[1], 'w') as f:
2393
2506
  json.dump(settings, f, indent=2)
2394
2507
  f.write('\n')
2395
2508
  print(' Regenerated settings.json')
2396
- PYEOF
2509
+ " "$target/settings.json"
2397
2510
  fi
2398
2511
  fi
2399
2512
  }
@@ -2441,17 +2554,18 @@ _search_skillsmp() {
2441
2554
  fi
2442
2555
 
2443
2556
  # Parse JSON response with python3
2444
- python3 << PYEOF
2557
+ python3 - "$tmp_response" "$keyword" << 'PYEOF'
2445
2558
  import json, sys
2446
2559
  try:
2447
- with open("$tmp_response") as f:
2560
+ response_file, keyword = sys.argv[1], sys.argv[2]
2561
+ with open(response_file) as f:
2448
2562
  data = json.load(f)
2449
2563
  container = data.get("data", {})
2450
2564
  skills = container.get("skills", [])
2451
2565
  pagination = container.get("pagination", {})
2452
2566
  total = pagination.get("total", len(skills))
2453
2567
  if not skills:
2454
- print(" No skills found matching '$keyword'")
2568
+ print(f" No skills found matching '{keyword}'")
2455
2569
  print("")
2456
2570
  print(" Try broader terms or check https://skillsmp.com")
2457
2571
  sys.exit(0)
@@ -2474,7 +2588,7 @@ try:
2474
2588
  if len(parts) >= 2:
2475
2589
  print(f" Install: ./install.sh --add-skill {parts[0]}/{parts[1]}")
2476
2590
  break
2477
- print(f" Browse all: https://skillsmp.com/?q=$keyword")
2591
+ print(f" Browse all: https://skillsmp.com/?q={keyword}")
2478
2592
  except Exception as e:
2479
2593
  print(f" Error parsing response: {e}")
2480
2594
  print(" Falling back to local registry")
@@ -2841,26 +2955,78 @@ install_claude() {
2841
2955
  cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
2842
2956
  cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
2843
2957
 
2958
+ local hooks_json
2959
+ hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
2844
2960
  if [ ! -f "$TARGET/settings.json" ]; then
2845
- # Generate settings.json with hooks from compose.yml
2846
- local hooks_json
2847
- hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
2961
+ # Fresh install: generate settings.json from scratch
2848
2962
  if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
2849
- # Build settings.json with generated hooks
2850
- python3 -c "
2851
- import json
2852
- permissions = $(cat "$SCRIPT_DIR/settings.json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('permissions',{})))")
2853
- hooks = $hooks_json
2854
- with open('$TARGET/settings.json', 'w') as f:
2963
+ echo "$hooks_json" | python3 -c "
2964
+ import json, sys
2965
+ hooks = json.load(sys.stdin)
2966
+ with open(sys.argv[1]) as f:
2967
+ permissions = json.load(f).get('permissions', {})
2968
+ with open(sys.argv[2], 'w') as f:
2855
2969
  json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
2856
2970
  f.write('\n')
2857
- "
2971
+ " "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
2858
2972
  echo " Generated settings.json from hooks/compose.yml"
2859
2973
  else
2860
2974
  cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
2861
2975
  fi
2862
2976
  else
2863
- echo "settings.json already exists skipping (check manually for updates)"
2977
+ # Merge: preserve user customizations, add new hooks + permissions
2978
+ python3 -c "
2979
+ import json, sys, shutil, os
2980
+
2981
+ raw = sys.stdin.read().strip()
2982
+ hooks = json.loads(raw) if raw else {}
2983
+
2984
+ # Load current settings (tolerate corrupted JSON)
2985
+ try:
2986
+ with open(sys.argv[1]) as f:
2987
+ current = json.load(f)
2988
+ except Exception:
2989
+ backup = sys.argv[1] + '.bak'
2990
+ if os.path.isfile(sys.argv[1]):
2991
+ shutil.copy2(sys.argv[1], backup)
2992
+ print(f' Warning: settings.json corrupted — backed up to {os.path.basename(backup)}', file=sys.stderr)
2993
+ current = {}
2994
+
2995
+ try:
2996
+ with open(sys.argv[2]) as f:
2997
+ source = json.load(f)
2998
+ except Exception:
2999
+ source = {}
3000
+
3001
+ changes = []
3002
+
3003
+ # Merge hooks (regenerate from compose.yml)
3004
+ if hooks and hooks != {}:
3005
+ current['hooks'] = hooks
3006
+ changes.append('hooks regenerated')
3007
+
3008
+ # Merge permissions (union: keep user additions + add new from source)
3009
+ src_perms = source.get('permissions', {})
3010
+ cur_perms = current.get('permissions', {})
3011
+ for key in ('allow', 'deny'):
3012
+ src_list = src_perms.get(key, [])
3013
+ cur_list = cur_perms.get(key, [])
3014
+ added = [p for p in src_list if p not in cur_list]
3015
+ if added:
3016
+ cur_list.extend(added)
3017
+ changes.append(f'{len(added)} new {key} permission(s)')
3018
+ cur_perms[key] = cur_list
3019
+ current['permissions'] = cur_perms
3020
+
3021
+ with open(sys.argv[1], 'w') as f:
3022
+ json.dump(current, f, indent=2)
3023
+ f.write('\n')
3024
+
3025
+ if changes:
3026
+ print(' Merged settings.json: ' + ', '.join(changes))
3027
+ else:
3028
+ print(' settings.json up to date — no merge needed')
3029
+ " "$TARGET/settings.json" "$SCRIPT_DIR/settings.json" <<< "${hooks_json:-}"
2864
3030
  fi
2865
3031
 
2866
3032
  PROJECT_ROOT="$(dirname "$TARGET")"
@@ -2897,11 +3063,12 @@ with open('$TARGET/settings.json', 'w') as f:
2897
3063
 
2898
3064
  run_audit "$(dirname "$TARGET")"
2899
3065
  update_gitignore "$(dirname "$TARGET")" ".claude"
3066
+ setup_templates "$(dirname "$TARGET")"
2900
3067
 
2901
3068
  echo ""
2902
3069
  echo "CortexHawk installed successfully for Claude Code!"
2903
3070
  echo ""
2904
- echo " 32 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
3071
+ echo " 33 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
2905
3072
  echo ""
2906
3073
  do_quickstart
2907
3074
  echo ""
@@ -3749,7 +3916,6 @@ else
3749
3916
  install_codex
3750
3917
  ;;
3751
3918
  auto)
3752
- local detected
3753
3919
  detected=$(detect_installed_clis)
3754
3920
  if [ -z "$detected" ]; then
3755
3921
  echo "Error: no supported CLI found (claude, kimi, codex)"
@@ -3758,7 +3924,7 @@ else
3758
3924
  fi
3759
3925
  echo "Auto-detected CLIs: $detected"
3760
3926
  echo ""
3761
- local auto_count=0
3927
+ auto_count=0
3762
3928
  for cli in $detected; do
3763
3929
  [ $auto_count -gt 0 ] && echo "" && echo "---" && echo ""
3764
3930
  case "$cli" in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortexhawk",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Open-source development toolkit for Claude Code — optimized agents, skills, commands, hooks, and modes",
5
5
  "bin": {
6
6
  "cortexhawk": "./cortexhawk"
@@ -57,7 +57,11 @@ $SKILL_JSON
57
57
  ]
58
58
  }
59
59
  EOF
60
- SKILL_COUNT=$(echo "$SKILLS" | tr ',' '\n' | wc -l | tr -d ' ')
60
+ if [ -z "$SKILLS" ]; then
61
+ SKILL_COUNT=0
62
+ else
63
+ SKILL_COUNT=$(echo "$SKILLS" | tr ',' '\n' | wc -l | tr -d ' ')
64
+ fi
61
65
 
62
66
  # --- Stack snapshot ---
63
67
  if [ -d "docs/.context" ]; then
@@ -20,11 +20,13 @@ echo " 1) Claude Code (default) — fully supported"
20
20
  echo " 2) Kimi CLI (experimental)"
21
21
  echo " 3) Codex CLI (experimental)"
22
22
  echo " 4) All (install for all CLIs simultaneously)"
23
- read -r -p "Choice [1-4] (default: 1): " target_choice
23
+ echo " 5) Auto-detect (install for all CLIs found on system)"
24
+ read -r -p "Choice [1-5] (default: 1): " target_choice
24
25
  case "$target_choice" in
25
26
  2) TARGET_CLI="kimi" ;;
26
27
  3) TARGET_CLI="codex" ;;
27
28
  4) TARGET_CLI="all" ;;
29
+ 5) TARGET_CLI="auto" ;;
28
30
  *) TARGET_CLI="claude" ;;
29
31
  esac
30
32
  green " → $TARGET_CLI"
@@ -32,14 +34,19 @@ echo ""
32
34
 
33
35
  # --- Step 2: Scope ---
34
36
  bold "2. Install Scope"
35
- echo " 1) Local project install in current directory (default)"
36
- echo " 2) Global — install in ~/.${TARGET_CLI}/ (shared across all projects)"
37
- read -r -p "Choice [1-2] (default: 1): " scope_choice
38
- case "$scope_choice" in
39
- 2) GLOBAL=true ;;
40
- *) GLOBAL=false ;;
41
- esac
42
- green "$([ "$GLOBAL" = true ] && echo "global" || echo "local")"
37
+ if [ "$TARGET_CLI" = "all" ] || [ "$TARGET_CLI" = "auto" ]; then
38
+ GLOBAL=false
39
+ green " → local (always local for multi-target install)"
40
+ else
41
+ echo " 1) Local project — install in current directory (default)"
42
+ echo " 2) Global — install in ~/.${TARGET_CLI}/ (shared across all projects)"
43
+ read -r -p "Choice [1-2] (default: 1): " scope_choice
44
+ case "$scope_choice" in
45
+ 2) GLOBAL=true ;;
46
+ *) GLOBAL=false ;;
47
+ esac
48
+ green " → $([ "$GLOBAL" = true ] && echo "global" || echo "local")"
49
+ fi
43
50
  echo ""
44
51
 
45
52
  # --- Step 3: Skills ---
@@ -122,7 +129,7 @@ echo ""
122
129
  read -r -p " API key (press Enter to skip): " SKILLSMP_KEY
123
130
  if [ -n "$SKILLSMP_KEY" ]; then
124
131
  if [ -f ".env" ] && grep -q '^SKILLSMP_API_KEY=' .env 2>/dev/null; then
125
- sed -i "s/^SKILLSMP_API_KEY=.*/SKILLSMP_API_KEY=$SKILLSMP_KEY/" .env
132
+ sed_inplace "s/^SKILLSMP_API_KEY=.*/SKILLSMP_API_KEY=$SKILLSMP_KEY/" .env
126
133
  else
127
134
  echo "SKILLSMP_API_KEY=$SKILLSMP_KEY" >> .env
128
135
  fi
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # refresh-context.sh — Regenerate docs/.context/_shared.md mid-session
3
+ # Called by agents after modifying backlog, completing tasks, or committing code
4
+
5
+ [ -d "docs/.context" ] && [ ! -L "docs/.context" ] || exit 0
6
+
7
+ SHARED="docs/.context/_shared.md"
8
+
9
+ echo "# Shared Context" > "$SHARED"
10
+ echo "" >> "$SHARED"
11
+ echo "_Auto-generated at $(date '+%Y-%m-%d %H:%M'). Last refresh: $(date '+%H:%M:%S')._" >> "$SHARED"
12
+ echo "" >> "$SHARED"
13
+
14
+ # Backlog summary
15
+ if [ -f "docs/backlog.md" ]; then
16
+ ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null || true)
17
+ : "${ACTIVE:=0}"
18
+ DEFERRED=$(grep -c '| deferred |' docs/backlog.md 2>/dev/null || true)
19
+ : "${DEFERRED:=0}"
20
+ DONE=$(grep -c '| done |' docs/backlog.md 2>/dev/null || true)
21
+ : "${DONE:=0}"
22
+ echo "## Backlog" >> "$SHARED"
23
+ echo "- Active: $ACTIVE | Deferred: $DEFERRED | Done: $DONE" >> "$SHARED"
24
+ grep '| todo |' docs/backlog.md >> "$SHARED" 2>/dev/null || true
25
+ echo "" >> "$SHARED"
26
+ fi
27
+
28
+ # Recent commits
29
+ if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
30
+ echo "## Recent Commits" >> "$SHARED"
31
+ git log --oneline -5 2>/dev/null >> "$SHARED" || true
32
+ echo "" >> "$SHARED"
33
+ fi
34
+
35
+ # User context
36
+ if [ -f "docs/.context/_user.md" ]; then
37
+ cat "docs/.context/_user.md" >> "$SHARED"
38
+ echo "" >> "$SHARED"
39
+ fi
40
+
41
+ # Active warnings
42
+ echo "## Warnings" >> "$SHARED"
43
+ WARNINGS=0
44
+ if [ -f ".env.example" ] && [ ! -f ".env" ]; then
45
+ echo "- .env missing — copy from .env.example" >> "$SHARED"
46
+ WARNINGS=$((WARNINGS + 1))
47
+ fi
48
+ if [ "$WARNINGS" -eq 0 ]; then
49
+ echo "- None" >> "$SHARED"
50
+ fi
51
+ echo "" >> "$SHARED"