@alphazede/bearing-lite 0.1.0 → 0.1.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 CHANGED
@@ -5,93 +5,111 @@
5
5
 
6
6
  **Bearing Lite** (`@alphazede/bearing-lite`) is a skills-first Agent Plugin for
7
7
  planning, routing, bounded execution, and independent review of repository work.
8
- It ships portable skills, references, templates, and optional client hooks. It
9
- does **not** ship a CLI, MCP server, browser control room, local scheduler, or
10
- hidden runtime database.
8
+ It ships portable skills, references, templates, and optional client hooks.
11
9
 
12
- An agent cannot certify its own work. The router selects the smallest valid
13
- route; independent assurance roles run only when declared, owner-selected, or
14
- required by a mandatory integrated phase gate. Owner Authority remains human-only.
10
+ An agent cannot certify its own work. The stateful Router owns the planning
11
+ conversation and visible Journey state, then dispatches fresh bounded sessions.
12
+ Independent assurance runs at the owner's selected cadence: per slice, per
13
+ execution/correction round, or once at the end. Owner Authority remains human-only.
15
14
 
16
- Bearing Lite was created by William Rumph at AlphaZede.
15
+ Bearing Lite was created by William Rumph.
17
16
 
18
17
  ## Install
19
18
 
20
- Install as an Agent Plugin in a compatible client (for example Codex or Claude
21
- Code marketplaces that discover Agent Plugins `plugin.json` and `skills/`).
19
+ Install as a host plugin. The portable identity is always `bearing-lite` /
20
+ `@alphazede/bearing-lite`. No postinstall script, global hook copy, or
21
+ host-config mutation is required.
22
22
 
23
23
  ```sh
24
- # Example: npm package for packaging and inspection
25
- npm pack @alphazede/bearing-lite
24
+ # Claude Code
25
+ claude plugin marketplace add /path/to/bearing-lite
26
+ claude plugin install bearing-lite@bearing-lite
27
+
28
+ # Codex
29
+ codex plugin marketplace add /path/to/bearing-lite
30
+ codex plugin add bearing-lite@bearing-lite
31
+
32
+ # Grok Build
33
+ grok plugin marketplace add /path/to/bearing-lite
34
+ grok plugin install bearing-lite --trust
35
+
36
+ # Cursor
37
+ # Add the checkout as a Cursor marketplace/plugin (.cursor-plugin/)
38
+
39
+ # Kimi Code
40
+ # /plugins install /path/to/bearing-lite
41
+
42
+ # AGY (Antigravity) — install the .agy root, not the repo root
43
+ agy plugin install /path/to/bearing-lite/.agy
44
+
45
+ # Pi — skills package, no command hooks
46
+ pi install git:github.com/alphazede/bearing-lite
26
47
  ```
27
48
 
28
- Client install names follow the host plugin UI. The portable identity is always
29
- `bearing-lite` / `@alphazede/bearing-lite`. No postinstall script, global hook
30
- copy, or host-config mutation is required.
49
+ Claude Code, Codex, Grok Build, Cursor, and Kimi Code are **partial** hook
50
+ clients: session start runs the activation advisory and stop runs the closeout
51
+ advisory. AGY and Pi are **skills-only**. Transition-order and protected-action
52
+ checks stay in the skills on every host. Node.js must be on `PATH` for the
53
+ hook adapters.
31
54
 
32
- Node.js is not required to *use* the skills. Optional typed hook adapters under
33
- `hooks/` run only when a client registers them; hookless clients remain
34
- first-class and use procedural skill checks.
55
+ **Skills-only copy** of `skills/` into a host skills directory does not
56
+ register hooks. That path remains first-class. See
57
+ [`hooks/com.anthropic.claude-code/mapping.md`](hooks/com.anthropic.claude-code/mapping.md).
35
58
 
36
59
  ## What it does
37
60
 
38
- 1. **Locate** the project plan and next ready task.
61
+ 1. **Ask** whether the Journey is an Explorer Journey or an Expedition.
39
62
  2. **Fill only missing planning stages:** Repository Fit → Set Bearings → Gather
