role-os 1.3.0 → 1.5.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/CHANGELOG.md +56 -0
- package/README.md +5 -1
- package/bin/roleos.mjs +23 -1
- package/package.json +1 -1
- package/src/hooks.mjs +469 -0
- package/src/session.mjs +395 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
#### Hook Spine / Runtime Enforcement (Phase R)
|
|
8
|
+
- 5 lifecycle hooks: SessionStart, UserPromptSubmit, PreToolUse, SubagentStart, Stop
|
|
9
|
+
- `scaffoldHooks()` generates all 5 hook scripts in .claude/hooks/
|
|
10
|
+
- `roleos init claude` now scaffolds hooks + settings.local.json with hook config
|
|
11
|
+
- `roleos doctor` now checks for hook scripts (check 7) and settings hooks (check 8)
|
|
12
|
+
|
|
13
|
+
#### SessionStart hook
|
|
14
|
+
- Establishes session contract on every new session
|
|
15
|
+
- Records session ID, timestamp, initializes state tracking
|
|
16
|
+
- Adds context reminding Claude to use /roleos-route for non-trivial tasks
|
|
17
|
+
|
|
18
|
+
#### UserPromptSubmit hook
|
|
19
|
+
- Classifies prompts as substantial (>50 chars + action verbs)
|
|
20
|
+
- After 2+ substantial prompts without a route card, adds context reminder
|
|
21
|
+
- Does not block — advisory enforcement
|
|
22
|
+
|
|
23
|
+
#### PreToolUse hook
|
|
24
|
+
- Records all tool usage in session state
|
|
25
|
+
- Flags write tools (Bash, Write, Edit) used without route card after substantial work
|
|
26
|
+
- Advisory, not blocking — preserves operator control
|
|
27
|
+
|
|
28
|
+
#### SubagentStart hook
|
|
29
|
+
- Injects active role contract into delegated agents
|
|
30
|
+
- Ensures subagents inherit the Role OS session context
|
|
31
|
+
|
|
32
|
+
#### Stop hook
|
|
33
|
+
- Warns when substantial sessions end without route card or outcome artifact
|
|
34
|
+
- Advisory — does not block session exit
|
|
35
|
+
- Trivial sessions (< 2 substantial prompts) are exempt
|
|
36
|
+
|
|
37
|
+
### Evidence
|
|
38
|
+
- 358 tests, zero failures
|
|
39
|
+
- 23 new hook tests covering all 5 lifecycle hooks
|
|
40
|
+
|
|
41
|
+
## 1.4.0
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
#### Session Spine (Phase Q)
|
|
46
|
+
- `roleos init claude` — scaffolds Claude Code integration: CLAUDE.md instructions, /roleos-route + /roleos-review + /roleos-status slash commands
|
|
47
|
+
- `roleos doctor` — verifies repo is correctly wired for Role OS sessions (6 checks: .claude/ dir, CLAUDE.md section, /roleos-route command, context files, role contracts, packets)
|
|
48
|
+
- Route card generation — session header artifact proving Role OS was engaged (task type, pack, confidence, composite status, success artifact)
|
|
49
|
+
- CLAUDE.md template instructs Claude to route through Role OS before non-trivial work
|
|
50
|
+
- /roleos-route command produces structured route cards
|
|
51
|
+
- /roleos-review command guides structured verdict production
|
|
52
|
+
- /roleos-status command shows active work and context health
|
|
53
|
+
- Appends to existing CLAUDE.md without overwriting (detects Role OS section)
|
|
54
|
+
- --force flag overwrites existing command files
|
|
55
|
+
|
|
56
|
+
### Evidence
|
|
57
|
+
- 335 tests, zero failures
|
|
58
|
+
|
|
3
59
|
## 1.3.0
|
|
4
60
|
|
|
5
61
|
### Added
|
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ Role OS operates **locally only**. It copies markdown templates and writes packe
|
|
|
178
178
|
| **Mixed-task decomposition** | Detects composite work, splits into child packets, assigns packs, preserves dependencies. | ✓ Shipped |
|
|
179
179
|
| **Composite execution** | Runs child packets in dependency order with artifact passing, branch recovery, and synthesis. | ✓ Shipped |
|
|
180
180
|
| **Adaptive replanning** | Mid-run scope changes, findings, or new requirements update the plan without restarting. | ✓ Shipped |
|
|
181
|
+
| **Session spine** | `roleos init claude` scaffolds CLAUDE.md, /roleos-route, /roleos-review, /roleos-status. `roleos doctor` verifies wiring. Route cards prove engagement. | ✓ Shipped |
|
|
182
|
+
| **Hook spine** | 5 lifecycle hooks (SessionStart, PromptSubmit, PreToolUse, SubagentStart, Stop). Advisory enforcement: route card reminders, write-tool gating, subagent role injection, completion audit. | ✓ Shipped |
|
|
181
183
|
|
|
182
184
|
## Status
|
|
183
185
|
|
|
@@ -186,7 +188,9 @@ Role OS operates **locally only**. It copies markdown templates and writes packe
|
|
|
186
188
|
- v1.0.2: Role OS lockdown (bootstrap truth fixes, init --force)
|
|
187
189
|
- v1.1.0: 31 roles, full routing spine, conflict detection, escalation, evidence, dispatch, 7 proven team packs. 35 execution trials. 212 tests.
|
|
188
190
|
- v1.2.0: Calibrated packs promoted to default entry. Auto-selection, mismatch detection, alternative suggestion, free-routing fallback. 246 tests.
|
|
189
|
-
-
|
|
191
|
+
- v1.3.0: Outcome calibration, mixed-task decomposition, composite execution, adaptive replanning. 317 tests.
|
|
192
|
+
- v1.4.0: Session spine — `roleos init claude`, `roleos doctor`, route cards, /roleos-route + /roleos-review + /roleos-status commands. 335 tests.
|
|
193
|
+
- **v1.5.0**: Hook spine — 5 lifecycle hooks for runtime enforcement. Advisory route card reminders, write-tool gating, subagent role injection, completion audit. 358 tests.
|
|
190
194
|
|
|
191
195
|
## License
|
|
192
196
|
|
package/bin/roleos.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { routeCommand } from "../src/route.mjs";
|
|
|
9
9
|
import { reviewCommand } from "../src/review.mjs";
|
|
10
10
|
import { statusCommand } from "../src/status.mjs";
|
|
11
11
|
import { packsCommand } from "../src/packs-cmd.mjs";
|
|
12
|
+
import { scaffoldClaude, doctor, formatDoctor } from "../src/session.mjs";
|
|
12
13
|
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
@@ -29,6 +30,7 @@ Usage:
|
|
|
29
30
|
roleos packs list List all available team packs
|
|
30
31
|
roleos packs suggest <packet-file> Suggest a pack for a packet
|
|
31
32
|
roleos packs show <pack-key> Show full detail for a named pack
|
|
33
|
+
roleos doctor Verify repo is wired for Role OS sessions
|
|
32
34
|
roleos help Show this help
|
|
33
35
|
|
|
34
36
|
Verdicts: accept | accept-with-notes | reject | blocked
|
|
@@ -64,8 +66,28 @@ const args = process.argv.slice(3);
|
|
|
64
66
|
try {
|
|
65
67
|
switch (command) {
|
|
66
68
|
case "init":
|
|
67
|
-
|
|
69
|
+
if (args[0] === "claude") {
|
|
70
|
+
const force = args.includes("--force");
|
|
71
|
+
const result = scaffoldClaude(process.cwd(), { force });
|
|
72
|
+
if (result.created.length > 0) {
|
|
73
|
+
console.log(`Created:`);
|
|
74
|
+
result.created.forEach(f => console.log(` + ${f}`));
|
|
75
|
+
}
|
|
76
|
+
if (result.skipped.length > 0) {
|
|
77
|
+
console.log(`Skipped:`);
|
|
78
|
+
result.skipped.forEach(f => console.log(` ~ ${f}`));
|
|
79
|
+
}
|
|
80
|
+
console.log(`\nDone. Claude Code will now use Role OS for routing.\nRun: roleos doctor to verify.`);
|
|
81
|
+
} else {
|
|
82
|
+
await initCommand(args);
|
|
83
|
+
}
|
|
68
84
|
break;
|
|
85
|
+
case "doctor": {
|
|
86
|
+
const result = doctor(process.cwd());
|
|
87
|
+
console.log(formatDoctor(result));
|
|
88
|
+
if (!result.healthy) process.exit(1);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
69
91
|
case "packet":
|
|
70
92
|
await packetCommand(args);
|
|
71
93
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "role-os",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Role OS — a multi-Claude operating system where 31 specialized roles execute work through contracts, conflict detection, escalation, and structured evidence. 7 proven team packs for common task families.",
|
|
5
5
|
"homepage": "https://mcp-tool-shop-org.github.io/role-os/",
|
|
6
6
|
"bugs": {
|
package/src/hooks.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Spine — v1.5.0 Runtime Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Hooks verify, gate, and record. The actual routing intelligence
|
|
5
|
+
* stays in explicit artifacts (route cards, pack selection).
|
|
6
|
+
* Hooks are the proof layer, not the decision layer.
|
|
7
|
+
*
|
|
8
|
+
* Four guarantees:
|
|
9
|
+
* 1. No substantial task begins without a route artifact
|
|
10
|
+
* 2. No tool execution drifts outside the selected role envelope
|
|
11
|
+
* 3. No subagent runs without inheriting the role contract
|
|
12
|
+
* 4. No session ends without an outcome artifact
|
|
13
|
+
*
|
|
14
|
+
* Hook lifecycle events used:
|
|
15
|
+
* - SessionStart: establish session contract
|
|
16
|
+
* - UserPromptSubmit: classify before improvising
|
|
17
|
+
* - PreToolUse: enforce role-specific tool law
|
|
18
|
+
* - SubagentStart: inject role contract into delegation
|
|
19
|
+
* - Stop: prevent false completion
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
// ── Hook script generators ────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate the settings.json hooks configuration.
|
|
29
|
+
* Each hook is a shell command that runs a Node script from .claude/hooks/.
|
|
30
|
+
*
|
|
31
|
+
* @returns {object} The hooks configuration object for settings.json
|
|
32
|
+
*/
|
|
33
|
+
export function generateHooksConfig() {
|
|
34
|
+
return {
|
|
35
|
+
hooks: {
|
|
36
|
+
SessionStart: [
|
|
37
|
+
{
|
|
38
|
+
matcher: "",
|
|
39
|
+
hooks: [{
|
|
40
|
+
type: "command",
|
|
41
|
+
command: "node .claude/hooks/session-start.mjs",
|
|
42
|
+
}],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
UserPromptSubmit: [
|
|
46
|
+
{
|
|
47
|
+
matcher: "",
|
|
48
|
+
hooks: [{
|
|
49
|
+
type: "command",
|
|
50
|
+
command: "node .claude/hooks/prompt-submit.mjs",
|
|
51
|
+
}],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
PreToolUse: [
|
|
55
|
+
{
|
|
56
|
+
matcher: "",
|
|
57
|
+
hooks: [{
|
|
58
|
+
type: "command",
|
|
59
|
+
command: "node .claude/hooks/pre-tool-use.mjs",
|
|
60
|
+
}],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
SubagentStart: [
|
|
64
|
+
{
|
|
65
|
+
matcher: "",
|
|
66
|
+
hooks: [{
|
|
67
|
+
type: "command",
|
|
68
|
+
command: "node .claude/hooks/subagent-start.mjs",
|
|
69
|
+
}],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
Stop: [
|
|
73
|
+
{
|
|
74
|
+
matcher: "",
|
|
75
|
+
hooks: [{
|
|
76
|
+
type: "command",
|
|
77
|
+
command: "node .claude/hooks/stop.mjs",
|
|
78
|
+
}],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Session state ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const SESSION_STATE_FILE = ".claude/hooks/session-state.json";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read or create session state.
|
|
91
|
+
* @param {string} cwd
|
|
92
|
+
* @returns {object}
|
|
93
|
+
*/
|
|
94
|
+
export function getSessionState(cwd) {
|
|
95
|
+
const path = join(cwd, SESSION_STATE_FILE);
|
|
96
|
+
if (existsSync(path)) {
|
|
97
|
+
try { return JSON.parse(readFileSync(path, "utf-8")); }
|
|
98
|
+
catch { /* fall through */ }
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
sessionId: null,
|
|
102
|
+
routeCardPresent: false,
|
|
103
|
+
activeRole: null,
|
|
104
|
+
activePack: null,
|
|
105
|
+
toolsUsed: [],
|
|
106
|
+
promptCount: 0,
|
|
107
|
+
substantivePrompts: 0,
|
|
108
|
+
outcomeRecorded: false,
|
|
109
|
+
startedAt: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Save session state.
|
|
115
|
+
* @param {string} cwd
|
|
116
|
+
* @param {object} state
|
|
117
|
+
*/
|
|
118
|
+
export function saveSessionState(cwd, state) {
|
|
119
|
+
const dir = join(cwd, ".claude", "hooks");
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
writeFileSync(join(cwd, SESSION_STATE_FILE), JSON.stringify(state, null, 2));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Hook logic ────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* SessionStart hook logic.
|
|
128
|
+
* Establishes session contract, checks for existing route artifacts.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} input - Hook input (session_id, cwd, etc.)
|
|
131
|
+
* @returns {{ addContext?: string }}
|
|
132
|
+
*/
|
|
133
|
+
export function onSessionStart(input) {
|
|
134
|
+
const cwd = input.cwd || process.cwd();
|
|
135
|
+
const state = getSessionState(cwd);
|
|
136
|
+
|
|
137
|
+
state.sessionId = input.session_id || `session-${Date.now()}`;
|
|
138
|
+
state.startedAt = new Date().toISOString();
|
|
139
|
+
state.routeCardPresent = false;
|
|
140
|
+
state.promptCount = 0;
|
|
141
|
+
state.substantivePrompts = 0;
|
|
142
|
+
state.outcomeRecorded = false;
|
|
143
|
+
|
|
144
|
+
saveSessionState(cwd, state);
|
|
145
|
+
|
|
146
|
+
// Check if Role OS is initialized
|
|
147
|
+
const hasRoleOs = existsSync(join(cwd, ".claude", "agents"));
|
|
148
|
+
|
|
149
|
+
if (hasRoleOs) {
|
|
150
|
+
return {
|
|
151
|
+
addContext: "Role OS is active in this repo. For non-trivial tasks, run /roleos-route to produce a route card before beginning work. The route card proves the task was classified and the right team was chosen.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* UserPromptSubmit hook logic.
|
|
160
|
+
* Detects substantial prompts and warns if no route artifact exists.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} input - { prompt, session_id, cwd }
|
|
163
|
+
* @returns {{ addContext?: string, block?: { reason: string } }}
|
|
164
|
+
*/
|
|
165
|
+
export function onPromptSubmit(input) {
|
|
166
|
+
const cwd = input.cwd || process.cwd();
|
|
167
|
+
const state = getSessionState(cwd);
|
|
168
|
+
const prompt = input.prompt || "";
|
|
169
|
+
|
|
170
|
+
state.promptCount++;
|
|
171
|
+
|
|
172
|
+
// Classify as substantial if > 50 chars and contains action words
|
|
173
|
+
const isSubstantial = prompt.length > 50 && /\b(implement|add|fix|build|create|refactor|review|ship|deploy|test|write|update|change|remove|migrate)\b/i.test(prompt);
|
|
174
|
+
|
|
175
|
+
if (isSubstantial) {
|
|
176
|
+
state.substantivePrompts++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
saveSessionState(cwd, state);
|
|
180
|
+
|
|
181
|
+
// If this is the 2nd+ substantial prompt without a route card, remind
|
|
182
|
+
if (isSubstantial && state.substantivePrompts >= 2 && !state.routeCardPresent) {
|
|
183
|
+
return {
|
|
184
|
+
addContext: "Note: This is a substantial task and no Role OS route card has been produced yet. Consider running /roleos-route to classify the task and choose the right team. A route card ensures the work is staffed correctly.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* PreToolUse hook logic.
|
|
193
|
+
* Checks tool usage against active role envelope.
|
|
194
|
+
*
|
|
195
|
+
* @param {object} input - { tool_name, tool_input, session_id, cwd }
|
|
196
|
+
* @returns {{ allow?: boolean, deny?: { reason: string }, addContext?: string }}
|
|
197
|
+
*/
|
|
198
|
+
export function onPreToolUse(input) {
|
|
199
|
+
const cwd = input.cwd || process.cwd();
|
|
200
|
+
const state = getSessionState(cwd);
|
|
201
|
+
const toolName = input.tool_name || "";
|
|
202
|
+
|
|
203
|
+
// Record tool usage
|
|
204
|
+
if (!state.toolsUsed.includes(toolName)) {
|
|
205
|
+
state.toolsUsed.push(toolName);
|
|
206
|
+
saveSessionState(cwd, state);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Advisory: flag write tools without route card after substantial prompts
|
|
210
|
+
const writeTools = ["Bash", "Write", "Edit", "NotebookEdit"];
|
|
211
|
+
if (writeTools.includes(toolName) && !state.routeCardPresent && (state.substantivePrompts || 0) >= 2) {
|
|
212
|
+
return {
|
|
213
|
+
addContext: `Write tool "${toolName}" used without a route card. If this is substantial work, consider routing first.`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If a role is active, read-only tools are always fine
|
|
218
|
+
if (state.activeRole && state.activePack) {
|
|
219
|
+
const readOnlyTools = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
|
220
|
+
if (readOnlyTools.includes(toolName)) {
|
|
221
|
+
return { allow: true };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* SubagentStart hook logic.
|
|
230
|
+
* Injects role contract context into delegated agents.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} input - { agent_id, agent_type, session_id, cwd }
|
|
233
|
+
* @returns {{ addContext?: string }}
|
|
234
|
+
*/
|
|
235
|
+
export function onSubagentStart(input) {
|
|
236
|
+
const cwd = input.cwd || process.cwd();
|
|
237
|
+
const state = getSessionState(cwd);
|
|
238
|
+
|
|
239
|
+
if (state.activeRole) {
|
|
240
|
+
return {
|
|
241
|
+
addContext: `This subagent is operating under Role OS. Active role: ${state.activeRole}. Pack: ${state.activePack || "free routing"}. Follow the role contract and produce structured handoffs.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Stop hook logic.
|
|
250
|
+
* Prevents false completion — warns if no route card or outcome exists.
|
|
251
|
+
*
|
|
252
|
+
* @param {object} input - { session_id, cwd, stop_reason }
|
|
253
|
+
* @returns {{ block?: { reason: string }, addContext?: string }}
|
|
254
|
+
*/
|
|
255
|
+
export function onStop(input) {
|
|
256
|
+
const cwd = input.cwd || process.cwd();
|
|
257
|
+
const state = getSessionState(cwd);
|
|
258
|
+
|
|
259
|
+
// Only enforce on sessions that had substantial work
|
|
260
|
+
if (state.substantivePrompts < 2) {
|
|
261
|
+
return {}; // Trivial session, let it end
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const warnings = [];
|
|
265
|
+
|
|
266
|
+
if (!state.routeCardPresent) {
|
|
267
|
+
warnings.push("No route card was produced during this session.");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!state.outcomeRecorded) {
|
|
271
|
+
warnings.push("No outcome artifact was recorded.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (warnings.length > 0) {
|
|
275
|
+
return {
|
|
276
|
+
addContext: `Role OS session audit: ${warnings.join(" ")} Consider documenting the outcome before ending.`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Hook script file generators ───────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate hook script files for .claude/hooks/.
|
|
287
|
+
* These are the actual scripts that settings.json points to.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} cwd
|
|
290
|
+
* @returns {{ created: string[], skipped: string[] }}
|
|
291
|
+
*/
|
|
292
|
+
export function scaffoldHooks(cwd) {
|
|
293
|
+
const created = [];
|
|
294
|
+
const skipped = [];
|
|
295
|
+
const hooksDir = join(cwd, ".claude", "hooks");
|
|
296
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
297
|
+
|
|
298
|
+
const scripts = {
|
|
299
|
+
"session-start.mjs": generateSessionStartScript(),
|
|
300
|
+
"prompt-submit.mjs": generatePromptSubmitScript(),
|
|
301
|
+
"pre-tool-use.mjs": generatePreToolUseScript(),
|
|
302
|
+
"subagent-start.mjs": generateSubagentStartScript(),
|
|
303
|
+
"stop.mjs": generateStopScript(),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
for (const [name, content] of Object.entries(scripts)) {
|
|
307
|
+
const path = join(hooksDir, name);
|
|
308
|
+
if (!existsSync(path)) {
|
|
309
|
+
writeFileSync(path, content);
|
|
310
|
+
created.push(`.claude/hooks/${name}`);
|
|
311
|
+
} else {
|
|
312
|
+
skipped.push(`.claude/hooks/${name}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { created, skipped };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function generateSessionStartScript() {
|
|
320
|
+
return `#!/usr/bin/env node
|
|
321
|
+
// Role OS SessionStart hook — establishes session contract
|
|
322
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
323
|
+
import { join } from "node:path";
|
|
324
|
+
|
|
325
|
+
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
|
|
326
|
+
const cwd = input.cwd || process.cwd();
|
|
327
|
+
const stateDir = join(cwd, ".claude", "hooks");
|
|
328
|
+
mkdirSync(stateDir, { recursive: true });
|
|
329
|
+
|
|
330
|
+
const state = {
|
|
331
|
+
sessionId: input.session_id || \`session-\${Date.now()}\`,
|
|
332
|
+
startedAt: new Date().toISOString(),
|
|
333
|
+
routeCardPresent: false,
|
|
334
|
+
activeRole: null,
|
|
335
|
+
activePack: null,
|
|
336
|
+
toolsUsed: [],
|
|
337
|
+
promptCount: 0,
|
|
338
|
+
substantivePrompts: 0,
|
|
339
|
+
outcomeRecorded: false,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
writeFileSync(join(stateDir, "session-state.json"), JSON.stringify(state, null, 2));
|
|
343
|
+
|
|
344
|
+
const hasRoleOs = existsSync(join(cwd, ".claude", "agents"));
|
|
345
|
+
if (hasRoleOs) {
|
|
346
|
+
console.log(JSON.stringify({
|
|
347
|
+
addContext: "Role OS is active. For non-trivial tasks, run /roleos-route first.",
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function generatePromptSubmitScript() {
|
|
354
|
+
return `#!/usr/bin/env node
|
|
355
|
+
// Role OS UserPromptSubmit hook — classify before improvising
|
|
356
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
357
|
+
import { join } from "node:path";
|
|
358
|
+
|
|
359
|
+
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
|
|
360
|
+
const cwd = input.cwd || process.cwd();
|
|
361
|
+
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
362
|
+
|
|
363
|
+
let state = {};
|
|
364
|
+
if (existsSync(statePath)) {
|
|
365
|
+
try { state = JSON.parse(readFileSync(statePath, "utf-8")); } catch {}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const prompt = input.prompt || "";
|
|
369
|
+
state.promptCount = (state.promptCount || 0) + 1;
|
|
370
|
+
|
|
371
|
+
const isSubstantial = prompt.length > 50 &&
|
|
372
|
+
/\\b(implement|add|fix|build|create|refactor|review|ship|deploy|test|write|update|change|remove|migrate)\\b/i.test(prompt);
|
|
373
|
+
|
|
374
|
+
if (isSubstantial) state.substantivePrompts = (state.substantivePrompts || 0) + 1;
|
|
375
|
+
|
|
376
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
377
|
+
|
|
378
|
+
if (isSubstantial && (state.substantivePrompts || 0) >= 2 && !state.routeCardPresent) {
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
addContext: "No Role OS route card yet. Consider /roleos-route to classify this task.",
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function generatePreToolUseScript() {
|
|
387
|
+
return `#!/usr/bin/env node
|
|
388
|
+
// Role OS PreToolUse hook — enforce role-specific tool law
|
|
389
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
390
|
+
import { join } from "node:path";
|
|
391
|
+
|
|
392
|
+
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
|
|
393
|
+
const cwd = input.cwd || process.cwd();
|
|
394
|
+
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
395
|
+
|
|
396
|
+
let state = {};
|
|
397
|
+
if (existsSync(statePath)) {
|
|
398
|
+
try { state = JSON.parse(readFileSync(statePath, "utf-8")); } catch {}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const toolName = input.tool_name || "";
|
|
402
|
+
if (!state.toolsUsed) state.toolsUsed = [];
|
|
403
|
+
if (!state.toolsUsed.includes(toolName)) {
|
|
404
|
+
state.toolsUsed.push(toolName);
|
|
405
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Advisory: flag write tools without route card
|
|
409
|
+
const writeTools = ["Bash", "Write", "Edit", "NotebookEdit"];
|
|
410
|
+
if (writeTools.includes(toolName) && !state.routeCardPresent && (state.substantivePrompts || 0) >= 2) {
|
|
411
|
+
console.log(JSON.stringify({
|
|
412
|
+
addContext: \`Write tool "\${toolName}" used without route card. Consider /roleos-route.\`,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function generateSubagentStartScript() {
|
|
419
|
+
return `#!/usr/bin/env node
|
|
420
|
+
// Role OS SubagentStart hook — inject role contract
|
|
421
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
422
|
+
import { join } from "node:path";
|
|
423
|
+
|
|
424
|
+
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
|
|
425
|
+
const cwd = input.cwd || process.cwd();
|
|
426
|
+
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
427
|
+
|
|
428
|
+
let state = {};
|
|
429
|
+
if (existsSync(statePath)) {
|
|
430
|
+
try { state = JSON.parse(readFileSync(statePath, "utf-8")); } catch {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (state.activeRole) {
|
|
434
|
+
console.log(JSON.stringify({
|
|
435
|
+
addContext: \`Role OS active. Role: \${state.activeRole}. Pack: \${state.activePack || "free routing"}. Follow role contract.\`,
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function generateStopScript() {
|
|
442
|
+
return `#!/usr/bin/env node
|
|
443
|
+
// Role OS Stop hook — prevent false completion
|
|
444
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
445
|
+
import { join } from "node:path";
|
|
446
|
+
|
|
447
|
+
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
|
|
448
|
+
const cwd = input.cwd || process.cwd();
|
|
449
|
+
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
450
|
+
|
|
451
|
+
let state = {};
|
|
452
|
+
if (existsSync(statePath)) {
|
|
453
|
+
try { state = JSON.parse(readFileSync(statePath, "utf-8")); } catch {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Only enforce on sessions with substantial work
|
|
457
|
+
if ((state.substantivePrompts || 0) < 2) process.exit(0);
|
|
458
|
+
|
|
459
|
+
const warnings = [];
|
|
460
|
+
if (!state.routeCardPresent) warnings.push("No route card produced.");
|
|
461
|
+
if (!state.outcomeRecorded) warnings.push("No outcome artifact recorded.");
|
|
462
|
+
|
|
463
|
+
if (warnings.length > 0) {
|
|
464
|
+
console.log(JSON.stringify({
|
|
465
|
+
addContext: \`Role OS audit: \${warnings.join(" ")} Consider documenting the outcome.\`,
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
}
|
package/src/session.mjs
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Spine — Phase Q (v1.4.0)
|
|
3
|
+
*
|
|
4
|
+
* Makes Role-OS the session substrate, not just a package.
|
|
5
|
+
* Scaffolds CLAUDE.md instructions, skills, and hooks so that
|
|
6
|
+
* Claude Code enters every session through role-os routing.
|
|
7
|
+
*
|
|
8
|
+
* Extension points used:
|
|
9
|
+
* - CLAUDE.md: project instructions loaded at session start
|
|
10
|
+
* - .claude/commands/: slash commands (skills) invokable by user or auto-matched
|
|
11
|
+
* - Hooks: lifecycle events configured in settings
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { writeFileSafe } from "./fs-utils.mjs";
|
|
17
|
+
import { scaffoldHooks, generateHooksConfig } from "./hooks.mjs";
|
|
18
|
+
|
|
19
|
+
// ── roleos init claude ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scaffold Claude Code integration files into a repo.
|
|
23
|
+
* Creates: CLAUDE.md addition, /roleos-route command, and session guidance.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} cwd - Working directory
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @param {boolean} [options.force] - Overwrite existing files
|
|
28
|
+
* @returns {{ created: string[], skipped: string[] }}
|
|
29
|
+
*/
|
|
30
|
+
export function scaffoldClaude(cwd, options = {}) {
|
|
31
|
+
const created = [];
|
|
32
|
+
const skipped = [];
|
|
33
|
+
|
|
34
|
+
// 1. CLAUDE.md — session entry instructions
|
|
35
|
+
const claudeMd = join(cwd, "CLAUDE.md");
|
|
36
|
+
const claudeContent = generateClaudeMd();
|
|
37
|
+
if (!existsSync(claudeMd) || options.force) {
|
|
38
|
+
writeFileSync(claudeMd, claudeContent);
|
|
39
|
+
created.push("CLAUDE.md");
|
|
40
|
+
} else {
|
|
41
|
+
// Append role-os section if not already present
|
|
42
|
+
const existing = readFileSync(claudeMd, "utf-8");
|
|
43
|
+
if (!existing.includes("## Role OS")) {
|
|
44
|
+
writeFileSync(claudeMd, existing + "\n\n" + claudeContent);
|
|
45
|
+
created.push("CLAUDE.md (appended)");
|
|
46
|
+
} else {
|
|
47
|
+
skipped.push("CLAUDE.md (Role OS section already present)");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Slash command: /roleos-route
|
|
52
|
+
const cmdDir = join(cwd, ".claude", "commands");
|
|
53
|
+
mkdirSync(cmdDir, { recursive: true });
|
|
54
|
+
const routeCmd = join(cmdDir, "roleos-route.md");
|
|
55
|
+
if (!existsSync(routeCmd) || options.force) {
|
|
56
|
+
writeFileSync(routeCmd, generateRouteCommand());
|
|
57
|
+
created.push(".claude/commands/roleos-route.md");
|
|
58
|
+
} else {
|
|
59
|
+
skipped.push(".claude/commands/roleos-route.md");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Slash command: /roleos-review
|
|
63
|
+
const reviewCmd = join(cmdDir, "roleos-review.md");
|
|
64
|
+
if (!existsSync(reviewCmd) || options.force) {
|
|
65
|
+
writeFileSync(reviewCmd, generateReviewCommand());
|
|
66
|
+
created.push(".claude/commands/roleos-review.md");
|
|
67
|
+
} else {
|
|
68
|
+
skipped.push(".claude/commands/roleos-review.md");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Slash command: /roleos-status
|
|
72
|
+
const statusCmd = join(cmdDir, "roleos-status.md");
|
|
73
|
+
if (!existsSync(statusCmd) || options.force) {
|
|
74
|
+
writeFileSync(statusCmd, generateStatusCommand());
|
|
75
|
+
created.push(".claude/commands/roleos-status.md");
|
|
76
|
+
} else {
|
|
77
|
+
skipped.push(".claude/commands/roleos-status.md");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 5. Hook scripts
|
|
81
|
+
const hookResult = scaffoldHooks(cwd);
|
|
82
|
+
created.push(...hookResult.created);
|
|
83
|
+
skipped.push(...hookResult.skipped);
|
|
84
|
+
|
|
85
|
+
// 6. Settings.json with hooks config
|
|
86
|
+
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
87
|
+
if (!existsSync(settingsPath) || options.force) {
|
|
88
|
+
const hooksConfig = generateHooksConfig();
|
|
89
|
+
writeFileSync(settingsPath, JSON.stringify(hooksConfig, null, 2));
|
|
90
|
+
created.push(".claude/settings.local.json");
|
|
91
|
+
} else {
|
|
92
|
+
// Check if hooks are already configured
|
|
93
|
+
try {
|
|
94
|
+
const existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
95
|
+
if (!existing.hooks) {
|
|
96
|
+
const hooksConfig = generateHooksConfig();
|
|
97
|
+
existing.hooks = hooksConfig.hooks;
|
|
98
|
+
writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
99
|
+
created.push(".claude/settings.local.json (hooks added)");
|
|
100
|
+
} else {
|
|
101
|
+
skipped.push(".claude/settings.local.json (hooks already configured)");
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
skipped.push(".claude/settings.local.json (could not parse existing)");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { created, skipped };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── roleos doctor ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {Object} DoctorCheck
|
|
115
|
+
* @property {string} name
|
|
116
|
+
* @property {"pass"|"fail"|"warn"} status
|
|
117
|
+
* @property {string} detail
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verify that a repo is correctly wired for Role-OS session integration.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} cwd
|
|
124
|
+
* @returns {{ checks: DoctorCheck[], healthy: boolean }}
|
|
125
|
+
*/
|
|
126
|
+
export function doctor(cwd) {
|
|
127
|
+
const checks = [];
|
|
128
|
+
|
|
129
|
+
// Check 1: .claude/ directory exists
|
|
130
|
+
const claudeDir = join(cwd, ".claude");
|
|
131
|
+
checks.push({
|
|
132
|
+
name: ".claude/ directory",
|
|
133
|
+
status: existsSync(claudeDir) ? "pass" : "fail",
|
|
134
|
+
detail: existsSync(claudeDir) ? "exists" : "missing — run roleos init first",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Check 2: CLAUDE.md exists and has Role OS section
|
|
138
|
+
const claudeMd = join(cwd, "CLAUDE.md");
|
|
139
|
+
if (existsSync(claudeMd)) {
|
|
140
|
+
const content = readFileSync(claudeMd, "utf-8");
|
|
141
|
+
if (content.includes("## Role OS")) {
|
|
142
|
+
checks.push({ name: "CLAUDE.md Role OS section", status: "pass", detail: "present" });
|
|
143
|
+
} else {
|
|
144
|
+
checks.push({ name: "CLAUDE.md Role OS section", status: "warn", detail: "CLAUDE.md exists but has no Role OS section — run roleos init claude" });
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
checks.push({ name: "CLAUDE.md", status: "fail", detail: "missing — run roleos init claude" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check 3: /roleos-route command exists
|
|
151
|
+
const routeCmd = join(cwd, ".claude", "commands", "roleos-route.md");
|
|
152
|
+
checks.push({
|
|
153
|
+
name: "/roleos-route command",
|
|
154
|
+
status: existsSync(routeCmd) ? "pass" : "fail",
|
|
155
|
+
detail: existsSync(routeCmd) ? "exists" : "missing — run roleos init claude",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Check 4: Context files exist
|
|
159
|
+
const contextDir = join(cwd, ".claude", "context");
|
|
160
|
+
const contextFiles = ["product-brief.md", "repo-map.md", "brand-rules.md", "current-priorities.md"];
|
|
161
|
+
const filledContext = contextFiles.filter(f => {
|
|
162
|
+
const path = join(contextDir, f);
|
|
163
|
+
if (!existsSync(path)) return false;
|
|
164
|
+
const content = readFileSync(path, "utf-8");
|
|
165
|
+
// Check if it's still a template (all comments, no real content)
|
|
166
|
+
const lines = content.split("\n").filter(l => l.trim() && !l.trim().startsWith("#") && !l.trim().startsWith("<!--") && !l.trim().startsWith("//"));
|
|
167
|
+
return lines.length > 2;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (filledContext.length === 4) {
|
|
171
|
+
checks.push({ name: "context files", status: "pass", detail: "all 4 filled" });
|
|
172
|
+
} else if (filledContext.length > 0) {
|
|
173
|
+
checks.push({ name: "context files", status: "warn", detail: `${filledContext.length}/4 filled — empty context reduces routing quality` });
|
|
174
|
+
} else {
|
|
175
|
+
checks.push({ name: "context files", status: "fail", detail: "no context files filled — routing will be low-confidence" });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check 5: Role contracts exist
|
|
179
|
+
const agentsDir = join(cwd, ".claude", "agents");
|
|
180
|
+
if (existsSync(agentsDir)) {
|
|
181
|
+
checks.push({ name: "role contracts", status: "pass", detail: "agents/ directory exists" });
|
|
182
|
+
} else {
|
|
183
|
+
checks.push({ name: "role contracts", status: "fail", detail: "no agents/ directory — run roleos init first" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check 6: Packets directory exists
|
|
187
|
+
const packetsDir = join(cwd, ".claude", "packets");
|
|
188
|
+
checks.push({
|
|
189
|
+
name: "packets directory",
|
|
190
|
+
status: existsSync(packetsDir) ? "pass" : "warn",
|
|
191
|
+
detail: existsSync(packetsDir) ? "exists" : "no packets yet — run roleos packet new",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Check 7: Hook scripts exist
|
|
195
|
+
const hooksDir = join(cwd, ".claude", "hooks");
|
|
196
|
+
const hookFiles = ["session-start.mjs", "prompt-submit.mjs", "pre-tool-use.mjs", "subagent-start.mjs", "stop.mjs"];
|
|
197
|
+
const existingHooks = hookFiles.filter(f => existsSync(join(hooksDir, f)));
|
|
198
|
+
if (existingHooks.length === hookFiles.length) {
|
|
199
|
+
checks.push({ name: "hook scripts", status: "pass", detail: `all ${hookFiles.length} hooks present` });
|
|
200
|
+
} else if (existingHooks.length > 0) {
|
|
201
|
+
checks.push({ name: "hook scripts", status: "warn", detail: `${existingHooks.length}/${hookFiles.length} hooks present` });
|
|
202
|
+
} else {
|
|
203
|
+
checks.push({ name: "hook scripts", status: "warn", detail: "no hooks — run roleos init claude for runtime enforcement" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check 8: Settings has hooks configured
|
|
207
|
+
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
208
|
+
if (existsSync(settingsPath)) {
|
|
209
|
+
try {
|
|
210
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
211
|
+
if (settings.hooks) {
|
|
212
|
+
checks.push({ name: "hooks in settings", status: "pass", detail: "configured" });
|
|
213
|
+
} else {
|
|
214
|
+
checks.push({ name: "hooks in settings", status: "warn", detail: "settings.local.json exists but no hooks section" });
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
checks.push({ name: "hooks in settings", status: "warn", detail: "settings.local.json exists but could not parse" });
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
checks.push({ name: "hooks in settings", status: "warn", detail: "no settings.local.json — hooks not active" });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const healthy = checks.every(c => c.status !== "fail");
|
|
224
|
+
|
|
225
|
+
return { checks, healthy };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Route card ────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate a route card — the session header artifact that proves
|
|
232
|
+
* role-os was engaged.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} routeResult
|
|
235
|
+
* @returns {string} Markdown route card
|
|
236
|
+
*/
|
|
237
|
+
export function generateRouteCard(routeResult) {
|
|
238
|
+
const lines = [
|
|
239
|
+
`## Route Card`,
|
|
240
|
+
``,
|
|
241
|
+
`| Field | Value |`,
|
|
242
|
+
`|-------|-------|`,
|
|
243
|
+
`| Task type | ${routeResult.type || "unknown"} |`,
|
|
244
|
+
`| Pack | ${routeResult.pack || "free routing"} |`,
|
|
245
|
+
`| Pack confidence | ${routeResult.packConfidence || "n/a"} |`,
|
|
246
|
+
`| Composite | ${routeResult.isComposite ? "yes — " + routeResult.compositeReason : "no"} |`,
|
|
247
|
+
`| Chain | ${routeResult.chain || "pending"} |`,
|
|
248
|
+
`| Confidence | ${routeResult.confidence || "unknown"} |`,
|
|
249
|
+
`| Routed at | ${new Date().toISOString()} |`,
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
if (routeResult.mismatch) {
|
|
253
|
+
lines.push(`| Mismatch | ${routeResult.mismatch} |`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lines.push(``);
|
|
257
|
+
lines.push(`**Why:** ${routeResult.reason || "Scored against packet content."}`);
|
|
258
|
+
|
|
259
|
+
if (routeResult.successArtifact) {
|
|
260
|
+
lines.push(``);
|
|
261
|
+
lines.push(`**Success artifact:** ${routeResult.successArtifact}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Format doctor output ──────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {{ checks: DoctorCheck[], healthy: boolean }} result
|
|
271
|
+
* @returns {string}
|
|
272
|
+
*/
|
|
273
|
+
export function formatDoctor(result) {
|
|
274
|
+
const lines = [
|
|
275
|
+
`\nRole OS Doctor`,
|
|
276
|
+
`──────────────`,
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for (const check of result.checks) {
|
|
280
|
+
const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "!" : "✗";
|
|
281
|
+
lines.push(` ${icon} ${check.name}: ${check.detail}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
lines.push(``);
|
|
285
|
+
lines.push(result.healthy
|
|
286
|
+
? `Healthy — Role OS is wired correctly.`
|
|
287
|
+
: `Unhealthy — run roleos init claude to fix.`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Generators ────────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function generateClaudeMd() {
|
|
296
|
+
return `## Role OS
|
|
297
|
+
|
|
298
|
+
Before starting non-trivial work in this repo, route the task through Role OS:
|
|
299
|
+
|
|
300
|
+
1. Run \`/roleos-route\` or \`roleos route <packet-file>\` to classify the task
|
|
301
|
+
2. Follow the recommended pack or role chain
|
|
302
|
+
3. Use structured handoffs between roles
|
|
303
|
+
4. Review with evidence-based verdicts
|
|
304
|
+
|
|
305
|
+
Role OS provides 31 specialized roles across 8 packs (engineering, design, product, research, growth, treatment, marketing, core). It detects broken chains, auto-routes recovery, and requires structured evidence in every verdict.
|
|
306
|
+
|
|
307
|
+
If the task is composite (feature + docs + launch), Role OS will recommend splitting into child packets with dependency ordering.
|
|
308
|
+
|
|
309
|
+
**Route card required:** Every substantial task should produce a route card at the start showing task type, chosen pack/role, confidence, and expected success artifact. If no route card exists, Role OS was not engaged.
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function generateRouteCommand() {
|
|
314
|
+
return `# Route Task Through Role OS
|
|
315
|
+
|
|
316
|
+
Classify the current task and recommend the right team.
|
|
317
|
+
|
|
318
|
+
## Steps
|
|
319
|
+
|
|
320
|
+
1. Read the task description or packet file
|
|
321
|
+
2. Run \`roleos route\` against it (or reason through the routing logic)
|
|
322
|
+
3. Emit a **route card** with:
|
|
323
|
+
- Task type (feature / bugfix / docs / research / security / launch / treatment)
|
|
324
|
+
- Recommended pack or free-routing chain
|
|
325
|
+
- Why this pack/chain was chosen
|
|
326
|
+
- Whether the task is composite (needs splitting)
|
|
327
|
+
- Expected success artifact
|
|
328
|
+
- Confidence level
|
|
329
|
+
|
|
330
|
+
## Route card format
|
|
331
|
+
|
|
332
|
+
\`\`\`
|
|
333
|
+
## Route Card
|
|
334
|
+
|
|
335
|
+
| Field | Value |
|
|
336
|
+
|-------|-------|
|
|
337
|
+
| Task type | feature |
|
|
338
|
+
| Pack | feature (high confidence) |
|
|
339
|
+
| Composite | no |
|
|
340
|
+
| Chain | Product Strategist → Spec Writer → Backend Engineer → Test Engineer → Critic Reviewer |
|
|
341
|
+
| Confidence | high |
|
|
342
|
+
|
|
343
|
+
**Why:** Packet contains implementation scope with clear deliverable type.
|
|
344
|
+
**Success artifact:** Working implementation with tests and Critic verdict.
|
|
345
|
+
\`\`\`
|
|
346
|
+
|
|
347
|
+
## Rules
|
|
348
|
+
- If confidence is low, say so — do not force a pack
|
|
349
|
+
- If composite, recommend splitting before execution
|
|
350
|
+
- If the task is trivial (< 3 steps), skip routing and just do it
|
|
351
|
+
- The route card is the proof Role OS was engaged
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function generateReviewCommand() {
|
|
356
|
+
return `# Review Work Through Role OS
|
|
357
|
+
|
|
358
|
+
Review the completed work using Role OS structured verdict.
|
|
359
|
+
|
|
360
|
+
## Steps
|
|
361
|
+
|
|
362
|
+
1. Identify which role should review (usually Critic Reviewer)
|
|
363
|
+
2. Check the work against the original packet's done definition
|
|
364
|
+
3. Produce a structured verdict:
|
|
365
|
+
- **Verdict:** accept / accept-with-notes / reject / blocked
|
|
366
|
+
- **Evidence:** specific items supporting the verdict
|
|
367
|
+
- **Gaps:** what's missing, if anything
|
|
368
|
+
- **Next owner:** who receives this next
|
|
369
|
+
|
|
370
|
+
## Rules
|
|
371
|
+
- Tie every verdict to specific evidence, not impressions
|
|
372
|
+
- If evidence is insufficient to judge, say so
|
|
373
|
+
- Reject honestly — do not approve weak work to be nice
|
|
374
|
+
- For reject/blocked: state what must change and who should do it
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function generateStatusCommand() {
|
|
379
|
+
return `# Role OS Status
|
|
380
|
+
|
|
381
|
+
Check the current state of Role OS work in this repo.
|
|
382
|
+
|
|
383
|
+
## Steps
|
|
384
|
+
|
|
385
|
+
1. Run \`roleos status\` to see active packets, verdicts, and context health
|
|
386
|
+
2. Report:
|
|
387
|
+
- Active/blocked/completed packets
|
|
388
|
+
- Recent verdicts
|
|
389
|
+
- Context file health
|
|
390
|
+
- Any conflicts or escalations
|
|
391
|
+
|
|
392
|
+
## If no packets exist
|
|
393
|
+
Role OS has not been engaged in this session. Consider running \`/roleos-route\` first.
|
|
394
|
+
`;
|
|
395
|
+
}
|