role-os 1.0.2 → 1.2.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 +74 -0
- package/README.md +55 -23
- package/bin/roleos.mjs +8 -1
- package/package.json +13 -3
- package/src/conflicts.mjs +217 -0
- package/src/dispatch.mjs +310 -0
- package/src/escalation.mjs +288 -0
- package/src/evidence.mjs +288 -0
- package/src/packs-cmd.mjs +143 -0
- package/src/packs.mjs +331 -0
- package/src/review.mjs +12 -0
- package/src/route.mjs +477 -82
- package/src/trial.mjs +252 -0
package/src/route.mjs
CHANGED
|
@@ -1,121 +1,416 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve, dirname } from "node:path";
|
|
3
3
|
import { readFileSafe } from "./fs-utils.mjs";
|
|
4
|
+
import { detectConflicts } from "./conflicts.mjs";
|
|
5
|
+
import { resolveConflict, resolveSplit, formatEscalation } from "./escalation.mjs";
|
|
6
|
+
import { suggestPack, getPack, checkPackMismatch, getPackRoles } from "./packs.mjs";
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
8
|
+
// ── Full 32-Role Catalog ─────────────────────────────────────────────────────
|
|
9
|
+
// Every role in the OS is scoreable. Keywords from routing-rules.md + contracts.
|
|
10
|
+
// Triggers are strong multi-word signals worth bonus points.
|
|
11
|
+
|
|
12
|
+
export const ROLE_CATALOG = [
|
|
13
|
+
// ── CORE ──
|
|
14
|
+
{
|
|
15
|
+
name: "Orchestrator", pack: "core", phase: 0,
|
|
16
|
+
alwaysInclude: true,
|
|
17
|
+
keywords: [],
|
|
18
|
+
triggers: ["multi-step", "cross-functional", "decomposition", "sequencing"],
|
|
19
|
+
excludeWhen: [],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "Product Strategist", pack: "product", phase: 1,
|
|
23
|
+
keywords: ["product", "scope", "intent", "prioritize", "tradeoff", "framing", "user value", "feature shaping"],
|
|
24
|
+
triggers: ["problem framing", "scope definition", "tradeoff decision"],
|
|
25
|
+
excludeWhen: [],
|
|
26
|
+
deliverableAffinity: ["Plan"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Critic Reviewer", pack: "core", phase: 99,
|
|
30
|
+
alwaysInclude: true,
|
|
31
|
+
keywords: [],
|
|
32
|
+
triggers: ["final acceptance", "quality gate", "truthful rejection"],
|
|
33
|
+
excludeWhen: [],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// ── DESIGN ──
|
|
37
|
+
{
|
|
38
|
+
name: "UI Designer", pack: "design", phase: 2,
|
|
39
|
+
keywords: ["ui", "screen", "layout", "hierarchy", "interaction", "visual", "design", "flow", "component", "wireframe"],
|
|
40
|
+
triggers: ["information hierarchy", "user flow", "interaction design", "screen structure"],
|
|
41
|
+
excludeWhen: ["no user interface", "cli only", "backend only"],
|
|
42
|
+
deliverableAffinity: ["Design"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Brand Guardian", pack: "design", phase: 2,
|
|
46
|
+
keywords: ["brand", "identity", "terminology", "tone", "contamination", "fork", "residue", "purge", "naming"],
|
|
47
|
+
triggers: ["identity contamination", "fork residue", "terminology consistency", "replacement doctrine"],
|
|
48
|
+
excludeWhen: ["no brand concern", "internal tooling only"],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// ── ENGINEERING ──
|
|
52
|
+
{
|
|
53
|
+
name: "Backend Engineer", pack: "engineering", phase: 3,
|
|
54
|
+
keywords: ["api", "server", "data", "persistence", "contract", "model", "migration", "bridge", "wiring", "session", "state", "database", "endpoint"],
|
|
55
|
+
triggers: ["server-side", "data flow", "system contract"],
|
|
56
|
+
excludeWhen: ["frontend only", "docs only"],
|
|
57
|
+
deliverableAffinity: ["Code"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Frontend Developer", pack: "engineering", phase: 3,
|
|
61
|
+
keywords: ["frontend", "render", "component", "client", "tui", "display", "view", "react", "css", "html", "dom"],
|
|
62
|
+
triggers: ["ui implementation", "client state", "interaction wiring", "frontend integration"],
|
|
63
|
+
excludeWhen: ["backend only", "no ui"],
|
|
64
|
+
deliverableAffinity: ["Code"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "Test Engineer", pack: "engineering", phase: 4,
|
|
68
|
+
keywords: ["test", "verify", "regression", "coverage", "assertion", "edge case", "vitest", "jest", "spec"],
|
|
69
|
+
triggers: ["test plan", "regression defense", "verification coverage"],
|
|
70
|
+
excludeWhen: [],
|
|
71
|
+
deliverableAffinity: ["Test"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Performance Engineer", pack: "engineering", phase: 3,
|
|
75
|
+
keywords: ["performance", "profiling", "latency", "memory", "benchmark", "optimization", "hot path", "budget", "slow", "bottleneck"],
|
|
76
|
+
triggers: ["performance regression", "hot path analysis", "performance budget"],
|
|
77
|
+
excludeWhen: ["no performance concern"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "Refactor Engineer", pack: "engineering", phase: 3,
|
|
81
|
+
keywords: ["refactor", "duplication", "boundary", "complexity", "cleanup", "modularize", "split", "extract", "simplify"],
|
|
82
|
+
triggers: ["structure cleanup", "module boundary", "complexity reduction", "duplication elimination"],
|
|
83
|
+
excludeWhen: ["new feature", "behavior change required"],
|
|
84
|
+
deliverableAffinity: ["Code"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "Security Reviewer", pack: "engineering", phase: 3,
|
|
88
|
+
keywords: ["security", "injection", "auth", "authentication", "authorization", "secrets", "owasp", "threat", "vulnerability", "xss", "csrf", "sanitize"],
|
|
89
|
+
triggers: ["security review", "threat model", "owasp pattern", "secret scanning"],
|
|
90
|
+
excludeWhen: ["no security surface"],
|
|
91
|
+
deliverableAffinity: ["Review"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "Dependency Auditor", pack: "engineering", phase: 3,
|
|
95
|
+
keywords: ["dependency", "dependencies", "vulnerability", "supply chain", "stale", "outdated", "audit", "npm audit", "dependabot"],
|
|
96
|
+
triggers: ["dependency health", "supply-chain risk", "vulnerability scanning"],
|
|
97
|
+
excludeWhen: ["no external dependencies"],
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ── TREATMENT ──
|
|
101
|
+
{
|
|
102
|
+
name: "Repo Researcher", pack: "treatment", phase: 1,
|
|
103
|
+
keywords: ["repo structure", "entrypoint", "seam", "build command", "test command", "codebase", "architecture", "map"],
|
|
104
|
+
triggers: ["repo structure mapping", "entrypoint discovery", "dependency verification"],
|
|
105
|
+
excludeWhen: ["repo already well understood"],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "Repo Translator", pack: "treatment", phase: 5,
|
|
109
|
+
keywords: ["translate", "translation", "localize", "localization", "i18n", "multilingual", "language", "readme translation", "polyglot"],
|
|
110
|
+
triggers: ["readme translation", "docs translation", "cross-audience adaptation"],
|
|
111
|
+
excludeWhen: ["english only", "no translation needed"],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "Docs Architect", pack: "treatment", phase: 3,
|
|
115
|
+
keywords: ["documentation", "handbook", "docs", "starlight", "guide", "tutorial", "reference", "api docs", "navigation", "hierarchy", "findability", "labeling", "taxonomy", "sitemap"],
|
|
116
|
+
triggers: ["handbook creation", "docs site", "starlight setup", "documentation restructuring", "navigation design", "content organization", "information structure"],
|
|
117
|
+
excludeWhen: ["no docs needed"],
|
|
118
|
+
deliverableAffinity: ["Plan"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "Metadata Curator", pack: "treatment", phase: 5,
|
|
122
|
+
keywords: ["metadata", "manifest", "package.json", "badge", "topic", "homepage", "description", "registry", "npm"],
|
|
123
|
+
triggers: ["package manifest audit", "badge verification", "registry metadata"],
|
|
124
|
+
excludeWhen: ["no package metadata"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "Coverage Auditor", pack: "treatment", phase: 4,
|
|
128
|
+
keywords: ["coverage", "test coverage", "uncovered", "false confidence", "missing test", "untested"],
|
|
129
|
+
triggers: ["coverage assessment", "false confidence detection", "missing defense"],
|
|
130
|
+
excludeWhen: ["no test suite"],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "Deployment Verifier", pack: "treatment", phase: 6,
|
|
134
|
+
keywords: ["deploy", "deployment", "live", "landing page", "published", "badge", "verify live", "spot check"],
|
|
135
|
+
triggers: ["post-deploy verification", "landing page check", "badge resolution", "translation spot-check"],
|
|
136
|
+
excludeWhen: ["not yet deployed"],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "Release Engineer", pack: "treatment", phase: 5,
|
|
140
|
+
keywords: ["release", "version", "changelog", "tag", "publish", "package", "bump", "npm publish", "staging"],
|
|
141
|
+
triggers: ["version bump", "changelog update", "publish readiness", "release execution"],
|
|
142
|
+
excludeWhen: ["not releasing"],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ── GROWTH ──
|
|
146
|
+
{
|
|
147
|
+
name: "Launch Strategist", pack: "growth", phase: 6,
|
|
148
|
+
keywords: ["launch", "go-to-market", "channel", "timing", "proof", "success criteria", "announcement"],
|
|
149
|
+
triggers: ["launch planning", "proof packaging", "channel selection", "success criteria"],
|
|
150
|
+
excludeWhen: ["internal tool", "not launching"],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "Content Strategist", pack: "growth", phase: 6,
|
|
154
|
+
keywords: ["content", "article", "blog", "case study", "tutorial", "marketing content", "content calendar"],
|
|
155
|
+
triggers: ["content planning", "technical article", "case study angle", "docs-to-marketing bridge"],
|
|
156
|
+
excludeWhen: ["no content needed"],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "Community Manager", pack: "growth", phase: 6,
|
|
160
|
+
keywords: ["community", "issue triage", "discussion", "contribution", "contributor", "feedback loop", "open source"],
|
|
161
|
+
triggers: ["community response", "contribution guidance", "community health"],
|
|
162
|
+
excludeWhen: ["private repo", "no community"],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "Support Triage Lead", pack: "growth", phase: 6,
|
|
166
|
+
keywords: ["support", "triage", "bug report", "user error", "recurring", "priority assignment"],
|
|
167
|
+
triggers: ["support classification", "bug vs user error", "recurring pattern"],
|
|
168
|
+
excludeWhen: ["no support surface"],
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// ── MARKETING ──
|
|
172
|
+
{
|
|
173
|
+
name: "Launch Copywriter", pack: "marketing", phase: 6,
|
|
174
|
+
keywords: ["copy", "messaging", "positioning", "release notes", "announcement", "conversion"],
|
|
175
|
+
triggers: ["launch messaging", "release notes", "positioning copy"],
|
|
176
|
+
excludeWhen: ["internal tool", "not launching"],
|
|
177
|
+
deliverableAffinity: ["Copy"],
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// ── PRODUCT ──
|
|
181
|
+
{
|
|
182
|
+
name: "Feedback Synthesizer", pack: "product", phase: 1,
|
|
183
|
+
keywords: ["feedback", "signal", "cluster", "theme", "complaint", "user signal", "sentiment"],
|
|
184
|
+
triggers: ["signal clustering", "theme extraction", "complaint-to-action"],
|
|
185
|
+
excludeWhen: ["no user feedback available"],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "Roadmap Prioritizer", pack: "product", phase: 1,
|
|
189
|
+
keywords: ["roadmap", "prioritize", "backlog", "sequence", "leverage", "dependency", "stop doing"],
|
|
190
|
+
triggers: ["work sequencing", "backlog ordering", "dependency mapping", "stop doing"],
|
|
191
|
+
excludeWhen: ["single task", "no prioritization needed"],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "Spec Writer", pack: "product", phase: 2,
|
|
195
|
+
keywords: ["spec", "specification", "acceptance criteria", "edge case", "requirements", "nfr", "non-functional"],
|
|
196
|
+
triggers: ["execution-grade spec", "acceptance criteria", "edge case enumeration"],
|
|
197
|
+
excludeWhen: ["spec already exists"],
|
|
198
|
+
deliverableAffinity: ["Plan"],
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// ── RESEARCH ──
|
|
202
|
+
{
|
|
203
|
+
name: "UX Researcher", pack: "research", phase: 1,
|
|
204
|
+
keywords: ["usability", "friction", "heuristic", "user flow", "ux", "user experience", "pain point"],
|
|
205
|
+
triggers: ["user flow friction", "heuristic evaluation", "usability issue"],
|
|
206
|
+
excludeWhen: ["no user interface", "cli only"],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "Competitive Analyst", pack: "research", phase: 1,
|
|
210
|
+
keywords: ["competitive", "competitor", "differentiation", "positioning", "landscape", "alternative"],
|
|
211
|
+
triggers: ["competitive landscape", "differentiation assessment", "positioning gap"],
|
|
212
|
+
excludeWhen: ["no competitors"],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "Trend Researcher", pack: "research", phase: 1,
|
|
216
|
+
keywords: ["trend", "ecosystem", "adoption", "market", "emerging", "signal"],
|
|
217
|
+
triggers: ["technology trend", "ecosystem signal", "adoption timing"],
|
|
218
|
+
excludeWhen: ["no trend relevance"],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "User Interview Synthesizer", pack: "research", phase: 1,
|
|
222
|
+
keywords: ["interview", "mental model", "unmet need", "user research", "synthesis", "qualitative"],
|
|
223
|
+
triggers: ["interview theme", "mental model mapping", "unmet needs ranking"],
|
|
224
|
+
excludeWhen: ["no interview data"],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
30
227
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
],
|
|
41
|
-
identity: [
|
|
42
|
-
"Orchestrator", "Product Strategist", "UI Designer",
|
|
43
|
-
"Frontend Developer", "Test Engineer", "Critic Reviewer",
|
|
44
|
-
],
|
|
228
|
+
// ── Deliverable type → role affinity ──────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
const DELIVERABLE_TYPES = ["Plan", "Design", "Code", "Test", "Copy", "Review"];
|
|
231
|
+
|
|
232
|
+
// ── Packet type → role score bias ─────────────────────────────────────────────
|
|
233
|
+
// These boost relevant roles by packet type, but don't lock the chain.
|
|
234
|
+
|
|
235
|
+
const TYPE_BIAS = {
|
|
236
|
+
feature: ["Product Strategist", "UI Designer", "Backend Engineer", "Frontend Developer", "Test Engineer"],
|
|
237
|
+
integration: ["Backend Engineer", "Frontend Developer", "Test Engineer", "Refactor Engineer"],
|
|
238
|
+
identity: ["Brand Guardian", "Metadata Curator", "UI Designer", "Frontend Developer"],
|
|
45
239
|
};
|
|
46
240
|
|
|
47
|
-
|
|
241
|
+
// ── Phase ordering (for chain assembly) ───────────────────────────────────────
|
|
242
|
+
// Lower phase = earlier in chain. Same phase = sorted by pack then name.
|
|
243
|
+
|
|
244
|
+
function phaseOf(role) {
|
|
245
|
+
return role.phase ?? 3;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Scoring ───────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
const MIN_SCORE_THRESHOLD = 2; // Minimum score to be included in chain
|
|
251
|
+
|
|
252
|
+
function scoreRole(role, content, packetType, deliverableType) {
|
|
253
|
+
if (role.alwaysInclude) return { score: Infinity, reasons: ["always included"], matched: [] };
|
|
254
|
+
|
|
48
255
|
const lower = content.toLowerCase();
|
|
256
|
+
let score = 0;
|
|
257
|
+
const matched = [];
|
|
258
|
+
|
|
259
|
+
// Keyword hits (1 point each)
|
|
260
|
+
for (const kw of role.keywords) {
|
|
261
|
+
if (lower.includes(kw)) {
|
|
262
|
+
score += 1;
|
|
263
|
+
matched.push(kw);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Trigger phrase bonus (2 points each — strong signals)
|
|
268
|
+
for (const trigger of role.triggers) {
|
|
269
|
+
if (lower.includes(trigger)) {
|
|
270
|
+
score += 2;
|
|
271
|
+
if (!matched.includes(trigger)) matched.push(`[trigger] ${trigger}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Packet type bias (+1 if role is in the type's preferred set)
|
|
276
|
+
const biased = TYPE_BIAS[packetType] || [];
|
|
277
|
+
if (biased.includes(role.name)) {
|
|
278
|
+
score += 1;
|
|
279
|
+
matched.push(`[type-bias] ${packetType}`);
|
|
280
|
+
}
|
|
49
281
|
|
|
282
|
+
// Deliverable type affinity (+1)
|
|
283
|
+
if (deliverableType && role.deliverableAffinity?.includes(deliverableType)) {
|
|
284
|
+
score += 1;
|
|
285
|
+
matched.push(`[deliverable] ${deliverableType}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// excludeWhen enforcement — suppress role if exclusion patterns match
|
|
289
|
+
if (score > 0 && role.excludeWhen) {
|
|
290
|
+
for (const exclusion of role.excludeWhen) {
|
|
291
|
+
if (lower.includes(exclusion)) {
|
|
292
|
+
return { score: 0, reasons: [`excluded: "${exclusion}"`], matched: [] };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { score, reasons: matched.length > 0 ? matched : [], matched };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Type detection ────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function detectType(content) {
|
|
50
303
|
const typeMatch = content.match(/## Packet Type\n(\w+)/);
|
|
51
|
-
if (typeMatch &&
|
|
304
|
+
if (typeMatch && ["feature", "integration", "identity"].includes(typeMatch[1])) {
|
|
52
305
|
return typeMatch[1];
|
|
53
306
|
}
|
|
54
307
|
|
|
55
|
-
|
|
308
|
+
const lower = content.toLowerCase();
|
|
309
|
+
if (lower.includes("contamination") || lower.includes("residue") || lower.includes("purge")) {
|
|
56
310
|
return "identity";
|
|
57
311
|
}
|
|
58
|
-
|
|
312
|
+
// Use word-boundary-aware matching to avoid false positives like "integration testing"
|
|
313
|
+
if (lower.includes("wiring") || lower.includes("bridge") || /\bintegration\b(?!\s+test)/.test(lower) || lower.includes("seam")) {
|
|
59
314
|
return "integration";
|
|
60
315
|
}
|
|
61
316
|
return "feature";
|
|
62
317
|
}
|
|
63
318
|
|
|
64
|
-
|
|
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
|
-
}
|
|
319
|
+
// ── Deliverable type extraction ───────────────────────────────────────────────
|
|
75
320
|
|
|
76
|
-
|
|
321
|
+
function extractDeliverableType(content) {
|
|
322
|
+
const match = content.match(/## Deliverable Type\n(\w+)/);
|
|
323
|
+
if (match && DELIVERABLE_TYPES.includes(match[1])) return match[1];
|
|
324
|
+
return null;
|
|
77
325
|
}
|
|
78
326
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
const chain =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
327
|
+
// ── Chain assembly ────────────────────────────────────────────────────────────
|
|
328
|
+
// Scored roles → ordered chain. Phase-sorted, not template-locked.
|
|
329
|
+
|
|
330
|
+
function assembleChain(scoredRoles) {
|
|
331
|
+
// Sort by phase, then pack, then name
|
|
332
|
+
const chain = scoredRoles.sort((a, b) => {
|
|
333
|
+
const pa = phaseOf(a.role);
|
|
334
|
+
const pb = phaseOf(b.role);
|
|
335
|
+
if (pa !== pb) return pa - pb;
|
|
336
|
+
if (a.role.pack !== b.role.pack) return a.role.pack.localeCompare(b.role.pack);
|
|
337
|
+
return a.role.name.localeCompare(b.role.name);
|
|
90
338
|
});
|
|
91
339
|
|
|
92
340
|
return chain;
|
|
93
341
|
}
|
|
94
342
|
|
|
343
|
+
// ── Confidence assessment ─────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
function assessConfidence(scoredRoles) {
|
|
346
|
+
const strongRoles = scoredRoles.filter(r => !r.role.alwaysInclude && r.score >= MIN_SCORE_THRESHOLD);
|
|
347
|
+
if (strongRoles.length >= 3) return "high";
|
|
348
|
+
if (strongRoles.length >= 1) return "medium";
|
|
349
|
+
return "low";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── File reference extraction ─────────────────────────────────────────────────
|
|
353
|
+
|
|
95
354
|
function extractFileRefs(content, packetDir) {
|
|
96
355
|
const refs = [];
|
|
97
356
|
const inputsMatch = content.match(/## Inputs\n([\s\S]*?)(?=\n## |\n---)/);
|
|
98
357
|
if (!inputsMatch) return refs;
|
|
99
358
|
|
|
100
359
|
const inputsSection = inputsMatch[1];
|
|
101
|
-
// Match file paths — look for patterns like path/to/file.ext or ./path/to/file
|
|
102
360
|
const pathPattern = /(?:^|\s|`)((?:\.\/|\.\.\/|[a-zA-Z][\w\-]*\/)[^\s`\n,)]+\.\w+)/gm;
|
|
103
361
|
let match;
|
|
104
362
|
while ((match = pathPattern.exec(inputsSection)) !== null) {
|
|
105
363
|
const ref = match[1];
|
|
106
364
|
const resolved = resolve(dirname(packetDir), "..", "..", ref);
|
|
107
|
-
refs.push({
|
|
108
|
-
ref,
|
|
109
|
-
resolved,
|
|
110
|
-
exists: existsSync(resolved),
|
|
111
|
-
});
|
|
365
|
+
refs.push({ ref, resolved, exists: existsSync(resolved) });
|
|
112
366
|
}
|
|
113
367
|
|
|
114
368
|
return refs;
|
|
115
369
|
}
|
|
116
370
|
|
|
371
|
+
// ── Handoff hints ─────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
const HANDOFF_HINTS = {
|
|
374
|
+
"Orchestrator": "decomposes into role-owned packets with verified dependencies",
|
|
375
|
+
"Product Strategist": "hands off: scope doc, prioritized requirements, tradeoff decisions",
|
|
376
|
+
"UI Designer": "hands off: screen structure, interaction flow, visual direction",
|
|
377
|
+
"Brand Guardian": "hands off: terminology audit, contamination report, replacement rules",
|
|
378
|
+
"Backend Engineer": "hands off: implemented APIs, data contracts, migration scripts",
|
|
379
|
+
"Frontend Developer": "hands off: implemented UI, client state, integration wiring",
|
|
380
|
+
"Test Engineer": "hands off: test results, coverage report, edge case findings",
|
|
381
|
+
"Performance Engineer": "hands off: profiling results, identified hot paths, optimization plan",
|
|
382
|
+
"Refactor Engineer": "hands off: restructured code, boundary changes, diff summary",
|
|
383
|
+
"Security Reviewer": "hands off: threat model, flagged patterns, recommended mitigations",
|
|
384
|
+
"Dependency Auditor": "hands off: audit report, vulnerability triage, update recommendations",
|
|
385
|
+
"Repo Researcher": "hands off: repo map, entrypoints, build/test commands, seam inventory",
|
|
386
|
+
"Repo Translator": "hands off: translated files, verification notes, degenerate output flags",
|
|
387
|
+
"Docs Architect": "hands off: docs structure, handbook setup, navigation hierarchy",
|
|
388
|
+
"Metadata Curator": "hands off: updated manifests, badge status, registry alignment report",
|
|
389
|
+
"Coverage Auditor": "hands off: coverage report, false confidence inventory, missing defenses",
|
|
390
|
+
"Deployment Verifier": "hands off: verification checklist, live artifact status, broken links",
|
|
391
|
+
"Release Engineer": "hands off: versioned package, changelog, tag, publish confirmation",
|
|
392
|
+
"Launch Strategist": "hands off: launch plan, channel selection, success criteria, timing",
|
|
393
|
+
"Content Strategist": "hands off: content plan, article outlines, calendar",
|
|
394
|
+
"Community Manager": "hands off: triage report, response drafts, health assessment",
|
|
395
|
+
"Support Triage Lead": "hands off: classified tickets, priority assignments, recurring patterns",
|
|
396
|
+
"Launch Copywriter": "hands off: release notes, positioning copy, announcement drafts",
|
|
397
|
+
"Feedback Synthesizer": "hands off: signal clusters, themes, actionable insights",
|
|
398
|
+
"Roadmap Prioritizer": "hands off: sequenced backlog, dependency map, stop-doing list",
|
|
399
|
+
"Spec Writer": "hands off: execution-grade spec, acceptance criteria, edge cases, NFRs",
|
|
400
|
+
"UX Researcher": "hands off: friction inventory, heuristic findings, design input",
|
|
401
|
+
"Competitive Analyst": "hands off: landscape map, differentiation analysis, positioning gaps",
|
|
402
|
+
"Trend Researcher": "hands off: trend report, impact assessment, timing recommendations",
|
|
403
|
+
"User Interview Synthesizer": "hands off: theme report, mental models, unmet needs ranking",
|
|
404
|
+
"Critic Reviewer": "accepts, rejects, or blocks based on contract and evidence",
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// ── Main route command ────────────────────────────────────────────────────────
|
|
408
|
+
|
|
117
409
|
export async function routeCommand(args) {
|
|
118
|
-
const
|
|
410
|
+
const verbose = args.includes("--verbose");
|
|
411
|
+
const packFlag = args.find(a => a.startsWith("--pack="));
|
|
412
|
+
const requestedPack = packFlag ? packFlag.split("=")[1] : null;
|
|
413
|
+
const packetFile = args.find(a => !a.startsWith("--"));
|
|
119
414
|
|
|
120
415
|
if (!packetFile) {
|
|
121
416
|
const err = new Error("Usage: roleos route <packet-file>");
|
|
@@ -133,24 +428,110 @@ export async function routeCommand(args) {
|
|
|
133
428
|
}
|
|
134
429
|
|
|
135
430
|
const type = detectType(content);
|
|
136
|
-
const
|
|
137
|
-
|
|
431
|
+
const deliverableType = extractDeliverableType(content);
|
|
432
|
+
|
|
433
|
+
// Score all 32 roles
|
|
434
|
+
const allScored = ROLE_CATALOG.map(role => ({
|
|
435
|
+
role,
|
|
436
|
+
...scoreRole(role, content, type, deliverableType),
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
// Partition: always-include + above-threshold + considered + not-triggered
|
|
440
|
+
const alwaysInclude = allScored.filter(r => r.role.alwaysInclude);
|
|
441
|
+
const recommended = allScored.filter(r => !r.role.alwaysInclude && r.score >= MIN_SCORE_THRESHOLD);
|
|
442
|
+
const considered = allScored.filter(r => !r.role.alwaysInclude && r.score > 0 && r.score < MIN_SCORE_THRESHOLD);
|
|
443
|
+
const notTriggered = allScored.filter(r => !r.role.alwaysInclude && r.score === 0);
|
|
444
|
+
|
|
445
|
+
// Assemble chain from always-include + recommended
|
|
446
|
+
const chainRoles = assembleChain([...alwaysInclude, ...recommended]);
|
|
447
|
+
const confidence = assessConfidence(allScored);
|
|
138
448
|
const fileRefs = extractFileRefs(content, resolve(packetFile));
|
|
139
449
|
|
|
450
|
+
// Chain size warning
|
|
451
|
+
const chainWarning = chainRoles.length > 7
|
|
452
|
+
? "\n ⚠ Large chain (>7 roles). Consider splitting into sub-packets."
|
|
453
|
+
: "";
|
|
454
|
+
|
|
455
|
+
// ── Output ──
|
|
456
|
+
|
|
140
457
|
console.log(`\nroleos route — ${packetFile}\n`);
|
|
141
458
|
console.log(`Detected type: ${type}`);
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
|
|
459
|
+
if (deliverableType) console.log(`Deliverable type: ${deliverableType}`);
|
|
460
|
+
|
|
461
|
+
// ── Pack suggestion / selection ──
|
|
462
|
+
const packSuggestion = suggestPack(content);
|
|
463
|
+
if (requestedPack) {
|
|
464
|
+
const pack = getPack(requestedPack);
|
|
465
|
+
if (!pack) {
|
|
466
|
+
console.log(`\n⚠ Unknown pack: "${requestedPack}". Falling back to free routing.`);
|
|
467
|
+
} else {
|
|
468
|
+
const mismatch = checkPackMismatch(requestedPack, content);
|
|
469
|
+
if (mismatch) {
|
|
470
|
+
console.log(`\n⚠ Pack mismatch detected: ${mismatch.reason}`);
|
|
471
|
+
console.log(` → Suggested alternative: roleos route --pack=${mismatch.suggestInstead} ${packetFile}`);
|
|
472
|
+
console.log(` Falling back to free routing for this task.`);
|
|
473
|
+
} else {
|
|
474
|
+
const packRoles = getPackRoles(requestedPack);
|
|
475
|
+
console.log(`\nUsing pack: ${pack.name} (${packRoles.length} roles)`);
|
|
476
|
+
console.log(`Chain: ${pack.chainOrder}`);
|
|
477
|
+
console.log(`Roles: ${packRoles.join(" → ")}`);
|
|
478
|
+
console.log(`\nPack artifacts: ${pack.requiredArtifacts.join(", ")}`);
|
|
479
|
+
console.log(`Stop conditions:`);
|
|
480
|
+
for (const sc of pack.stopConditions) {
|
|
481
|
+
console.log(` • ${sc}`);
|
|
482
|
+
}
|
|
483
|
+
console.log(`\nNext: assign roles and begin execution.`);
|
|
484
|
+
return; // Pack selected — skip free routing output
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else if (packSuggestion && packSuggestion.confidence !== "low") {
|
|
488
|
+
console.log(`\nSuggested pack: ${packSuggestion.pack} (${packSuggestion.confidence} confidence)`);
|
|
489
|
+
console.log(` → Use: roleos route --pack=${packSuggestion.pack} ${packetFile}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log(`\nRouting confidence: ${confidence}`);
|
|
493
|
+
|
|
494
|
+
if (confidence === "low") {
|
|
495
|
+
console.log(` ↳ Few strong role signals detected. Consider reviewing the packet for missing context.`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
console.log(`\nRecommended chain (${chainRoles.length} roles):${chainWarning}`);
|
|
499
|
+
chainRoles.forEach((r, i) => {
|
|
500
|
+
const hint = HANDOFF_HINTS[r.role.name] || "";
|
|
501
|
+
const reason = r.role.alwaysInclude
|
|
502
|
+
? "always included"
|
|
503
|
+
: `${r.matched.join(", ")} (score: ${r.score})`;
|
|
504
|
+
console.log(` ${i + 1}. ${r.role.name}`);
|
|
505
|
+
console.log(` why: ${reason}`);
|
|
506
|
+
if (hint) console.log(` handoff: ${hint}`);
|
|
145
507
|
});
|
|
146
508
|
|
|
147
|
-
if (
|
|
148
|
-
console.log(`\
|
|
149
|
-
for (const
|
|
150
|
-
console.log(` ${role}: ${
|
|
509
|
+
if (considered.length > 0) {
|
|
510
|
+
console.log(`\nAlso considered (scored but below threshold of ${MIN_SCORE_THRESHOLD}):`);
|
|
511
|
+
for (const r of considered.sort((a, b) => b.score - a.score)) {
|
|
512
|
+
console.log(` - ${r.role.name}: ${r.matched.join(", ")} (score: ${r.score})`);
|
|
151
513
|
}
|
|
152
514
|
}
|
|
153
515
|
|
|
516
|
+
if (verbose) {
|
|
517
|
+
console.log(`\nNot triggered: ${notTriggered.length} roles with 0 keyword signals`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Conflict detection + escalation routing ──
|
|
521
|
+
const conflicts = detectConflicts(chainRoles);
|
|
522
|
+
if (conflicts.length > 0) {
|
|
523
|
+
console.log(`\nConflict detection (${conflicts.length} finding${conflicts.length > 1 ? "s" : ""}):`);
|
|
524
|
+
for (const f of conflicts) {
|
|
525
|
+
const icon = f.severity === "error" ? "✗" : "!";
|
|
526
|
+
console.log(` ${icon} [${f.type}] ${f.message}`);
|
|
527
|
+
console.log(` repair: ${f.repair}`);
|
|
528
|
+
const escalation = resolveConflict(f);
|
|
529
|
+
console.log(` escalation: ${escalation.targetRole} (${escalation.recovery}) → ${escalation.requiredArtifact}`);
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
console.log(`\nConflict detection: clean — no conflicts found.`);
|
|
533
|
+
}
|
|
534
|
+
|
|
154
535
|
if (fileRefs.length > 0) {
|
|
155
536
|
console.log(`\nDependency verification:`);
|
|
156
537
|
let hasIssues = false;
|
|
@@ -165,5 +546,19 @@ export async function routeCommand(args) {
|
|
|
165
546
|
}
|
|
166
547
|
}
|
|
167
548
|
|
|
549
|
+
// Stop conditions with escalation routing
|
|
550
|
+
console.log(`\nStop conditions (auto-routed):`);
|
|
551
|
+
console.log(` • blocked verdict → auto-routes based on block reason (missing info → Product Strategist, dependency → Orchestrator, etc.)`);
|
|
552
|
+
console.log(` • rejected verdict → routes back to producing role or Orchestrator based on rejection type`);
|
|
553
|
+
if (chainRoles.length > 7) {
|
|
554
|
+
const splitEsc = resolveSplit(chainRoles.length);
|
|
555
|
+
console.log(` • split needed:`);
|
|
556
|
+
console.log(formatEscalation(splitEsc));
|
|
557
|
+
}
|
|
558
|
+
|
|
168
559
|
console.log(`\nNext: assign roles and begin execution, or adjust the chain.`);
|
|
169
560
|
}
|
|
561
|
+
|
|
562
|
+
// ── Exports for testing ───────────────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
export { scoreRole, detectType, assembleChain, assessConfidence, extractFileRefs, extractDeliverableType, MIN_SCORE_THRESHOLD };
|