axel-setup 0.2.0 → 0.3.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 +25 -11
- package/README.md +20 -7
- package/axel-manifest.json +2 -2
- package/bin/axel-setup.js +50 -3
- package/bootstrap.sh +49 -16
- package/hooks/gsd-context-monitor.js +3 -3
- package/hooks/linear-lifecycle-sync.sh +19 -13
- package/hooks/post-commit-verify.sh +15 -9
- package/hooks/post-edit-lint.sh +4 -10
- package/hooks/proactive-resolver.sh +29 -18
- package/hooks/session-log-action.sh +9 -6
- package/hooks/session-log-prompt.sh +9 -12
- package/hooks/session-restore.sh +9 -4
- package/hooks/validate-commit-format.sh +14 -12
- package/install.sh +1 -1
- package/package.json +1 -1
- package/templates/merge-settings.jq +6 -3
- package/templates/settings.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -9,21 +9,35 @@ Releases are grouped by date and logical scope. npm package releases use semver
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
<!-- Nothing pending. -->
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [0.3.0] 2026-06-13 security and hooks hardening
|
|
17
|
+
|
|
12
18
|
### Added
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
-
|
|
16
|
-
- `
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
+
- `SECURITY.md` with vulnerability scope, reporting channel (GitHub private security advisories), and response commitment.
|
|
20
|
+
- `CODE_OF_CONDUCT.md` adopting Contributor Covenant v2.1.
|
|
21
|
+
- `.github/dependabot.yml` for weekly GitHub Actions dependency updates.
|
|
22
|
+
- `.editorconfig` with consistent LF, UTF-8, final newline, and 2-space indent across shell, JSON, YAML, JS, and Markdown files.
|
|
23
|
+
- `.shellcheckrc` placeholder committing the project to ShellCheck defaults with a comment explaining the policy.
|
|
24
|
+
- README badges (npm version, CI status, license, Node requirement) added just below the H1.
|
|
25
|
+
- README Security section summarising install footprint, permission profile escalation, and vulnerability reporting.
|
|
19
26
|
|
|
20
27
|
### Changed
|
|
21
|
-
-
|
|
22
|
-
-
|
|
28
|
+
- `install.sh`: default package pinned to `axel-setup@0.3.0` (overridable via `AXEL_SETUP_PACKAGE` env). Prevents unintended upgrades via `@latest` on a curl pipe.
|
|
29
|
+
- `README.md`: `npx axel-setup@0.3.0` is now the primary recommended install path, above the curl wrapper. The curl URL now points to the release tag (`v0.3.0`) instead of `main`.
|
|
30
|
+
- `README.md`: Maintainer Publish Checklist no longer contains a hardcoded personal path; uses `<repo-root>` placeholder.
|
|
31
|
+
- `README.md`: Language/Customization section clarifies that the Spanish default applies only to the generated personal `CLAUDE.md` template, not to the CLI or documentation.
|
|
32
|
+
- `package.json`: version bumped to `0.3.0`.
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [0.2.1] 2026-06-13 OIDC trusted publishing
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Release workflow now publishes exclusively through npm Trusted Publishing (OIDC); removed the `NPM_TOKEN` fallback now that the package exists on the registry.
|
|
40
|
+
- First release published with build provenance attestation generated from GitHub Actions.
|
|
27
41
|
|
|
28
42
|
---
|
|
29
43
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# AXEL Setup — Claude Code Power Configuration
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/axel-setup)
|
|
4
|
+
[](https://github.com/cveralyon/axel-setup/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/axel-setup)
|
|
7
|
+
|
|
3
8
|
**AXEL** = **A**utonomous e**X**celsior **E**ngineering **L**ayer
|
|
4
9
|
|
|
5
10
|
A complete, production-grade configuration package for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that transforms it into a proactive engineering partner. Includes session persistence, automatic memory, proactive error resolution, a curated suite of specialized agents and slash commands, and a real-time usage monitor.
|
|
@@ -46,18 +51,18 @@ See [AXEL Multi-Runtime Roadmap](docs/roadmap/multi-runtime.md) for the adapter
|
|
|
46
51
|
|
|
47
52
|
## Quick Start
|
|
48
53
|
|
|
49
|
-
The
|
|
54
|
+
The recommended install path is directly through npx:
|
|
50
55
|
|
|
51
56
|
```bash
|
|
52
|
-
npx axel-setup --dry-run --user-name "Your Name"
|
|
57
|
+
npx axel-setup@0.3.0 --dry-run --user-name "Your Name"
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
By default AXEL uses `--profile core`, a public safe Claude Code install that keeps conservative permissions and skips optional side effects such as plugin installation, usage monitor launchd setup, keybindings, and the external GSD installer. Use `--profile personal` or `--profile full` when you explicitly want the fuller local automation setup.
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
Alternatively, for a macOS one-line installer, use the pinned curl wrapper. It checks for Claude Code, jq, zsh, and Node before delegating to the packaged npm release:
|
|
58
63
|
|
|
59
64
|
```bash
|
|
60
|
-
curl -fsSL https://raw.githubusercontent.com/cveralyon/axel-setup/
|
|
65
|
+
curl -fsSL https://raw.githubusercontent.com/cveralyon/axel-setup/v0.3.0/install.sh | bash -s -- --dry-run --user-name "Your Name"
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
Before the first npm publish, use the same wrapper against the GitHub package source:
|
|
@@ -85,7 +90,7 @@ bash bootstrap.sh --user-name "Your Name"
|
|
|
85
90
|
Run publish commands from the repository root, where `package.json` lives:
|
|
86
91
|
|
|
87
92
|
```bash
|
|
88
|
-
cd
|
|
93
|
+
cd <repo-root>
|
|
89
94
|
npm whoami
|
|
90
95
|
npm run check
|
|
91
96
|
npm run publish:dry-run
|
|
@@ -95,7 +100,7 @@ npm publish --access public
|
|
|
95
100
|
From another directory, pass the package path explicitly:
|
|
96
101
|
|
|
97
102
|
```bash
|
|
98
|
-
npm publish
|
|
103
|
+
npm publish <repo-root> --access public
|
|
99
104
|
```
|
|
100
105
|
|
|
101
106
|
### Maintainer Release Automation
|
|
@@ -438,7 +443,7 @@ Edit `~/.claude/settings.json` and add entries to the relevant event:
|
|
|
438
443
|
|
|
439
444
|
### Language
|
|
440
445
|
|
|
441
|
-
|
|
446
|
+
The default language in the generated `CLAUDE.md` personal template is Spanish. This applies only to the template file written during install — the CLI itself, the documentation, and all hook/agent code are in English. Change `"language"` in `settings.json` to your preferred language for hook output (session summaries, memory extraction, etc.).
|
|
442
447
|
|
|
443
448
|
### Team CLAUDE.md
|
|
444
449
|
|
|
@@ -450,6 +455,14 @@ The template at `~/CLAUDE.md` (created only if you don't have one) includes:
|
|
|
450
455
|
|
|
451
456
|
Customize it with your team's specific repos, conventions, and rules.
|
|
452
457
|
|
|
458
|
+
## Security
|
|
459
|
+
|
|
460
|
+
AXEL writes exclusively to `~/.claude` (hooks, agents, commands, skills, settings). All installs are additive: existing files are never silently overwritten, a backup is proposed before any replacement, and `settings.json` is deep-merged rather than replaced.
|
|
461
|
+
|
|
462
|
+
**Permission profiles:** the default `--profile core` uses `acceptEdits` mode, which requires explicit confirmation before file edits. The `--profile personal` and `--profile full` profiles elevate to `bypassPermissions`, granting the agent broad autonomy. Only use those profiles when you understand and accept the expanded trust boundary.
|
|
463
|
+
|
|
464
|
+
**Reporting vulnerabilities:** please use [GitHub private security advisories](https://github.com/cveralyon/axel-setup/security/advisories/new) to report security issues. See [`SECURITY.md`](./SECURITY.md) for scope, response commitment, and contact details.
|
|
465
|
+
|
|
453
466
|
## Contributing
|
|
454
467
|
|
|
455
468
|
Contributions are welcome when they improve AXEL as reusable developer tooling. Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md), open a focused issue when the scope is unclear, and keep private company context out of public examples.
|
package/axel-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "axel-setup",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0"
|
|
6
6
|
},
|
|
7
7
|
"defaultTarget": "claude",
|
|
8
8
|
"targets": {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"id": "gsd",
|
|
124
124
|
"name": "get-shit-done-cc",
|
|
125
125
|
"managedExternally": true,
|
|
126
|
-
"installCommand": "npx -y get-shit-done-cc@
|
|
126
|
+
"installCommand": "npx -y get-shit-done-cc@1.42.3"
|
|
127
127
|
}
|
|
128
128
|
]
|
|
129
129
|
}
|
package/bin/axel-setup.js
CHANGED
|
@@ -50,7 +50,7 @@ function parseRuntimeArgs(argv, command) {
|
|
|
50
50
|
const arg = argv[index];
|
|
51
51
|
if (arg === "--home") {
|
|
52
52
|
requireValue(argv, index, arg);
|
|
53
|
-
home = argv[index + 1];
|
|
53
|
+
home = path.resolve(argv[index + 1]);
|
|
54
54
|
index += 1;
|
|
55
55
|
} else if (arg === "--target") {
|
|
56
56
|
requireValue(argv, index, arg);
|
|
@@ -58,11 +58,11 @@ function parseRuntimeArgs(argv, command) {
|
|
|
58
58
|
index += 1;
|
|
59
59
|
} else if (arg === "--codex-home") {
|
|
60
60
|
requireValue(argv, index, arg);
|
|
61
|
-
codexHome = argv[index + 1];
|
|
61
|
+
codexHome = path.resolve(argv[index + 1]);
|
|
62
62
|
index += 1;
|
|
63
63
|
} else if (arg === "--output") {
|
|
64
64
|
requireValue(argv, index, arg);
|
|
65
|
-
output = argv[index + 1];
|
|
65
|
+
output = path.resolve(argv[index + 1]);
|
|
66
66
|
index += 1;
|
|
67
67
|
} else if (arg === "--apply" && command === "uninstall") {
|
|
68
68
|
apply = true;
|
|
@@ -511,8 +511,55 @@ function pruneEmptyParents(startDir, stopDir) {
|
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
const DANGEROUS_ROOTS = new Set(["/", "/etc", "/usr", "/bin", "/sbin", "/var", "/tmp"]);
|
|
515
|
+
|
|
516
|
+
function assertSafeInstallRoot(installRoot, apply) {
|
|
517
|
+
if (!apply) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Block exact dangerous roots
|
|
522
|
+
if (DANGEROUS_ROOTS.has(installRoot)) {
|
|
523
|
+
console.error(`Refusing to uninstall from dangerous root: ${installRoot}`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Block exact home directory (subdir is fine: ~/.claude is allowed)
|
|
528
|
+
if (installRoot === os.homedir()) {
|
|
529
|
+
console.error(`Refusing to uninstall from home directory directly: ${installRoot}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function assertManifestValid(manifestPath, installRoot, apply) {
|
|
535
|
+
if (!apply) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!fs.existsSync(manifestPath)) {
|
|
540
|
+
console.error(`Refusing to uninstall: no axel-manifest.json found at ${manifestPath}`);
|
|
541
|
+
console.error(`This directory does not appear to be an AXEL install root.`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const manifest = readJson(manifestPath);
|
|
547
|
+
if (!manifest || typeof manifest !== "object" || !manifest.package || !manifest.package.name) {
|
|
548
|
+
throw new Error("Manifest missing required package.name field");
|
|
549
|
+
}
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.error(`Refusing to uninstall: axel-manifest.json at ${manifestPath} is invalid: ${err.message}`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
514
556
|
function runUninstall(argv) {
|
|
515
557
|
const { apply, installRoot, manifestPath, target } = resolveRuntime(argv, "uninstall");
|
|
558
|
+
|
|
559
|
+
// Hard guards before any destructive operation
|
|
560
|
+
assertSafeInstallRoot(installRoot, apply);
|
|
561
|
+
assertManifestValid(manifestPath, installRoot, apply);
|
|
562
|
+
|
|
516
563
|
const manifest = readInstalledManifest(manifestPath, target);
|
|
517
564
|
|
|
518
565
|
if (!manifest) {
|
package/bootstrap.sh
CHANGED
|
@@ -249,31 +249,53 @@ run() {
|
|
|
249
249
|
fi
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
# Escape a value for safe interpolation into the replacement side of a sed
|
|
253
|
+
# `s|...|...|` command. The base template ships safe-by-default; the only place
|
|
254
|
+
# permissions are elevated is settings_for_profile() below, explicitly and
|
|
255
|
+
# visibly. User-supplied values, however, can contain sed metacharacters
|
|
256
|
+
# (`\`, `|`, `&`) that would corrupt the substitution — escape them here.
|
|
257
|
+
sed_escape() {
|
|
258
|
+
# Order matters: escape backslashes first, then the delimiter and `&`.
|
|
259
|
+
printf '%s' "$1" | sed -e 's/[\\]/\\\\/g' -e 's/|/\\|/g' -e 's/&/\\&/g'
|
|
260
|
+
}
|
|
261
|
+
|
|
252
262
|
settings_for_profile() {
|
|
253
263
|
local src="$1"
|
|
254
264
|
local dest="$2"
|
|
255
265
|
|
|
256
|
-
|
|
266
|
+
# The base template (templates/settings.json) is SAFE BY DEFAULT:
|
|
267
|
+
# defaultMode "acceptEdits", no "Bash(*)" in allow, and
|
|
268
|
+
# skipDangerousModePermissionPrompt false. Most profiles ship it verbatim.
|
|
269
|
+
if [ "$PROFILE" != "personal" ] && [ "$PROFILE" != "full" ]; then
|
|
257
270
|
cp "$src" "$dest"
|
|
258
271
|
return
|
|
259
272
|
fi
|
|
260
273
|
|
|
274
|
+
# personal/full ELEVATE permissions explicitly and visibly: full autonomy
|
|
275
|
+
# bypass mode, Bash(*) allowance, and the dangerous-mode prompt suppressed.
|
|
276
|
+
# This keeps the distributed file safe while preserving per-profile behavior.
|
|
261
277
|
jq '
|
|
262
|
-
.permissions.defaultMode = "
|
|
263
|
-
.permissions.allow = ((.permissions.allow // [])
|
|
264
|
-
.skipDangerousModePermissionPrompt =
|
|
278
|
+
.permissions.defaultMode = "bypassPermissions" |
|
|
279
|
+
.permissions.allow = ((.permissions.allow // []) + (if ((.permissions.allow // []) | index("Bash(*)")) then [] else ["Bash(*)"] end)) |
|
|
280
|
+
.skipDangerousModePermissionPrompt = true
|
|
265
281
|
' "$src" > "$dest"
|
|
266
282
|
}
|
|
267
283
|
|
|
268
284
|
write_processed_skill() {
|
|
269
285
|
local src="$1"
|
|
270
286
|
local dest="$2"
|
|
287
|
+
local posthog_context assistant_language user_name user_context
|
|
288
|
+
|
|
289
|
+
posthog_context=$(sed_escape "$POSTHOG_PROJECT_CONTEXT")
|
|
290
|
+
assistant_language=$(sed_escape "$ASSISTANT_LANGUAGE")
|
|
291
|
+
user_name=$(sed_escape "$USER_NAME")
|
|
292
|
+
user_context=$(sed_escape "$USER_CONTEXT")
|
|
271
293
|
|
|
272
294
|
sed \
|
|
273
|
-
-e "s|{{POSTHOG_PROJECT_CONTEXT}}|$
|
|
274
|
-
-e "s|{{ASSISTANT_LANGUAGE}}|$
|
|
275
|
-
-e "s|{{USER_NAME}}|$
|
|
276
|
-
-e "s|{{USER_CONTEXT}}|$
|
|
295
|
+
-e "s|{{POSTHOG_PROJECT_CONTEXT}}|$posthog_context|g" \
|
|
296
|
+
-e "s|{{ASSISTANT_LANGUAGE}}|$assistant_language|g" \
|
|
297
|
+
-e "s|{{USER_NAME}}|$user_name|g" \
|
|
298
|
+
-e "s|{{USER_CONTEXT}}|$user_context|g" \
|
|
277
299
|
"$src" > "$dest"
|
|
278
300
|
}
|
|
279
301
|
|
|
@@ -343,7 +365,11 @@ add_or_upgrade() {
|
|
|
343
365
|
cp "$src" "$UPGRADES_DIR/$upgrade_subdir/$(basename "$dest")"
|
|
344
366
|
fi
|
|
345
367
|
upgrade "$label"
|
|
346
|
-
eval
|
|
368
|
+
# Increment the named counter without eval. printf -v writes to the
|
|
369
|
+
# variable named in $upgraded_var; ${!upgraded_var} reads its value.
|
|
370
|
+
# This stays portable to Bash 3.2 (macOS default), where `local -n`
|
|
371
|
+
# namerefs are unavailable.
|
|
372
|
+
printf -v "$upgraded_var" '%s' "$(( ${!upgraded_var} + 1 ))"
|
|
347
373
|
fi
|
|
348
374
|
# Same content — nothing to do
|
|
349
375
|
else
|
|
@@ -352,7 +378,7 @@ add_or_upgrade() {
|
|
|
352
378
|
else
|
|
353
379
|
cp "$src" "$dest"
|
|
354
380
|
fi
|
|
355
|
-
|
|
381
|
+
printf -v "$added_var" '%s' "$(( ${!added_var} + 1 ))"
|
|
356
382
|
fi
|
|
357
383
|
}
|
|
358
384
|
|
|
@@ -630,11 +656,15 @@ for hook_file in "$SCRIPT_DIR/hooks/"*; do
|
|
|
630
656
|
|
|
631
657
|
# Hooks need placeholder substitution, so we use a temp file for comparison.
|
|
632
658
|
# Keep the sed list in sync with any new {{PLACEHOLDER}} added to hook files.
|
|
659
|
+
# User values are escaped so sed metacharacters (\ | &) can't corrupt the sub.
|
|
633
660
|
PROCESSED=$(mktemp)
|
|
661
|
+
HOOK_USER_NAME=$(sed_escape "$USER_NAME")
|
|
662
|
+
HOOK_USER_CONTEXT=$(sed_escape "$USER_CONTEXT")
|
|
663
|
+
HOOK_ASSISTANT_LANGUAGE=$(sed_escape "$ASSISTANT_LANGUAGE")
|
|
634
664
|
sed \
|
|
635
|
-
-e "s|{{USER_NAME}}|$
|
|
636
|
-
-e "s|{{USER_CONTEXT}}|$
|
|
637
|
-
-e "s|{{ASSISTANT_LANGUAGE}}|$
|
|
665
|
+
-e "s|{{USER_NAME}}|$HOOK_USER_NAME|g" \
|
|
666
|
+
-e "s|{{USER_CONTEXT}}|$HOOK_USER_CONTEXT|g" \
|
|
667
|
+
-e "s|{{ASSISTANT_LANGUAGE}}|$HOOK_ASSISTANT_LANGUAGE|g" \
|
|
638
668
|
"$hook_file" > "$PROCESSED"
|
|
639
669
|
|
|
640
670
|
if [ -f "$DEST" ]; then
|
|
@@ -915,9 +945,12 @@ if ! $SKIP_MONITOR && [[ "$OSTYPE" == "darwin"* ]]; then
|
|
|
915
945
|
info "launchd agent skipped by --no-launchd or profile"
|
|
916
946
|
elif ! $DRY_RUN; then
|
|
917
947
|
mkdir -p "$(dirname "$PLIST_DEST")"
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
948
|
+
PLIST_USERNAME=$(sed_escape "${USERNAME:-$(whoami)}")
|
|
949
|
+
PLIST_HOME=$(sed_escape "$HOME")
|
|
950
|
+
PLIST_NODE_BIN=$(sed_escape "$NODE_BIN")
|
|
951
|
+
sed -e "s|{{USERNAME}}|$PLIST_USERNAME|g" \
|
|
952
|
+
-e "s|{{HOME}}|$PLIST_HOME|g" \
|
|
953
|
+
-e "s|{{NODE_PATH}}|$PLIST_NODE_BIN|g" \
|
|
921
954
|
"$PLIST_SRC" > "$PLIST_DEST"
|
|
922
955
|
launchctl load "$PLIST_DEST" 2>/dev/null && \
|
|
923
956
|
log " Usage monitor started at http://localhost:9119" || \
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
// as additionalContext, which the agent sees in its conversation
|
|
13
13
|
//
|
|
14
14
|
// Thresholds:
|
|
15
|
-
// WARNING (remaining <=
|
|
16
|
-
// CRITICAL (remaining <=
|
|
15
|
+
// WARNING (remaining <= 15%): Agent should wrap up current task
|
|
16
|
+
// CRITICAL (remaining <= 8%): Agent should stop immediately and save state
|
|
17
17
|
//
|
|
18
|
-
// Debounce:
|
|
18
|
+
// Debounce: 10 tool uses between warnings to avoid spam
|
|
19
19
|
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
@@ -27,35 +27,41 @@ case "$TICKET_PATTERN" in "{{"*"}}"|"") TICKET_PATTERN="[A-Z]+-[0-9]+" ;; esac
|
|
|
27
27
|
LINEAR_TEAM="{{LINEAR_TEAM}}"
|
|
28
28
|
case "$LINEAR_TEAM" in "{{"*"}}"|"") LINEAR_TEAM="your team" ;; esac
|
|
29
29
|
|
|
30
|
+
# Name of the Linear state for "PR opened, awaiting review". Some teams call it
|
|
31
|
+
# "In Review", others "PR In Review". Bootstrap substitutes the placeholder; the
|
|
32
|
+
# case fallback keeps the script working when run without bootstrap.
|
|
33
|
+
PR_REVIEW_STATE="{{PR_REVIEW_STATE}}"
|
|
34
|
+
case "$PR_REVIEW_STATE" in "{{"*"}}"|"") PR_REVIEW_STATE="In Review" ;; esac
|
|
35
|
+
|
|
30
36
|
# Guard against recursive invocation (this script spawns claude -p)
|
|
31
37
|
if [ -n "$CLAUDE_LINEAR_SYNC" ]; then exit 0; fi
|
|
32
38
|
|
|
33
39
|
INPUT=$(cat)
|
|
34
|
-
COMMAND=$(
|
|
35
|
-
CWD=$(
|
|
40
|
+
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
41
|
+
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
36
42
|
|
|
37
43
|
# Only run in configured repos (skip if filter is empty — runs everywhere)
|
|
38
|
-
if [ -n "$REPO_PATH_FILTER" ] && !
|
|
44
|
+
if [ -n "$REPO_PATH_FILTER" ] && ! printf '%s' "$CWD" | grep -qE "$REPO_PATH_FILTER"; then exit 0; fi
|
|
39
45
|
|
|
40
46
|
# --- Detect action type ---
|
|
41
47
|
ACTION=""
|
|
42
48
|
TICKETS=""
|
|
43
49
|
|
|
44
|
-
if
|
|
50
|
+
if printf '%s' "$COMMAND" | grep -qE '(^|[[:space:]&;|(])git[[:space:]]+commit([[:space:]]|$)'; then
|
|
45
51
|
ACTION="in_progress"
|
|
46
|
-
TICKETS=$(
|
|
52
|
+
TICKETS=$(printf '%s' "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
|
|
47
53
|
|
|
48
|
-
elif
|
|
54
|
+
elif printf '%s' "$COMMAND" | grep -qE '(^|[[:space:]&;|(])gh[[:space:]]+pr[[:space:]]+create'; then
|
|
49
55
|
ACTION="in_review"
|
|
50
|
-
TICKETS=$(
|
|
56
|
+
TICKETS=$(printf '%s' "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
|
|
51
57
|
if [ -z "$TICKETS" ]; then
|
|
52
58
|
TICKETS=$(git -C "$CWD" log --not --remotes --pretty=format:"%s %b" 2>/dev/null \
|
|
53
59
|
| grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
|
|
54
60
|
fi
|
|
55
61
|
|
|
56
|
-
elif
|
|
62
|
+
elif printf '%s' "$COMMAND" | grep -qE '(^|[[:space:]&;|(])gh[[:space:]]+pr[[:space:]]+merge'; then
|
|
57
63
|
ACTION="done"
|
|
58
|
-
TICKETS=$(
|
|
64
|
+
TICKETS=$(printf '%s' "$COMMAND" | grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
|
|
59
65
|
if [ -z "$TICKETS" ]; then
|
|
60
66
|
TICKETS=$(git -C "$CWD" log --not --remotes --pretty=format:"%s %b" 2>/dev/null \
|
|
61
67
|
| grep -oE "$TICKET_PATTERN" | sort -u | tr '\n' ' ' | sed 's/ $//')
|
|
@@ -77,7 +83,7 @@ touch "$RATE_FILE"
|
|
|
77
83
|
# --- Determine target state label ---
|
|
78
84
|
case "$ACTION" in
|
|
79
85
|
in_progress) TARGET="In Progress" ;;
|
|
80
|
-
in_review) TARGET="
|
|
86
|
+
in_review) TARGET="$PR_REVIEW_STATE" ;;
|
|
81
87
|
done) TARGET="Done" ;;
|
|
82
88
|
*) exit 0 ;;
|
|
83
89
|
esac
|
|
@@ -92,8 +98,8 @@ Instructions:
|
|
|
92
98
|
1. Call list_issue_statuses to find the state ID for '$TARGET' in the $LINEAR_TEAM team.
|
|
93
99
|
2. For each ticket, call get_issue to check its current state name.
|
|
94
100
|
3. Skip rules:
|
|
95
|
-
- If target is 'In Progress' and current state is already In Progress,
|
|
96
|
-
- If target is '
|
|
101
|
+
- If target is 'In Progress' and current state is already In Progress, $PR_REVIEW_STATE, or Done → skip.
|
|
102
|
+
- If target is '$PR_REVIEW_STATE' and current state is already $PR_REVIEW_STATE or Done → skip.
|
|
97
103
|
- If target is 'Done' and current state is already Done → skip.
|
|
98
104
|
4. For tickets that need updating, call save_issue with the new stateId.
|
|
99
105
|
5. Output format (one line per ticket):
|
|
@@ -103,7 +109,7 @@ Instructions:
|
|
|
103
109
|
HOOK_TMP=$(mktemp -d 2>/dev/null)
|
|
104
110
|
REAL_TMP=$(cd "$HOOK_TMP" 2>/dev/null && pwd -P)
|
|
105
111
|
RESULT=$(cd "$HOOK_TMP" 2>/dev/null && printf '%s' "$PROMPT" | CLAUDE_LINEAR_SYNC=1 claude -p --model haiku 2>/dev/null)
|
|
106
|
-
GHOST_SLUG=$(
|
|
112
|
+
GHOST_SLUG=$(printf '%s' "$REAL_TMP" | sed 's|[^a-zA-Z0-9]|-|g')
|
|
107
113
|
rm -rf "$HOOK_TMP" "$HOME/.claude/projects/${GHOST_SLUG}" 2>/dev/null
|
|
108
114
|
|
|
109
115
|
mkdir -p "$HOME/.claude/logs"
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
# PostToolUse hook:
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook (Bash): after a git commit, outputs a reminder for AXEL to
|
|
3
3
|
# launch excelsior-verifier on the committed files.
|
|
4
4
|
# This is a lightweight trigger — the actual verification runs as a subagent.
|
|
5
|
+
#
|
|
6
|
+
# Contract: the hook payload arrives as JSON on stdin. PostToolUse does NOT
|
|
7
|
+
# include an exit code, so commit success is inferred from the tool output
|
|
8
|
+
# (absence of common git failure signals).
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
OUT=$(printf '%s' "$INPUT" | jq -r '.tool_response.text // .tool_response // empty' 2>/dev/null)
|
|
5
13
|
|
|
6
14
|
# Only trigger on Bash tool calls that contain 'git commit'
|
|
7
|
-
|
|
8
|
-
if [[ "$TOOL_INPUT" != *"git commit"* ]]; then
|
|
15
|
+
if [[ "$CMD" != *"git commit"* ]]; then
|
|
9
16
|
exit 0
|
|
10
17
|
fi
|
|
11
18
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
if [[ "$TOOL_IS_ERROR" == "1" ]] || [[ "$TOOL_IS_ERROR" == "true" ]]; then
|
|
19
|
+
# Infer failure from the tool output: if git refused the commit, bail out.
|
|
20
|
+
if printf '%s' "$OUT" | grep -qiE "nothing to commit|no changes added|error:|fatal:|hook declined|rejected"; then
|
|
15
21
|
exit 0
|
|
16
22
|
fi
|
|
17
23
|
|
|
@@ -21,7 +27,7 @@ if [ -z "$CHANGED_FILES" ]; then
|
|
|
21
27
|
exit 0
|
|
22
28
|
fi
|
|
23
29
|
|
|
24
|
-
FILE_COUNT=$(
|
|
30
|
+
FILE_COUNT=$(printf '%s' "$CHANGED_FILES" | wc -l | tr -d ' ')
|
|
25
31
|
COMMIT_MSG=$(git log --oneline -1 2>/dev/null)
|
|
26
32
|
|
|
27
33
|
# Only trigger for non-trivial commits (2+ files or test/feature commits)
|
|
@@ -32,7 +38,7 @@ fi
|
|
|
32
38
|
# Output advisory to AXEL to launch verifier
|
|
33
39
|
cat << EOF
|
|
34
40
|
Post-commit verification advisory: Commit "$COMMIT_MSG" modified $FILE_COUNT files:
|
|
35
|
-
$(
|
|
41
|
+
$(printf '%s' "$CHANGED_FILES" | sed 's/^/ - /')
|
|
36
42
|
|
|
37
43
|
Consider launching excelsior-verifier as a background agent to verify this commit.
|
|
38
44
|
Command: Agent({ subagent_type: "excelsior-verifier", run_in_background: true, prompt: "Verify commit: $COMMIT_MSG. Files: $CHANGED_FILES" })
|
package/hooks/post-edit-lint.sh
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
#!/bin/zsh
|
|
2
|
-
# PostToolUse hook:
|
|
3
|
-
#
|
|
4
|
-
# falls back to TOOL_INPUT_FILE_PATH env var.
|
|
2
|
+
# PostToolUse hook (Edit|Write): auto-lint/fix files after Edit or Write.
|
|
3
|
+
# Reads the hook payload as JSON from stdin and extracts .tool_input.file_path.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
FILE
|
|
8
|
-
if [ -n "$TOOL_INPUT_FILE_PATH" ]; then
|
|
9
|
-
FILE="$TOOL_INPUT_FILE_PATH"
|
|
10
|
-
elif [ -n "$HOOK_TOOL_INPUT" ]; then
|
|
11
|
-
FILE=$(echo "$HOOK_TOOL_INPUT" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('file_path',''))" 2>/dev/null)
|
|
12
|
-
fi
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
13
7
|
|
|
14
8
|
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
|
15
9
|
exit 0
|
|
@@ -3,19 +3,30 @@
|
|
|
3
3
|
# Philosophy: ANY failure that can be auto-resolved SHOULD be auto-resolved.
|
|
4
4
|
# This hook handles the fast, known patterns. For everything else,
|
|
5
5
|
# the CLAUDE.md Excelsior principle tells the model to investigate and resolve.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
#
|
|
7
|
+
# Contract: the hook payload arrives as JSON on stdin. PostToolUse does NOT
|
|
8
|
+
# include an exit code, so instead of gating on a status code we act whenever
|
|
9
|
+
# the tool output text contains a known error signal.
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
TOOL_OUTPUT=$(printf '%s' "$INPUT" | jq -r '.tool_response.text // .tool_response // empty' 2>/dev/null)
|
|
13
|
+
|
|
14
|
+
# Nothing to inspect → nothing to do.
|
|
15
|
+
[ -z "$TOOL_OUTPUT" ] && exit 0
|
|
16
|
+
|
|
17
|
+
# Cheap early-out: only proceed if the output looks like it contains an error.
|
|
18
|
+
# This keeps the hook a no-op on successful commands without relying on an
|
|
19
|
+
# exit code (which the PostToolUse contract does not provide).
|
|
20
|
+
if ! printf '%s' "$TOOL_OUTPUT" | grep -qiE "error|refused|not connect|cannot connect|cannot find|no module|not found|does not exist|already in use|pending|failed|denied"; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
12
23
|
|
|
13
24
|
RESOLVED=""
|
|
14
25
|
|
|
15
26
|
# === SERVICE DETECTION & AUTO-START ===
|
|
16
27
|
|
|
17
28
|
# Docker (any Docker-related error)
|
|
18
|
-
if
|
|
29
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "docker daemon|Cannot connect to the Docker|docker\.sock|Is the docker daemon running|docker:.*command not found"; then
|
|
19
30
|
if [ "$(uname)" = "Darwin" ] && ! docker info >/dev/null 2>&1; then
|
|
20
31
|
open -a "Docker" 2>/dev/null
|
|
21
32
|
for i in $(seq 1 15); do docker info >/dev/null 2>&1 && break; sleep 1; done
|
|
@@ -28,7 +39,7 @@ if echo "$TOOL_OUTPUT" | grep -qiE "docker daemon|Cannot connect to the Docker|d
|
|
|
28
39
|
fi
|
|
29
40
|
|
|
30
41
|
# PostgreSQL (any PG connection error)
|
|
31
|
-
if
|
|
42
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "PG::|postgresql.*refused|could not connect.*5432|could not connect.*5433|connection to server.*failed.*postgres"; then
|
|
32
43
|
if [ "$(uname)" = "Darwin" ]; then
|
|
33
44
|
brew services start postgresql@16 2>/dev/null || brew services start postgresql 2>/dev/null
|
|
34
45
|
STOPPED_PG=$(docker ps -a --filter "ancestor=postgres" --filter "status=exited" --format "{{.Names}}" 2>/dev/null | head -1)
|
|
@@ -38,19 +49,19 @@ if echo "$TOOL_OUTPUT" | grep -qiE "PG::|postgresql.*refused|could not connect.*
|
|
|
38
49
|
fi
|
|
39
50
|
|
|
40
51
|
# Redis
|
|
41
|
-
if
|
|
52
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "Redis.*refused|ECONNREFUSED.*6379|Redis::CannotConnectError|redis.*not connect"; then
|
|
42
53
|
brew services start redis 2>/dev/null && RESOLVED="Redis started via Homebrew."
|
|
43
54
|
fi
|
|
44
55
|
|
|
45
56
|
# MySQL
|
|
46
|
-
if
|
|
57
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "mysql.*refused|ECONNREFUSED.*3306|Access denied for user.*mysql"; then
|
|
47
58
|
brew services start mysql 2>/dev/null && RESOLVED="MySQL started via Homebrew."
|
|
48
59
|
fi
|
|
49
60
|
|
|
50
61
|
# === DEPENDENCY RESOLUTION ===
|
|
51
62
|
|
|
52
63
|
# Node modules missing
|
|
53
|
-
if
|
|
64
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "Cannot find module|MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND" && [ -f "package.json" ]; then
|
|
54
65
|
if [ -f "pnpm-lock.yaml" ]; then
|
|
55
66
|
RESOLVED="HINT: Run 'pnpm install' — missing node_modules detected."
|
|
56
67
|
elif [ -f "yarn.lock" ]; then
|
|
@@ -61,12 +72,12 @@ if echo "$TOOL_OUTPUT" | grep -qiE "Cannot find module|MODULE_NOT_FOUND|ERR_MODU
|
|
|
61
72
|
fi
|
|
62
73
|
|
|
63
74
|
# Ruby gems missing
|
|
64
|
-
if
|
|
75
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "Could not find gem|Bundler::GemNotFound|bundle install|Gem::MissingSpecError"; then
|
|
65
76
|
RESOLVED="HINT: Run 'bundle install' — missing gems detected."
|
|
66
77
|
fi
|
|
67
78
|
|
|
68
79
|
# Python deps missing
|
|
69
|
-
if
|
|
80
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "ModuleNotFoundError|No module named|ImportError.*No module"; then
|
|
70
81
|
if [ -f "requirements.txt" ]; then
|
|
71
82
|
RESOLVED="HINT: Run 'pip install -r requirements.txt' — missing Python module."
|
|
72
83
|
elif [ -f "pyproject.toml" ]; then
|
|
@@ -77,21 +88,21 @@ fi
|
|
|
77
88
|
# === ENVIRONMENT ISSUES ===
|
|
78
89
|
|
|
79
90
|
# Port in use — identify the blocker
|
|
80
|
-
if
|
|
81
|
-
PORT=$(
|
|
91
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "EADDRINUSE|Address already in use|port.*already.*use|bind.*address already"; then
|
|
92
|
+
PORT=$(printf '%s' "$TOOL_OUTPUT" | grep -oE "[0-9]{4,5}" | head -1)
|
|
82
93
|
[ -n "$PORT" ] && PID=$(lsof -ti ":$PORT" 2>/dev/null | head -1)
|
|
83
94
|
[ -n "$PID" ] && PROC=$(ps -p "$PID" -o comm= 2>/dev/null)
|
|
84
95
|
[ -n "$PROC" ] && RESOLVED="HINT: Port $PORT blocked by $PROC (PID $PID). Kill with: kill $PID"
|
|
85
96
|
fi
|
|
86
97
|
|
|
87
98
|
# Database not created
|
|
88
|
-
if
|
|
89
|
-
DB=$(
|
|
99
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "database.*does not exist|Unknown database|FATAL.*database.*not exist"; then
|
|
100
|
+
DB=$(printf '%s' "$TOOL_OUTPUT" | grep -oE '"[^"]*"' | head -1 | tr -d '"')
|
|
90
101
|
RESOLVED="HINT: Database '$DB' doesn't exist. Create it: rails db:create or createdb $DB"
|
|
91
102
|
fi
|
|
92
103
|
|
|
93
104
|
# Migrations pending
|
|
94
|
-
if
|
|
105
|
+
if printf '%s' "$TOOL_OUTPUT" | grep -qiE "Migrations are pending|pending migration|migrate.*first"; then
|
|
95
106
|
RESOLVED="HINT: Pending migrations. Run: rails db:migrate RAILS_ENV=test"
|
|
96
107
|
fi
|
|
97
108
|
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Captures
|
|
2
|
+
# PostToolUse hook: log significant tool actions during the session for context
|
|
3
|
+
# persistence. Captures file edits, bash commands, and agent launches — the
|
|
4
|
+
# "what was done". Reads the hook payload as JSON from stdin.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
4
7
|
|
|
5
8
|
PROJECT_NAME=$(basename "$(pwd)")
|
|
6
9
|
SESSION_LOG="/tmp/claude-session-log-${PROJECT_NAME}.md"
|
|
7
10
|
TIMESTAMP=$(date +%H:%M)
|
|
8
11
|
|
|
9
|
-
TOOL_NAME_VAR
|
|
12
|
+
TOOL_NAME_VAR=$(printf '%s' "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null)
|
|
10
13
|
|
|
11
14
|
# Parse tool input for meaningful context
|
|
12
15
|
case "$TOOL_NAME_VAR" in
|
|
13
16
|
Edit|Write)
|
|
14
|
-
FILE_PATH=$(
|
|
17
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
15
18
|
if [ -n "$FILE_PATH" ]; then
|
|
16
19
|
echo "- [$TIMESTAMP] **$TOOL_NAME_VAR**: \`$(basename "$FILE_PATH")\`" >> "$SESSION_LOG"
|
|
17
20
|
fi
|
|
18
21
|
;;
|
|
19
22
|
Bash)
|
|
20
|
-
CMD=$(
|
|
23
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null | head -c 120)
|
|
21
24
|
if [ -n "$CMD" ]; then
|
|
22
25
|
echo "- [$TIMESTAMP] **Bash**: \`$CMD\`" >> "$SESSION_LOG"
|
|
23
26
|
fi
|
|
24
27
|
;;
|
|
25
28
|
Agent)
|
|
26
|
-
DESC=$(
|
|
29
|
+
DESC=$(printf '%s' "$INPUT" | jq -r '.tool_input.description // empty' 2>/dev/null)
|
|
27
30
|
if [ -n "$DESC" ]; then
|
|
28
31
|
echo "- [$TIMESTAMP] **Agent**: $DESC" >> "$SESSION_LOG"
|
|
29
32
|
fi
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Appends to a temp file that session-save.sh reads at the end
|
|
2
|
+
# UserPromptSubmit hook: log each user prompt during the session for context
|
|
3
|
+
# persistence. Appends to a temp file that session-save.sh reads at the end.
|
|
4
|
+
# Reads the hook payload as JSON from stdin and extracts .prompt.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
4
7
|
|
|
5
8
|
PROJECT_NAME=$(basename "$(pwd)")
|
|
6
9
|
SESSION_LOG="/tmp/claude-session-log-${PROJECT_NAME}.md"
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
USER_TEXT="$PROMPT"
|
|
10
|
-
|
|
11
|
-
if [ -z "$USER_TEXT" ]; then
|
|
12
|
-
# Try reading from hook input
|
|
13
|
-
USER_TEXT=$(echo "$TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('prompt',''))" 2>/dev/null)
|
|
14
|
-
fi
|
|
11
|
+
USER_TEXT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
|
|
15
12
|
|
|
16
13
|
if [ -n "$USER_TEXT" ]; then
|
|
17
14
|
TIMESTAMP=$(date +%H:%M)
|
|
18
|
-
# Truncate long prompts to keep log manageable
|
|
19
|
-
TRUNCATED=$(
|
|
20
|
-
echo "### [$TIMESTAMP]
|
|
15
|
+
# Truncate long prompts to keep the log manageable
|
|
16
|
+
TRUNCATED=$(printf '%s' "$USER_TEXT" | head -c 500)
|
|
17
|
+
echo "### [$TIMESTAMP] User" >> "$SESSION_LOG"
|
|
21
18
|
echo "$TRUNCATED" >> "$SESSION_LOG"
|
|
22
19
|
echo "" >> "$SESSION_LOG"
|
|
23
20
|
fi
|
package/hooks/session-restore.sh
CHANGED
|
@@ -13,7 +13,7 @@ if [ -z "$SESSIONS" ]; then
|
|
|
13
13
|
exit 0
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
|
-
LATEST=$(
|
|
16
|
+
LATEST=$(printf '%s' "$SESSIONS" | head -1)
|
|
17
17
|
SESSION_DATE=$(grep "^date:" "$LATEST" 2>/dev/null | cut -d' ' -f2)
|
|
18
18
|
SESSION_TIME=$(grep "^time:" "$LATEST" 2>/dev/null | cut -d' ' -f2)
|
|
19
19
|
|
|
@@ -27,14 +27,19 @@ sed -n '/^---$/,/^---$/!p' "$LATEST" | head -80
|
|
|
27
27
|
echo ""
|
|
28
28
|
|
|
29
29
|
# Show previous sessions as one-liners
|
|
30
|
-
OTHERS=$(
|
|
30
|
+
OTHERS=$(printf '%s' "$SESSIONS" | tail -n +2)
|
|
31
31
|
if [ -n "$OTHERS" ]; then
|
|
32
32
|
echo "### Sesiones anteriores:"
|
|
33
33
|
for f in $OTHERS; do
|
|
34
34
|
DATE=$(grep "^date:" "$f" 2>/dev/null | cut -d' ' -f2)
|
|
35
35
|
TIME=$(grep "^time:" "$f" 2>/dev/null | cut -d' ' -f2)
|
|
36
|
-
#
|
|
37
|
-
|
|
36
|
+
# Language-agnostic summary: take the body after the frontmatter, drop the
|
|
37
|
+
# leading H1 title and any markdown headings/blank lines, then keep the
|
|
38
|
+
# first real content line. Works regardless of the summary language.
|
|
39
|
+
SUMMARY=$(sed -n '/^---$/,/^---$/!p' "$f" 2>/dev/null \
|
|
40
|
+
| grep -vE '^[[:space:]]*$|^#' \
|
|
41
|
+
| head -1 \
|
|
42
|
+
| head -c 100)
|
|
38
43
|
echo "- **$DATE $TIME**: $SUMMARY"
|
|
39
44
|
done
|
|
40
45
|
fi
|
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Reads
|
|
2
|
+
# PreToolUse hook (Bash): validates commit message format before the command runs.
|
|
3
|
+
# Reads the hook payload as JSON from stdin, extracts .tool_input.command,
|
|
4
|
+
# then validates the commit message.
|
|
4
5
|
# Valid types: feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert
|
|
6
|
+
# Exit codes: 0 = allow, 2 = block (stderr shown to the model).
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
7
10
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
[ -z "$CMD" ] && CMD="$TOOL_JSON"
|
|
11
|
+
# Fallback: if stdin was not JSON, treat the whole payload as the command.
|
|
12
|
+
[ -z "$CMD" ] && CMD="$INPUT"
|
|
11
13
|
|
|
12
14
|
# Only check git commit commands
|
|
13
|
-
|
|
15
|
+
printf '%s' "$CMD" | grep -qE 'git commit' || exit 0
|
|
14
16
|
|
|
15
17
|
# Extract commit message from -m flag
|
|
16
18
|
# Handles: -m "msg", -m 'msg', -m "$(cat <<'EOF'\nmsg\nEOF\n)"
|
|
17
|
-
MSG=$(
|
|
19
|
+
MSG=$(printf '%s' "$CMD" | sed -n 's/.*-m[[:space:]]*["'\'']\{0,1\}\(.*\)/\1/p' | sed 's/["'\'']\{0,1\}[[:space:]]*$//' | head -1)
|
|
18
20
|
|
|
19
21
|
# For heredoc pattern: extract the first content line
|
|
20
|
-
if
|
|
21
|
-
MSG=$(
|
|
22
|
+
if printf '%s' "$MSG" | grep -q '<<'; then
|
|
23
|
+
MSG=$(printf '%s' "$CMD" | tr '\n' '|' | sed 's/.*<<[^|]*|//' | sed 's/|.*//' | sed 's/^[[:space:]]*//')
|
|
22
24
|
fi
|
|
23
25
|
|
|
24
26
|
# If we couldn't extract a message, skip validation (might be --amend or interactive)
|
|
25
27
|
[ -z "$MSG" ] && exit 0
|
|
26
28
|
|
|
27
29
|
# Validate format: tipo (Scope): message
|
|
28
|
-
if !
|
|
30
|
+
if ! printf '%s' "$MSG" | grep -qE '^(feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert)\s*\('; then
|
|
29
31
|
echo "BLOCKED: Commit format is incorrect." >&2
|
|
30
32
|
echo "Required: type (Scope): Descriptive message" >&2
|
|
31
33
|
echo "Canonical example: feat (AuthController): add OAuth2 login flow" >&2
|
|
32
34
|
echo "Valid types: feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert" >&2
|
|
33
35
|
echo "Do not use --no-verify to bypass this check. Fix the message instead." >&2
|
|
34
36
|
echo "Got: $MSG" >&2
|
|
35
|
-
exit
|
|
37
|
+
exit 2
|
|
36
38
|
fi
|
|
37
39
|
|
|
38
40
|
exit 0
|
package/install.sh
CHANGED
package/package.json
CHANGED
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
($event): (
|
|
24
24
|
($existing_hooks[$event] // []) as $existing_entries |
|
|
25
25
|
($axel_hooks[$event] // []) as $axel_entries |
|
|
26
|
-
|
|
26
|
+
# Identify each entry by the FULL ordered list of its hook commands, not
|
|
27
|
+
# just the first one. This keeps dedup correct for multi-command entries
|
|
28
|
+
# (two entries that share a first command but differ later are distinct).
|
|
29
|
+
($existing_entries | map((.hooks // []) | map(.command // ""))) as $existing_cmds |
|
|
27
30
|
($axel_entries | map(
|
|
28
31
|
select(
|
|
29
|
-
(.hooks[
|
|
30
|
-
($existing_cmds | map(. == $
|
|
32
|
+
((.hooks // []) | map(.command // "")) as $cmds |
|
|
33
|
+
($existing_cmds | map(. == $cmds) | any | not)
|
|
31
34
|
)
|
|
32
35
|
)) as $new_entries |
|
|
33
36
|
$existing_entries + $new_entries
|
package/templates/settings.json
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
},
|
|
8
8
|
"permissions": {
|
|
9
9
|
"allow": [
|
|
10
|
-
"Bash(*)",
|
|
11
10
|
"Read(*)",
|
|
12
11
|
"Edit(*)",
|
|
13
12
|
"Write(*)",
|
|
@@ -20,7 +19,7 @@
|
|
|
20
19
|
"Bash(rm -rf *)",
|
|
21
20
|
"Bash(dd *)"
|
|
22
21
|
],
|
|
23
|
-
"defaultMode": "
|
|
22
|
+
"defaultMode": "acceptEdits"
|
|
24
23
|
},
|
|
25
24
|
"hooks": {
|
|
26
25
|
"PreToolUse": [
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
"hooks": [
|
|
30
29
|
{
|
|
31
30
|
"type": "command",
|
|
32
|
-
"command": "
|
|
31
|
+
"command": "CMD=$(jq -r '.tool_input.command // empty'); printf '%s' \"$CMD\" | grep -qE '\\-\\-no-verify' && echo 'BLOCKED: --no-verify is prohibited' >&2 && exit 2 || exit 0"
|
|
33
32
|
}
|
|
34
33
|
]
|
|
35
34
|
},
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
"hooks": [
|
|
48
47
|
{
|
|
49
48
|
"type": "command",
|
|
50
|
-
"command": "
|
|
49
|
+
"command": "CMD=$(jq -r '.tool_input.command // empty'); printf '%s' \"$CMD\" | grep -qE 'RAILS_ENV=staging' && echo 'BLOCKED: staging = PRODUCTION. Confirm with the user before running.' >&2 && exit 2 || exit 0"
|
|
51
50
|
}
|
|
52
51
|
]
|
|
53
52
|
},
|
|
@@ -251,5 +250,5 @@
|
|
|
251
250
|
"autoMemoryDirectory": "~/.claude/memory",
|
|
252
251
|
"autoDreamEnabled": true,
|
|
253
252
|
"showThinkingSummaries": true,
|
|
254
|
-
"skipDangerousModePermissionPrompt":
|
|
253
|
+
"skipDangerousModePermissionPrompt": false
|
|
255
254
|
}
|