role-os 1.0.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 +13 -0
- package/LICENSE +21 -0
- package/README.es.md +160 -0
- package/README.fr.md +160 -0
- package/README.hi.md +160 -0
- package/README.it.md +160 -0
- package/README.ja.md +160 -0
- package/README.md +160 -0
- package/README.pt-BR.md +160 -0
- package/README.zh.md +160 -0
- package/bin/roleos.mjs +90 -0
- package/package.json +41 -0
- package/src/fs-utils.mjs +60 -0
- package/src/init.mjs +36 -0
- package/src/packet.mjs +144 -0
- package/src/prompts.mjs +76 -0
- package/src/review.mjs +94 -0
- package/src/route.mjs +169 -0
- package/src/status.mjs +352 -0
- package/starter-pack/.claude/workflows/full-treatment.md +74 -0
- package/starter-pack/README.md +74 -0
- package/starter-pack/agents/core/critic-reviewer.md +39 -0
- package/starter-pack/agents/core/orchestrator.md +40 -0
- package/starter-pack/agents/core/product-strategist.md +40 -0
- package/starter-pack/agents/design/brand-guardian.md +41 -0
- package/starter-pack/agents/design/ui-designer.md +42 -0
- package/starter-pack/agents/engineering/backend-engineer.md +39 -0
- package/starter-pack/agents/engineering/dependency-auditor.md +40 -0
- package/starter-pack/agents/engineering/frontend-developer.md +40 -0
- package/starter-pack/agents/engineering/performance-engineer.md +42 -0
- package/starter-pack/agents/engineering/refactor-engineer.md +41 -0
- package/starter-pack/agents/engineering/security-reviewer.md +42 -0
- package/starter-pack/agents/engineering/test-engineer.md +38 -0
- package/starter-pack/agents/growth/community-manager.md +41 -0
- package/starter-pack/agents/growth/content-strategist.md +41 -0
- package/starter-pack/agents/growth/launch-strategist.md +42 -0
- package/starter-pack/agents/growth/support-triage-lead.md +41 -0
- package/starter-pack/agents/marketing/launch-copywriter.md +39 -0
- package/starter-pack/agents/product/feedback-synthesizer.md +39 -0
- package/starter-pack/agents/product/information-architect.md +40 -0
- package/starter-pack/agents/product/roadmap-prioritizer.md +41 -0
- package/starter-pack/agents/product/spec-writer.md +42 -0
- package/starter-pack/agents/research/competitive-analyst.md +40 -0
- package/starter-pack/agents/research/trend-researcher.md +40 -0
- package/starter-pack/agents/research/user-interview-synthesizer.md +41 -0
- package/starter-pack/agents/research/ux-researcher.md +40 -0
- package/starter-pack/agents/treatment/coverage-auditor.md +40 -0
- package/starter-pack/agents/treatment/deployment-verifier.md +41 -0
- package/starter-pack/agents/treatment/docs-architect.md +40 -0
- package/starter-pack/agents/treatment/metadata-curator.md +40 -0
- package/starter-pack/agents/treatment/release-engineer.md +43 -0
- package/starter-pack/agents/treatment/repo-researcher.md +40 -0
- package/starter-pack/agents/treatment/repo-translator.md +38 -0
- package/starter-pack/context/brand-rules.md +52 -0
- package/starter-pack/context/current-priorities.md +33 -0
- package/starter-pack/context/product-brief.md +47 -0
- package/starter-pack/context/repo-map.md +45 -0
- package/starter-pack/examples/feature-packet.md +39 -0
- package/starter-pack/examples/identity-packet.md +39 -0
- package/starter-pack/examples/integration-packet.md +39 -0
- package/starter-pack/handbook.md +67 -0
- package/starter-pack/policy/done-definition.md +15 -0
- package/starter-pack/policy/escalation-rules.md +20 -0
- package/starter-pack/policy/routing-rules.md +199 -0
- package/starter-pack/policy/tool-permissions.md +134 -0
- package/starter-pack/schemas/handoff.md +52 -0
- package/starter-pack/schemas/review-verdict.md +26 -0
- package/starter-pack/schemas/task-packet.md +44 -0
- package/starter-pack/workflows/fix-bug.md +18 -0
- package/starter-pack/workflows/launch-update.md +15 -0
- package/starter-pack/workflows/ship-feature.md +22 -0
package/src/review.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolve, dirname, basename } from "node:path";
|
|
2
|
+
import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
|
|
3
|
+
import { askRequired, askWithDefault, closePrompts } from "./prompts.mjs";
|
|
4
|
+
|
|
5
|
+
const VERDICTS = ["accept", "accept-with-notes", "reject", "blocked"];
|
|
6
|
+
|
|
7
|
+
export async function reviewCommand(args) {
|
|
8
|
+
const packetFile = args[0];
|
|
9
|
+
const verdict = args[1];
|
|
10
|
+
|
|
11
|
+
if (!packetFile || !verdict) {
|
|
12
|
+
const err = new Error(`Usage: roleos review <packet-file> <verdict>\nVerdicts: ${VERDICTS.join(" | ")}`);
|
|
13
|
+
err.exitCode = 1;
|
|
14
|
+
err.hint = "Provide a packet file and a verdict.";
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!VERDICTS.includes(verdict)) {
|
|
19
|
+
const err = new Error(`Unknown verdict: ${verdict}\nVerdicts: ${VERDICTS.join(" | ")}`);
|
|
20
|
+
err.exitCode = 1;
|
|
21
|
+
err.hint = `Valid verdicts: ${VERDICTS.join(", ")}`;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = readFileSafe(packetFile);
|
|
26
|
+
if (content === null) {
|
|
27
|
+
const err = new Error(`Packet not found: ${packetFile}`);
|
|
28
|
+
err.exitCode = 1;
|
|
29
|
+
err.hint = "Check the file path and try again.";
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract task ID from the packet
|
|
34
|
+
const taskIdMatch = content.match(/## Task ID\n(.+)/);
|
|
35
|
+
const taskId = taskIdMatch ? taskIdMatch[1].trim() : basename(packetFile, ".md");
|
|
36
|
+
|
|
37
|
+
console.log(`\nroleos review — ${verdict}\n`);
|
|
38
|
+
console.log(`Packet: ${packetFile}`);
|
|
39
|
+
console.log(`Task ID: ${taskId}\n`);
|
|
40
|
+
|
|
41
|
+
const reviewer = await askWithDefault("Reviewer role", "Critic Reviewer");
|
|
42
|
+
const reason = await askRequired("Reason (tied to contract/evidence)");
|
|
43
|
+
|
|
44
|
+
let corrections = "";
|
|
45
|
+
if (verdict === "reject" || verdict === "accept-with-notes") {
|
|
46
|
+
corrections = await askRequired("Required corrections");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const nextOwner = await askWithDefault("Next owner", verdict === "accept" ? "None — done" : "");
|
|
50
|
+
|
|
51
|
+
// Contract check
|
|
52
|
+
console.log("\nContract check (y/n for each):");
|
|
53
|
+
const scopeRespected = await askWithDefault(" Scope respected?", "y");
|
|
54
|
+
const outputComplete = await askWithDefault(" Output shape complete?", "y");
|
|
55
|
+
const qualityMet = await askWithDefault(" Quality bar met?", "y");
|
|
56
|
+
const risksSurfaced = await askWithDefault(" Risks surfaced honestly?", "y");
|
|
57
|
+
const readyForNext = await askWithDefault(" Ready for next stage?", verdict === "accept" ? "y" : "n");
|
|
58
|
+
|
|
59
|
+
const verdictContent = `# Review Verdict
|
|
60
|
+
|
|
61
|
+
## Reviewer
|
|
62
|
+
${reviewer}
|
|
63
|
+
|
|
64
|
+
## Task ID
|
|
65
|
+
${taskId}
|
|
66
|
+
|
|
67
|
+
## Verdict
|
|
68
|
+
${verdict}
|
|
69
|
+
|
|
70
|
+
## Why
|
|
71
|
+
${reason}
|
|
72
|
+
|
|
73
|
+
## Contract Check
|
|
74
|
+
- Scope respected? ${scopeRespected}
|
|
75
|
+
- Output shape complete? ${outputComplete}
|
|
76
|
+
- Quality bar met? ${qualityMet}
|
|
77
|
+
- Risks surfaced honestly? ${risksSurfaced}
|
|
78
|
+
- Ready for next stage? ${readyForNext}
|
|
79
|
+
|
|
80
|
+
${corrections ? `## Required Corrections\n${corrections}\n` : ""}## Next Owner
|
|
81
|
+
${nextOwner}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
closePrompts();
|
|
85
|
+
|
|
86
|
+
// Write verdict as companion file
|
|
87
|
+
const packetBase = basename(packetFile, ".md");
|
|
88
|
+
const verdictPath = resolve(dirname(packetFile), `${packetBase}.verdict.md`);
|
|
89
|
+
|
|
90
|
+
const wrote = writeFileSafe(verdictPath, verdictContent, { force: true });
|
|
91
|
+
if (wrote) {
|
|
92
|
+
console.log(`\nVerdict recorded: ${verdictPath}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/route.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { readFileSafe } from "./fs-utils.mjs";
|
|
4
|
+
|
|
5
|
+
const ROLE_KEYWORDS = {
|
|
6
|
+
"Product Strategist": [
|
|
7
|
+
"product", "scope", "intent", "prioritize", "tradeoff", "framing",
|
|
8
|
+
"feature shaping", "user value",
|
|
9
|
+
],
|
|
10
|
+
"UI Designer": [
|
|
11
|
+
"ui", "screen", "layout", "hierarchy", "interaction", "visual",
|
|
12
|
+
"design", "flow", "component",
|
|
13
|
+
],
|
|
14
|
+
"Backend Engineer": [
|
|
15
|
+
"api", "server", "data", "persistence", "contract", "model",
|
|
16
|
+
"migration", "bridge", "wiring", "session", "state",
|
|
17
|
+
],
|
|
18
|
+
"Frontend Developer": [
|
|
19
|
+
"frontend", "render", "component", "client", "tui", "display",
|
|
20
|
+
"view", "screen implementation",
|
|
21
|
+
],
|
|
22
|
+
"Test Engineer": [
|
|
23
|
+
"test", "verify", "regression", "coverage", "assertion", "edge case",
|
|
24
|
+
],
|
|
25
|
+
"Launch Copywriter": [
|
|
26
|
+
"release notes", "launch", "messaging", "copy", "positioning",
|
|
27
|
+
"announcement",
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const CHAINS = {
|
|
32
|
+
feature: [
|
|
33
|
+
"Orchestrator", "Product Strategist", "UI Designer",
|
|
34
|
+
"Backend Engineer", "Frontend Developer", "Test Engineer",
|
|
35
|
+
"Critic Reviewer",
|
|
36
|
+
],
|
|
37
|
+
integration: [
|
|
38
|
+
"Orchestrator", "Backend Engineer", "Frontend Developer",
|
|
39
|
+
"Test Engineer", "Critic Reviewer",
|
|
40
|
+
],
|
|
41
|
+
identity: [
|
|
42
|
+
"Orchestrator", "Product Strategist", "UI Designer",
|
|
43
|
+
"Frontend Developer", "Test Engineer", "Critic Reviewer",
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function detectType(content) {
|
|
48
|
+
const lower = content.toLowerCase();
|
|
49
|
+
|
|
50
|
+
const typeMatch = content.match(/## Packet Type\n(\w+)/);
|
|
51
|
+
if (typeMatch && CHAINS[typeMatch[1]]) {
|
|
52
|
+
return typeMatch[1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (lower.includes("contamination") || lower.includes("residue") || lower.includes("identity") || lower.includes("purge")) {
|
|
56
|
+
return "identity";
|
|
57
|
+
}
|
|
58
|
+
if (lower.includes("wiring") || lower.includes("bridge") || lower.includes("integration") || lower.includes("seam")) {
|
|
59
|
+
return "integration";
|
|
60
|
+
}
|
|
61
|
+
return "feature";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scoreRoles(content) {
|
|
65
|
+
const lower = content.toLowerCase();
|
|
66
|
+
const scores = {};
|
|
67
|
+
|
|
68
|
+
for (const [role, keywords] of Object.entries(ROLE_KEYWORDS)) {
|
|
69
|
+
let score = 0;
|
|
70
|
+
for (const kw of keywords) {
|
|
71
|
+
if (lower.includes(kw)) score++;
|
|
72
|
+
}
|
|
73
|
+
if (score > 0) scores[role] = score;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return scores;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recommendChain(type, scores) {
|
|
80
|
+
const base = CHAINS[type] || CHAINS.feature;
|
|
81
|
+
const relevant = new Set(Object.keys(scores));
|
|
82
|
+
|
|
83
|
+
// Always keep Orchestrator and Critic Reviewer
|
|
84
|
+
const chain = base.filter((role) => {
|
|
85
|
+
if (role === "Orchestrator" || role === "Critic Reviewer") return true;
|
|
86
|
+
if (relevant.has(role)) return true;
|
|
87
|
+
// Keep roles in the base chain even without keyword hits —
|
|
88
|
+
// the type-based chain is the proven default
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return chain;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractFileRefs(content, packetDir) {
|
|
96
|
+
const refs = [];
|
|
97
|
+
const inputsMatch = content.match(/## Inputs\n([\s\S]*?)(?=\n## |\n---)/);
|
|
98
|
+
if (!inputsMatch) return refs;
|
|
99
|
+
|
|
100
|
+
const inputsSection = inputsMatch[1];
|
|
101
|
+
// Match file paths — look for patterns like path/to/file.ext or ./path/to/file
|
|
102
|
+
const pathPattern = /(?:^|\s|`)((?:\.\/|\.\.\/|[a-zA-Z][\w\-]*\/)[^\s`\n,)]+\.\w+)/gm;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = pathPattern.exec(inputsSection)) !== null) {
|
|
105
|
+
const ref = match[1];
|
|
106
|
+
const resolved = resolve(dirname(packetDir), "..", "..", ref);
|
|
107
|
+
refs.push({
|
|
108
|
+
ref,
|
|
109
|
+
resolved,
|
|
110
|
+
exists: existsSync(resolved),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return refs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function routeCommand(args) {
|
|
118
|
+
const packetFile = args[0];
|
|
119
|
+
|
|
120
|
+
if (!packetFile) {
|
|
121
|
+
const err = new Error("Usage: roleos route <packet-file>");
|
|
122
|
+
err.exitCode = 1;
|
|
123
|
+
err.hint = "Provide the path to a packet file.";
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const content = readFileSafe(packetFile);
|
|
128
|
+
if (content === null) {
|
|
129
|
+
const err = new Error(`Packet not found: ${packetFile}`);
|
|
130
|
+
err.exitCode = 1;
|
|
131
|
+
err.hint = "Check the file path and try again.";
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const type = detectType(content);
|
|
136
|
+
const scores = scoreRoles(content);
|
|
137
|
+
const chain = recommendChain(type, scores);
|
|
138
|
+
const fileRefs = extractFileRefs(content, resolve(packetFile));
|
|
139
|
+
|
|
140
|
+
console.log(`\nroleos route — ${packetFile}\n`);
|
|
141
|
+
console.log(`Detected type: ${type}`);
|
|
142
|
+
console.log(`\nRecommended chain (${chain.length} roles):`);
|
|
143
|
+
chain.forEach((role, i) => {
|
|
144
|
+
console.log(` ${i + 1}. ${role}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (Object.keys(scores).length > 0) {
|
|
148
|
+
console.log(`\nRole signals:`);
|
|
149
|
+
for (const [role, score] of Object.entries(scores).sort((a, b) => b[1] - a[1])) {
|
|
150
|
+
console.log(` ${role}: ${score} keyword hit${score > 1 ? "s" : ""}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (fileRefs.length > 0) {
|
|
155
|
+
console.log(`\nDependency verification:`);
|
|
156
|
+
let hasIssues = false;
|
|
157
|
+
for (const ref of fileRefs) {
|
|
158
|
+
const status = ref.exists ? "OK" : "MISSING";
|
|
159
|
+
const marker = ref.exists ? "+" : "!";
|
|
160
|
+
console.log(` ${marker} ${ref.ref} — ${status}`);
|
|
161
|
+
if (!ref.exists) hasIssues = true;
|
|
162
|
+
}
|
|
163
|
+
if (hasIssues) {
|
|
164
|
+
console.log(`\n WARNING: Some referenced files are missing. Verify before proceeding.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\nNext: assign roles and begin execution, or adjust the chain.`);
|
|
169
|
+
}
|
package/src/status.mjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
|
|
4
|
+
|
|
5
|
+
// --- Parsers ---
|
|
6
|
+
|
|
7
|
+
function parsePacket(filePath) {
|
|
8
|
+
const content = readFileSafe(filePath);
|
|
9
|
+
if (!content) return null;
|
|
10
|
+
|
|
11
|
+
const get = (heading) => {
|
|
12
|
+
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |\\n---|$)`);
|
|
13
|
+
const m = content.match(re);
|
|
14
|
+
return m ? m[1].trim() : null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const taskId = get("Task ID");
|
|
18
|
+
const title = get("Title");
|
|
19
|
+
const type = get("Packet Type");
|
|
20
|
+
const openQuestions = get("Open Questions");
|
|
21
|
+
const assignedRole = get("Assigned Role");
|
|
22
|
+
|
|
23
|
+
const hasContent = taskId || title;
|
|
24
|
+
if (!hasContent) return { malformed: true, file: basename(filePath) };
|
|
25
|
+
|
|
26
|
+
// Check for companion verdict
|
|
27
|
+
const verdictPath = filePath.replace(/\.md$/, ".verdict.md");
|
|
28
|
+
const verdict = parseVerdict(verdictPath);
|
|
29
|
+
|
|
30
|
+
const hasBlockers = openQuestions &&
|
|
31
|
+
!openQuestions.startsWith("<!--") &&
|
|
32
|
+
openQuestions.length > 0;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
file: basename(filePath),
|
|
36
|
+
taskId: taskId || basename(filePath, ".md"),
|
|
37
|
+
title: title || "(untitled)",
|
|
38
|
+
type: type || "unknown",
|
|
39
|
+
assignedRole: assignedRole && !assignedRole.startsWith("<!--") ? assignedRole : null,
|
|
40
|
+
openQuestions: hasBlockers ? openQuestions : null,
|
|
41
|
+
verdict,
|
|
42
|
+
status: verdict ? (verdict.verdict === "accept" || verdict.verdict === "accept-with-notes" ? "completed" : verdict.verdict) : "active",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseVerdict(filePath) {
|
|
47
|
+
const content = readFileSafe(filePath);
|
|
48
|
+
if (!content) return null;
|
|
49
|
+
|
|
50
|
+
const get = (heading) => {
|
|
51
|
+
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
52
|
+
const m = content.match(re);
|
|
53
|
+
return m ? m[1].trim() : null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const verdict = get("Verdict");
|
|
57
|
+
const reviewer = get("Reviewer");
|
|
58
|
+
const why = get("Why");
|
|
59
|
+
const taskId = get("Task ID");
|
|
60
|
+
const nextOwner = get("Next Owner");
|
|
61
|
+
|
|
62
|
+
if (!verdict) return { malformed: true, file: basename(filePath) };
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
file: basename(filePath),
|
|
66
|
+
taskId,
|
|
67
|
+
verdict,
|
|
68
|
+
reviewer: reviewer || "unknown",
|
|
69
|
+
why: why || "",
|
|
70
|
+
nextOwner: nextOwner || null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Data collection ---
|
|
75
|
+
|
|
76
|
+
function collectPackets(claudeDir) {
|
|
77
|
+
const packetsDir = join(claudeDir, "packets");
|
|
78
|
+
if (!existsSync(packetsDir)) return [];
|
|
79
|
+
|
|
80
|
+
const files = readdirSync(packetsDir)
|
|
81
|
+
.filter((f) => f.endsWith(".md") && !f.endsWith(".verdict.md"))
|
|
82
|
+
.sort();
|
|
83
|
+
|
|
84
|
+
return files
|
|
85
|
+
.map((f) => parsePacket(join(packetsDir, f)))
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkContextHealth(claudeDir) {
|
|
90
|
+
const contextFiles = [
|
|
91
|
+
"context/product-brief.md",
|
|
92
|
+
"context/repo-map.md",
|
|
93
|
+
"context/current-priorities.md",
|
|
94
|
+
"context/brand-rules.md",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return contextFiles.map((f) => ({
|
|
98
|
+
file: f,
|
|
99
|
+
exists: existsSync(join(claudeDir, f)),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkSpineHealth(claudeDir) {
|
|
104
|
+
const checks = [
|
|
105
|
+
"agents/core/orchestrator.md",
|
|
106
|
+
"agents/core/critic-reviewer.md",
|
|
107
|
+
"schemas/task-packet.md",
|
|
108
|
+
"policy/done-definition.md",
|
|
109
|
+
"workflows/ship-feature.md",
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return checks.map((f) => ({
|
|
113
|
+
file: f,
|
|
114
|
+
exists: existsSync(join(claudeDir, f)),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Health warnings ---
|
|
119
|
+
|
|
120
|
+
function getWarnings(packets, contextHealth, spineHealth) {
|
|
121
|
+
const warnings = [];
|
|
122
|
+
|
|
123
|
+
// Missing context files
|
|
124
|
+
for (const c of contextHealth) {
|
|
125
|
+
if (!c.exists) warnings.push(`Missing context: ${c.file}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Missing spine files
|
|
129
|
+
for (const s of spineHealth) {
|
|
130
|
+
if (!s.exists) warnings.push(`Missing spine: ${s.file}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Malformed packets
|
|
134
|
+
for (const p of packets) {
|
|
135
|
+
if (p.malformed) warnings.push(`Malformed packet: ${p.file}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Active packets with no review
|
|
139
|
+
const active = packets.filter((p) => !p.malformed && p.status === "active");
|
|
140
|
+
if (active.length > 3) {
|
|
141
|
+
warnings.push(`${active.length} active packets — consider reviewing before adding more`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Verdicts for missing packets (orphaned verdict files)
|
|
145
|
+
const packetsDir = join(".", ".claude", "packets");
|
|
146
|
+
if (existsSync(packetsDir)) {
|
|
147
|
+
const verdictFiles = readdirSync(packetsDir).filter((f) => f.endsWith(".verdict.md"));
|
|
148
|
+
for (const vf of verdictFiles) {
|
|
149
|
+
const packetFile = vf.replace(".verdict.md", ".md");
|
|
150
|
+
if (!existsSync(join(packetsDir, packetFile))) {
|
|
151
|
+
warnings.push(`Orphaned verdict: ${vf} (no matching packet)`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// No work at all
|
|
157
|
+
if (packets.length === 0) {
|
|
158
|
+
warnings.push("No packets found — create one with 'roleos packet new feature'");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return warnings;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Output formatters ---
|
|
165
|
+
|
|
166
|
+
function formatTerminal(data) {
|
|
167
|
+
const lines = [];
|
|
168
|
+
lines.push("roleos status\n");
|
|
169
|
+
|
|
170
|
+
// Active packets
|
|
171
|
+
const active = data.packets.filter((p) => !p.malformed && p.status === "active");
|
|
172
|
+
lines.push(`Active packets: ${active.length}`);
|
|
173
|
+
if (active.length > 0) {
|
|
174
|
+
for (const p of active) {
|
|
175
|
+
const role = p.assignedRole ? ` [${p.assignedRole}]` : "";
|
|
176
|
+
lines.push(` - ${p.title} (${p.type})${role}`);
|
|
177
|
+
if (p.openQuestions) {
|
|
178
|
+
lines.push(` Open: ${p.openQuestions.split("\n")[0].slice(0, 80)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
lines.push("");
|
|
183
|
+
|
|
184
|
+
// Blocked/rejected
|
|
185
|
+
const blocked = data.packets.filter((p) => !p.malformed && (p.status === "blocked" || p.status === "reject"));
|
|
186
|
+
if (blocked.length > 0) {
|
|
187
|
+
lines.push(`Blocked/rejected: ${blocked.length}`);
|
|
188
|
+
for (const p of blocked) {
|
|
189
|
+
const why = p.verdict?.why ? ` — ${p.verdict.why.split("\n")[0].slice(0, 60)}` : "";
|
|
190
|
+
lines.push(` - ${p.title}${why}`);
|
|
191
|
+
}
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Recent verdicts
|
|
196
|
+
const withVerdicts = data.packets.filter((p) => !p.malformed && p.verdict && !p.verdict.malformed);
|
|
197
|
+
if (withVerdicts.length > 0) {
|
|
198
|
+
lines.push(`Recent verdicts: ${withVerdicts.length}`);
|
|
199
|
+
const recent = withVerdicts.slice(-5);
|
|
200
|
+
for (const p of recent) {
|
|
201
|
+
const v = p.verdict;
|
|
202
|
+
lines.push(` - ${p.title}: ${v.verdict} (${v.reviewer})`);
|
|
203
|
+
}
|
|
204
|
+
lines.push("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Completed
|
|
208
|
+
const completed = data.packets.filter((p) => !p.malformed && p.status === "completed");
|
|
209
|
+
if (completed.length > 0) {
|
|
210
|
+
lines.push(`Completed: ${completed.length}`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Context health
|
|
215
|
+
const missingContext = data.contextHealth.filter((c) => !c.exists);
|
|
216
|
+
if (missingContext.length > 0) {
|
|
217
|
+
lines.push(`Context: ${data.contextHealth.length - missingContext.length}/${data.contextHealth.length} present`);
|
|
218
|
+
} else {
|
|
219
|
+
lines.push(`Context: all ${data.contextHealth.length} files present`);
|
|
220
|
+
}
|
|
221
|
+
lines.push("");
|
|
222
|
+
|
|
223
|
+
// Warnings
|
|
224
|
+
if (data.warnings.length > 0) {
|
|
225
|
+
lines.push("Warnings:");
|
|
226
|
+
for (const w of data.warnings) {
|
|
227
|
+
lines.push(` ! ${w}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Summary
|
|
233
|
+
const total = data.packets.filter((p) => !p.malformed).length;
|
|
234
|
+
lines.push(`Total: ${total} packet(s) — ${active.length} active, ${completed.length} completed, ${blocked.length} blocked/rejected`);
|
|
235
|
+
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatMarkdown(data) {
|
|
240
|
+
const lines = [];
|
|
241
|
+
const ts = new Date().toISOString().replace("T", " ").split(".")[0] + " UTC";
|
|
242
|
+
lines.push(`# Status\n`);
|
|
243
|
+
lines.push(`Generated: ${ts}\n`);
|
|
244
|
+
|
|
245
|
+
// Active
|
|
246
|
+
const active = data.packets.filter((p) => !p.malformed && p.status === "active");
|
|
247
|
+
lines.push(`## Active Packets`);
|
|
248
|
+
if (active.length === 0) {
|
|
249
|
+
lines.push("None\n");
|
|
250
|
+
} else {
|
|
251
|
+
for (const p of active) {
|
|
252
|
+
const role = p.assignedRole ? ` — ${p.assignedRole}` : "";
|
|
253
|
+
lines.push(`- **${p.title}** (${p.type})${role}`);
|
|
254
|
+
if (p.openQuestions) {
|
|
255
|
+
lines.push(` - Open: ${p.openQuestions.split("\n")[0]}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push("");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Blockers
|
|
262
|
+
const blocked = data.packets.filter((p) => !p.malformed && (p.status === "blocked" || p.status === "reject"));
|
|
263
|
+
lines.push(`## Blockers`);
|
|
264
|
+
if (blocked.length === 0) {
|
|
265
|
+
lines.push("None\n");
|
|
266
|
+
} else {
|
|
267
|
+
for (const p of blocked) {
|
|
268
|
+
const why = p.verdict?.why ? `: ${p.verdict.why.split("\n")[0]}` : "";
|
|
269
|
+
lines.push(`- **${p.title}**${why}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push("");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Recent verdicts
|
|
275
|
+
const withVerdicts = data.packets.filter((p) => !p.malformed && p.verdict && !p.verdict.malformed);
|
|
276
|
+
lines.push(`## Recent Verdicts`);
|
|
277
|
+
if (withVerdicts.length === 0) {
|
|
278
|
+
lines.push("None\n");
|
|
279
|
+
} else {
|
|
280
|
+
for (const p of withVerdicts.slice(-5)) {
|
|
281
|
+
lines.push(`- **${p.title}**: ${p.verdict.verdict} (${p.verdict.reviewer})`);
|
|
282
|
+
}
|
|
283
|
+
lines.push("");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Completed
|
|
287
|
+
const completed = data.packets.filter((p) => !p.malformed && p.status === "completed");
|
|
288
|
+
lines.push(`## Completed`);
|
|
289
|
+
if (completed.length === 0) {
|
|
290
|
+
lines.push("None\n");
|
|
291
|
+
} else {
|
|
292
|
+
for (const p of completed) {
|
|
293
|
+
lines.push(`- ${p.title} (${p.type})`);
|
|
294
|
+
}
|
|
295
|
+
lines.push("");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Context health
|
|
299
|
+
lines.push(`## Context Health`);
|
|
300
|
+
for (const c of data.contextHealth) {
|
|
301
|
+
lines.push(`- ${c.exists ? "+" : "!"} ${c.file}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
|
|
305
|
+
// Warnings
|
|
306
|
+
if (data.warnings.length > 0) {
|
|
307
|
+
lines.push(`## Warnings`);
|
|
308
|
+
for (const w of data.warnings) {
|
|
309
|
+
lines.push(`- ${w}`);
|
|
310
|
+
}
|
|
311
|
+
lines.push("");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Command ---
|
|
318
|
+
|
|
319
|
+
export async function statusCommand(args) {
|
|
320
|
+
const claudeDir = join(".", ".claude");
|
|
321
|
+
|
|
322
|
+
if (!existsSync(claudeDir)) {
|
|
323
|
+
const err = new Error("No .claude/ directory found. Run 'roleos init' first.");
|
|
324
|
+
err.exitCode = 1;
|
|
325
|
+
err.hint = "Run 'roleos init' to scaffold Role OS.";
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const packets = collectPackets(claudeDir);
|
|
330
|
+
const contextHealth = checkContextHealth(claudeDir);
|
|
331
|
+
const spineHealth = checkSpineHealth(claudeDir);
|
|
332
|
+
const warnings = getWarnings(packets, contextHealth, spineHealth);
|
|
333
|
+
|
|
334
|
+
const data = { packets, contextHealth, spineHealth, warnings };
|
|
335
|
+
|
|
336
|
+
const isJson = args.includes("--json");
|
|
337
|
+
const isWrite = args.includes("--write");
|
|
338
|
+
|
|
339
|
+
if (isJson) {
|
|
340
|
+
console.log(JSON.stringify(data, null, 2));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(formatTerminal(data));
|
|
345
|
+
|
|
346
|
+
if (isWrite) {
|
|
347
|
+
const statusDir = join(claudeDir, "status");
|
|
348
|
+
const indexPath = join(statusDir, "index.md");
|
|
349
|
+
writeFileSafe(indexPath, formatMarkdown(data), { force: true });
|
|
350
|
+
console.log(`\nWritten: .claude/status/index.md`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Full Treatment
|
|
2
|
+
|
|
3
|
+
Role OS does not own or redefine the full treatment protocol. The canonical protocol lives in Claude project memory (`memory/full-treatment.md`) and is a 7-phase polish + publish playbook.
|
|
4
|
+
|
|
5
|
+
## Canonical source
|
|
6
|
+
|
|
7
|
+
The full treatment is defined in Claude project memory as a 7-phase protocol:
|
|
8
|
+
|
|
9
|
+
1. Pre-flight + finalize README + hand off translations
|
|
10
|
+
2. Scaffold landing page (Astro site-theme)
|
|
11
|
+
3. Handbook (Starlight docs)
|
|
12
|
+
4. Repo metadata + coverage
|
|
13
|
+
5. Repo Knowledge DB entry
|
|
14
|
+
6. Commit and deploy
|
|
15
|
+
7. Post-deploy verification
|
|
16
|
+
|
|
17
|
+
Read `memory/full-treatment.md` and `memory/handbook-playbook.md` before starting.
|
|
18
|
+
|
|
19
|
+
## Gate: Shipcheck runs first
|
|
20
|
+
|
|
21
|
+
Full treatment does not start until shipcheck passes. Shipcheck is the 31-item quality gate (hard gates A-D block release). The canonical shipcheck reference lives in Claude project memory (`memory/shipcheck.md`).
|
|
22
|
+
|
|
23
|
+
Order: `npx @mcptoolshop/shipcheck audit` → exits 0 → then full treatment.
|
|
24
|
+
|
|
25
|
+
No v1.0.0 bump without passing hard gates A-D.
|
|
26
|
+
|
|
27
|
+
## Role chain per phase
|
|
28
|
+
|
|
29
|
+
Each treatment phase maps to specific roles:
|
|
30
|
+
|
|
31
|
+
### Phase 1 — Pre-flight + README + translations
|
|
32
|
+
- **Repo Researcher** — verify repo state, Pages config, package.json
|
|
33
|
+
- **Brand Guardian** — verify logo, README identity, footer
|
|
34
|
+
- **Repo Translator** — hand off and verify translations
|
|
35
|
+
|
|
36
|
+
### Phase 2 — Scaffold landing page
|
|
37
|
+
- **Docs Architect** — site-theme init, site-config.ts
|
|
38
|
+
- **Frontend Developer** — landing page content and build
|
|
39
|
+
- **Brand Guardian** — verify brand alignment
|
|
40
|
+
|
|
41
|
+
### Phase 3 — Handbook (Starlight docs)
|
|
42
|
+
- **Docs Architect** — Starlight setup, page structure, content
|
|
43
|
+
- **Repo Translator** — docs translation if applicable
|
|
44
|
+
|
|
45
|
+
### Phase 4 — Repo metadata + coverage
|
|
46
|
+
- **Metadata Curator** — GitHub metadata, badges, manifest
|
|
47
|
+
- **Coverage Auditor** — test coverage assessment, CI integration
|
|
48
|
+
|
|
49
|
+
### Phase 5 — Repo Knowledge DB entry
|
|
50
|
+
- **Repo Researcher** — thesis, architecture, relationships
|
|
51
|
+
- **Metadata Curator** — verify entry completeness
|
|
52
|
+
|
|
53
|
+
### Phase 6 — Commit and deploy
|
|
54
|
+
- **Release Engineer** — staging, version, tag, push
|
|
55
|
+
|
|
56
|
+
### Phase 7 — Post-deploy verification
|
|
57
|
+
- **Deployment Verifier** — landing page, handbook, package, badges, translations
|
|
58
|
+
|
|
59
|
+
### Final gate
|
|
60
|
+
- **Critic Reviewer** — accept or reject treatment completeness
|
|
61
|
+
|
|
62
|
+
## What Role OS adds
|
|
63
|
+
|
|
64
|
+
- Explicit role ownership for each treatment phase
|
|
65
|
+
- Structured handoffs between phases
|
|
66
|
+
- Review gate on treatment completeness
|
|
67
|
+
- Routing and escalation law
|
|
68
|
+
|
|
69
|
+
## What Role OS does not own
|
|
70
|
+
|
|
71
|
+
- The treatment protocol itself
|
|
72
|
+
- The shipcheck gate
|
|
73
|
+
- Claude project memory
|
|
74
|
+
- Treatment history and repo facts (those live in Claude project memory)
|