role-os 1.2.0 → 1.4.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.
@@ -0,0 +1,337 @@
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
+
18
+ // ── roleos init claude ────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Scaffold Claude Code integration files into a repo.
22
+ * Creates: CLAUDE.md addition, /roleos-route command, and session guidance.
23
+ *
24
+ * @param {string} cwd - Working directory
25
+ * @param {object} [options]
26
+ * @param {boolean} [options.force] - Overwrite existing files
27
+ * @returns {{ created: string[], skipped: string[] }}
28
+ */
29
+ export function scaffoldClaude(cwd, options = {}) {
30
+ const created = [];
31
+ const skipped = [];
32
+
33
+ // 1. CLAUDE.md — session entry instructions
34
+ const claudeMd = join(cwd, "CLAUDE.md");
35
+ const claudeContent = generateClaudeMd();
36
+ if (!existsSync(claudeMd) || options.force) {
37
+ writeFileSync(claudeMd, claudeContent);
38
+ created.push("CLAUDE.md");
39
+ } else {
40
+ // Append role-os section if not already present
41
+ const existing = readFileSync(claudeMd, "utf-8");
42
+ if (!existing.includes("## Role OS")) {
43
+ writeFileSync(claudeMd, existing + "\n\n" + claudeContent);
44
+ created.push("CLAUDE.md (appended)");
45
+ } else {
46
+ skipped.push("CLAUDE.md (Role OS section already present)");
47
+ }
48
+ }
49
+
50
+ // 2. Slash command: /roleos-route
51
+ const cmdDir = join(cwd, ".claude", "commands");
52
+ mkdirSync(cmdDir, { recursive: true });
53
+ const routeCmd = join(cmdDir, "roleos-route.md");
54
+ if (!existsSync(routeCmd) || options.force) {
55
+ writeFileSync(routeCmd, generateRouteCommand());
56
+ created.push(".claude/commands/roleos-route.md");
57
+ } else {
58
+ skipped.push(".claude/commands/roleos-route.md");
59
+ }
60
+
61
+ // 3. Slash command: /roleos-review
62
+ const reviewCmd = join(cmdDir, "roleos-review.md");
63
+ if (!existsSync(reviewCmd) || options.force) {
64
+ writeFileSync(reviewCmd, generateReviewCommand());
65
+ created.push(".claude/commands/roleos-review.md");
66
+ } else {
67
+ skipped.push(".claude/commands/roleos-review.md");
68
+ }
69
+
70
+ // 4. Slash command: /roleos-status
71
+ const statusCmd = join(cmdDir, "roleos-status.md");
72
+ if (!existsSync(statusCmd) || options.force) {
73
+ writeFileSync(statusCmd, generateStatusCommand());
74
+ created.push(".claude/commands/roleos-status.md");
75
+ } else {
76
+ skipped.push(".claude/commands/roleos-status.md");
77
+ }
78
+
79
+ return { created, skipped };
80
+ }
81
+
82
+ // ── roleos doctor ─────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * @typedef {Object} DoctorCheck
86
+ * @property {string} name
87
+ * @property {"pass"|"fail"|"warn"} status
88
+ * @property {string} detail
89
+ */
90
+
91
+ /**
92
+ * Verify that a repo is correctly wired for Role-OS session integration.
93
+ *
94
+ * @param {string} cwd
95
+ * @returns {{ checks: DoctorCheck[], healthy: boolean }}
96
+ */
97
+ export function doctor(cwd) {
98
+ const checks = [];
99
+
100
+ // Check 1: .claude/ directory exists
101
+ const claudeDir = join(cwd, ".claude");
102
+ checks.push({
103
+ name: ".claude/ directory",
104
+ status: existsSync(claudeDir) ? "pass" : "fail",
105
+ detail: existsSync(claudeDir) ? "exists" : "missing — run roleos init first",
106
+ });
107
+
108
+ // Check 2: CLAUDE.md exists and has Role OS section
109
+ const claudeMd = join(cwd, "CLAUDE.md");
110
+ if (existsSync(claudeMd)) {
111
+ const content = readFileSync(claudeMd, "utf-8");
112
+ if (content.includes("## Role OS")) {
113
+ checks.push({ name: "CLAUDE.md Role OS section", status: "pass", detail: "present" });
114
+ } else {
115
+ checks.push({ name: "CLAUDE.md Role OS section", status: "warn", detail: "CLAUDE.md exists but has no Role OS section — run roleos init claude" });
116
+ }
117
+ } else {
118
+ checks.push({ name: "CLAUDE.md", status: "fail", detail: "missing — run roleos init claude" });
119
+ }
120
+
121
+ // Check 3: /roleos-route command exists
122
+ const routeCmd = join(cwd, ".claude", "commands", "roleos-route.md");
123
+ checks.push({
124
+ name: "/roleos-route command",
125
+ status: existsSync(routeCmd) ? "pass" : "fail",
126
+ detail: existsSync(routeCmd) ? "exists" : "missing — run roleos init claude",
127
+ });
128
+
129
+ // Check 4: Context files exist
130
+ const contextDir = join(cwd, ".claude", "context");
131
+ const contextFiles = ["product-brief.md", "repo-map.md", "brand-rules.md", "current-priorities.md"];
132
+ const filledContext = contextFiles.filter(f => {
133
+ const path = join(contextDir, f);
134
+ if (!existsSync(path)) return false;
135
+ const content = readFileSync(path, "utf-8");
136
+ // Check if it's still a template (all comments, no real content)
137
+ const lines = content.split("\n").filter(l => l.trim() && !l.trim().startsWith("#") && !l.trim().startsWith("<!--") && !l.trim().startsWith("//"));
138
+ return lines.length > 2;
139
+ });
140
+
141
+ if (filledContext.length === 4) {
142
+ checks.push({ name: "context files", status: "pass", detail: "all 4 filled" });
143
+ } else if (filledContext.length > 0) {
144
+ checks.push({ name: "context files", status: "warn", detail: `${filledContext.length}/4 filled — empty context reduces routing quality` });
145
+ } else {
146
+ checks.push({ name: "context files", status: "fail", detail: "no context files filled — routing will be low-confidence" });
147
+ }
148
+
149
+ // Check 5: Role contracts exist
150
+ const agentsDir = join(cwd, ".claude", "agents");
151
+ if (existsSync(agentsDir)) {
152
+ checks.push({ name: "role contracts", status: "pass", detail: "agents/ directory exists" });
153
+ } else {
154
+ checks.push({ name: "role contracts", status: "fail", detail: "no agents/ directory — run roleos init first" });
155
+ }
156
+
157
+ // Check 6: Packets directory exists
158
+ const packetsDir = join(cwd, ".claude", "packets");
159
+ checks.push({
160
+ name: "packets directory",
161
+ status: existsSync(packetsDir) ? "pass" : "warn",
162
+ detail: existsSync(packetsDir) ? "exists" : "no packets yet — run roleos packet new",
163
+ });
164
+
165
+ const healthy = checks.every(c => c.status !== "fail");
166
+
167
+ return { checks, healthy };
168
+ }
169
+
170
+ // ── Route card ────────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * Generate a route card — the session header artifact that proves
174
+ * role-os was engaged.
175
+ *
176
+ * @param {object} routeResult
177
+ * @returns {string} Markdown route card
178
+ */
179
+ export function generateRouteCard(routeResult) {
180
+ const lines = [
181
+ `## Route Card`,
182
+ ``,
183
+ `| Field | Value |`,
184
+ `|-------|-------|`,
185
+ `| Task type | ${routeResult.type || "unknown"} |`,
186
+ `| Pack | ${routeResult.pack || "free routing"} |`,
187
+ `| Pack confidence | ${routeResult.packConfidence || "n/a"} |`,
188
+ `| Composite | ${routeResult.isComposite ? "yes — " + routeResult.compositeReason : "no"} |`,
189
+ `| Chain | ${routeResult.chain || "pending"} |`,
190
+ `| Confidence | ${routeResult.confidence || "unknown"} |`,
191
+ `| Routed at | ${new Date().toISOString()} |`,
192
+ ];
193
+
194
+ if (routeResult.mismatch) {
195
+ lines.push(`| Mismatch | ${routeResult.mismatch} |`);
196
+ }
197
+
198
+ lines.push(``);
199
+ lines.push(`**Why:** ${routeResult.reason || "Scored against packet content."}`);
200
+
201
+ if (routeResult.successArtifact) {
202
+ lines.push(``);
203
+ lines.push(`**Success artifact:** ${routeResult.successArtifact}`);
204
+ }
205
+
206
+ return lines.join("\n");
207
+ }
208
+
209
+ // ── Format doctor output ──────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * @param {{ checks: DoctorCheck[], healthy: boolean }} result
213
+ * @returns {string}
214
+ */
215
+ export function formatDoctor(result) {
216
+ const lines = [
217
+ `\nRole OS Doctor`,
218
+ `──────────────`,
219
+ ];
220
+
221
+ for (const check of result.checks) {
222
+ const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "!" : "✗";
223
+ lines.push(` ${icon} ${check.name}: ${check.detail}`);
224
+ }
225
+
226
+ lines.push(``);
227
+ lines.push(result.healthy
228
+ ? `Healthy — Role OS is wired correctly.`
229
+ : `Unhealthy — run roleos init claude to fix.`
230
+ );
231
+
232
+ return lines.join("\n");
233
+ }
234
+
235
+ // ── Generators ────────────────────────────────────────────────────────────────
236
+
237
+ function generateClaudeMd() {
238
+ return `## Role OS
239
+
240
+ Before starting non-trivial work in this repo, route the task through Role OS:
241
+
242
+ 1. Run \`/roleos-route\` or \`roleos route <packet-file>\` to classify the task
243
+ 2. Follow the recommended pack or role chain
244
+ 3. Use structured handoffs between roles
245
+ 4. Review with evidence-based verdicts
246
+
247
+ 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.
248
+
249
+ If the task is composite (feature + docs + launch), Role OS will recommend splitting into child packets with dependency ordering.
250
+
251
+ **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.
252
+ `;
253
+ }
254
+
255
+ function generateRouteCommand() {
256
+ return `# Route Task Through Role OS
257
+
258
+ Classify the current task and recommend the right team.
259
+
260
+ ## Steps
261
+
262
+ 1. Read the task description or packet file
263
+ 2. Run \`roleos route\` against it (or reason through the routing logic)
264
+ 3. Emit a **route card** with:
265
+ - Task type (feature / bugfix / docs / research / security / launch / treatment)
266
+ - Recommended pack or free-routing chain
267
+ - Why this pack/chain was chosen
268
+ - Whether the task is composite (needs splitting)
269
+ - Expected success artifact
270
+ - Confidence level
271
+
272
+ ## Route card format
273
+
274
+ \`\`\`
275
+ ## Route Card
276
+
277
+ | Field | Value |
278
+ |-------|-------|
279
+ | Task type | feature |
280
+ | Pack | feature (high confidence) |
281
+ | Composite | no |
282
+ | Chain | Product Strategist → Spec Writer → Backend Engineer → Test Engineer → Critic Reviewer |
283
+ | Confidence | high |
284
+
285
+ **Why:** Packet contains implementation scope with clear deliverable type.
286
+ **Success artifact:** Working implementation with tests and Critic verdict.
287
+ \`\`\`
288
+
289
+ ## Rules
290
+ - If confidence is low, say so — do not force a pack
291
+ - If composite, recommend splitting before execution
292
+ - If the task is trivial (< 3 steps), skip routing and just do it
293
+ - The route card is the proof Role OS was engaged
294
+ `;
295
+ }
296
+
297
+ function generateReviewCommand() {
298
+ return `# Review Work Through Role OS
299
+
300
+ Review the completed work using Role OS structured verdict.
301
+
302
+ ## Steps
303
+
304
+ 1. Identify which role should review (usually Critic Reviewer)
305
+ 2. Check the work against the original packet's done definition
306
+ 3. Produce a structured verdict:
307
+ - **Verdict:** accept / accept-with-notes / reject / blocked
308
+ - **Evidence:** specific items supporting the verdict
309
+ - **Gaps:** what's missing, if anything
310
+ - **Next owner:** who receives this next
311
+
312
+ ## Rules
313
+ - Tie every verdict to specific evidence, not impressions
314
+ - If evidence is insufficient to judge, say so
315
+ - Reject honestly — do not approve weak work to be nice
316
+ - For reject/blocked: state what must change and who should do it
317
+ `;
318
+ }
319
+
320
+ function generateStatusCommand() {
321
+ return `# Role OS Status
322
+
323
+ Check the current state of Role OS work in this repo.
324
+
325
+ ## Steps
326
+
327
+ 1. Run \`roleos status\` to see active packets, verdicts, and context health
328
+ 2. Report:
329
+ - Active/blocked/completed packets
330
+ - Recent verdicts
331
+ - Context file health
332
+ - Any conflicts or escalations
333
+
334
+ ## If no packets exist
335
+ Role OS has not been engaged in this session. Consider running \`/roleos-route\` first.
336
+ `;
337
+ }