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.
- package/CHANGELOG.md +218 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/agents/api-design.md +51 -0
- package/agents/bughunter.md +136 -0
- package/agents/changelog.md +89 -0
- package/agents/cleanup.md +126 -0
- package/agents/compare-branch.md +35 -0
- package/agents/cross-repo.md +97 -0
- package/agents/db-check.md +14 -0
- package/agents/debug.md +47 -0
- package/agents/deploy-check.md +100 -0
- package/agents/draft-message.md +19 -0
- package/agents/excelsior-coordinator.md +75 -0
- package/agents/excelsior-verifier.md +94 -0
- package/agents/feature.md +48 -0
- package/agents/harness-optimizer.md +40 -0
- package/agents/incident.md +48 -0
- package/agents/linear-task.md +18 -0
- package/agents/onboard.md +24 -0
- package/agents/perf.md +44 -0
- package/agents/production-validator.md +96 -0
- package/agents/review.md +113 -0
- package/agents/security-check.md +29 -0
- package/agents/sprint-summary.md +15 -0
- package/agents/tdd-mainder.md +178 -0
- package/agents/test-gen.md +39 -0
- package/axel-manifest.json +129 -0
- package/bin/axel-setup.js +597 -0
- package/bootstrap.sh +1087 -0
- package/commands/create-pr.md +13 -0
- package/commands/daily.md +182 -0
- package/commands/deslop.md +13 -0
- package/commands/draft-message.md +23 -0
- package/commands/eod-review.md +154 -0
- package/commands/execute-prp.md +37 -0
- package/commands/generate-prp.md +75 -0
- package/commands/multi-repo-feature.md +60 -0
- package/commands/roadmap.md +31 -0
- package/commands/sprint-status.md +486 -0
- package/commands/style.md +68 -0
- package/commands/visualize.md +17 -0
- package/docs/roadmap/multi-runtime.md +73 -0
- package/docs/superpowers/plans/2026-06-12-setup-hardening-roadmap.md +61 -0
- package/hooks/desktop-notify.sh +26 -0
- package/hooks/enforce-agent-model.jq +14 -0
- package/hooks/gsd-context-monitor.js +156 -0
- package/hooks/linear-lifecycle-sync.sh +112 -0
- package/hooks/memory-dedup.sh +122 -0
- package/hooks/memory-extractor.sh +218 -0
- package/hooks/post-commit-memory-trigger.sh +16 -0
- package/hooks/post-commit-verify.sh +41 -0
- package/hooks/post-edit-lint.sh +43 -0
- package/hooks/precompact-save-context.sh +124 -0
- package/hooks/priority-map-staleness.sh +29 -0
- package/hooks/proactive-resolver.sh +104 -0
- package/hooks/session-auto-title.sh +165 -0
- package/hooks/session-checkpoint.sh +97 -0
- package/hooks/session-cost-log.sh +77 -0
- package/hooks/session-log-action.sh +36 -0
- package/hooks/session-log-prompt.sh +25 -0
- package/hooks/session-restore.sh +45 -0
- package/hooks/session-save.sh +81 -0
- package/hooks/session-summarize.sh +154 -0
- package/hooks/validate-commit-format.sh +38 -0
- package/hooks/weekly-priority-map-review.sh +143 -0
- package/install.sh +46 -0
- package/package.json +67 -0
- package/scripts/ci/bootstrap-dry-run.sh +40 -0
- package/scripts/ci/check.sh +65 -0
- package/scripts/posthog-snapshot-loader.sh +112 -0
- package/skills/context-budget/SKILL.md +86 -0
- package/skills/memory-review/SKILL.md +100 -0
- package/skills/model-routing/SKILL.md +70 -0
- package/skills/posthog-weekly/SKILL.md +271 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/templates/AGENTS.runtime.md +17 -0
- package/templates/CLAUDE.md +252 -0
- package/templates/claude-monitor.plist +35 -0
- package/templates/keybindings.json +13 -0
- package/templates/merge-settings.jq +53 -0
- package/templates/review-upgrades.md +44 -0
- package/templates/settings.json +255 -0
- package/templates/statusline-command.sh +182 -0
- package/tests/fixtures/hooks/events.json +32 -0
- package/tools/session-costs-view.sh +128 -0
- package/tools/session-dashboard-gen.sh +369 -0
- package/tools/session-live.sh +173 -0
- 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
|