xtrm-tools 2.1.17 → 2.1.21
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/README.md +81 -709
- package/cli/dist/index.cjs +812 -425
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/pi/extensions/beads.ts +185 -0
- package/config/pi/extensions/core/adapter.ts +45 -0
- package/config/pi/extensions/core/index.ts +3 -0
- package/config/pi/extensions/core/logger.ts +45 -0
- package/config/pi/extensions/core/runner.ts +71 -0
- package/config/pi/extensions/custom-footer.ts +19 -53
- package/config/pi/extensions/main-guard-post-push.ts +44 -0
- package/config/pi/extensions/main-guard.ts +126 -0
- package/config/pi/extensions/quality-gates.ts +67 -0
- package/config/pi/extensions/service-skills.ts +88 -0
- package/config/pi/extensions/xtrm-loader.ts +89 -0
- package/hooks/README.md +35 -310
- package/hooks/beads-gate-messages.mjs +20 -37
- package/hooks/main-guard.mjs +16 -58
- package/package.json +1 -1
- package/config/pi/extensions/git-guard.ts +0 -45
- package/config/pi/extensions/safe-guard.ts +0 -46
- package/hooks/gitnexus-impact-reminder.py +0 -35
- package/hooks/test_agent_context.py +0 -112
|
@@ -8,38 +8,27 @@
|
|
|
8
8
|
// ── Shared workflow steps ────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
export const WORKFLOW_STEPS =
|
|
11
|
-
' 1. git checkout -b feature/<name
|
|
12
|
-
' 2. bd create + bd update in_progress
|
|
13
|
-
' 3. Edit
|
|
11
|
+
' 1. git checkout -b feature/<name>\n' +
|
|
12
|
+
' 2. bd create + bd update in_progress\n' +
|
|
13
|
+
' 3. Edit / write code\n' +
|
|
14
14
|
' 4. bd close <id> && git add && git commit\n' +
|
|
15
15
|
' 5. git push -u origin feature/<name>\n' +
|
|
16
16
|
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
17
|
-
' 7. git checkout master && git reset --hard origin/master\n'
|
|
17
|
+
' 7. git checkout master && git reset --hard origin/master\n';;
|
|
18
18
|
|
|
19
19
|
export const SESSION_CLOSE_PROTOCOL =
|
|
20
|
-
'
|
|
21
|
-
' 4. git add <files> && git commit -m "..." commit your changes\n' +
|
|
22
|
-
' 5. git push -u origin <feature-branch> push feature branch\n' +
|
|
23
|
-
' 6. gh pr create --fill create PR\n' +
|
|
24
|
-
' 7. gh pr merge --squash merge PR\n' +
|
|
25
|
-
' 8. git checkout master && git reset --hard origin/master\n';
|
|
20
|
+
' bd close <id> → commit → push → gh pr create --fill → gh pr merge --squash\n';;
|
|
26
21
|
|
|
27
22
|
export const COMMIT_NEXT_STEPS =
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
32
|
-
' 7. git checkout master && git reset --hard origin/master\n';
|
|
23
|
+
' bd close <id> && git add && git commit\n' +
|
|
24
|
+
' git push -u origin <feature-branch>\n' +
|
|
25
|
+
' gh pr create --fill && gh pr merge --squash\n';;
|
|
33
26
|
|
|
34
27
|
// ── Edit gate messages ───────────────────────────────────────────────────────
|
|
35
28
|
|
|
36
29
|
export function editBlockMessage(sessionId) {
|
|
37
30
|
return (
|
|
38
|
-
'🚫
|
|
39
|
-
' bd update <id> --status=in_progress\n' +
|
|
40
|
-
` bd kv set "claimed:${sessionId}" "<id>"\n\n` +
|
|
41
|
-
'Or create a new issue:\n' +
|
|
42
|
-
' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
|
|
31
|
+
'🚫 No active claim — claim an issue first.\n' +
|
|
43
32
|
' bd update <id> --status=in_progress\n' +
|
|
44
33
|
` bd kv set "claimed:${sessionId}" "<id>"\n`
|
|
45
34
|
);
|
|
@@ -47,11 +36,9 @@ export function editBlockMessage(sessionId) {
|
|
|
47
36
|
|
|
48
37
|
export function editBlockFallbackMessage() {
|
|
49
38
|
return (
|
|
50
|
-
'🚫
|
|
51
|
-
' bd create --title="<
|
|
52
|
-
' bd update <id> --status=in_progress\n
|
|
53
|
-
'Full workflow (do this every session):\n' +
|
|
54
|
-
WORKFLOW_STEPS
|
|
39
|
+
'🚫 No active issue — create one before editing.\n' +
|
|
40
|
+
' bd create --title="<task>" --type=task --priority=2\n' +
|
|
41
|
+
' bd update <id> --status=in_progress\n'
|
|
55
42
|
);
|
|
56
43
|
}
|
|
57
44
|
|
|
@@ -60,8 +47,8 @@ export function editBlockFallbackMessage() {
|
|
|
60
47
|
export function commitBlockMessage(summary, claimed) {
|
|
61
48
|
const issueSummary = summary ?? ` Claimed: ${claimed}`;
|
|
62
49
|
return (
|
|
63
|
-
'🚫
|
|
64
|
-
|
|
50
|
+
'🚫 Close open issues before committing.\n\n' +
|
|
51
|
+
`${issueSummary}\n\n` +
|
|
65
52
|
'Next steps:\n' + COMMIT_NEXT_STEPS
|
|
66
53
|
);
|
|
67
54
|
}
|
|
@@ -71,9 +58,9 @@ export function commitBlockMessage(summary, claimed) {
|
|
|
71
58
|
export function stopBlockMessage(summary, claimed) {
|
|
72
59
|
const issueSummary = summary ?? ` Claimed: ${claimed}`;
|
|
73
60
|
return (
|
|
74
|
-
'🚫
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
'🚫 Unresolved issues — close before stopping.\n\n' +
|
|
62
|
+
`${issueSummary}\n\n` +
|
|
63
|
+
SESSION_CLOSE_PROTOCOL
|
|
77
64
|
);
|
|
78
65
|
}
|
|
79
66
|
|
|
@@ -81,12 +68,8 @@ export function stopBlockMessage(summary, claimed) {
|
|
|
81
68
|
|
|
82
69
|
export function memoryPromptMessage() {
|
|
83
70
|
return (
|
|
84
|
-
'🧠
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
' YES → bd remember "<precise, durable insight>"\n' +
|
|
88
|
-
' NO → explicitly note "nothing worth persisting" and continue\n\n' +
|
|
89
|
-
'When done, signal completion and stop again:\n' +
|
|
90
|
-
' touch .beads/.memory-gate-done\n'
|
|
71
|
+
'🧠 Memory gate: for each closed issue, worth persisting?\n' +
|
|
72
|
+
' YES → bd remember "<insight>" NO → note "nothing to persist"\n' +
|
|
73
|
+
' touch .beads/.memory-gate-done when done.\n'
|
|
91
74
|
);
|
|
92
75
|
}
|
package/hooks/main-guard.mjs
CHANGED
|
@@ -55,28 +55,16 @@ const WRITE_TOOLS = new Set([
|
|
|
55
55
|
]);
|
|
56
56
|
|
|
57
57
|
if (WRITE_TOOLS.has(tool)) {
|
|
58
|
-
deny(
|
|
59
|
-
|
|
60
|
-
'Full workflow:\n' +
|
|
61
|
-
' 1. git checkout -b feature/<name> ← start here\n' +
|
|
62
|
-
' 2. bd create + bd update in_progress track your work\n' +
|
|
63
|
-
' 3. Edit files / write code\n' +
|
|
64
|
-
' 4. bd close <id> && git add && git commit\n' +
|
|
65
|
-
' 5. git push -u origin feature/<name>\n' +
|
|
66
|
-
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
67
|
-
' 7. git checkout master && git reset --hard origin/master\n'
|
|
68
|
-
);
|
|
58
|
+
deny(`⛔ On '${branch}' — checkout a feature branch first.\n`
|
|
59
|
+
+ ' git checkout -b feature/<name>\n');
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
const WORKFLOW =
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
' 5. git push -u origin feature/<name>\n' +
|
|
78
|
-
' 6. gh pr create --fill && gh pr merge --squash\n' +
|
|
79
|
-
' 7. git checkout master && git reset --hard origin/master\n';
|
|
63
|
+
' 1. git checkout -b feature/<name>\n'
|
|
64
|
+
+ ' 2. bd create + bd update in_progress\n'
|
|
65
|
+
+ ' 3. bd close <id> && git add && git commit\n'
|
|
66
|
+
+ ' 4. git push -u origin feature/<name>\n'
|
|
67
|
+
+ ' 5. gh pr create --fill && gh pr merge --squash\n';
|
|
80
68
|
|
|
81
69
|
if (tool === 'Bash') {
|
|
82
70
|
const cmd = (input.tool_input?.command ?? '').trim().replace(/\s+/g, ' ');
|
|
@@ -90,17 +78,8 @@ if (tool === 'Bash') {
|
|
|
90
78
|
// Must check BEFORE the gh allowlist pattern
|
|
91
79
|
if (/^gh\s+pr\s+merge\b/.test(cmd)) {
|
|
92
80
|
if (!/--squash\b/.test(cmd)) {
|
|
93
|
-
deny(
|
|
94
|
-
|
|
95
|
-
'Why squash?\n' +
|
|
96
|
-
' - Keeps history linear and easy to read\n' +
|
|
97
|
-
' - One commit per PR = easy to revert\n' +
|
|
98
|
-
' - Matches the workflow documented in AGENTS.md\n\n' +
|
|
99
|
-
'Correct usage:\n' +
|
|
100
|
-
' gh pr merge --squash\n\n' +
|
|
101
|
-
'If you really need a merge commit, use:\n' +
|
|
102
|
-
' MAIN_GUARD_ALLOW_BASH=1 gh pr merge --merge\n'
|
|
103
|
-
);
|
|
81
|
+
deny('⛔ Squash only: gh pr merge --squash\n'
|
|
82
|
+
+ ' (override: MAIN_GUARD_ALLOW_BASH=1 gh pr merge --merge)\n');
|
|
104
83
|
}
|
|
105
84
|
// --squash present — allow
|
|
106
85
|
process.exit(0);
|
|
@@ -129,10 +108,8 @@ if (tool === 'Bash') {
|
|
|
129
108
|
|
|
130
109
|
// Specific messages for common blocked operations
|
|
131
110
|
if (/^git\s+commit\b/.test(cmd)) {
|
|
132
|
-
deny(
|
|
133
|
-
|
|
134
|
-
WORKFLOW
|
|
135
|
-
);
|
|
111
|
+
deny(`⛔ No commits on '${branch}' — use a feature branch.\n`
|
|
112
|
+
+ ' git checkout -b feature/<name>\n');
|
|
136
113
|
}
|
|
137
114
|
|
|
138
115
|
if (/^git\s+push\b/.test(cmd)) {
|
|
@@ -141,36 +118,17 @@ if (tool === 'Bash') {
|
|
|
141
118
|
const explicitProtected = protectedBranches.some(b => lastToken === b || lastToken.endsWith(`:${b}`));
|
|
142
119
|
const impliedProtected = tokens.length <= 3 && protectedBranches.includes(branch);
|
|
143
120
|
if (explicitProtected || impliedProtected) {
|
|
144
|
-
deny(
|
|
145
|
-
|
|
146
|
-
'Next steps:\n' +
|
|
147
|
-
' 5. git push -u origin <feature-branch> \u2190 push your branch\n' +
|
|
148
|
-
' 6. gh pr create --fill create PR\n' +
|
|
149
|
-
' gh pr merge --squash merge it\n' +
|
|
150
|
-
' 7. git checkout master sync master\n' +
|
|
151
|
-
' git reset --hard origin/master\n\n' +
|
|
152
|
-
"If you're not on a feature branch yet:\n" +
|
|
153
|
-
' git checkout -b feature/<name> (then re-commit and push)\n'
|
|
154
|
-
);
|
|
121
|
+
deny(`⛔ No direct push to '${branch}' — push a feature branch and open a PR.\n`
|
|
122
|
+
+ ' git push -u origin <feature-branch> && gh pr create --fill\n');
|
|
155
123
|
}
|
|
156
124
|
// Pushing to a feature branch — allow
|
|
157
125
|
process.exit(0);
|
|
158
126
|
}
|
|
159
127
|
|
|
160
128
|
// Default deny — block everything else on protected branches
|
|
161
|
-
deny(
|
|
162
|
-
|
|
163
|
-
'
|
|
164
|
-
' git status / log / diff / branch / fetch / pull / stash\n' +
|
|
165
|
-
' git checkout -b <name> (create feature branch \u2014 the exit path)\n' +
|
|
166
|
-
' git switch -c <name> (same)\n' +
|
|
167
|
-
' git worktree / config\n' +
|
|
168
|
-
' gh <any> (GitHub CLI)\n' +
|
|
169
|
-
' bd <any> (beads issue tracking)\n\n' +
|
|
170
|
-
'To run arbitrary commands:\n' +
|
|
171
|
-
' 1. git checkout -b feature/<name> \u2190 move to a feature branch, or\n' +
|
|
172
|
-
' 2. MAIN_GUARD_ALLOW_BASH=1 <command> (escape hatch, use sparingly)\n'
|
|
173
|
-
);
|
|
129
|
+
deny(`⛔ Bash restricted on '${branch}'. Allowed: git status/log/diff/pull/stash, gh, bd.\n`
|
|
130
|
+
+ ' Exit: git checkout -b feature/<name>\n'
|
|
131
|
+
+ ' Override: MAIN_GUARD_ALLOW_BASH=1 <cmd>\n');
|
|
174
132
|
}
|
|
175
133
|
|
|
176
134
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oh-pi Git Checkpoint Extension
|
|
3
|
-
*
|
|
4
|
-
* Auto-stash before each turn, notify on agent completion.
|
|
5
|
-
* Combines git-checkpoint + notify + dirty-repo-guard.
|
|
6
|
-
*/
|
|
7
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
-
|
|
9
|
-
function terminalNotify(title: string, body: string): void {
|
|
10
|
-
if (process.env.KITTY_WINDOW_ID) {
|
|
11
|
-
process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
|
|
12
|
-
process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
|
|
13
|
-
} else {
|
|
14
|
-
process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function (pi: ExtensionAPI) {
|
|
19
|
-
let turnCount = 0;
|
|
20
|
-
|
|
21
|
-
// Warn on dirty repo at session start
|
|
22
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
23
|
-
try {
|
|
24
|
-
const { stdout } = await pi.exec("git", ["status", "--porcelain"]);
|
|
25
|
-
if (stdout.trim() && ctx.hasUI) {
|
|
26
|
-
const lines = stdout.trim().split("\n").length;
|
|
27
|
-
ctx.ui.notify(`⚠️ Dirty repo: ${lines} uncommitted change(s)`, "warning");
|
|
28
|
-
}
|
|
29
|
-
} catch { /* not a git repo, ignore */ }
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Stash checkpoint before each turn
|
|
33
|
-
pi.on("turn_start", async () => {
|
|
34
|
-
turnCount++;
|
|
35
|
-
try {
|
|
36
|
-
await pi.exec("git", ["stash", "create", "-m", `oh-pi-turn-${turnCount}`]);
|
|
37
|
-
} catch { /* not a git repo */ }
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Notify when agent is done
|
|
41
|
-
pi.on("agent_end", async () => {
|
|
42
|
-
terminalNotify("oh-pi", `Done after ${turnCount} turn(s). Ready for input.`);
|
|
43
|
-
turnCount = 0;
|
|
44
|
-
});
|
|
45
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oh-pi Safe Guard Extension
|
|
3
|
-
*
|
|
4
|
-
* Combines destructive command confirmation + protected paths in one extension.
|
|
5
|
-
*/
|
|
6
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
|
|
8
|
-
export const DANGEROUS_PATTERNS = [
|
|
9
|
-
/\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*-rf\b|.*--force\b)/,
|
|
10
|
-
/\bsudo\s+rm\b/,
|
|
11
|
-
/\b(DROP|TRUNCATE|DELETE\s+FROM)\b/i,
|
|
12
|
-
/\bchmod\s+777\b/,
|
|
13
|
-
/\bmkfs\b/,
|
|
14
|
-
/\bdd\s+if=/,
|
|
15
|
-
/>\s*\/dev\/sd[a-z]/,
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
export const PROTECTED_PATHS = [".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"];
|
|
19
|
-
|
|
20
|
-
export default function (pi: ExtensionAPI) {
|
|
21
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
22
|
-
// Check bash commands for dangerous patterns
|
|
23
|
-
if (event.toolName === "bash") {
|
|
24
|
-
const cmd = (event.input as { command?: string }).command ?? "";
|
|
25
|
-
const match = DANGEROUS_PATTERNS.find((p) => p.test(cmd));
|
|
26
|
-
if (match && ctx.hasUI) {
|
|
27
|
-
const ok = await ctx.ui.confirm("⚠️ Dangerous Command", `Execute: ${cmd}?`);
|
|
28
|
-
if (!ok) return { block: true, reason: "Blocked by user" };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Check write/edit for protected paths
|
|
33
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
34
|
-
const path = (event.input as { path?: string }).path ?? "";
|
|
35
|
-
const hit = PROTECTED_PATHS.find((p) => path.includes(p));
|
|
36
|
-
if (hit) {
|
|
37
|
-
if (ctx.hasUI) {
|
|
38
|
-
const ok = await ctx.ui.confirm("🛡️ Protected Path", `Allow write to ${path}?`);
|
|
39
|
-
if (!ok) return { block: true, reason: `Protected path: ${hit}` };
|
|
40
|
-
} else {
|
|
41
|
-
return { block: true, reason: `Protected path: ${hit}` };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
6
|
-
from agent_context import AgentContext
|
|
7
|
-
|
|
8
|
-
EDIT_KEYWORDS = [
|
|
9
|
-
'fix', 'refactor', 'change', 'update', 'modify', 'edit',
|
|
10
|
-
'rename', 'move', 'delete', 'remove', 'rewrite', 'implement',
|
|
11
|
-
'add', 'replace', 'extract', 'migrate', 'upgrade',
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
REMINDER = """*** GITNEXUS: Run impact analysis before editing any symbol ***
|
|
15
|
-
|
|
16
|
-
Before modifying a function, class, or method:
|
|
17
|
-
npx gitnexus impact <symbolName> --direction upstream --repo xtrm-tools
|
|
18
|
-
|
|
19
|
-
Review d=1 items (WILL BREAK) before proceeding.
|
|
20
|
-
Skip for docs, configs, and test-only changes.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
ctx = AgentContext()
|
|
25
|
-
|
|
26
|
-
if ctx.event == 'UserPromptSubmit':
|
|
27
|
-
prompt_lower = ctx.prompt.lower()
|
|
28
|
-
if any(kw in prompt_lower for kw in EDIT_KEYWORDS):
|
|
29
|
-
ctx.allow(additional_context=REMINDER)
|
|
30
|
-
|
|
31
|
-
ctx.fail_open()
|
|
32
|
-
|
|
33
|
-
except Exception as e:
|
|
34
|
-
print(f"Hook error: {e}", file=sys.stderr)
|
|
35
|
-
sys.exit(0)
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Test AgentContext hook output formatting"""
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
from io import StringIO
|
|
6
|
-
|
|
7
|
-
# Mock sys.stdin and sys.exit for testing
|
|
8
|
-
class MockExit(Exception):
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
def test_hook_output(event_name, method_name, *args, **kwargs):
|
|
12
|
-
"""Test a specific hook method and return its JSON output"""
|
|
13
|
-
import agent_context
|
|
14
|
-
|
|
15
|
-
# Prepare mock input
|
|
16
|
-
mock_input = {
|
|
17
|
-
"hook_event_name": event_name,
|
|
18
|
-
"tool_name": "Read",
|
|
19
|
-
"tool_input": {"file_path": "/test/file.txt"}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# Mock stdin
|
|
23
|
-
original_stdin = sys.stdin
|
|
24
|
-
original_exit = sys.exit
|
|
25
|
-
sys.stdin = StringIO(json.dumps(mock_input))
|
|
26
|
-
|
|
27
|
-
# Capture stdout
|
|
28
|
-
output_buffer = StringIO()
|
|
29
|
-
original_stdout = sys.stdout
|
|
30
|
-
sys.stdout = output_buffer
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
ctx = agent_context.AgentContext()
|
|
34
|
-
|
|
35
|
-
# Call the method
|
|
36
|
-
method = getattr(ctx, method_name)
|
|
37
|
-
try:
|
|
38
|
-
method(*args, **kwargs)
|
|
39
|
-
except SystemExit:
|
|
40
|
-
pass # Expected
|
|
41
|
-
|
|
42
|
-
# Get output
|
|
43
|
-
output = output_buffer.getvalue().strip()
|
|
44
|
-
return json.loads(output) if output else {}
|
|
45
|
-
|
|
46
|
-
finally:
|
|
47
|
-
sys.stdin = original_stdin
|
|
48
|
-
sys.stdout = original_stdout
|
|
49
|
-
sys.exit = original_exit
|
|
50
|
-
|
|
51
|
-
def main():
|
|
52
|
-
print("Testing AgentContext hook output formats...\n")
|
|
53
|
-
|
|
54
|
-
# Test 1: PreToolUse allow() with systemMessage
|
|
55
|
-
print("Test 1: PreToolUse allow() with systemMessage")
|
|
56
|
-
output = test_hook_output("PreToolUse", "allow", system_message="Test message")
|
|
57
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
58
|
-
assert "systemMessage" in output, "Should have systemMessage"
|
|
59
|
-
assert output["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
|
|
60
|
-
assert output["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
61
|
-
assert "decision" not in output, "Should NOT have top-level 'decision'"
|
|
62
|
-
print("✓ PASS\n")
|
|
63
|
-
|
|
64
|
-
# Test 2: PreToolUse allow() with additionalContext
|
|
65
|
-
print("Test 2: PreToolUse allow() with additionalContext")
|
|
66
|
-
output = test_hook_output("PreToolUse", "allow", additional_context="Extra context")
|
|
67
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
68
|
-
assert output["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
69
|
-
assert output["hookSpecificOutput"]["additionalContext"] == "Extra context"
|
|
70
|
-
print("✓ PASS\n")
|
|
71
|
-
|
|
72
|
-
# Test 3: UserPromptSubmit allow() with systemMessage (no permissionDecision)
|
|
73
|
-
print("Test 3: UserPromptSubmit allow() with systemMessage")
|
|
74
|
-
output = test_hook_output("UserPromptSubmit", "allow", system_message="Reminder")
|
|
75
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
76
|
-
assert "systemMessage" in output
|
|
77
|
-
assert "permissionDecision" not in output.get("hookSpecificOutput", {}), \
|
|
78
|
-
"UserPromptSubmit should NOT have permissionDecision"
|
|
79
|
-
print("✓ PASS\n")
|
|
80
|
-
|
|
81
|
-
# Test 4: UserPromptSubmit allow() with additionalContext
|
|
82
|
-
print("Test 4: UserPromptSubmit allow() with additionalContext")
|
|
83
|
-
output = test_hook_output("UserPromptSubmit", "allow", additional_context="Context")
|
|
84
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
85
|
-
assert output["hookSpecificOutput"]["additionalContext"] == "Context"
|
|
86
|
-
assert "permissionDecision" not in output["hookSpecificOutput"]
|
|
87
|
-
print("✓ PASS\n")
|
|
88
|
-
|
|
89
|
-
# Test 5: PreToolUse block()
|
|
90
|
-
print("Test 5: PreToolUse block()")
|
|
91
|
-
output = test_hook_output("PreToolUse", "block", "Dangerous operation")
|
|
92
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
93
|
-
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
94
|
-
assert output["hookSpecificOutput"]["permissionDecisionReason"] == "Dangerous operation"
|
|
95
|
-
assert "decision" not in output, "Should NOT have top-level 'decision'"
|
|
96
|
-
print("✓ PASS\n")
|
|
97
|
-
|
|
98
|
-
# Test 6: UserPromptSubmit block() (should use continue: false)
|
|
99
|
-
print("Test 6: UserPromptSubmit block()")
|
|
100
|
-
output = test_hook_output("UserPromptSubmit", "block", "Blocked")
|
|
101
|
-
print(f"Output: {json.dumps(output, indent=2)}")
|
|
102
|
-
assert output.get("continue") == False, "Should have continue: false"
|
|
103
|
-
assert output.get("stopReason") == "Blocked"
|
|
104
|
-
assert "permissionDecision" not in output.get("hookSpecificOutput", {})
|
|
105
|
-
print("✓ PASS\n")
|
|
106
|
-
|
|
107
|
-
print("=" * 50)
|
|
108
|
-
print("All tests passed! ✓")
|
|
109
|
-
print("=" * 50)
|
|
110
|
-
|
|
111
|
-
if __name__ == "__main__":
|
|
112
|
-
main()
|