axel-setup 0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/README.md +518 -0
  5. package/agents/api-design.md +51 -0
  6. package/agents/bughunter.md +136 -0
  7. package/agents/changelog.md +89 -0
  8. package/agents/cleanup.md +126 -0
  9. package/agents/compare-branch.md +35 -0
  10. package/agents/cross-repo.md +97 -0
  11. package/agents/db-check.md +14 -0
  12. package/agents/debug.md +47 -0
  13. package/agents/deploy-check.md +100 -0
  14. package/agents/draft-message.md +19 -0
  15. package/agents/excelsior-coordinator.md +75 -0
  16. package/agents/excelsior-verifier.md +94 -0
  17. package/agents/feature.md +48 -0
  18. package/agents/harness-optimizer.md +40 -0
  19. package/agents/incident.md +48 -0
  20. package/agents/linear-task.md +18 -0
  21. package/agents/onboard.md +24 -0
  22. package/agents/perf.md +44 -0
  23. package/agents/production-validator.md +96 -0
  24. package/agents/review.md +113 -0
  25. package/agents/security-check.md +29 -0
  26. package/agents/sprint-summary.md +15 -0
  27. package/agents/tdd-mainder.md +178 -0
  28. package/agents/test-gen.md +39 -0
  29. package/axel-manifest.json +129 -0
  30. package/bin/axel-setup.js +597 -0
  31. package/bootstrap.sh +1087 -0
  32. package/commands/create-pr.md +13 -0
  33. package/commands/daily.md +182 -0
  34. package/commands/deslop.md +13 -0
  35. package/commands/draft-message.md +23 -0
  36. package/commands/eod-review.md +154 -0
  37. package/commands/execute-prp.md +37 -0
  38. package/commands/generate-prp.md +75 -0
  39. package/commands/multi-repo-feature.md +60 -0
  40. package/commands/roadmap.md +31 -0
  41. package/commands/sprint-status.md +486 -0
  42. package/commands/style.md +68 -0
  43. package/commands/visualize.md +17 -0
  44. package/docs/roadmap/multi-runtime.md +73 -0
  45. package/docs/superpowers/plans/2026-06-12-setup-hardening-roadmap.md +61 -0
  46. package/hooks/desktop-notify.sh +26 -0
  47. package/hooks/enforce-agent-model.jq +14 -0
  48. package/hooks/gsd-context-monitor.js +156 -0
  49. package/hooks/linear-lifecycle-sync.sh +112 -0
  50. package/hooks/memory-dedup.sh +122 -0
  51. package/hooks/memory-extractor.sh +218 -0
  52. package/hooks/post-commit-memory-trigger.sh +16 -0
  53. package/hooks/post-commit-verify.sh +41 -0
  54. package/hooks/post-edit-lint.sh +43 -0
  55. package/hooks/precompact-save-context.sh +124 -0
  56. package/hooks/priority-map-staleness.sh +29 -0
  57. package/hooks/proactive-resolver.sh +104 -0
  58. package/hooks/session-auto-title.sh +165 -0
  59. package/hooks/session-checkpoint.sh +97 -0
  60. package/hooks/session-cost-log.sh +77 -0
  61. package/hooks/session-log-action.sh +36 -0
  62. package/hooks/session-log-prompt.sh +25 -0
  63. package/hooks/session-restore.sh +45 -0
  64. package/hooks/session-save.sh +81 -0
  65. package/hooks/session-summarize.sh +154 -0
  66. package/hooks/validate-commit-format.sh +38 -0
  67. package/hooks/weekly-priority-map-review.sh +143 -0
  68. package/install.sh +46 -0
  69. package/package.json +67 -0
  70. package/scripts/ci/bootstrap-dry-run.sh +40 -0
  71. package/scripts/ci/check.sh +65 -0
  72. package/scripts/posthog-snapshot-loader.sh +112 -0
  73. package/skills/context-budget/SKILL.md +86 -0
  74. package/skills/memory-review/SKILL.md +100 -0
  75. package/skills/model-routing/SKILL.md +70 -0
  76. package/skills/posthog-weekly/SKILL.md +271 -0
  77. package/skills/ui-ux-pro-max/SKILL.md +377 -0
  78. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  79. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  80. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  81. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  82. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  83. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  84. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  85. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  86. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  87. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  88. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  89. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  90. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  91. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  92. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  93. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  94. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  95. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  96. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  97. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  98. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  99. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  100. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  101. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  102. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  103. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  104. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  105. package/templates/AGENTS.runtime.md +17 -0
  106. package/templates/CLAUDE.md +252 -0
  107. package/templates/claude-monitor.plist +35 -0
  108. package/templates/keybindings.json +13 -0
  109. package/templates/merge-settings.jq +53 -0
  110. package/templates/review-upgrades.md +44 -0
  111. package/templates/settings.json +255 -0
  112. package/templates/statusline-command.sh +182 -0
  113. package/tests/fixtures/hooks/events.json +32 -0
  114. package/tools/session-costs-view.sh +128 -0
  115. package/tools/session-dashboard-gen.sh +369 -0
  116. package/tools/session-live.sh +173 -0
  117. package/tools/session-server.js +441 -0
