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 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
- - **Current**: Outcome calibration, mixed-task decomposition, composite execution, adaptive replanning. 317 tests.
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
- await initCommand(args);
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.0",
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
+ }
@@ -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
+ }