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/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
- 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
- };
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
- 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
- ],
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
- function detectType(content) {
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 && CHAINS[typeMatch[1]]) {
304
+ if (typeMatch && ["feature", "integration", "identity"].includes(typeMatch[1])) {
52
305
  return typeMatch[1];
53
306
  }
54
307
 
55
- if (lower.includes("contamination") || lower.includes("residue") || lower.includes("identity") || lower.includes("purge")) {
308
+ const lower = content.toLowerCase();
309
+ if (lower.includes("contamination") || lower.includes("residue") || lower.includes("purge")) {
56
310
  return "identity";
57
311
  }
58
- if (lower.includes("wiring") || lower.includes("bridge") || lower.includes("integration") || lower.includes("seam")) {
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
- 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
- }
319
+ // ── Deliverable type extraction ───────────────────────────────────────────────
75
320
 
76
- return scores;
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
- 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;
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 packetFile = args[0];
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 scores = scoreRoles(content);
137
- const chain = recommendChain(type, scores);
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(`\nRecommended chain (${chain.length} roles):`);
143
- chain.forEach((role, i) => {
144
- console.log(` ${i + 1}. ${role}`);
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 (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" : ""}`);
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 };