@@ -0,0 +1,61 @@
1
+ # AXEL Setup Hardening Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make AXEL publishable and safer for third-party installation by hardening bootstrap behavior, tests, release automation, and public roadmap hygiene.
6
+
7
+ **Architecture:** Keep `bootstrap.sh` as the installer entrypoint, but move validation coverage into shell-based integration tests under `tests/`. Extend `bin/axel-setup.js` with subcommands that do not change installation semantics unless explicitly requested.
8
+
9
+ **Tech Stack:** Bash, Node.js built-ins, jq, GitHub Actions, npm.
10
+
11
+ ---
12
+
13
+ ### Task 1: Bootstrap Safety And Assets
14
+
15
+ **Files:**
16
+ - Modify: `bootstrap.sh`
17
+ - Test: `tests/bootstrap-behavior.sh`
18
+
19
+ - [ ] Write tests that prove `--dry-run` does not create `~/.claude`, recursive skill assets install, and skip flags prevent plugin, GSD, and launchd side effects.
20
+ - [ ] Run `bash tests/bootstrap-behavior.sh` and verify the new assertions fail.
21
+ - [ ] Update `bootstrap.sh` so dry-run wraps all filesystem writes, skill directories copy recursively, and `--skip-plugins`, `--skip-gsd`, `--no-launchd`, and `--profile` are parsed.
22
+ - [ ] Re-run `bash tests/bootstrap-behavior.sh` and verify it passes.
23
+
24
+ ### Task 2: Profiles, Manifest, And Doctor
25
+
26
+ **Files:**
27
+ - Modify: `bootstrap.sh`
28
+ - Modify: `bin/axel-setup.js`
29
+ - Add: `axel-manifest.json`
30
+ - Test: `tests/cli-doctor.sh`
31
+
32
+ - [ ] Write tests that prove `axel-setup doctor --home <tmp>` detects installed files and missing files without mutating state.
33
+ - [ ] Run `bash tests/cli-doctor.sh` and verify it fails before implementation.
34
+ - [ ] Add manifest metadata for installable components and make bootstrap install the manifest into `~/.claude/axel-manifest.json`.
35
+ - [ ] Extend `bin/axel-setup.js` with `doctor`, `--help`, and bootstrap passthrough behavior.
36
+ - [ ] Re-run `bash tests/cli-doctor.sh` and verify it passes.
37
+
38
+ ### Task 3: CI, Release, And Docs
39
+
40
+ **Files:**
41
+ - Modify: `scripts/ci/check.sh`
42
+ - Modify: `.github/workflows/ci.yml`
43
+ - Add: `.github/workflows/release.yml`
44
+ - Modify: `README.md`
45
+ - Modify: `CHANGELOG.md`
46
+
47
+ - [ ] Add checks for shfmt and shellcheck when available, without making local machines fail if the tools are absent.
48
+ - [ ] Add GitHub Actions installation for shellcheck and shfmt.
49
+ - [ ] Add npm provenance release workflow triggered by `v*` tags.
50
+ - [ ] Update README to make `npx axel-setup` the primary path after npm publish and document profiles and skip flags.
51
+ - [ ] Run `npm run check` and `npm run publish:dry-run`.
52
+
53
+ ### Task 4: GitHub Roadmap Hygiene
54
+
55
+ **Files:**
56
+ - No repo file changes required.
57
+
58
+ - [ ] Create labels for roadmap phases and feature classes.
59
+ - [ ] Create milestone `v0.2.0 Setup Hardening`.
60
+ - [ ] Open public issues for release automation, installer one-liner, OSS core separation, harness tests, upgrade workflow, and metrics.
61
+ - [ ] Push branch and open PR.
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # Desktop notification ONLY when Claude Code CLI finishes a main response
3
+ # Ignores: subagents, Cursor, other AI tools
4
+
5
+ # Guard: only notify from Claude Code CLI — skip Cursor, VS Code, and any non-CLI entrypoint
6
+ if [ "$CLAUDE_CODE_ENTRYPOINT" != "cli" ]; then
7
+ exit 0
8
+ fi
9
+
10
+ # Guard: skip if this is a subagent (spawned by Agent tool)
11
+ # Subagents have CLAUDE_AGENT_ID or run in /tmp worktrees
12
+ if [ -n "$CLAUDE_AGENT_ID" ]; then
13
+ exit 0
14
+ fi
15
+ if echo "$(pwd)" | grep -qE '/tmp/claude|\.worktrees/'; then
16
+ exit 0
17
+ fi
18
+
19
+ # Only notify if terminal is NOT the active app
20
+ ACTIVE_APP=$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null)
21
+
22
+ if [[ "$ACTIVE_APP" != "Terminal" ]] && [[ "$ACTIVE_APP" != "iTerm2" ]] && [[ "$ACTIVE_APP" != "Ghostty" ]] && [[ "$ACTIVE_APP" != "WezTerm" ]] && [[ "$ACTIVE_APP" != "kitty" ]] && [[ "$ACTIVE_APP" != "Alacritty" ]]; then
23
+ osascript -e 'display notification "Claude terminó de responder" with title "Claude Code" sound name "Glass"' 2>/dev/null
24
+ fi
25
+
26
+ exit 0
@@ -0,0 +1,14 @@
1
+ # PreToolUse hook filter (matcher: Agent). Blocks Agent calls without an explicit model param.
2
+ # Companion to ~/.claude/skills/model-routing/SKILL.md: without model, subagents inherit the
3
+ # expensive session model (Fable). Emits a deny decision so the model retries with model set.
4
+ if (.tool_input.model // "") == "" then
5
+ {
6
+ hookSpecificOutput: {
7
+ hookEventName: "PreToolUse",
8
+ permissionDecision: "deny",
9
+ permissionDecisionReason: "Agent call blocked: missing required model param (subagents inherit the expensive session model otherwise). Retry the SAME Agent call adding model per the model-routing skill: sonnet (exploration, research, bounded implementation), haiku (mechanical reduction, log/test triage, inventories), opus (risky implementation, code review, adversarial verification). Use fable only if judgment itself must run in a subagent."
10
+ }
11
+ }
12
+ else
13
+ empty
14
+ end
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // gsd-hook-version: 1.30.0
3
+ // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
4
+ // Reads context metrics from the statusline bridge file and injects
5
+ // warnings when context usage is high. This makes the AGENT aware of
6
+ // context limits (the statusline only shows the user).
7
+ //
8
+ // How it works:
9
+ // 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
10
+ // 2. This hook reads those metrics after each tool use
11
+ // 3. When remaining context drops below thresholds, it injects a warning
12
+ // as additionalContext, which the agent sees in its conversation
13
+ //
14
+ // Thresholds:
15
+ // WARNING (remaining <= 35%): Agent should wrap up current task
16
+ // CRITICAL (remaining <= 25%): Agent should stop immediately and save state
17
+ //
18
+ // Debounce: 5 tool uses between warnings to avoid spam
19
+ // Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const WARNING_THRESHOLD = 15; // remaining_percentage <= 15% (lowered for 1M context)
26
+ const CRITICAL_THRESHOLD = 8; // remaining_percentage <= 8%
27
+ const STALE_SECONDS = 60; // ignore metrics older than 60s
28
+ const DEBOUNCE_CALLS = 10; // min tool uses between warnings
29
+
30
+ let input = '';
31
+ // Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
32
+ // Windows/Git Bash, or slow Claude Code piping during large outputs),
33
+ // exit silently instead of hanging until Claude Code kills the process
34
+ // and reports "hook error". See #775, #1162.
35
+ const stdinTimeout = setTimeout(() => process.exit(0), 10000);
36
+ process.stdin.setEncoding('utf8');
37
+ process.stdin.on('data', chunk => input += chunk);
38
+ process.stdin.on('end', () => {
39
+ clearTimeout(stdinTimeout);
40
+ try {
41
+ const data = JSON.parse(input);
42
+ const sessionId = data.session_id;
43
+
44
+ if (!sessionId) {
45
+ process.exit(0);
46
+ }
47
+
48
+ // Check if context warnings are disabled via config
49
+ const cwd = data.cwd || process.cwd();
50
+ const configPath = path.join(cwd, '.planning', 'config.json');
51
+ if (fs.existsSync(configPath)) {
52
+ try {
53
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
54
+ if (config.hooks?.context_warnings === false) {
55
+ process.exit(0);
56
+ }
57
+ } catch (e) {
58
+ // Ignore config parse errors
59
+ }
60
+ }
61
+
62
+ const tmpDir = os.tmpdir();
63
+ const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
64
+
65
+ // If no metrics file, this is a subagent or fresh session -- exit silently
66
+ if (!fs.existsSync(metricsPath)) {
67
+ process.exit(0);
68
+ }
69
+
70
+ const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
71
+ const now = Math.floor(Date.now() / 1000);
72
+
73
+ // Ignore stale metrics
74
+ if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
75
+ process.exit(0);
76
+ }
77
+
78
+ const remaining = metrics.remaining_percentage;
79
+ const usedPct = metrics.used_pct;
80
+
81
+ // No warning needed
82
+ if (remaining > WARNING_THRESHOLD) {
83
+ process.exit(0);
84
+ }
85
+
86
+ // Debounce: check if we warned recently
87
+ const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
88
+ let warnData = { callsSinceWarn: 0, lastLevel: null };
89
+ let firstWarn = true;
90
+
91
+ if (fs.existsSync(warnPath)) {
92
+ try {
93
+ warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
94
+ firstWarn = false;
95
+ } catch (e) {
96
+ // Corrupted file, reset
97
+ }
98
+ }
99
+
100
+ warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
101
+
102
+ const isCritical = remaining <= CRITICAL_THRESHOLD;
103
+ const currentLevel = isCritical ? 'critical' : 'warning';
104
+
105
+ // Emit immediately on first warning, then debounce subsequent ones
106
+ // Severity escalation (WARNING -> CRITICAL) bypasses debounce
107
+ const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
108
+ if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
109
+ // Update counter and exit without warning
110
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
111
+ process.exit(0);
112
+ }
113
+
114
+ // Reset debounce counter
115
+ warnData.callsSinceWarn = 0;
116
+ warnData.lastLevel = currentLevel;
117
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
118
+
119
+ // Detect if GSD is active (has .planning/STATE.md in working directory)
120
+ const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
121
+
122
+ // Build advisory warning message (never use imperative commands that
123
+ // override user preferences — see #884)
124
+ let message;
125
+ if (isCritical) {
126
+ message = isGsdActive
127
+ ? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
128
+ 'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
129
+ 'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
130
+ '/gsd:pause-work at the next natural stopping point.'
131
+ : `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
132
+ 'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
133
+ 'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
134
+ } else {
135
+ message = isGsdActive
136
+ ? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
137
+ 'Context is getting limited. Avoid starting new complex work. If not between ' +
138
+ 'defined plan steps, inform the user so they can prepare to pause.'
139
+ : `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
140
+ 'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
141
+ 'starting new complex work.';
142
+ }
143
+
144
+ const output = {
145
+ hookSpecificOutput: {
146
+ hookEventName: process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse",
147
+ additionalContext: message
148
+ }
149
+ };
150
+
151
+ process.stdout.write(JSON.stringify(output));
152
+ } catch (e) {
153
+ // Silent fail -- never block tool execution
154
+ process.exit(0);
155
+ }
156
+ });
@@ -0,0 +1,112 @@
1
+ #!/bin/bash
2
+ # linear-lifecycle-sync.sh
3
+ # Auto-syncs Linear card state based on git/gh actions.
4
+ # Runs as PostToolUse Bash hook (async).
5
+ #
6
+ # Requirements:
7
+ # - Linear MCP server connected (linear-server)
8
+ # - claude CLI available (for the haiku sub-call)
9
+ # - Tickets in commit messages follow the pattern: KEY-123
10
+ # (configure TICKET_PATTERN below to match your team's prefix)
11
+ #
12
+ # Behavior:
13
+ # git commit (KEY-123) → In Progress (if currently Todo/Backlog)
14
+ # gh pr create → In Review
15
+ # gh pr merge → Done
16
+ #
17
+ # Bootstrap substitution: set REPO_PATH_FILTER to a regex matching your
18
+ # repo paths so the hook only fires in those directories.
19
+ # Example: '/home/user/projects/mycompany/'
20
+ # Leave empty ("") to run in all directories.
21
+ REPO_PATH_FILTER="{{REPO_PATH_FILTER}}"
22
+ case "$REPO_PATH_FILTER" in "{{"*"}}"|"") REPO_PATH_FILTER="" ;; esac
23
+
24
+ TICKET_PATTERN="{{TICKET_PATTERN}}"
25
+ case "$TICKET_PATTERN" in "{{"*"}}"|"") TICKET_PATTERN="[A-Z]+-[0-9]+" ;; esac
26
+
27
+ LINEAR_TEAM="{{LINEAR_TEAM}}"
28
+ case "$LINEAR_TEAM" in "{{"*"}}"|"") LINEAR_TEAM="your team" ;; esac
29
+
30
+ # Guard against recursive invocation (this script spawns claude -p)
31
+ if [ -n "$CLAUDE_LINEAR_SYNC" ]; then exit 0; fi
32
+
33
+ INPUT=$(cat)
34
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
35
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
36
+
37
+ # Only run in configured repos (skip if filter is empty — runs everywhere)
38
+ if [ -n "$REPO_PATH_FILTER" ] && ! echo "$CWD" | grep -qE "$REPO_PATH_FILTER"; then exit 0; fi
39
+
40
+ # --- Detect action type ---
41
+ ACTION=""
42
+ TICKETS=""
43
+
44
+ if echo "$COMMAND" | grep -qE '(^|[[:space:]&;|(])git[[:space:]]+commit([[:space:]]|$)'; then
45
+ ACTION="in_progress"
46
+ TICKETS=$(echo "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
47
+
48
+ elif echo "$COMMAND" | grep -qE '(^|[[:space:]&;|(])gh[[:space:]]+pr[[:space:]]+create'; then
49
+ ACTION="in_review"
50
+ TICKETS=$(echo "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
51
+ if [ -z "$TICKETS" ]; then
52
+ TICKETS=$(git -C "$CWD" log --not --remotes --pretty=format:"%s %b" 2>/dev/null \
53
+ | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
54
+ fi
55
+
56
+ elif echo "$COMMAND" | grep -qE '(^|[[:space:]&;|(])gh[[:space:]]+pr[[:space:]]+merge'; then
57
+ ACTION="done"
58
+ TICKETS=$(echo "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
59
+ if [ -z "$TICKETS" ]; then
60
+ TICKETS=$(git -C "$CWD" log --not --remotes --pretty=format:"%s %b" 2>/dev/null \
61
+ | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
62
+ fi
63
+ fi
64
+
65
+ [ -z "$ACTION" ] && exit 0
66
+ [ -z "$TICKETS" ] && exit 0
67
+
68
+ # --- Rate limit: 90s debounce per action type ---
69
+ RATE_FILE="$HOME/.claude/.linear_sync_${ACTION}"
70
+ if [ -f "$RATE_FILE" ]; then
71
+ LAST=$(stat -f %m "$RATE_FILE" 2>/dev/null || stat -c %Y "$RATE_FILE" 2>/dev/null)
72
+ NOW=$(date +%s)
73
+ if [ -n "$LAST" ] && [ $((NOW - LAST)) -lt 90 ]; then exit 0; fi
74
+ fi
75
+ touch "$RATE_FILE"
76
+
77
+ # --- Determine target state label ---
78
+ case "$ACTION" in
79
+ in_progress) TARGET="In Progress" ;;
80
+ in_review) TARGET="In Review" ;;
81
+ done) TARGET="Done" ;;
82
+ *) exit 0 ;;
83
+ esac
84
+
85
+ PROMPT="You are a Linear lifecycle sync agent. Update card states silently and efficiently. No explanations, no headers — just one line per ticket.
86
+
87
+ Tickets to process: $TICKETS
88
+ Target state: $TARGET
89
+ Team: $LINEAR_TEAM
90
+
91
+ Instructions:
92
+ 1. Call list_issue_statuses to find the state ID for '$TARGET' in the $LINEAR_TEAM team.
93
+ 2. For each ticket, call get_issue to check its current state name.
94
+ 3. Skip rules:
95
+ - If target is 'In Progress' and current state is already In Progress, In Review, or Done → skip.
96
+ - If target is 'In Review' and current state is already In Review or Done → skip.
97
+ - If target is 'Done' and current state is already Done → skip.
98
+ 4. For tickets that need updating, call save_issue with the new stateId.
99
+ 5. Output format (one line per ticket):
100
+ KEY-123 → In Progress
101
+ KEY-456: already In Review, skipped"
102
+
103
+ HOOK_TMP=$(mktemp -d 2>/dev/null)
104
+ REAL_TMP=$(cd "$HOOK_TMP" 2>/dev/null && pwd -P)
105
+ RESULT=$(cd "$HOOK_TMP" 2>/dev/null && printf '%s' "$PROMPT" | CLAUDE_LINEAR_SYNC=1 claude -p --model haiku 2>/dev/null)
106
+ GHOST_SLUG=$(echo "$REAL_TMP" | sed 's|[^a-zA-Z0-9]|-|g')
107
+ rm -rf "$HOOK_TMP" "$HOME/.claude/projects/${GHOST_SLUG}" 2>/dev/null
108
+
109
+ mkdir -p "$HOME/.claude/logs"
110
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$ACTION] $TICKETS → $RESULT" >> "$HOME/.claude/logs/linear-sync.log"
111
+
112
+ exit 0
@@ -0,0 +1,122 @@
1
+ #!/bin/zsh
2
+ # Memory dedup check — hash-based duplicate detection + orphan cleanup
3
+ # Inspired by Claw Code's hash-based instruction file dedup.
4
+ # Uses zsh for associative array support on macOS.
5
+ #
6
+ # Run standalone: zsh memory-dedup.sh
7
+ # Exit codes: 0 = clean/fixed
8
+
9
+ MEMORY_DIR="${MEMORY_DIR:-$HOME/.claude/memory}"
10
+ MEMORY_INDEX="$MEMORY_DIR/MEMORY.md"
11
+ LOG_FILE="$HOME/.claude/logs/memory-dedup.log"
12
+ QUIET="${QUIET:-false}"
13
+
14
+ mkdir -p "$(dirname "$LOG_FILE")"
15
+
16
+ log() { echo "[$(date +%H:%M:%S)] $1" >> "$LOG_FILE"; [[ "$QUIET" == "true" ]] || echo "$1"; }
17
+
18
+ found_issues=0
19
+
20
+ # --- 1. Content-based duplicate detection ---
21
+ typeset -A hash_map
22
+
23
+ for f in "$MEMORY_DIR"/*.md(N); do
24
+ [[ ! -f "$f" ]] && continue
25
+ local BASENAME="${f:t}"
26
+ [[ "$BASENAME" == "MEMORY.md" ]] && continue
27
+
28
+ local BODY=$(awk 'BEGIN{c=0} /^---$/{c++; next} c>=2{print}' "$f")
29
+ [[ -z "$BODY" ]] && continue
30
+ local HASH=$(echo "$BODY" | md5 -q)
31
+
32
+ if [[ -n "${hash_map[$HASH]}" ]]; then
33
+ local EXISTING="${hash_map[$HASH]}"
34
+ log "DUPLICATE: $BASENAME has identical content to $EXISTING"
35
+
36
+ if grep -qF "$EXISTING" "$MEMORY_INDEX" 2>/dev/null; then
37
+ log " -> Removing $BASENAME (keeping indexed $EXISTING)"
38
+ rm "$f"
39
+ found_issues=1
40
+ elif grep -qF "$BASENAME" "$MEMORY_INDEX" 2>/dev/null; then
41
+ log " -> Removing $EXISTING (keeping indexed $BASENAME)"
42
+ rm "$MEMORY_DIR/$EXISTING"
43
+ hash_map[$HASH]="$BASENAME"
44
+ found_issues=1
45
+ else
46
+ if [[ "$f" -nt "$MEMORY_DIR/$EXISTING" ]]; then
47
+ log " -> Removing $EXISTING (keeping newer $BASENAME)"
48
+ rm "$MEMORY_DIR/$EXISTING"
49
+ hash_map[$HASH]="$BASENAME"
50
+ else
51
+ log " -> Removing $BASENAME (keeping newer $EXISTING)"
52
+ rm "$f"
53
+ fi
54
+ found_issues=1
55
+ fi
56
+ else
57
+ hash_map[$HASH]="$BASENAME"
58
+ fi
59
+ done
60
+
61
+ # --- 2. Semantic duplicate detection (same frontmatter name) ---
62
+ typeset -A name_map
63
+
64
+ for f in "$MEMORY_DIR"/*.md(N); do
65
+ [[ ! -f "$f" ]] && continue
66
+ local BASENAME="${f:t}"
67
+ [[ "$BASENAME" == "MEMORY.md" ]] && continue
68
+
69
+ local NAME=$(awk '/^name:/{sub(/^name: */, ""); print; exit}' "$f")
70
+ [[ -z "$NAME" ]] && continue
71
+ local NAME_LOWER="${NAME:l}"
72
+
73
+ if [[ -n "${name_map[$NAME_LOWER]}" ]]; then
74
+ local EXISTING="${name_map[$NAME_LOWER]}"
75
+ [[ "$BASENAME" == "$EXISTING" ]] && continue
76
+ log "SAME NAME: $BASENAME and $EXISTING share name \"$NAME\""
77
+ log " -> Manual review recommended (content may differ)"
78
+ found_issues=1
79
+ else
80
+ name_map[$NAME_LOWER]="$BASENAME"
81
+ fi
82
+ done
83
+
84
+ # --- 3. Orphan detection (files not in MEMORY.md index) ---
85
+ if [[ -f "$MEMORY_INDEX" ]]; then
86
+ for f in "$MEMORY_DIR"/*.md(N); do
87
+ [[ ! -f "$f" ]] && continue
88
+ local BASENAME="${f:t}"
89
+ [[ "$BASENAME" == "MEMORY.md" ]] && continue
90
+
91
+ if ! grep -qF "$BASENAME" "$MEMORY_INDEX"; then
92
+ log "ORPHAN: $BASENAME not in MEMORY.md index"
93
+
94
+ local NAME=$(awk '/^name:/{sub(/^name: */, ""); print; exit}' "$f")
95
+ local DESC=$(awk '/^description:/{sub(/^description: */, ""); print; exit}' "$f")
96
+ if [[ -n "$NAME" ]] && [[ -n "$DESC" ]]; then
97
+ local INDEX_LINE="- [$NAME]($BASENAME) — $DESC"
98
+ echo "$INDEX_LINE" >> "$MEMORY_INDEX"
99
+ log " -> Auto-indexed: $INDEX_LINE"
100
+ else
101
+ log " -> Cannot auto-index (missing name/description frontmatter)"
102
+ fi
103
+ found_issues=1
104
+ fi
105
+ done
106
+ fi
107
+
108
+ # --- 4. Dead link detection ---
109
+ if [[ -f "$MEMORY_INDEX" ]]; then
110
+ grep -oE '\([^)]+\.md\)' "$MEMORY_INDEX" | tr -d '()' | while read -r fname; do
111
+ if [[ ! -f "$MEMORY_DIR/$fname" ]]; then
112
+ log "DEAD LINK: $fname referenced in MEMORY.md but file missing"
113
+ sed -i '' "/${fname//\//\\/}/d" "$MEMORY_INDEX" 2>/dev/null
114
+ log " -> Removed dead link from MEMORY.md"
115
+ found_issues=1
116
+ fi
117
+ done
118
+ fi
119
+
120
+ [[ $found_issues -eq 0 ]] && log "Memory check: all clean ($(ls "$MEMORY_DIR"/*.md 2>/dev/null | grep -v MEMORY.md | wc -l | tr -d ' ') files)"
121
+
122
+ exit 0