40
63
  Supplies → Map the Route.
41
- 3. **Choose the smallest role route** that preserves dependencies and
42
- `required_assurance`.
43
- 4. **Record task state** only in the project's human-readable plan (Markdown).
44
- 5. **Return structured handoffs**; parent coordinators write plan transitions.
64
+ 3. **Confirm** the user-owned primary/fallback lineup and review cadence.
65
+ 4. **Dispatch fresh sessions** with bounded context, authority, and return types.
66
+ 5. **Record state visibly** in human-readable Markdown artifacts only.
45
67
 
46
68
  Bearing Lite never selects models, providers, credentials, or launchers. The
47
- owner or client maps available agents to role capability needs.
69
+ owner provides each role's primary/fallback agent or harness, model, and
70
+ reasoning level in `~/.agents/bearing-lite/default-role-lineup.md`, then confirms
71
+ the applicable Journey snapshot before implementation.
48
72
 
49
73
  ## Routes and scaling
50
74
 
51
75
  | Route | When | Cost |
52
76
  |---|---|---|
53
- | **Direct** | One bounded implementer packet | Lowest coordination |
54
- | **Explorer (wave)** | One wave, one or more Crewmates | One lane controller |
77
+ | **Explorer Journey** | One bounded packet or one wave | Direct Crewmate or one Explorer |
55
78
  | **Expedition** | Multi-phase or concurrent independent lanes | Navigator; Trail Boss only for concurrent/conflicting waves |
56
- | **Long multi-phase** | Owner-approved intent across sessions | Optional Delegate Authority |
57
79
 
58
- **Explorer** keeps one controller over a compact or sequential set of slices.
80
+ An **Explorer Journey** uses a direct Crewmate for one ready packet or one
81
+ Explorer over a compact/sequential wave.
59
82
  **Expedition** adds navigation (and sometimes a Trail Boss) so independent lanes
60
83
  stay small and sharp instead of degrading in one long context. Either shape can
61
84
  use substantial tokens; the product does not impose a default budget ceiling.
62
85
 
63
86
  ### Role routing (explanatory)
64
87
 
65
- ![Bearing Lite role routing: Owner Authority and the router select missing planning stages, then the smallest route among Direct Crewmate, Explorer wave, Expedition Navigator, or long multi-phase Delegate Authority; optional Trail Boss, Sub-explorer, and assurance roles appear only when required](skills/bearing-lite/assets/role-routing.png)
66
-
67
- Reviewable Mermaid source: [`skills/bearing-lite/references/role-routing.mmd`](skills/bearing-lite/references/role-routing.mmd)
68
- (plan-local twin under `docs/plans/2026-08-09-bearing-skills-first-architecture/assets/`).
88
+ Current routing diagram source: [`skills/bearing-lite/references/role-routing.mmd`](skills/bearing-lite/references/role-routing.mmd).
89
+ The text below remains authoritative for clients that do not render Mermaid.
69
90
 
70
91
  **Authoritative text (vision optional):** Owner Authority remains human-only. The
71
- Bearing Lite Router is entry, not a work role. It invokes only missing planning
72
- stages, then picks the least costly route: Direct Crewmate; Explorer for one
73
- wave; Navigator for an expedition (Trail Boss only when waves conflict or run
74
- concurrently); Delegate Authority only for explicit long multi-phase owner
75
- delegation. Nested Sub-explorer opens only when a lane must split. After
76
- Crewmate work, `required_assurance: none` means author self-check plus
77
- coordinator confirmation; Validator, Park Ranger, and Surveyor appear only when
78
- listed, owner-selected at slice level, or required by a mandatory phase gate.
79
- Diagrams explain orientation; they never authorize a transition.
92
+ Bearing Lite Router is the stateful planning controller, not a work role. It
93
+ invokes only missing planning stages in fresh sessions, confirms the owner's
94
+ lineup and cadence, then dispatches an Explorer Journey or Expedition. Nested
95
+ Sub-Explorer opens only for proven-independent lanes. Validator, Park Ranger,
96
+ and Surveyor appear only when declared and when the selected per-slice,
97
+ per-round, or at-end boundary is reached. Diagrams explain orientation; they
98
+ never authorize a transition.
80
99
 
