portable-agent-layer 0.66.1 → 0.68.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/assets/skills/pal-analyze/SKILL.md +1 -1
- package/assets/skills/pal-reflect/SKILL.md +1 -1
- package/assets/skills/projects/SKILL.md +1 -1
- package/assets/skills/telos/SKILL.md +1 -1
- package/assets/templates/hooks.codex.json +18 -4
- package/assets/templates/hooks.copilot.json +31 -12
- package/assets/templates/hooks.cursor.json +23 -7
- package/assets/templates/settings.claude.json +50 -8
- package/package.json +3 -2
- package/src/cli/index.ts +7 -3
- package/src/hooks/LedgerCommit.ts +29 -0
- package/src/hooks/LedgerSnapshot.ts +30 -0
- package/src/hooks/LedgerUnapplied.ts +69 -0
- package/src/hooks/LoadContext.ts +4 -2
- package/src/hooks/lib/actor.ts +4 -4
- package/src/hooks/lib/agent.ts +31 -7
- package/src/hooks/lib/ledger-hook.ts +276 -0
- package/src/hooks/lib/ledger.ts +348 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/sensitive-path.ts +117 -0
- package/src/hooks/lib/settings.ts +5 -0
- package/src/targets/lib.ts +37 -7
- package/src/targets/opencode/plugin.ts +22 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pal-analyze
|
|
3
3
|
license: MIT
|
|
4
4
|
description: "Run learning analysis — surface rating trends, recurring failure patterns, and graduation candidates. Use when learning analysis is due, or when the user asks about performance patterns, low ratings, or what to improve."
|
|
5
|
-
argument-hint: [optional: --actionable for AI-generated recommendations]
|
|
5
|
+
argument-hint: "[optional: --actionable for AI-generated recommendations]"
|
|
6
6
|
metadata:
|
|
7
7
|
source: portable-agent-layer
|
|
8
8
|
triggers:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pal-reflect
|
|
3
3
|
license: MIT
|
|
4
4
|
description: "Run relationship reflect — promote recurring behavioral observations into tracked opinions. Use when relationship reflect is due, or when the user asks to review what patterns have been observed."
|
|
5
|
-
argument-hint: [optional: --dry-run to preview without writing]
|
|
5
|
+
argument-hint: "[optional: --dry-run to preview without writing]"
|
|
6
6
|
metadata:
|
|
7
7
|
source: portable-agent-layer
|
|
8
8
|
triggers:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: projects
|
|
3
3
|
license: MIT
|
|
4
4
|
description: "Project context management. PROACTIVE — use when the user references a project (by name or as \"this repo\", \"current work\"), asks to add/update/complete a project, says \"store under a named project\", \"track this\", \"what am I working on\", \"my projects\", \"my priorities\". Also triggered by ISC/ISA terminology — ISC stands for Ideal State Criteria, a project's verifiable done-conditions stored as `ISC-N:` lines in the Criteria section of its ISA.md spec. Phrases like \"ISC\", \"open/opening the ISC\", \"open ISCs\" (unfinished criteria), \"create/creating a ticket for a project\", \"new ticket\", or \"project ticket\" all map here: use `list-isc` to read them, `add-isc` to create one, `complete-isc`/`reopen-isc` to change status."
|
|
5
|
-
argument-hint: [list | create | resume | add-next | add-blocker | add-decision | add-handoff | update-section | criteria | isa-init | complete | archive | pause]
|
|
5
|
+
argument-hint: "[list | create | resume | add-next | add-blocker | add-decision | add-handoff | update-section | criteria | isa-init | complete | archive | pause]"
|
|
6
6
|
metadata:
|
|
7
7
|
source: portable-agent-layer
|
|
8
8
|
triggers:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telos
|
|
3
3
|
description: "Personal context management. Use when discussing goals, beliefs, challenges, identity, updating telos, life context, changing a goal, what do I believe, current obstacles, mission, or strategies."
|
|
4
|
-
argument-hint: [area to view or update]
|
|
4
|
+
argument-hint: "[area to view or update]"
|
|
5
5
|
metadata:
|
|
6
6
|
source: portable-agent-layer
|
|
7
7
|
derived-from: https://github.com/danielmiessler/LifeOS
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"hooks": [
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
|
-
"command": "
|
|
8
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts --agent=codex"
|
|
9
9
|
}
|
|
10
10
|
]
|
|
11
11
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hooks": [
|
|
16
16
|
{
|
|
17
17
|
"type": "command",
|
|
18
|
-
"command": "
|
|
18
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts --agent=codex"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
}
|
|
@@ -25,7 +25,21 @@
|
|
|
25
25
|
"hooks": [
|
|
26
26
|
{
|
|
27
27
|
"type": "command",
|
|
28
|
-
"command": "
|
|
28
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=codex"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerSnapshot.ts --agent=codex"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"PostToolUse": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerCommit.ts --agent=codex"
|
|
29
43
|
}
|
|
30
44
|
]
|
|
31
45
|
}
|
|
@@ -35,7 +49,7 @@
|
|
|
35
49
|
"hooks": [
|
|
36
50
|
{
|
|
37
51
|
"type": "command",
|
|
38
|
-
"command": "
|
|
52
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=codex"
|
|
39
53
|
}
|
|
40
54
|
]
|
|
41
55
|
}
|
|
@@ -4,39 +4,58 @@
|
|
|
4
4
|
"sessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"type": "command",
|
|
7
|
-
"bash": "
|
|
8
|
-
"powershell": "
|
|
7
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts --agent=copilot",
|
|
8
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts --agent=copilot"
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"userPromptSubmitted": [
|
|
12
12
|
{
|
|
13
13
|
"type": "command",
|
|
14
|
-
"bash": "
|
|
15
|
-
"powershell": "
|
|
14
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts --agent=copilot",
|
|
15
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts --agent=copilot"
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"preToolUse": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
|
-
"bash": "
|
|
22
|
-
"powershell": "
|
|
21
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=copilot",
|
|
22
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=copilot"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"type": "command",
|
|
26
|
-
"bash": "
|
|
27
|
-
"powershell": "
|
|
26
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts --agent=copilot",
|
|
27
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts --agent=copilot"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"bash": "
|
|
32
|
-
"powershell": "
|
|
31
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts --agent=copilot",
|
|
32
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts --agent=copilot"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/LedgerSnapshot.ts --agent=copilot",
|
|
37
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/LedgerSnapshot.ts --agent=copilot"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"postToolUse": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/LedgerCommit.ts --agent=copilot",
|
|
44
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/LedgerCommit.ts --agent=copilot"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"postToolUseFailure": [
|
|
48
|
+
{
|
|
49
|
+
"type": "command",
|
|
50
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/LedgerUnapplied.ts --agent=copilot --event=postToolUseFailure",
|
|
51
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/LedgerUnapplied.ts --agent=copilot --event=postToolUseFailure"
|
|
33
52
|
}
|
|
34
53
|
],
|
|
35
54
|
"agentStop": [
|
|
36
55
|
{
|
|
37
56
|
"type": "command",
|
|
38
|
-
"bash": "
|
|
39
|
-
"powershell": "
|
|
57
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=copilot",
|
|
58
|
+
"powershell": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=copilot"
|
|
40
59
|
}
|
|
41
60
|
]
|
|
42
61
|
}
|
|
@@ -4,39 +4,55 @@
|
|
|
4
4
|
"sessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"type": "command",
|
|
7
|
-
"command": "
|
|
7
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts --agent=cursor"
|
|
8
8
|
}
|
|
9
9
|
],
|
|
10
10
|
"beforeSubmitPrompt": [
|
|
11
11
|
{
|
|
12
12
|
"type": "command",
|
|
13
|
-
"command": "
|
|
13
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts --agent=cursor"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"preToolUse": [
|
|
17
17
|
{
|
|
18
18
|
"type": "command",
|
|
19
|
-
"command": "
|
|
19
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=cursor"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"type": "command",
|
|
23
|
-
"command": "
|
|
23
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts --agent=cursor"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "command",
|
|
27
|
-
"command": "
|
|
27
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts --agent=cursor"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerSnapshot.ts --agent=cursor"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"postToolUse": [
|
|
35
|
+
{
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerCommit.ts --agent=cursor"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"postToolUseFailure": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerUnapplied.ts --agent=cursor"
|
|
28
44
|
}
|
|
29
45
|
],
|
|
30
46
|
"beforeShellExecution": [
|
|
31
47
|
{
|
|
32
48
|
"type": "command",
|
|
33
|
-
"command": "
|
|
49
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=cursor"
|
|
34
50
|
}
|
|
35
51
|
],
|
|
36
52
|
"stop": [
|
|
37
53
|
{
|
|
38
54
|
"type": "command",
|
|
39
|
-
"command": "
|
|
55
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=cursor"
|
|
40
56
|
}
|
|
41
57
|
]
|
|
42
58
|
}
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"hooks": [
|
|
70
70
|
{
|
|
71
71
|
"type": "command",
|
|
72
|
-
"command": "
|
|
72
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts --agent=claude"
|
|
73
73
|
}
|
|
74
74
|
]
|
|
75
75
|
},
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"hooks": [
|
|
79
79
|
{
|
|
80
80
|
"type": "command",
|
|
81
|
-
"command": "
|
|
81
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/CompactRecover.ts --agent=claude"
|
|
82
82
|
}
|
|
83
83
|
]
|
|
84
84
|
}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"hooks": [
|
|
90
90
|
{
|
|
91
91
|
"type": "command",
|
|
92
|
-
"command": "
|
|
92
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts --agent=claude"
|
|
93
93
|
}
|
|
94
94
|
]
|
|
95
95
|
}
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"hooks": [
|
|
101
101
|
{
|
|
102
102
|
"type": "command",
|
|
103
|
-
"command": "
|
|
103
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts --agent=claude"
|
|
104
104
|
}
|
|
105
105
|
]
|
|
106
106
|
}
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"hooks": [
|
|
112
112
|
{
|
|
113
113
|
"type": "command",
|
|
114
|
-
"command": "
|
|
114
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=claude"
|
|
115
115
|
}
|
|
116
116
|
]
|
|
117
117
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"hooks": [
|
|
121
121
|
{
|
|
122
122
|
"type": "command",
|
|
123
|
-
"command": "
|
|
123
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts --agent=claude"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -129,7 +129,49 @@
|
|
|
129
129
|
"hooks": [
|
|
130
130
|
{
|
|
131
131
|
"type": "command",
|
|
132
|
-
"command": "
|
|
132
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts --agent=claude"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"matcher": "Write|Edit",
|
|
138
|
+
"hooks": [
|
|
139
|
+
{
|
|
140
|
+
"type": "command",
|
|
141
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerSnapshot.ts --agent=claude"
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"PostToolUse": [
|
|
147
|
+
{
|
|
148
|
+
"matcher": "Write|Edit",
|
|
149
|
+
"hooks": [
|
|
150
|
+
{
|
|
151
|
+
"type": "command",
|
|
152
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerCommit.ts --agent=claude"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"PostToolUseFailure": [
|
|
158
|
+
{
|
|
159
|
+
"matcher": "Write|Edit",
|
|
160
|
+
"hooks": [
|
|
161
|
+
{
|
|
162
|
+
"type": "command",
|
|
163
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerUnapplied.ts --agent=claude"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"PermissionDenied": [
|
|
169
|
+
{
|
|
170
|
+
"matcher": "Write|Edit",
|
|
171
|
+
"hooks": [
|
|
172
|
+
{
|
|
173
|
+
"type": "command",
|
|
174
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LedgerUnapplied.ts --agent=claude"
|
|
133
175
|
}
|
|
134
176
|
]
|
|
135
177
|
}
|
|
@@ -140,7 +182,7 @@
|
|
|
140
182
|
"hooks": [
|
|
141
183
|
{
|
|
142
184
|
"type": "command",
|
|
143
|
-
"command": "
|
|
185
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts --agent=claude"
|
|
144
186
|
}
|
|
145
187
|
]
|
|
146
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"lint": "biome lint",
|
|
43
43
|
"lint-write": "biome lint --write",
|
|
44
44
|
"check": "biome check",
|
|
45
|
-
"check-write": "
|
|
45
|
+
"check-write": "bun .agents/scripts/check-write.ts",
|
|
46
46
|
"knip": "knip-bun",
|
|
47
47
|
"klint": "klint",
|
|
48
48
|
"jscpd": "jscpd --noTips",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"dependencies": {
|
|
97
97
|
"@clack/prompts": "^1.4.0",
|
|
98
98
|
"adm-zip": "^0.5.17",
|
|
99
|
+
"fast-myers-diff": "^3.2.0",
|
|
99
100
|
"marked": "18.0.4",
|
|
100
101
|
"pdf-lib": "1.17.1",
|
|
101
102
|
"playwright": "^1.60.0",
|
package/src/cli/index.ts
CHANGED
|
@@ -469,7 +469,11 @@ function checkCodexHooksRegistered(): boolean {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
function checkCopilotInstructionsPresent(): boolean {
|
|
472
|
-
|
|
472
|
+
const instructionsDir = resolve(platform.copilotDir(), "instructions");
|
|
473
|
+
if (!existsSync(instructionsDir)) return false;
|
|
474
|
+
return readdirSync(instructionsDir).some(
|
|
475
|
+
(f) => f.startsWith("pal-") && f.endsWith(".instructions.md")
|
|
476
|
+
);
|
|
473
477
|
}
|
|
474
478
|
|
|
475
479
|
// ── Install integrity (Tier 2 doctor checks) ──
|
|
@@ -939,8 +943,8 @@ function doctor(silent = false): DoctorResult {
|
|
|
939
943
|
? ok("Copilot hooks registered")
|
|
940
944
|
: fail("Copilot hooks — not registered (run 'pal cli install --copilot')");
|
|
941
945
|
checkCopilotInstructionsPresent()
|
|
942
|
-
? ok("
|
|
943
|
-
: warn("
|
|
946
|
+
? ok("Copilot instructions present")
|
|
947
|
+
: warn("Copilot instructions missing (written at first session stop)");
|
|
944
948
|
}
|
|
945
949
|
if (codex.available) {
|
|
946
950
|
checkCodexHooksRegistered()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PostToolUse — pairs the parked before-state with the result and writes
|
|
3
|
+
* the ledger entry.
|
|
4
|
+
*
|
|
5
|
+
* Only a call that actually landed reaches this event, so every entry written
|
|
6
|
+
* here is an applied one. A denied or failed call fires the snapshot half and
|
|
7
|
+
* never this one, leaving its snapshot unclaimed — which is why claiming also
|
|
8
|
+
* reaps the ones that were abandoned.
|
|
9
|
+
*
|
|
10
|
+
* Silent and fail-open, for the same reason as its other half.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { commitApplied, ledgeredCalls } from "./lib/ledger-hook";
|
|
14
|
+
import { logDebug } from "./lib/log";
|
|
15
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
19
|
+
if (!input) process.exit(0);
|
|
20
|
+
|
|
21
|
+
for (const call of ledgeredCalls(input)) {
|
|
22
|
+
const entry = commitApplied(call);
|
|
23
|
+
if (entry) {
|
|
24
|
+
logDebug("LedgerCommit", `recorded ${entry.id} ${entry.tool} ${entry.target}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PreToolUse — parks the target file's current contents before an edit.
|
|
3
|
+
*
|
|
4
|
+
* This half exists because the other half cannot do its job alone. A post-tool
|
|
5
|
+
* event fires once the file has already been rewritten, so the prior contents
|
|
6
|
+
* are gone and "what changed" is unanswerable from it. Reading the file here,
|
|
7
|
+
* before the tool runs, is the only moment the before-state still exists.
|
|
8
|
+
*
|
|
9
|
+
* Silent and fail-open: stdout is the agent's protocol channel, and a ledger
|
|
10
|
+
* that could block an edit would be a worse thing than a ledger with a gap.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ledgeredCalls, snapshotCall } from "./lib/ledger-hook";
|
|
14
|
+
import { logDebug } from "./lib/log";
|
|
15
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
19
|
+
if (!input) process.exit(0);
|
|
20
|
+
|
|
21
|
+
const calls = ledgeredCalls(input);
|
|
22
|
+
if (calls.length === 0) process.exit(0);
|
|
23
|
+
|
|
24
|
+
for (const call of calls) {
|
|
25
|
+
snapshotCall(call);
|
|
26
|
+
logDebug("LedgerSnapshot", `captured ${call.tool} ${call.toolUseId}`);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: PostToolUseFailure and PermissionDenied — records an edit that was
|
|
3
|
+
* attempted and did not land.
|
|
4
|
+
*
|
|
5
|
+
* A log that keeps only what succeeded cannot answer what was attempted, and
|
|
6
|
+
* that is usually the question being asked of it. So the two endings that are
|
|
7
|
+
* not success are recorded too, and kept apart: `failed` is the tool's own
|
|
8
|
+
* attempt breaking, `denied` is something refusing to let it run.
|
|
9
|
+
*
|
|
10
|
+
* One hook serves both events because they differ only in which key carries the
|
|
11
|
+
* reason — a difference `unappliedVerdictOf` owns, so registering this on a
|
|
12
|
+
* third such event later is a line in that table rather than a new file.
|
|
13
|
+
*
|
|
14
|
+
* Not everything that fails to land reaches here. A manual denial at the
|
|
15
|
+
* permission dialog, a `deny` rule, and a PreToolUse hook's own block all fire
|
|
16
|
+
* PreToolUse and nothing after it; a schema rejection fires no hook at all.
|
|
17
|
+
* Those attempts leave a snapshot no half ever claims, which is what the reaper
|
|
18
|
+
* in ledger.ts is for.
|
|
19
|
+
*
|
|
20
|
+
* Silent and fail-open, for the same reason as the other halves.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import {
|
|
25
|
+
claimPending,
|
|
26
|
+
type PendingSnapshot,
|
|
27
|
+
reapStalePending,
|
|
28
|
+
recordAction,
|
|
29
|
+
} from "./lib/ledger";
|
|
30
|
+
import { ledgeredCalls, unappliedVerdictOf } from "./lib/ledger-hook";
|
|
31
|
+
import { logDebug } from "./lib/log";
|
|
32
|
+
import { readStdinJSON } from "./lib/stdin";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The snapshot is the trustworthy source, but its absence is recoverable here
|
|
36
|
+
* in a way it never is after a successful edit: nothing landed, so whatever is
|
|
37
|
+
* on disk now is still the before-state.
|
|
38
|
+
*/
|
|
39
|
+
function beforeState(pending: PendingSnapshot | null, target: string): string | null {
|
|
40
|
+
if (pending) return pending.before;
|
|
41
|
+
return existsSync(target) ? readFileSync(target, "utf-8") : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const input = await readStdinJSON<Record<string, unknown>>();
|
|
46
|
+
if (!input) process.exit(0);
|
|
47
|
+
|
|
48
|
+
const calls = ledgeredCalls(input);
|
|
49
|
+
const verdict = calls.length > 0 && unappliedVerdictOf(input);
|
|
50
|
+
if (!verdict) process.exit(0);
|
|
51
|
+
|
|
52
|
+
for (const call of calls) {
|
|
53
|
+
const entry = recordAction({
|
|
54
|
+
tool: call.tool,
|
|
55
|
+
target: call.target,
|
|
56
|
+
outcome: verdict.outcome,
|
|
57
|
+
before: beforeState(claimPending(call.toolUseId), call.target),
|
|
58
|
+
// Nothing landed. That is what this event means, and it is the difference
|
|
59
|
+
// between this entry and an applied one.
|
|
60
|
+
after: null,
|
|
61
|
+
reason: verdict.reason,
|
|
62
|
+
});
|
|
63
|
+
logDebug("LedgerUnapplied", `recorded ${entry.id} ${entry.outcome} ${entry.target}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
reapStalePending();
|
|
67
|
+
} catch {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* This hook injects dynamic context only: wisdom principles, relationship notes,
|
|
6
6
|
* learning digest, signal trends, failure patterns, active work state.
|
|
7
7
|
*
|
|
8
|
-
* Copilot:
|
|
9
|
-
*
|
|
8
|
+
* Copilot: the CLI reads additionalContext from this hook's stdout, while
|
|
9
|
+
* ~/.copilot/instructions/ is read only by the VS Code extension. The merged
|
|
10
|
+
* context goes to both so either surface picks it up.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -63,6 +64,7 @@ try {
|
|
|
63
64
|
`---\napplyTo: "**"\n---\n\n${context}`,
|
|
64
65
|
"utf-8"
|
|
65
66
|
);
|
|
67
|
+
process.stdout.write(JSON.stringify({ additionalContext: context }));
|
|
66
68
|
}
|
|
67
69
|
logDebug(
|
|
68
70
|
"LoadContext",
|
package/src/hooks/lib/actor.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { resolve } from "node:path";
|
|
18
|
-
import { type AgentType,
|
|
18
|
+
import { type AgentType, declaredAgent } from "./agent";
|
|
19
19
|
import {
|
|
20
20
|
type IdentityBase,
|
|
21
21
|
loadIdentity,
|
|
@@ -50,8 +50,8 @@ export interface RecordAttribution {
|
|
|
50
50
|
machine: string;
|
|
51
51
|
/** Which person caused it. */
|
|
52
52
|
actor: string;
|
|
53
|
-
/** Which agent the actor was driving
|
|
54
|
-
runtime: AgentType;
|
|
53
|
+
/** Which agent the actor was driving, or "unknown" when none declared itself. */
|
|
54
|
+
runtime: AgentType | "unknown";
|
|
55
55
|
/** Whether a human turn was behind the call. */
|
|
56
56
|
authority: Authority;
|
|
57
57
|
}
|
|
@@ -165,7 +165,7 @@ export function currentAttribution(): RecordAttribution {
|
|
|
165
165
|
return {
|
|
166
166
|
machine: loadMachine().id,
|
|
167
167
|
actor: loadActor().id,
|
|
168
|
-
runtime:
|
|
168
|
+
runtime: declaredAgent() ?? "unknown",
|
|
169
169
|
authority: currentAuthority(),
|
|
170
170
|
};
|
|
171
171
|
}
|
package/src/hooks/lib/agent.ts
CHANGED
|
@@ -42,13 +42,20 @@ function agentFromArgv(): AgentType | undefined {
|
|
|
42
42
|
return value && KNOWN_AGENTS.has(value as AgentType) ? (value as AgentType) : undefined;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
export function getActiveAgent(): AgentType {
|
|
47
|
-
const declared = agentFromArgv() ?? agentFromEnv();
|
|
48
|
-
if (declared) return declared;
|
|
45
|
+
function agentFromRuntimeEnv(): AgentType | undefined {
|
|
49
46
|
if (process.env.CURSOR_VERSION) return "cursor";
|
|
50
47
|
if (process.env.CODEX_CLI_VERSION ?? process.env.OPENAI_CODEX) return "codex";
|
|
51
|
-
return
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The agent something actually said was running, or undefined if nothing did. */
|
|
52
|
+
export function declaredAgent(): AgentType | undefined {
|
|
53
|
+
return agentFromArgv() ?? agentFromEnv() ?? agentFromRuntimeEnv();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Which agent's conventions to follow. Assumes "claude" when undeclared. */
|
|
57
|
+
export function getActiveAgent(): AgentType {
|
|
58
|
+
return declaredAgent() ?? "claude";
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export const isClaude = () => getActiveAgent() === "claude";
|
|
@@ -71,10 +78,27 @@ function firstString(...values: unknown[]): string | undefined {
|
|
|
71
78
|
|
|
72
79
|
function firstObject(...values: unknown[]): Record<string, unknown> | undefined {
|
|
73
80
|
return values.find(
|
|
74
|
-
(v): v is Record<string, unknown> =>
|
|
81
|
+
(v): v is Record<string, unknown> =>
|
|
82
|
+
typeof v === "object" && v !== null && !Array.isArray(v)
|
|
75
83
|
);
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
/** Copilot's CLI sends toolArgs as JSON text where the others send an object. */
|
|
87
|
+
function parsedObject(value: unknown): Record<string, unknown> | undefined {
|
|
88
|
+
if (typeof value !== "string") return undefined;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(value);
|
|
91
|
+
return firstObject(parsed);
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toolInputOf(payload: Record<string, unknown>): Record<string, unknown> {
|
|
98
|
+
const candidates = [payload.tool_input, payload.toolArgs, payload.toolInput];
|
|
99
|
+
return firstObject(...candidates) ?? candidates.map(parsedObject).find(Boolean) ?? {};
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
/**
|
|
79
103
|
* Normalize a preToolUse payload across agents.
|
|
80
104
|
*
|
|
@@ -90,7 +114,7 @@ export function normalizeToolUse(raw: unknown): ToolUseRequest | null {
|
|
|
90
114
|
if (!toolName) return null;
|
|
91
115
|
return {
|
|
92
116
|
toolName,
|
|
93
|
-
toolInput:
|
|
117
|
+
toolInput: toolInputOf(payload),
|
|
94
118
|
hookEventName: firstString(payload.hook_event_name, payload.hookEventName),
|
|
95
119
|
};
|
|
96
120
|
}
|