@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 +70 -60
- package/hooks/activation.cjs +2 -1
- package/hooks/com.anthropic.claude-code/host.cjs +401 -0
- package/hooks/com.anthropic.claude-code/mapping.md +63 -0
- package/hooks/com.cursor/hooks.json +15 -0
- package/hooks/hooks.json +35 -0
- package/package.json +7 -4
- package/plugin.json +34 -2
- package/skills/bearing-lite/SKILL.md +45 -33
- package/skills/bearing-lite/agents/openai.yaml +7 -0
- package/skills/bearing-lite/assets/role-routing.png +0 -0
- package/skills/bearing-lite/references/peer-synthesis.md +21 -0
- package/skills/bearing-lite/references/role-routing.mmd +21 -28
- package/skills/bearing-lite/templates/default-role-lineup.md +26 -0
- package/skills/bearing-lite/templates/task.md +17 -0
- package/skills/crewmate/SKILL.md +26 -23
- package/skills/explorer/SKILL.md +28 -24
- package/skills/gather-supplies/SKILL.md +31 -19
- package/skills/map-the-route/SKILL.md +36 -26
- package/skills/map-the-route/references/artifact-grammar.md +10 -10
- package/skills/navigator/SKILL.md +28 -30
- package/skills/park-ranger/SKILL.md +25 -24
- package/skills/repository-fit/SKILL.md +25 -19
- package/skills/set-bearings/SKILL.md +25 -19
- package/skills/sub-explorer/SKILL.md +26 -24
- package/skills/surveyor/SKILL.md +26 -24
- package/skills/trail-boss/SKILL.md +26 -24
- package/skills/validator/SKILL.md +26 -27
- package/guide/migration.md +0 -168
- package/skills/delegate-authority/SKILL.md +0 -38
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.
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
15
|
+
Bearing Lite was created by William Rumph.
|
|
17
16
|
|
|
18
17
|
## Install
|
|
19
18
|
|
|
20
|
-
Install as
|
|
21
|
-
|
|
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
|
-
#
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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. **
|
|
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. **
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
| **
|
|
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**
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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** |
|
|
86
|
-
| **Navigator** | Expedition orchestrator |
|
|
87
|
-
| **Trail Boss** | Multi-wave controller |
|
|
88
|
-
| **Explorer** | One-wave
|
|
89
|
-
| **Sub-explorer** | Nested lane controller |
|
|
90
|
-
| **Crewmate** | Bounded implementer | yes |
|
|
91
|
-
| **Validator** | Evidence sufficiency |
|
|
92
|
-
| **Park Ranger** | Defect review |
|
|
93
|
-
| **Surveyor** | User-facing acceptance | no | Read-only
|
|
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
|
|
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/` |
|
|
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
|
package/hooks/activation.cjs
CHANGED
|
@@ -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
|
|
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.
|