81
100
  ## Roles and authority
82
101
 
83
102
  | Role | What it is | Executes | Notes |
84
103
  |---|---|---|---|
85
- | **Router** | Plugin entry procedure | no | Not a work role |
86
- | **Navigator** | Expedition orchestrator | yes | Owns cross-wave sequencing |
87
- | **Trail Boss** | Multi-wave controller | yes | Only concurrent or conflicting waves |
88
- | **Explorer** | One-wave lane controller | yes | Dispatches Crewmates |
89
- | **Sub-explorer** | Nested lane controller | yes | Only when a lane must split |
90
- | **Crewmate** | Bounded implementer | yes | Writes only inside declared authority |
91
- | **Validator** | Evidence sufficiency | yes | Independent of the author |
92
- | **Park Ranger** | Defect review | yes | Independent of the author |
93
- | **Surveyor** | User-facing acceptance | no | Read-only acceptance judgment |
94
- | **Delegate Authority** | Cross-session phase owner | yes | Only when owner explicitly delegates |
104
+ | **Router** | Stateful planning controller | no | User-facing; planning-state writer |
105
+ | **Navigator** | Expedition orchestrator | no | Owns cross-wave sequencing |
106
+ | **Trail Boss** | Multi-wave controller | no | Only concurrent or conflicting waves |
107
+ | **Explorer** | One-wave controller | no | Dispatches Crewmates |
108
+ | **Sub-explorer** | Nested lane controller | no | Only when lanes are proven independent |
109
+ | **Crewmate** | Bounded implementer | yes | Most hands-on work; exact write set |
110
+ | **Validator** | Evidence sufficiency | no | Independent of the author |
111
+ | **Park Ranger** | Defect review | no | Independent of the author |
112
+ | **Surveyor** | User-facing acceptance | no | Read-only outcome judgment |
95
113
  | **Owner Authority** | Human decision | n/a | Never an agent role |
96
114
 
97
115
  **Independent review:** a candidate author never provides their own Validator,
@@ -103,7 +121,7 @@ Failure escalates to the nearest role whose scope can see it:
103
121
  |---|---|
104
122
  | Within one slice or packet | Explorer or nearest parent |
105
123
  | Across slices in a wave | Trail Boss when present, else Navigator |
106
- | Across phases | Navigator or Delegate Authority |
124
+ | Across phases | Navigator |
107
125
  | Contract, security, or authority change | Owner Authority |
108
126
 
109
127
  ## Task state (explanatory)
@@ -177,7 +195,7 @@ flowchart TD
177
195
  |---|---|
178
196
  | `plugin.json` | Agent Plugins v1.0.0 manifest |
179
197
  | `skills/` | Router, planning stages, and role skills |
180
- | `hooks/` | Optional client-specific sequencing adapters |
198
+ | `hooks/` | Four portable class adapters plus the verified Claude Code / Codex mapping |
181
199
  | `README.md` and governance docs | Public product and conduct surfaces |
182
200
 
183
201
  There is no `mcp.json`, `bin` entrypoint, postinstall, or runtime dependency on
@@ -189,14 +207,6 @@ Skills declare **capabilities** (reasoning depth, repository access, mutation
189
207
  tools, independence, optional vision). They never pin a model, provider API key,
190
208
  default route, or launcher. Owners and clients choose how to satisfy each role.
191
209
 
192
- ## Migration note
193
-
194
- Bearing Lite does not import historical deep-harness run directories or hidden
195
- runtime state. Resume from the approved project plan and a verified
196
- human-readable handoff using native agent tools. See
197
- [guide/migration.md](guide/migration.md) for existing-run migration and
198
- owner distribution checkpoints.
199
-
200
210
  ## Contributing
