role-os 1.8.0 → 2.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/src/entry.mjs ADDED
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Unified Entry Path — Phase T (v1.9.0)
3
+ *
4
+ * When a user brings Role-OS a task, the system decides the best
5
+ * abstraction level automatically:
6
+ *
7
+ * 1. MISSION — when the task matches a proven recurring workflow
8
+ * 2. PACK — when the task is a known family but not a full mission shape
9
+ * 3. FREE ROUTING — when the task is novel, mixed, or uncertain
10
+ *
11
+ * The entry path is honest: it explains WHY it chose each level,
12
+ * and never forces work through the wrong abstraction.
13
+ */
14
+
15
+ import { suggestMission, getMission, MISSIONS } from "./mission.mjs";
16
+ import { suggestPack, getPack, checkPackMismatch, TEAM_PACKS } from "./packs.mjs";
17
+ import { detectComposite } from "./decompose.mjs";
18
+ import { ROLE_CATALOG } from "./route.mjs";
19
+
20
+ // ── Entry levels ────────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * @typedef {"mission"|"pack"|"free-routing"} EntryLevel
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} EntryDecision
28
+ * @property {EntryLevel} level - Which abstraction level was chosen
29
+ * @property {string} reason - Human-readable explanation
30
+ * @property {number} confidence - 0-1 confidence in this choice
31
+ * @property {object|null} mission - Mission details (when level=mission)
32
+ * @property {object|null} pack - Pack details (when level=pack)
33
+ * @property {object|null} freeRouting - Routing hints (when level=free-routing)
34
+ * @property {object|null} alternative - Next-best option if operator disagrees
35
+ * @property {boolean} isComposite - Whether the task looks like multiple jobs
36
+ * @property {string[]} warnings - Any concerns about the choice
37
+ */
38
+
39
+ // ── Confidence thresholds ───────────────────────────────────────────────────
40
+
41
+ const MISSION_HIGH_THRESHOLD = 0.7; // strong mission match → use mission
42
+ const MISSION_MEDIUM_THRESHOLD = 0.4; // decent match → mission if pack also agrees
43
+ const PACK_HIGH_THRESHOLD = 0.6; // strong pack match → use pack
44
+ const PACK_MEDIUM_THRESHOLD = 0.3; // decent match → use pack as fallback
45
+
46
+ // ── Core entry function ─────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Decide the best entry path for a task description.
50
+ *
51
+ * The fallback ladder:
52
+ * mission (when fit is strong)
53
+ * → pack (when mission fit is weak but task family is clear)
54
+ * → free routing (when the task is novel or mixed)
55
+ *
56
+ * @param {string} taskDescription
57
+ * @returns {EntryDecision}
58
+ */
59
+ export function decideEntry(taskDescription) {
60
+ if (!taskDescription || taskDescription.trim().length === 0) {
61
+ return {
62
+ level: "free-routing",
63
+ reason: "No task description provided — defaulting to free routing.",
64
+ confidence: 0,
65
+ mission: null,
66
+ pack: null,
67
+ freeRouting: { hint: "Provide a task description for better routing." },
68
+ alternative: null,
69
+ isComposite: false,
70
+ warnings: ["Empty task description"],
71
+ };
72
+ }
73
+
74
+ const text = taskDescription.trim();
75
+ const warnings = [];
76
+
77
+ // Step 1: Check for composite tasks (multi-job detection)
78
+ const composite = detectComposite(text);
79
+ const isComposite = composite.isComposite;
80
+ if (isComposite) {
81
+ warnings.push(
82
+ `Task looks composite (${composite.detectedCategories.map(c => c.category).join(" + ")}). ` +
83
+ `Consider decomposing before routing.`
84
+ );
85
+ }
86
+
87
+ // Step 2: Try mission suggestion
88
+ const missionSuggestion = suggestMission(text);
89
+ const missionScore = scoreMissionFit(missionSuggestion);
90
+
91
+ // Step 3: Try pack suggestion
92
+ const packSuggestion = suggestPack(text);
93
+ const packScore = scorePackFit(packSuggestion);
94
+
95
+ // Step 4: Check agreement (mission and pack point to the same family)
96
+ const agreement = checkAgreement(missionSuggestion, packSuggestion);
97
+
98
+ // Step 5: Apply the fallback ladder
99
+ return applyLadder(text, missionSuggestion, missionScore, packSuggestion, packScore, agreement, isComposite, composite, warnings);
100
+ }
101
+
102
+ // ── Scoring helpers ─────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Normalize mission suggestion into a 0-1 score.
106
+ */
107
+ function scoreMissionFit(suggestion) {
108
+ if (!suggestion) return 0;
109
+ switch (suggestion.confidence) {
110
+ case "high": return 0.9;
111
+ case "medium": return 0.55;
112
+ case "low": return 0.25;
113
+ default: return 0;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Normalize pack suggestion into a 0-1 score.
119
+ */
120
+ function scorePackFit(suggestion) {
121
+ if (!suggestion) return 0;
122
+ switch (suggestion.confidence) {
123
+ case "high": return 0.85;
124
+ case "medium": return 0.5;
125
+ case "low": return 0.2;
126
+ default: return 0;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Check if mission and pack suggestions agree (point to same family).
132
+ */
133
+ function checkAgreement(missionSuggestion, packSuggestion) {
134
+ if (!missionSuggestion || !packSuggestion) return false;
135
+ const mission = getMission(missionSuggestion.mission);
136
+ if (!mission) return false;
137
+ return mission.pack === packSuggestion.pack;
138
+ }
139
+
140
+ // ── Fallback ladder ─────────────────────────────────────────────────────────
141
+
142
+ function applyLadder(text, missionSug, missionScore, packSug, packScore, agreement, isComposite, composite, warnings) {
143
+ // ── MISSION: strong match, or medium match with pack agreement ──────────
144
+ if (missionScore >= MISSION_HIGH_THRESHOLD) {
145
+ const mission = getMission(missionSug.mission);
146
+ return {
147
+ level: "mission",
148
+ reason: `Strong mission match: "${mission.name}" (${missionSug.confidence} confidence, ${missionSug.reason}). ` +
149
+ `This is a proven recurring workflow with known role chain, artifact flow, and escalation branches.`,
150
+ confidence: missionScore,
151
+ mission: {
152
+ key: missionSug.mission,
153
+ name: mission.name,
154
+ pack: mission.pack,
155
+ roleChain: mission.roleChain,
156
+ entryPath: mission.entryPath,
157
+ stepCount: mission.artifactFlow.length,
158
+ },
159
+ pack: null,
160
+ freeRouting: null,
161
+ alternative: packSug ? {
162
+ level: "pack",
163
+ key: packSug.pack,
164
+ name: TEAM_PACKS[packSug.pack]?.name,
165
+ confidence: packScore,
166
+ } : null,
167
+ isComposite,
168
+ warnings,
169
+ };
170
+ }
171
+
172
+ if (missionScore >= MISSION_MEDIUM_THRESHOLD && agreement) {
173
+ const mission = getMission(missionSug.mission);
174
+ return {
175
+ level: "mission",
176
+ reason: `Mission match: "${mission.name}" (${missionSug.confidence} confidence). ` +
177
+ `Pack suggestion agrees (${packSug.pack}), reinforcing the mission choice.`,
178
+ confidence: Math.min(missionScore + 0.15, 0.85), // boost from agreement
179
+ mission: {
180
+ key: missionSug.mission,
181
+ name: mission.name,
182
+ pack: mission.pack,
183
+ roleChain: mission.roleChain,
184
+ entryPath: mission.entryPath,
185
+ stepCount: mission.artifactFlow.length,
186
+ },
187
+ pack: null,
188
+ freeRouting: null,
189
+ alternative: {
190
+ level: "pack",
191
+ key: packSug.pack,
192
+ name: TEAM_PACKS[packSug.pack]?.name,
193
+ confidence: packScore,
194
+ },
195
+ isComposite,
196
+ warnings,
197
+ };
198
+ }
199
+
200
+ // ── PACK: strong pack match but mission doesn't fit well ─────────────────
201
+ if (packScore >= PACK_HIGH_THRESHOLD) {
202
+ const pack = TEAM_PACKS[packSug.pack];
203
+ return {
204
+ level: "pack",
205
+ reason: `Task family match: "${pack.name}" pack (${packSug.confidence} confidence). ` +
206
+ (missionScore > 0
207
+ ? `Mission "${getMission(missionSug.mission)?.name}" was considered but fit was weak (${missionSug.confidence}).`
208
+ : `No mission matched — task family is clear but not a full recurring workflow.`),
209
+ confidence: packScore,
210
+ mission: null,
211
+ pack: {
212
+ key: packSug.pack,
213
+ name: pack.name,
214
+ roles: pack.roles,
215
+ chainOrder: pack.chainOrder,
216
+ description: pack.description,
217
+ },
218
+ freeRouting: null,
219
+ alternative: missionSug ? {
220
+ level: "mission",
221
+ key: missionSug.mission,
222
+ name: getMission(missionSug.mission)?.name,
223
+ confidence: missionScore,
224
+ } : null,
225
+ isComposite,
226
+ warnings,
227
+ };
228
+ }
229
+
230
+ if (packScore >= PACK_MEDIUM_THRESHOLD) {
231
+ const pack = TEAM_PACKS[packSug.pack];
232
+ return {
233
+ level: "pack",
234
+ reason: `Weak task family match: "${pack.name}" pack (${packSug.confidence} confidence). ` +
235
+ `Using pack as starting point — operator may want to adjust.`,
236
+ confidence: packScore,
237
+ mission: null,
238
+ pack: {
239
+ key: packSug.pack,
240
+ name: pack.name,
241
+ roles: pack.roles,
242
+ chainOrder: pack.chainOrder,
243
+ description: pack.description,
244
+ },
245
+ freeRouting: null,
246
+ alternative: {
247
+ level: "free-routing",
248
+ confidence: 0,
249
+ },
250
+ isComposite,
251
+ warnings: [...warnings, "Low pack confidence — consider free routing if this doesn't fit"],
252
+ };
253
+ }
254
+
255
+ // ── FREE ROUTING: novel, mixed, or uncertain ─────────────────────────────
256
+ const freeHints = buildFreeRoutingHints(text, missionSug, packSug, isComposite, composite);
257
+
258
+ return {
259
+ level: "free-routing",
260
+ reason: freeHints.reason,
261
+ confidence: 0.1,
262
+ mission: null,
263
+ pack: null,
264
+ freeRouting: freeHints,
265
+ alternative: missionSug ? {
266
+ level: "mission",
267
+ key: missionSug.mission,
268
+ name: getMission(missionSug.mission)?.name,
269
+ confidence: missionScore,
270
+ } : packSug ? {
271
+ level: "pack",
272
+ key: packSug.pack,
273
+ name: TEAM_PACKS[packSug.pack]?.name,
274
+ confidence: packScore,
275
+ } : null,
276
+ isComposite,
277
+ warnings: [...warnings, "Free routing selected — task will be scored against all 31 roles"],
278
+ };
279
+ }
280
+
281
+ function buildFreeRoutingHints(text, missionSug, packSug, isComposite, composite) {
282
+ let reason;
283
+ if (isComposite) {
284
+ reason = `Task looks composite (${composite.detectedCategories.map(c => c.category).join(" + ")}). ` +
285
+ `No single mission or pack covers all parts — use free routing with decomposition.`;
286
+ } else if (!missionSug && !packSug) {
287
+ reason = "No mission or pack matched. Task is novel — free routing will score all 31 roles.";
288
+ } else {
289
+ reason = "Mission and pack matches were too weak to commit. Free routing will let role scoring decide.";
290
+ }
291
+
292
+ return {
293
+ reason,
294
+ hint: isComposite
295
+ ? "Consider running `roleos route` on a packet for each sub-task."
296
+ : "Create a packet with `roleos packet new` and run `roleos route` for role-level routing.",
297
+ suggestedRoleCount: isComposite ? composite.detectedCategories.length * 3 : null,
298
+ };
299
+ }
300
+
301
+ // ── Format for display ──────────────────────────────────────────────────────
302
+
303
+ /**
304
+ * Format an entry decision as human-readable text.
305
+ * @param {EntryDecision} decision
306
+ * @returns {string}
307
+ */
308
+ export function formatEntryDecision(decision) {
309
+ const lines = [];
310
+ const conf = Math.round(decision.confidence * 100);
311
+
312
+ lines.push(`Entry Decision: ${decision.level.toUpperCase()} (${conf}% confidence)`);
313
+ lines.push("");
314
+ lines.push(`Reason: ${decision.reason}`);
315
+
316
+ if (decision.level === "mission" && decision.mission) {
317
+ lines.push("");
318
+ lines.push(`Mission: ${decision.mission.name} (${decision.mission.key})`);
319
+ lines.push(`Pack: ${decision.mission.pack}`);
320
+ lines.push(`Entry: ${decision.mission.entryPath}`);
321
+ lines.push(`Chain: ${decision.mission.roleChain.join(" → ")}`);
322
+ lines.push(`Steps: ${decision.mission.stepCount}`);
323
+ }
324
+
325
+ if (decision.level === "pack" && decision.pack) {
326
+ lines.push("");
327
+ lines.push(`Pack: ${decision.pack.name} (${decision.pack.key})`);
328
+ lines.push(`Roles: ${decision.pack.roles.join(", ")}`);
329
+ lines.push(`Chain: ${decision.pack.chainOrder}`);
330
+ }
331
+
332
+ if (decision.level === "free-routing" && decision.freeRouting) {
333
+ lines.push("");
334
+ lines.push(`Hint: ${decision.freeRouting.hint}`);
335
+ }
336
+
337
+ if (decision.alternative) {
338
+ lines.push("");
339
+ const altConf = Math.round((decision.alternative.confidence || 0) * 100);
340
+ lines.push(`Alternative: ${decision.alternative.level}${decision.alternative.name ? ` (${decision.alternative.name})` : ""} at ${altConf}% confidence`);
341
+ }
342
+
343
+ if (decision.isComposite) {
344
+ lines.push("");
345
+ lines.push(`Note: This task looks composite — consider decomposing before proceeding.`);
346
+ }
347
+
348
+ if (decision.warnings.length > 0) {
349
+ lines.push("");
350
+ lines.push("Warnings:");
351
+ for (const w of decision.warnings) {
352
+ lines.push(` - ${w}`);
353
+ }
354
+ }
355
+
356
+ return lines.join("\n");
357
+ }