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.
@@ -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": "PAL_AGENT=codex bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
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": "PAL_AGENT=codex bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
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": "PAL_AGENT=codex bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
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": "PAL_AGENT=codex bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts",
8
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts",
15
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts",
22
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts",
27
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts",
32
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
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": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts",
39
- "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
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": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
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": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
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": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
19
+ "command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts --agent=cursor"
20
20
  },
21
21
  {
22
22
  "type": "command",
23
- "command": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
23
+ "command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts --agent=cursor"
24
24
  },
25
25
  {
26
26
  "type": "command",
27
- "command": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
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": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
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": "PAL_AGENT=cursor bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/CompactRecover.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/PreCompactPersist.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
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": "PAL_AGENT=claude bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
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.66.1",
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": "biome 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
- return existsSync(resolve(platform.copilotDir(), "copilot-instructions.md"));
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("copilot-instructions.md present")
943
- : warn("copilot-instructions.md missing (run 'pal cli install --copilot')");
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
+ }
@@ -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: sessionStart output is ignored by the runtime. Instead, we write the merged
9
- * context directly to ~/.copilot/copilot-instructions.md so it is picked up on load.
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",
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { resolve } from "node:path";
18
- import { type AgentType, getActiveAgent } from "./agent";
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 — claude, codex, cursor, copilot, opencode. */
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: getActiveAgent(),
168
+ runtime: declaredAgent() ?? "unknown",
169
169
  authority: currentAuthority(),
170
170
  };
171
171
  }
@@ -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
- /** Detect which agent is currently running PAL. Defaults to "claude". */
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 "claude";
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> => typeof v === "object" && v !== null
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: firstObject(payload.tool_input, payload.toolArgs, payload.toolInput) ?? {},
117
+ toolInput: toolInputOf(payload),
94
118
  hookEventName: firstString(payload.hook_event_name, payload.hookEventName),
95
119
  };
96
120
  }