201
211
 
202
212
  Issues and carefully scoped pull requests help. See
@@ -4,7 +4,8 @@
4
4
  /**
5
5
  * Bearing Lite activation/context advisory adapter (CONTRACT-HOOK-01).
6
6
  * Advisory only: outcomes are ADVISE or UNAVAILABLE. Never fabricates BLOCK.
7
- * Skills-only clients: enforcement remains procedural until a verified host maps it.
7
+ * Skills-only clients: enforcement remains procedural. Claude Code and Codex
8
+ * use the verified partial mapping in hooks/com.anthropic.claude-code/.
8
9
  */
9
10
 
10
11
  const HOOK_CLASS = "activation";
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Verified host adapter (CONTRACT-HOOK-01, CONTRACT-CLIENT-01).
6
+ *
7
+ * Claude Code, Codex, Grok Build, Cursor, and Kimi Code deliver session
8
+ * events. This adapter derives only the Bearing fields that are visible in
9
+ * the project plan, then calls the portable class evaluators. It never
10
+ * fabricates BLOCK from a missing field.
11
+ *
12
+ * Coverage is partial: activation and closeout are executable; transition and
13
+ * protected_action stay procedural. Always exit 0 (RISK-12 / RISK-20). Host
14
+ * blocking, if ever required, is JSON-only — never process status.
15
+ */
16
+
17
+ const fs = require("node:fs");
18
+ const path = require("node:path");
19
+
20
+ const activation = require("../activation.cjs");
21
+ const closeout = require("../closeout.cjs");
22
+
23
+ const HOST = "com.anthropic.claude-code";
24
+ const COVERAGE = Object.freeze({
25
+ activation: Object.freeze({ executable: true, procedural: true }),
26
+ closeout: Object.freeze({ executable: true, procedural: true }),
27
+ transition: Object.freeze({ executable: false, procedural: true }),
28
+ protected_action: Object.freeze({ executable: false, procedural: true }),
29
+ });
30
+
31
+ const MAX_FILES = 20;
32
+ const MAX_BYTES = 256 * 1024;
33
+ const MAX_DEPTH = 3;
34
+ const SKIP_DIRS = new Set([
35
+ "node_modules",
36
+ ".git",
37
+ "dist",
38
+ "coverage",
39
+ ".next",
40
+ ".cache",
41
+ "target",
42
+ "vendor",
43
+ ".venv",
44
+ "venv",
45
+ "skills",
46
+ "hooks",
47
+ "test",
48
+ "tests",
49
+ "templates",
50
+ ]);
51
+
52
+ const TASK_MARK = /(?:^|\n)###\s*task_id:\s*\S+/;
53
+ const TASK_LINE = /^###\s*task_id:\s*(\S+)\s*$/;
54
+ const FIELD_LINE =
55
+ /^-\s*(assigned_role|next_action|status|required_assurance|blocker|evidence|candidate_ref|scope|authority|outcome|depends_on|receiving_role|plan_ref|role|subject):\s*(.*)$/;
56
+ const JOURNEY_LINE = /^-\s*journey:\s*(.+)$/i;
57
+
58
+ const ACTIVATION_EVENTS = new Set([
59
+ "SessionStart",
60
+ "UserPromptSubmit",
61
+ "UserPromptExpansion",
62
+ "CwdChanged",
63
+ "SubagentStart",
64
+ ]);
65
+ const CLOSEOUT_EVENTS = new Set(["Stop", "SubagentStop", "SessionEnd"]);
66
+
67
+ const EVENT_ALIASES = Object.freeze({
68
+ sessionstart: "SessionStart",
69
+ userpromptsubmit: "UserPromptSubmit",
70
+ beforesubmitprompt: "UserPromptSubmit",
71
+ userpromptexpansion: "UserPromptExpansion",
72
+ cwdchanged: "CwdChanged",
73
+ subagentstart: "SubagentStart",
74
+ stop: "Stop",
75
+ subagentstop: "SubagentStop",
76
+ sessionend: "SessionEnd",
77
+ pretooluse: "PreToolUse",
78
+ posttooluse: "PostToolUse",
79
+ beforeshellexecution: "PreToolUse",
80
+ afterfileedit: "PostToolUse",
81
+ });
82
+
83
+ /**
84
+ * Normalize Claude, Codex, Grok, Cursor, and Kimi event names.
85
+ * @param {unknown} name
86
+ */
87
+ function canonicalizeEvent(name) {
88
+ if (typeof name !== "string" || !name.trim()) return "";
89
+ const compact = name.trim().replace(/[_-]/g, "").toLowerCase();
90
+ return EVENT_ALIASES[compact] || name.trim();
91
+ }
92
+
93
+ function isPlainObject(value) {
94
+ return value !== null && typeof value === "object" && !Array.isArray(value);
95
+ }
96
+
97
+ function isPlaceholder(value) {
98
+ if (typeof value !== "string") return false;
99
+ const trimmed = value.trim();
100
+ return trimmed === "" || trimmed === "unassigned" || /^<[^>]+>$/.test(trimmed);
101
+ }
102
+
103
+ function presentString(value) {
104
+ if (typeof value !== "string") return undefined;
105
+ const trimmed = value.trim();
106
+ if (!trimmed || isPlaceholder(trimmed)) return undefined;
107
+ return trimmed;
108
+ }
109
+
110
+ function readStdinSync() {
111
+ try {
112
+ return fs.readFileSync(0, "utf8");
113
+ } catch (err) {
114
+ const code = err && err.code;
115
+ if (code === "EAGAIN" || code === "EOF") return "";
116
+ throw err;
117
+ }
118
+ }
119
+
120
+ function rankMarkdown(name) {
121
+ const n = name.toLowerCase();
122
+ if (n === "plan.md" || n === "progress.md") return 0;
123
+ if (n.includes("plan")) return 1;
124
+ if (n.includes("implementation") || n.includes("progress")) return 2;
125
+ return 5;
126
+ }
127
+
128
+ function walkMarkdown(root) {
129
+ const out = [];
130
+ function walk(dir, depth) {
131
+ if (out.length >= MAX_FILES || depth > MAX_DEPTH) return;
132
+ let entries;
133
+ try {
134
+ entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ } catch {
136
+ return;
137
+ }
138
+ const files = [];
139
+ const dirs = [];
140
+ for (const entry of entries) {
141
+ if (entry.isSymbolicLink()) continue;
142
+ if (entry.name.startsWith(".") && entry.name !== ".agents") continue;
143
+ if (entry.isDirectory()) {
144
+ if (!SKIP_DIRS.has(entry.name)) dirs.push(entry.name);
145
+ } else if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) {
146
+ files.push(entry.name);
147
+ }
148
+ }
149
+ files.sort((a, b) => rankMarkdown(a) - rankMarkdown(b) || a.localeCompare(b));
150
+ for (const name of files) {
151
+ if (out.length >= MAX_FILES) return;
152
+ out.push(path.join(dir, name));
153
+ }
154
+ dirs.sort((a, b) => {
155
+ if (a === "docs") return -1;
156
+ if (b === "docs") return 1;
157
+ if (a === "plans") return -1;
158
+ if (b === "plans") return 1;
159
+ return a.localeCompare(b);
160
+ });
161
+ for (const name of dirs) {
162
+ walk(path.join(dir, name), depth + 1);
163
+ }
164
+ }
165
+ walk(root, 0);
166
+ return out;
167
+ }
168
+
169
+ function parsePlanMarkdown(text) {
170
+ const tasks = [];
171
+ let journey = null;
172
+ let current = null;
173
+ for (const line of String(text).split(/\r?\n/)) {
174
+ const journeyMatch = line.match(JOURNEY_LINE);
175
+ if (journeyMatch) {
176
+ const value = presentString(journeyMatch[1]);
177
+ if (value) journey = value;
178
+ }
179
+ const taskMatch = line.match(TASK_LINE);
180
+ if (taskMatch) {
181
+ if (current) tasks.push(current);
182
+ current = { task_id: taskMatch[1] };
183
+ continue;
184
+ }
185
+ if (!current) continue;
186
+ const fieldMatch = line.match(FIELD_LINE);
187
+ if (fieldMatch) {
188
+ const value = presentString(fieldMatch[2]);
189
+ if (value !== undefined) current[fieldMatch[1]] = value;
190
+ }
191
+ }
192
+ if (current) tasks.push(current);
193
+ return { tasks, journey };
194
+ }
195
+
196
+ /**
197
+ * Derive Bearing domain fields from visible Markdown in cwd.
198
+ * Does not invent router_invoked, assigned_role, or next_action.
199
+ * @param {string} cwd
200
+ */
201
+ function deriveContext(cwd) {
202
+ const files = walkMarkdown(cwd);
203
+ const allTasks = [];
204
+ let journey = null;
205
+ let planPresent = false;
206
+ for (const file of files) {
207
+ let text;
208
+ try {
209
+ const st = fs.statSync(file);
210
+ if (!st.isFile() || st.size > MAX_BYTES) continue;
211
+ text = fs.readFileSync(file, "utf8");
212
+ } catch {
213
+ continue;
214
+ }
215
+ if (!TASK_MARK.test(text) && !/^-\s*assigned_role:/m.test(text)) continue;
216
+ planPresent = true;
217
+ const parsed = parsePlanMarkdown(text);
218
+ if (parsed.journey) journey = parsed.journey;
219
+ for (const task of parsed.tasks) allTasks.push(task);
220
+ }
221
+
222
+ const active =
223
+ allTasks.find((task) => {
224
+ const status = presentString(task.status);
225
+ return status && status !== "COMPLETE" && status !== "CANCELLED";
226
+ }) ||
227
+ allTasks[0] ||
228
+ null;
229
+
230
+ const assignedRole = active ? presentString(active.assigned_role) : undefined;
231
+ const nextAction = active ? presentString(active.next_action) : undefined;
232
+
233
+ return {
234
+ plan_present: planPresent,
235
+ router_invoked: Boolean(journey),
236
+ assigned_role: assignedRole,
237
+ next_action: nextAction,
238
+ next_action_known: Boolean(nextAction),
239
+ active_task: active,
240
+ journey,
241
+ };
242
+ }
243
+
244
+ function classForEvent(eventName) {
245
+ const canonical = canonicalizeEvent(eventName);
246
+ if (ACTIVATION_EVENTS.has(canonical)) return "activation";
247
+ if (CLOSEOUT_EVENTS.has(canonical)) return "closeout";
248
+ return null;
249
+ }
250
+
251
+ function formatAdvice(verdict) {
252
+ const lines = [
253
+ "Bearing Lite " + verdict.hook_class + ": " + verdict.outcome,
254
+ verdict.reason ? "reason: " + verdict.reason : "",
255
+ verdict.recovery ? "recovery: " + verdict.recovery : "",
256
+ "Coverage: this host mapping is partial. activation and closeout are executable; transition and protected_action remain procedural.",
257
+ ].filter(Boolean);
258
+ return lines.join("\n").slice(0, 4000);
259
+ }
260
+
261
+ function unavailableHost(eventName, reason) {
262
+ const event = eventName || "SessionStart";
263
+ return {
264
+ hookSpecificOutput: {
265
+ hookEventName: event,
266
+ additionalContext:
267
+ "Bearing Lite UNAVAILABLE: " +
268
+ reason +
269
+ ". Invoke the bearing-lite router manually and continue after context is loaded.",
270
+ },
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Translate a portable verdict into Claude Code / Codex JSON.
276
+ * Exit status is never used for policy (RISK-12 / RISK-20).
277
+ */
278
+ function toHostResponse(eventName, verdict) {
279
+ const event = eventName || "SessionStart";
280
+ const text = formatAdvice(verdict);
281
+ const body = {
282
+ hookSpecificOutput: {
283
+ hookEventName: event,
284
+ additionalContext: text,
285
+ },
286
+ };
287
+ // This mapping never requests protected_completion, so BLOCK should not
288
+ // appear. If a future caller passes a BLOCK verdict, use JSON only.
289
+ if (verdict.outcome === "BLOCK") {
290
+ body.decision = "block";
291
+ body.reason = text;
292
+ }
293
+ return body;
294
+ }
295
+
296
+ function evaluateForHost(eventName, derived) {
297
+ if (classForEvent(eventName) === "closeout") {
298
+ const task = derived.active_task || {};
299
+ return closeout.evaluate({
300
+ plan_ref: derived.plan_present ? "project-plan" : undefined,
301
+ role: presentString(task.assigned_role) || presentString(task.role),
302
+ subject: presentString(task.task_id) || presentString(task.subject),
303
+ depends_on: task.depends_on,
304
+ scope: presentString(task.scope),
305
+ authority: presentString(task.authority),
306
+ outcome: presentString(task.outcome),
307
+ evidence: presentString(task.evidence),
308
+ blocker: presentString(task.blocker) || "none",
309
+ next_action: presentString(task.next_action),
310
+ receiving_role: presentString(task.receiving_role),
311
+ required_assurance: task.required_assurance,
312
+ });
313
+ }
314
+ return activation.evaluate({
315
+ plan_present: derived.plan_present === true,
316
+ next_action_known: derived.next_action_known === true,
317
+ next_action: derived.next_action,
318
+ assigned_role: derived.assigned_role,
319
+ router_invoked: derived.router_invoked === true,
320
+ });
321
+ }
322
+
323
+ /**
324
+ * @param {unknown} input
325
+ */
326
+ function handle(input) {
327
+ let parsed = input;
328
+ if (typeof parsed === "string") {
329
+ try {
330
+ parsed = JSON.parse(parsed);
331
+ } catch {
332
+ return unavailableHost("SessionStart", "malformed_input");
333
+ }
334
+ }
335
+ if (parsed === undefined || parsed === null) {
336
+ return unavailableHost("SessionStart", "missing_input");
337
+ }
338
+ if (!isPlainObject(parsed)) {
339
+ return unavailableHost("SessionStart", "malformed_input");
340
+ }
341
+
342
+ const eventName = canonicalizeEvent(
343
+ parsed.hook_event_name || parsed.hookEventName
344
+ );
345
+ if (!classForEvent(eventName)) {
346
+ return unavailableHost(eventName || "SessionStart", "unmapped_host_event");
347
+ }
348
+
349
+ const cwd =
350
+ presentString(parsed.cwd) ||
351
+ presentString(parsed.workspaceRoot) ||
352
+ process.cwd();
353
+
354
+ let derived;
355
+ try {
356
+ derived = deriveContext(cwd);
357
+ } catch {
358
+ return toHostResponse(
359
+ eventName,
360
+ activation.evaluate({ infrastructure_failure: "permission_error" })
361
+ );
362
+ }
363
+
364
+ return toHostResponse(eventName, evaluateForHost(eventName, derived));
365
+ }
366
+
367
+ function main() {
368
+ let raw = "";
369
+ try {
370
+ raw = readStdinSync();
371
+ } catch {
372
+ process.stdout.write(JSON.stringify(unavailableHost("SessionStart", "stdin_read_failure")) + "\n");
373
+ process.exit(0);
374
+ return;
375
+ }
376
+
377
+ const trimmed = raw.trim();
378
+ if (!trimmed) {
379
+ process.stdout.write(JSON.stringify(unavailableHost("SessionStart", "missing_input")) + "\n");
380
+ process.exit(0);
381
+ return;
382
+ }
383
+
384
+ process.stdout.write(JSON.stringify(handle(trimmed)) + "\n");
385
+ process.exit(0);
386
+ }
387
+
388
+ module.exports = {
389
+ HOST,
390
+ COVERAGE,
391
+ canonicalizeEvent,
392
+ classForEvent,
393
+ deriveContext,
394
+ evaluateForHost,
395
+ handle,
396
+ toHostResponse,
397
+ };
398
+
399
+ if (require.main === module) {
400
+ main();
401
+ }
@@ -0,0 +1,63 @@
1
+ # Verified host mapping
2
+
3
+ This adapter is the verified native mapping for hosts that share a
4
+ session-start / stop command hook. Coverage is **partial**: activation and
5
+ closeout are executable; transition-order and protected-action stay
6
+ procedural.
7
+
8
+ ## Hosts
9
+
10
+ | Host | Plugin install | Hooks | Coverage |
11
+ |---|---|---|---|
12
+ | Claude Code | `.claude-plugin/` marketplace | `hooks/hooks.json` | partial |
13
+ | Codex | `.codex-plugin/` + `.agents/plugins/` | `hooks/hooks.json` | partial |
14
+ | Grok Build | `.grok-plugin/` marketplace | `hooks/hooks.json` | partial |
15
+ | Cursor | `.cursor-plugin/` | `hooks/com.cursor/hooks.json` (`sessionStart`, `stop`) | partial |
16
+ | Kimi Code | `.kimi-plugin/plugin.json` | manifest `hooks` array | partial |
17
+ | AGY | `.agy/` (strict `plugin.json`) | none | skills-only |
18
+ | Pi | `package.json` `"pi"` + `pi-package` | none (TypeScript extensions, not command hooks) | skills-only |
19
+
20
+ Do **not** set `hooks` on Claude, Codex, or Grok host manifests. Those hosts
21
+ auto-load `hooks/hooks.json`; declaring both is a duplicate-file error.
22
+
23
+ Skill-copy into a host skills directory does **not** register hooks. Plugin
24
+ install or disable uses the host's native controls. Bearing Lite never copies
25
+ adapters into global hook configuration.
26
+
27
+ ## Event map
28
+
29
+ | Host event names | Bearing class | Executable? |
30
+ |---|---|---|
31
+ | `SessionStart`, `sessionStart`, `session_start` | activation | yes, advisory |
32
+ | `Stop`, `stop` | closeout | yes, advisory only |
33
+ | any other host event | none | unmapped; fail open as `UNAVAILABLE` |
34
+
35
+ The adapter accepts snake_case and camelCase (`hook_event_name` /
36
+ `hookEventName`, `cwd` / `workspaceRoot`).
37
+
38
+ ## Derived fields
39
+
40
+ The host event supplies session metadata. The adapter reads visible Markdown
41
+ under `cwd` (bounded; skips `skills/`, `hooks/`, `test/`, and dependency
42
+ trees) and sets only:
43
+
44
+ | Bearing field | Source |
45
+ |---|---|
46
+ | `plan_present` | a Markdown file contains a `task_id` or `assigned_role` block |
47
+ | `router_invoked` | a `- journey:` setting is present and not a placeholder |
48
+ | `assigned_role` | active task `assigned_role` when not `unassigned` or `<…>` |
49
+ | `next_action` / `next_action_known` | active task `next_action` when not a placeholder |
50
+ | closeout handoff fields | matching task-record keys when present |
51
+
52
+ `assigned_role` and `router_invoked` are **not** inferred from the tool-call
53
+ payload. Missing values stay missing, so activation advises the router instead
54
+ of inventing context.
55
+
56
+ ## Outcome translation
57
+
58
+ | Outcome | Host JSON | Process exit |
59
+ |---|---|---|
60
+ | `ADVISE`, `REROUTE`, `UNAVAILABLE` | `hookSpecificOutput.additionalContext` | always `0` |
61
+ | `BLOCK` | JSON `decision: "block"` only; this mapping never requests protected completion | always `0` |
62
+
63
+ Never map policy or infrastructure to a non-zero exit.