@euanmsm/preflight 0.2.0 → 0.2.1
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 +11 -3
- package/package.json +1 -1
- package/src/gate.mjs +47 -4
package/README.md
CHANGED
|
@@ -83,11 +83,19 @@ skills are the only ones the call needs — path rules are not checked for that
|
|
|
83
83
|
call. The deny message names the tool when a tool rule matched and the file when
|
|
84
84
|
a path rule did.
|
|
85
85
|
|
|
86
|
+
## Subagents
|
|
87
|
+
|
|
88
|
+
A subagent or workflow agent must load the skills itself. The gate checks the
|
|
89
|
+
calling agent's own transcript, so skills the main session loaded do not count
|
|
90
|
+
for its subagents. That is deliberate: a subagent never sees the main session's
|
|
91
|
+
context, so it has not read those conventions either.
|
|
92
|
+
|
|
86
93
|
## When it does not block
|
|
87
94
|
|
|
88
|
-
The gate fails open. A missing map, an unreadable transcript, a
|
|
89
|
-
|
|
90
|
-
a tool that cannot do its job.
|
|
95
|
+
The gate fails open. A missing map, an unreadable transcript, a subagent
|
|
96
|
+
transcript it cannot find, a file outside the repository or a malformed payload
|
|
97
|
+
all allow the edit rather than halting work on a tool that cannot do its job.
|
|
98
|
+
`PREFLIGHT=off` disables it for one command.
|
|
91
99
|
|
|
92
100
|
Deliberate: a gate that breaks your session when its own config has a typo is a
|
|
93
101
|
gate you will remove within the week.
|
package/package.json
CHANGED
package/src/gate.mjs
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// PreToolUse hook. Blocks a tool call until the skills it needs have been
|
|
6
|
-
// loaded
|
|
7
|
-
// by the path of the file it writes.
|
|
6
|
+
// loaded by the agent making it — by the tool's name when a tool rule matches,
|
|
7
|
+
// otherwise by the path of the file it writes. A subagent is checked against
|
|
8
|
+
// its own transcript, not its parent's. Fails open on any error.
|
|
8
9
|
|
|
9
10
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
|
-
import { join, relative } from 'node:path';
|
|
12
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
12
13
|
|
|
13
14
|
import { compile, loadConfig, repoRoot } from '@euanmsm/devkit-core';
|
|
14
15
|
|
|
@@ -38,12 +39,26 @@ function deny(reason) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* Locates the
|
|
42
|
+
* Locates the transcript of the agent making the tool call.
|
|
42
43
|
*
|
|
43
44
|
* @param input - The hook payload
|
|
44
45
|
* @returns Path to the transcript, or null when it cannot be found
|
|
45
46
|
*/
|
|
46
47
|
export function findTranscript(input) {
|
|
48
|
+
const session = findSessionTranscript(input);
|
|
49
|
+
if (!session || !input.agent_id) return session;
|
|
50
|
+
|
|
51
|
+
return findAgentTranscript(session, input.agent_id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Locates the main session's transcript, which the payload names even when a
|
|
56
|
+
* subagent makes the call.
|
|
57
|
+
*
|
|
58
|
+
* @param input - The hook payload
|
|
59
|
+
* @returns Path to the transcript, or null when it cannot be found
|
|
60
|
+
*/
|
|
61
|
+
function findSessionTranscript(input) {
|
|
47
62
|
if (input.transcript_path && existsSync(input.transcript_path))
|
|
48
63
|
return input.transcript_path;
|
|
49
64
|
|
|
@@ -58,6 +73,30 @@ export function findTranscript(input) {
|
|
|
58
73
|
return null;
|
|
59
74
|
}
|
|
60
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Locates a subagent's transcript beside its session's.
|
|
78
|
+
*
|
|
79
|
+
* @param sessionTranscript - Path to the main session's transcript
|
|
80
|
+
* @param agentId - The subagent's id from the hook payload
|
|
81
|
+
* @returns Path to the transcript, or null when it cannot be found
|
|
82
|
+
*/
|
|
83
|
+
export function findAgentTranscript(sessionTranscript, agentId) {
|
|
84
|
+
const subagents = join(
|
|
85
|
+
dirname(sessionTranscript),
|
|
86
|
+
basename(sessionTranscript, '.jsonl'),
|
|
87
|
+
'subagents',
|
|
88
|
+
);
|
|
89
|
+
if (!existsSync(subagents)) return null;
|
|
90
|
+
|
|
91
|
+
// Workflow agents sit one level down, under workflows/<run>/.
|
|
92
|
+
const name = `agent-${agentId}.jsonl`;
|
|
93
|
+
const hit = readdirSync(subagents, { recursive: true }).find(
|
|
94
|
+
(entry) => basename(entry) === name,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return hit ? join(subagents, hit) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
/**
|
|
62
101
|
* Reads every skill loaded in a transcript.
|
|
63
102
|
*
|
|
@@ -176,9 +215,13 @@ export function main() {
|
|
|
176
215
|
if (missing.length === 0) allow();
|
|
177
216
|
|
|
178
217
|
const calls = missing.map((s) => ` Skill(skill: "${s}")`).join('\n');
|
|
218
|
+
const scope = input.agent_id
|
|
219
|
+
? `Skills loaded by the parent session do not count here — load them yourself.\n\n`
|
|
220
|
+
: '';
|
|
179
221
|
|
|
180
222
|
deny(
|
|
181
223
|
`BLOCKED — ${gate.subject} is governed by convention skills you have not loaded this session.\n\n` +
|
|
224
|
+
scope +
|
|
182
225
|
`Load them, then make this call again:\n${calls}\n\n` +
|
|
183
226
|
`These skills hold the conventions this call must follow. Do not work around this by ` +
|
|
184
227
|
`writing from memory. The mapping lives in .devkit/${CONFIG_NAME}.`,
|