role-os 1.6.0 → 1.8.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 CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Added
6
+
7
+ #### Mission Library (Phase S — Mission Hardening)
8
+ - 6 named, repeatable mission types: feature-ship, bugfix, treatment, docs-release, security-hardening, research-launch
9
+ - Each mission declares: pack, role chain, artifact flow, escalation branches, honest-partial definition, stop conditions, dispatch defaults, trial evidence
10
+ - Mission runner: create → step through → complete/fail → generate completion report
11
+ - Completion proof reporter with honest-partial and formatted text output
12
+ - `roleos mission list` — list all missions
13
+ - `roleos mission show <key>` — full mission detail
14
+ - `roleos mission suggest <text>` — signal-based mission suggestion
15
+ - `roleos mission validate [key]` — validate mission wiring against packs/roles
16
+
17
+ #### Mission Runner Engine
18
+ - `createRun()` — instantiate a mission with tracked steps
19
+ - `startNextStep()` / `completeStep()` / `failStep()` — step lifecycle
20
+ - `recordEscalation()` — re-opens completed steps on escalation loops
21
+ - `getRunPosition()` / `getArtifactChain()` — run introspection
22
+ - `generateCompletionReport()` / `formatCompletionReport()` — honest outcome reporting
23
+
24
+ ### Evidence
25
+ - 465 tests, zero failures (67 new)
26
+ - All 6 missions validate against live pack/role catalog
27
+ - Full lifecycle tests: end-to-end runs, escalation loops, partial completions, failure reporting
28
+
29
+ ## 1.7.0
30
+
31
+ ### Added
32
+
33
+ #### Completion Proof (Phase R)
34
+ - `roleos artifacts` CLI command: list, show, validate, chain subcommands
35
+ - 13 new CLI integration tests for artifact inspection
36
+ - Real task completion missions through the full stack
37
+
38
+ #### Completion Proof Evidence
39
+ - R1-1 Feature mission: `roleos artifacts` command shipped through feature pack
40
+ - Pack: feature (high confidence, correct)
41
+ - Chain: 5 roles, 0 escalations, 1 minor correction
42
+ - Artifact contracts: all 4 used and valid
43
+ - R1-2 Bugfix mission: README.zh.md npm anomaly
44
+ - Diagnosed correctly: npm auto-includes README* regardless of files field
45
+ - Escalated honestly: fix requires structural decision (translation file organization)
46
+ - Not force-closed: deferred to treatment pass
47
+
48
+ ### Evidence
49
+ - 398 tests, zero failures
50
+ - 3 missions run through the full stack
51
+ - Completion metrics recorded per mission
52
+
3
53
  ## 1.6.0
4
54
 
5
55
  ### Added
package/README.md CHANGED
@@ -192,7 +192,8 @@ Role OS operates **locally only**. It copies markdown templates and writes packe
192
192
  - v1.3.0: Outcome calibration, mixed-task decomposition, composite execution, adaptive replanning. 317 tests.
193
193
  - v1.4.0: Session spine — `roleos init claude`, `roleos doctor`, route cards, /roleos-route + /roleos-review + /roleos-status commands. 335 tests.
194
194
  - v1.5.0: Hook spine — 5 lifecycle hooks for runtime enforcement. 358 tests.
195
- - **v1.6.0**: Artifact spine — 20 per-role artifact contracts, 7 pack handoff contracts, structural validation, chain completeness checks. 385 tests.
195
+ - v1.6.0: Artifact spine — 20 per-role artifact contracts, 7 pack handoff contracts, structural validation. 385 tests.
196
+ - **v1.7.0**: Completion proof — real tasks run through the full stack. `roleos artifacts` CLI. Honest escalation on structural fixes. 398 tests.
196
197
 
197
198
  ## License
198
199
 
package/bin/roleos.mjs CHANGED
@@ -10,6 +10,8 @@ import { reviewCommand } from "../src/review.mjs";
10
10
  import { statusCommand } from "../src/status.mjs";
11
11
  import { packsCommand } from "../src/packs-cmd.mjs";
12
12
  import { scaffoldClaude, doctor, formatDoctor } from "../src/session.mjs";
13
+ import { artifactsCommand } from "../src/artifacts-cmd.mjs";
14
+ import { missionCommand } from "../src/mission-cmd.mjs";
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
17
  const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
@@ -30,6 +32,14 @@ Usage:
30
32
  roleos packs list List all available team packs
31
33
  roleos packs suggest <packet-file> Suggest a pack for a packet
32
34
  roleos packs show <pack-key> Show full detail for a named pack
35
+ roleos artifacts List all role artifact contracts
36
+ roleos artifacts show <role> Show artifact contract for a role
37
+ roleos artifacts validate <role> <file> Validate a file against a contract
38
+ roleos artifacts chain <pack> Show pack handoff flow
39
+ roleos mission list List all missions
40
+ roleos mission show <key> Show full mission detail
41
+ roleos mission suggest <text> Suggest a mission for a task
42
+ roleos mission validate [key] Validate mission wiring
33
43
  roleos doctor Verify repo is wired for Role OS sessions
34
44
  roleos help Show this help
35
45
 
@@ -103,6 +113,12 @@ try {
103
113
  case "packs":
104
114
  await packsCommand(args);
105
115
  break;
116
+ case "artifacts":
117
+ await artifactsCommand(args);
118
+ break;
119
+ case "mission":
120
+ await missionCommand(args);
121
+ break;
106
122
  case "help":
107
123
  case "--help":
108
124
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "role-os",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Role OS — a multi-Claude operating system where 31 specialized roles execute work through contracts, conflict detection, escalation, and structured evidence. 7 proven team packs for common task families.",
5
5
  "homepage": "https://mcp-tool-shop-org.github.io/role-os/",
6
6
  "bugs": {
@@ -0,0 +1,180 @@
1
+ /**
2
+ * CLI surface for artifact contracts — roleos artifacts
3
+ *
4
+ * Subcommands:
5
+ * - artifacts (bare) — list all roles with contracts
6
+ * - artifacts show <role> — display artifact contract
7
+ * - artifacts validate <role> <file> — validate file against contract
8
+ * - artifacts chain <pack> — show pack handoff flow
9
+ */
10
+
11
+ import { readFileSync, existsSync } from "node:fs";
12
+ import {
13
+ ROLE_ARTIFACT_CONTRACTS,
14
+ PACK_HANDOFF_CONTRACTS,
15
+ validateArtifact,
16
+ getArtifactContract,
17
+ getHandoffContract,
18
+ formatArtifactValidation,
19
+ formatPackChain,
20
+ validatePackChain,
21
+ } from "./artifacts.mjs";
22
+
23
+ /**
24
+ * Main command handler.
25
+ * @param {string[]} args
26
+ */
27
+ export async function artifactsCommand(args) {
28
+ const sub = args[0];
29
+
30
+ if (!sub || sub === "list") {
31
+ runList();
32
+ } else if (sub === "show") {
33
+ runShow(args.slice(1));
34
+ } else if (sub === "validate") {
35
+ runValidate(args.slice(1));
36
+ } else if (sub === "chain") {
37
+ runChain(args.slice(1));
38
+ } else {
39
+ const err = new Error(`Unknown artifacts subcommand: "${sub}"`);
40
+ err.exitCode = 1;
41
+ err.hint = "Valid subcommands: list, show <role>, validate <role> <file>, chain <pack>";
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ function runList() {
47
+ const roles = Object.entries(ROLE_ARTIFACT_CONTRACTS);
48
+ console.log(`\nRole artifact contracts (${roles.length} roles)\n`);
49
+ console.log(`${"Role".padEnd(25)} ${"Artifact Type".padEnd(22)} ${"Required".padEnd(8)} Consumed By`);
50
+ console.log(`${"─".repeat(25)} ${"─".repeat(22)} ${"─".repeat(8)} ${"─".repeat(30)}`);
51
+
52
+ for (const [role, contract] of roles) {
53
+ const consumers = contract.consumedBy.length > 0 ? contract.consumedBy.join(", ") : "terminal";
54
+ console.log(
55
+ `${role.padEnd(25)} ${contract.artifactType.padEnd(22)} ${String(contract.requiredSections.length).padEnd(8)} ${consumers}`
56
+ );
57
+ }
58
+ }
59
+
60
+ function runShow(args) {
61
+ if (args.length === 0) {
62
+ const err = new Error("Missing role name.");
63
+ err.exitCode = 1;
64
+ err.hint = "Usage: roleos artifacts show <role-name>";
65
+ throw err;
66
+ }
67
+
68
+ const input = args.join(" ");
69
+ const roleName = resolveRoleName(input);
70
+ if (!roleName) {
71
+ const err = new Error(`No artifact contract for role "${input}".`);
72
+ err.exitCode = 1;
73
+ err.hint = `Available roles: ${Object.keys(ROLE_ARTIFACT_CONTRACTS).join(", ")}`;
74
+ throw err;
75
+ }
76
+
77
+ const contract = getArtifactContract(roleName);
78
+
79
+ console.log(`\nArtifact Contract — ${roleName}\n`);
80
+ console.log(`Type: ${contract.artifactType}`);
81
+ console.log(`Required sections: ${contract.requiredSections.join(", ")}`);
82
+ if (contract.optionalSections.length > 0) {
83
+ console.log(`Optional sections: ${contract.optionalSections.join(", ")}`);
84
+ }
85
+ if (contract.requiredEvidence.length > 0) {
86
+ console.log(`Required evidence: ${contract.requiredEvidence.join(", ")}`);
87
+ }
88
+ console.log(`Consumed by: ${contract.consumedBy.length > 0 ? contract.consumedBy.join(", ") : "terminal (no downstream consumer)"}`);
89
+ console.log(`Completion rule: ${contract.completionRule}`);
90
+ }
91
+
92
+ function runValidate(args) {
93
+ if (args.length < 2) {
94
+ const err = new Error("Missing arguments.");
95
+ err.exitCode = 1;
96
+ err.hint = "Usage: roleos artifacts validate <role-name> <file-path>";
97
+ throw err;
98
+ }
99
+
100
+ // Last arg is the file, everything before is the role name
101
+ const filePath = args[args.length - 1];
102
+ const roleArgs = args.slice(0, -1);
103
+ const roleName = resolveRoleName(roleArgs.join(" "));
104
+
105
+ if (!roleName) {
106
+ const err = new Error(`Unknown role: "${roleArgs.join(" ")}".`);
107
+ err.exitCode = 1;
108
+ err.hint = `Available roles: ${Object.keys(ROLE_ARTIFACT_CONTRACTS).join(", ")}`;
109
+ throw err;
110
+ }
111
+
112
+ if (!existsSync(filePath)) {
113
+ const err = new Error(`File not found: "${filePath}".`);
114
+ err.exitCode = 1;
115
+ err.hint = "Provide a path to the artifact file to validate.";
116
+ throw err;
117
+ }
118
+
119
+ const content = readFileSync(filePath, "utf-8");
120
+ const validation = validateArtifact(roleName, content);
121
+ console.log(formatArtifactValidation(roleName, validation));
122
+
123
+ if (!validation.valid) {
124
+ process.exitCode = 1;
125
+ }
126
+ }
127
+
128
+ function runChain(args) {
129
+ const packName = args[0];
130
+ if (!packName) {
131
+ const err = new Error("Missing pack name.");
132
+ err.exitCode = 1;
133
+ err.hint = `Usage: roleos artifacts chain <pack-name>. Available: ${Object.keys(PACK_HANDOFF_CONTRACTS).join(", ")}`;
134
+ throw err;
135
+ }
136
+
137
+ const contract = getHandoffContract(packName);
138
+ if (!contract) {
139
+ const err = new Error(`No handoff contract for pack "${packName}".`);
140
+ err.exitCode = 1;
141
+ err.hint = `Available packs: ${Object.keys(PACK_HANDOFF_CONTRACTS).join(", ")}`;
142
+ throw err;
143
+ }
144
+
145
+ console.log(`\nHandoff Contract — ${packName} pack\n`);
146
+ console.log(`${"Step".padEnd(5)} ${"Role".padEnd(25)} ${"Produces".padEnd(22)} Consumed By`);
147
+ console.log(`${"─".repeat(5)} ${"─".repeat(25)} ${"─".repeat(22)} ${"─".repeat(25)}`);
148
+
149
+ contract.flow.forEach((step, i) => {
150
+ const consumer = step.consumedBy || "terminal";
151
+ console.log(
152
+ `${String(i + 1).padEnd(5)} ${step.role.padEnd(25)} ${step.produces.padEnd(22)} ${consumer}`
153
+ );
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Resolve a role name from user input.
159
+ * Handles both exact names ("Product Strategist") and slug forms ("product-strategist").
160
+ */
161
+ function resolveRoleName(input) {
162
+ if (!input || !input.trim()) return null;
163
+ const trimmed = input.trim();
164
+
165
+ // Exact match
166
+ if (ROLE_ARTIFACT_CONTRACTS[trimmed]) return trimmed;
167
+
168
+ // Case-insensitive match
169
+ for (const role of Object.keys(ROLE_ARTIFACT_CONTRACTS)) {
170
+ if (role.toLowerCase() === trimmed.toLowerCase()) return role;
171
+ }
172
+
173
+ // Slug match (product-strategist → Product Strategist)
174
+ const slugToName = trimmed.replace(/-/g, " ");
175
+ for (const role of Object.keys(ROLE_ARTIFACT_CONTRACTS)) {
176
+ if (role.toLowerCase() === slugToName.toLowerCase()) return role;
177
+ }
178
+
179
+ return null;
180
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Mission CLI — Phase S (v1.8.0)
3
+ *
4
+ * roleos mission list List all missions
5
+ * roleos mission show <key> Show full mission detail
6
+ * roleos mission suggest <text> Suggest a mission for a task
7
+ * roleos mission validate Validate all missions are properly wired
8
+ * roleos mission validate <key> Validate a specific mission
9
+ */
10
+
11
+ import {
12
+ listMissions,
13
+ getMission,
14
+ suggestMission,
15
+ validateMission,
16
+ validateAllMissions,
17
+ MISSIONS,
18
+ } from "./mission.mjs";
19
+
20
+ /**
21
+ * @param {string[]} args
22
+ */
23
+ export async function missionCommand(args) {
24
+ const sub = args[0];
25
+
26
+ switch (sub) {
27
+ case "list":
28
+ return cmdList();
29
+ case "show":
30
+ return cmdShow(args[1]);
31
+ case "suggest":
32
+ return cmdSuggest(args.slice(1).join(" "));
33
+ case "validate":
34
+ return cmdValidate(args[1]);
35
+ default: {
36
+ const err = new Error(
37
+ `Usage: roleos mission <list|show|suggest|validate>\n` +
38
+ ` list List all missions\n` +
39
+ ` show <key> Show full mission detail\n` +
40
+ ` suggest <text> Suggest a mission for a task description\n` +
41
+ ` validate [key] Validate mission wiring`
42
+ );
43
+ err.exitCode = 1;
44
+ err.hint = "Run 'roleos mission list' to see available missions.";
45
+ throw err;
46
+ }
47
+ }
48
+ }
49
+
50
+ function cmdList() {
51
+ const missions = listMissions();
52
+ console.log("\nMission Library\n");
53
+ console.log(" Key Pack Roles Description");
54
+ console.log(" " + "-".repeat(78));
55
+ for (const m of missions) {
56
+ const key = m.key.padEnd(20);
57
+ const pack = m.pack.padEnd(10);
58
+ const roles = String(m.roleCount).padEnd(6);
59
+ console.log(` ${key} ${pack} ${roles} ${m.description}`);
60
+ }
61
+ console.log(`\n ${missions.length} missions. Run 'roleos mission show <key>' for details.\n`);
62
+ }
63
+
64
+ function cmdShow(key) {
65
+ if (!key) {
66
+ const err = new Error("Usage: roleos mission show <key>");
67
+ err.exitCode = 1;
68
+ err.hint = `Available: ${Object.keys(MISSIONS).join(", ")}`;
69
+ throw err;
70
+ }
71
+
72
+ const mission = getMission(key);
73
+ if (!mission) {
74
+ const err = new Error(`Mission "${key}" not found`);
75
+ err.exitCode = 1;
76
+ err.hint = `Available: ${Object.keys(MISSIONS).join(", ")}`;
77
+ throw err;
78
+ }
79
+
80
+ console.log(`\n# ${mission.name}`);
81
+ console.log(`\n${mission.description}`);
82
+ console.log(`\nPack: ${mission.pack}`);
83
+ console.log(`Entry: ${mission.entryPath}`);
84
+
85
+ console.log("\n## Role Chain");
86
+ for (let i = 0; i < mission.roleChain.length; i++) {
87
+ const arrow = i < mission.roleChain.length - 1 ? " →" : "";
88
+ console.log(` ${i + 1}. ${mission.roleChain[i]}${arrow}`);
89
+ }
90
+
91
+ console.log("\n## Artifact Flow");
92
+ for (const step of mission.artifactFlow) {
93
+ const to = step.consumedBy ? ` → ${step.consumedBy}` : " (terminal)";
94
+ console.log(` ${step.role} produces "${step.produces}"${to}`);
95
+ }
96
+
97
+ console.log("\n## Escalation Branches");
98
+ for (const branch of mission.escalationBranches) {
99
+ console.log(` [${branch.trigger}] ${branch.from} → ${branch.to}: ${branch.action}`);
100
+ }
101
+
102
+ console.log("\n## Honest Partial");
103
+ console.log(` ${mission.honestPartial}`);
104
+
105
+ console.log("\n## Stop Conditions");
106
+ for (const cond of mission.stopConditions) {
107
+ console.log(` - ${cond}`);
108
+ }
109
+
110
+ console.log(`\n## Dispatch Defaults`);
111
+ const d = mission.dispatchDefaults;
112
+ console.log(` Model: ${d.model} | Max turns: ${d.maxTurns} | Budget: $${d.maxBudgetUsd}`);
113
+
114
+ if (mission.trialEvidence) {
115
+ console.log(`\n## Trial Evidence`);
116
+ console.log(` ${mission.trialEvidence}`);
117
+ }
118
+
119
+ console.log("");
120
+ }
121
+
122
+ function cmdSuggest(text) {
123
+ if (!text) {
124
+ const err = new Error("Usage: roleos mission suggest <task description>");
125
+ err.exitCode = 1;
126
+ err.hint = "Describe what you want to do and I'll suggest a mission.";
127
+ throw err;
128
+ }
129
+
130
+ const suggestion = suggestMission(text);
131
+ if (!suggestion) {
132
+ console.log("\nNo mission matched. Task may need manual routing.\n");
133
+ console.log("Available missions:");
134
+ for (const key of Object.keys(MISSIONS)) {
135
+ console.log(` - ${key}: ${MISSIONS[key].description}`);
136
+ }
137
+ console.log("");
138
+ return;
139
+ }
140
+
141
+ const mission = getMission(suggestion.mission);
142
+ console.log(`\nSuggested mission: ${suggestion.mission}`);
143
+ console.log(`Name: ${mission.name}`);
144
+ console.log(`Confidence: ${suggestion.confidence}`);
145
+ console.log(`Reason: ${suggestion.reason}`);
146
+ console.log(`Pack: ${mission.pack}`);
147
+ console.log(`Chain: ${mission.roleChain.join(" → ")}`);
148
+ console.log(`\nRun 'roleos mission show ${suggestion.mission}' for full details.\n`);
149
+ }
150
+
151
+ function cmdValidate(key) {
152
+ if (key) {
153
+ const result = validateMission(key);
154
+ if (result.valid) {
155
+ console.log(`\n[PASS] Mission "${key}" is properly wired.\n`);
156
+ } else {
157
+ console.log(`\n[FAIL] Mission "${key}" has issues:`);
158
+ for (const issue of result.issues) {
159
+ console.log(` - ${issue}`);
160
+ }
161
+ console.log("");
162
+ process.exit(1);
163
+ }
164
+ } else {
165
+ const { allValid, results } = validateAllMissions();
166
+ console.log("\nMission Validation\n");
167
+ for (const [k, r] of Object.entries(results)) {
168
+ const icon = r.valid ? "[PASS]" : "[FAIL]";
169
+ console.log(` ${icon} ${k}`);
170
+ if (!r.valid) {
171
+ for (const issue of r.issues) {
172
+ console.log(` - ${issue}`);
173
+ }
174
+ }
175
+ }
176
+ console.log(`\n ${Object.keys(results).length} missions checked. ${allValid ? "All valid." : "Some issues found."}\n`);
177
+ if (!allValid) process.exit(1);
178
+ }
179
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Mission Runner — Phase S (v1.8.0)
3
+ *
4
+ * Executes a mission through the full Role OS stack:
5
+ * mission → packet → route → pack → artifacts → review → completion report
6
+ *
7
+ * The runner does NOT execute code — it orchestrates the contract chain
8
+ * and tracks state so that operators (human or agent) know exactly
9
+ * where they are, what's been produced, and what's next.
10
+ */
11
+
12
+ import { MISSIONS, getMission, validateMission } from "./mission.mjs";
13
+ import { TEAM_PACKS } from "./packs.mjs";
14
+ import { validateArtifact, ROLE_ARTIFACT_CONTRACTS } from "./artifacts.mjs";
15
+
16
+ let _runCounter = 0;
17
+
18
+ // ── Run states ──────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * @typedef {"pending"|"active"|"completed"|"partial"|"failed"|"escalated"} StepStatus
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} MissionStep
26
+ * @property {string} role
27
+ * @property {string} produces - artifact type
28
+ * @property {string|null} consumedBy - next role or null
29
+ * @property {StepStatus} status
30
+ * @property {string|null} artifact - produced artifact content/reference
31
+ * @property {string|null} note - operator note
32
+ * @property {string|null} startedAt
33
+ * @property {string|null} completedAt
34
+ */
35
+
36
+ /**
37
+ * @typedef {"planning"|"running"|"completed"|"partial"|"failed"} RunStatus
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} MissionRun
42
+ * @property {string} id
43
+ * @property {string} missionKey
44
+ * @property {string} taskDescription
45
+ * @property {RunStatus} status
46
+ * @property {MissionStep[]} steps
47
+ * @property {Array<{from: string, to: string, trigger: string, action: string, timestamp: string}>} escalations
48
+ * @property {string} startedAt
49
+ * @property {string|null} completedAt
50
+ * @property {object|null} completionReport
51
+ */
52
+
53
+ // ── Create a run ────────────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Create a new mission run from a mission key and task description.
57
+ *
58
+ * @param {string} missionKey
59
+ * @param {string} taskDescription
60
+ * @returns {MissionRun}
61
+ */
62
+ export function createRun(missionKey, taskDescription) {
63
+ const mission = getMission(missionKey);
64
+ if (!mission) {
65
+ throw new Error(`Mission "${missionKey}" not found. Available: ${Object.keys(MISSIONS).join(", ")}`);
66
+ }
67
+
68
+ const validation = validateMission(missionKey);
69
+ if (!validation.valid) {
70
+ throw new Error(`Mission "${missionKey}" has issues: ${validation.issues.join("; ")}`);
71
+ }
72
+
73
+ const id = `${missionKey}-${Date.now()}-${++_runCounter}`;
74
+
75
+ const steps = mission.artifactFlow.map((step) => ({
76
+ role: step.role,
77
+ produces: step.produces,
78
+ consumedBy: step.consumedBy,
79
+ status: "pending",
80
+ artifact: null,
81
+ note: null,
82
+ startedAt: null,
83
+ completedAt: null,
84
+ }));
85
+
86
+ return {
87
+ id,
88
+ missionKey,
89
+ taskDescription,
90
+ status: "planning",
91
+ steps,
92
+ escalations: [],
93
+ startedAt: new Date().toISOString(),
94
+ completedAt: null,
95
+ completionReport: null,
96
+ };
97
+ }
98
+
99
+ // ── Step through a run ──────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Start the next pending step.
103
+ * @param {MissionRun} run
104
+ * @returns {MissionStep|null} The started step, or null if no pending steps
105
+ */
106
+ export function startNextStep(run) {
107
+ const next = run.steps.find((s) => s.status === "pending");
108
+ if (!next) return null;
109
+
110
+ next.status = "active";
111
+ next.startedAt = new Date().toISOString();
112
+ run.status = "running";
113
+
114
+ return next;
115
+ }
116
+
117
+ /**
118
+ * Complete the current active step with an artifact.
119
+ * @param {MissionRun} run
120
+ * @param {string} artifact - The produced artifact (content or reference)
121
+ * @param {string} [note] - Optional operator note
122
+ * @returns {MissionStep}
123
+ */
124
+ export function completeStep(run, artifact, note) {
125
+ const active = run.steps.find((s) => s.status === "active");
126
+ if (!active) {
127
+ throw new Error("No active step to complete");
128
+ }
129
+
130
+ active.status = "completed";
131
+ active.artifact = artifact;
132
+ active.note = note || null;
133
+ active.completedAt = new Date().toISOString();
134
+
135
+ // Check if all steps are done
136
+ const allDone = run.steps.every((s) => s.status === "completed");
137
+ if (allDone) {
138
+ run.status = "completed";
139
+ run.completedAt = new Date().toISOString();
140
+ }
141
+
142
+ return active;
143
+ }
144
+
145
+ /**
146
+ * Mark the current active step as failed/partial.
147
+ * @param {MissionRun} run
148
+ * @param {"partial"|"failed"} status
149
+ * @param {string} reason
150
+ * @returns {MissionStep}
151
+ */
152
+ export function failStep(run, status, reason) {
153
+ if (status !== "partial" && status !== "failed") {
154
+ throw new Error(`Invalid fail status: "${status}". Use "partial" or "failed"`);
155
+ }
156
+
157
+ const active = run.steps.find((s) => s.status === "active");
158
+ if (!active) {
159
+ throw new Error("No active step to fail");
160
+ }
161
+
162
+ active.status = status;
163
+ active.note = reason;
164
+ active.completedAt = new Date().toISOString();
165
+
166
+ // Block all downstream pending steps (S2-F2)
167
+ let foundActive = false;
168
+ for (const step of run.steps) {
169
+ if (step === active) { foundActive = true; continue; }
170
+ if (foundActive && step.status === "pending") {
171
+ step.status = "blocked";
172
+ step.note = `Blocked: upstream ${active.role} ${status}`;
173
+ }
174
+ }
175
+
176
+ // Mission status follows the worst step
177
+ run.status = status;
178
+ run.completedAt = new Date().toISOString();
179
+
180
+ return active;
181
+ }
182
+
183
+ /**
184
+ * Record an escalation during a run.
185
+ * @param {MissionRun} run
186
+ * @param {string} from - Role escalating
187
+ * @param {string} to - Role receiving
188
+ * @param {string} trigger - What triggered it
189
+ * @param {string} action - What needs to happen
190
+ * @returns {{from: string, to: string, trigger: string, action: string, timestamp: string, reopened: boolean, warning: string|null}}
191
+ */
192
+ export function recordEscalation(run, from, to, trigger, action) {
193
+ const escalation = {
194
+ from,
195
+ to,
196
+ trigger,
197
+ action,
198
+ timestamp: new Date().toISOString(),
199
+ reopened: false,
200
+ warning: null,
201
+ };
202
+ run.escalations.push(escalation);
203
+
204
+ // S5-F1: Find the LAST matching step for the target role, not the first.
205
+ // Security-hardening has Security Reviewer twice — escalation should target
206
+ // the latest occurrence (re-audit), not the initial audit.
207
+ let targetStep = null;
208
+ for (let i = run.steps.length - 1; i >= 0; i--) {
209
+ if (run.steps[i].role === to && run.steps[i].status === "completed") {
210
+ targetStep = run.steps[i];
211
+ break;
212
+ }
213
+ }
214
+
215
+ if (targetStep) {
216
+ targetStep.status = "pending";
217
+ targetStep.artifact = null;
218
+ targetStep.note = `Re-opened by escalation: ${trigger}`;
219
+ targetStep.completedAt = null;
220
+ escalation.reopened = true;
221
+ } else {
222
+ // S4-F2: Target role not found in chain (or no completed step to re-open).
223
+ // Warn the operator instead of silently doing nothing.
224
+ const inChain = run.steps.some((s) => s.role === to);
225
+ if (!inChain) {
226
+ escalation.warning = `Role "${to}" is not in this mission's step chain. Escalation recorded but no step re-opened.`;
227
+ } else {
228
+ escalation.warning = `Role "${to}" has no completed step to re-open. Escalation recorded.`;
229
+ }
230
+ }
231
+
232
+ return escalation;
233
+ }
234
+
235
+ // ── Run introspection ───────────────────────────────────────────────────────
236
+
237
+ /**
238
+ * Get the current position in a run.
239
+ * @param {MissionRun} run
240
+ * @returns {{currentStep: MissionStep|null, completedCount: number, totalSteps: number, progress: string}}
241
+ */
242
+ export function getRunPosition(run) {
243
+ const active = run.steps.find((s) => s.status === "active");
244
+ const completedCount = run.steps.filter((s) => s.status === "completed").length;
245
+ const totalSteps = run.steps.length;
246
+
247
+ return {
248
+ currentStep: active || null,
249
+ completedCount,
250
+ totalSteps,
251
+ progress: `${completedCount}/${totalSteps}`,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Get the artifact chain produced so far.
257
+ * @param {MissionRun} run
258
+ * @returns {Array<{role: string, type: string, artifact: string}>}
259
+ */
260
+ export function getArtifactChain(run) {
261
+ return run.steps
262
+ .filter((s) => s.status === "completed" && s.artifact)
263
+ .map((s) => ({
264
+ role: s.role,
265
+ type: s.produces,
266
+ artifact: s.artifact,
267
+ }));
268
+ }
269
+
270
+ // ── Completion report ───────────────────────────────────────────────────────
271
+
272
+ /**
273
+ * Generate a completion report for a finished (or partially finished) run.
274
+ * This is the "honest partial" — it tells you exactly what happened,
275
+ * not what you hoped would happen.
276
+ *
277
+ * @param {MissionRun} run
278
+ * @returns {object} Completion report
279
+ */
280
+ export function generateCompletionReport(run) {
281
+ const mission = getMission(run.missionKey);
282
+ const position = getRunPosition(run);
283
+ const artifactChain = getArtifactChain(run);
284
+
285
+ const stepSummaries = run.steps.map((s) => ({
286
+ role: s.role,
287
+ produces: s.produces,
288
+ status: s.status,
289
+ hasArtifact: !!s.artifact,
290
+ note: s.note,
291
+ }));
292
+
293
+ const isComplete = run.status === "completed";
294
+ const isPartial = run.status === "partial";
295
+ const isFailed = run.status === "failed";
296
+
297
+ const report = {
298
+ missionKey: run.missionKey,
299
+ missionName: mission.name,
300
+ taskDescription: run.taskDescription,
301
+ outcome: run.status,
302
+ progress: position.progress,
303
+ startedAt: run.startedAt,
304
+ completedAt: run.completedAt,
305
+ durationMs: run.completedAt
306
+ ? new Date(run.completedAt) - new Date(run.startedAt)
307
+ : null,
308
+ steps: stepSummaries,
309
+ artifactsProduced: artifactChain.length,
310
+ artifactChain,
311
+ escalationCount: run.escalations.length,
312
+ escalations: run.escalations,
313
+ honestPartial: isPartial || isFailed ? mission.honestPartial : null,
314
+ verdict: isComplete
315
+ ? "Mission completed — all artifacts produced, all steps passed."
316
+ : isPartial
317
+ ? `Mission partially completed (${position.progress}). ${mission.honestPartial}`
318
+ : isFailed
319
+ ? `Mission failed at step ${position.completedCount + 1}/${position.totalSteps} (${run.steps.find((s) => s.status === "failed")?.role || "unknown"}).`
320
+ : `Mission still running (${position.progress}).`,
321
+ };
322
+
323
+ // Attach to run
324
+ run.completionReport = report;
325
+
326
+ return report;
327
+ }
328
+
329
+ /**
330
+ * Format a completion report as human-readable text.
331
+ * @param {object} report
332
+ * @returns {string}
333
+ */
334
+ export function formatCompletionReport(report) {
335
+ const lines = [];
336
+
337
+ lines.push(`# Mission Report: ${report.missionName}`);
338
+ lines.push("");
339
+ lines.push(`**Task:** ${report.taskDescription}`);
340
+ lines.push(`**Outcome:** ${report.outcome.toUpperCase()}`);
341
+ lines.push(`**Progress:** ${report.progress}`);
342
+
343
+ if (report.durationMs) {
344
+ const seconds = Math.round(report.durationMs / 1000);
345
+ lines.push(`**Duration:** ${seconds}s`);
346
+ }
347
+
348
+ lines.push("");
349
+ lines.push("## Steps");
350
+ for (const step of report.steps) {
351
+ const icon = step.status === "completed" ? "[x]" :
352
+ step.status === "active" ? "[>]" :
353
+ step.status === "partial" ? "[~]" :
354
+ step.status === "failed" ? "[!]" :
355
+ step.status === "blocked" ? "[-]" : "[ ]";
356
+ const artifact = step.hasArtifact ? ` → ${step.produces}` : "";
357
+ const note = step.note ? ` (${step.note})` : "";
358
+ lines.push(` ${icon} ${step.role}${artifact}${note}`);
359
+ }
360
+
361
+ if (report.escalationCount > 0) {
362
+ lines.push("");
363
+ lines.push("## Escalations");
364
+ for (const esc of report.escalations) {
365
+ lines.push(` - ${esc.from} → ${esc.to}: ${esc.trigger} → ${esc.action}`);
366
+ }
367
+ }
368
+
369
+ if (report.honestPartial) {
370
+ lines.push("");
371
+ lines.push("## Honest Partial");
372
+ lines.push(report.honestPartial);
373
+ }
374
+
375
+ lines.push("");
376
+ lines.push("## Verdict");
377
+ lines.push(report.verdict);
378
+
379
+ return lines.join("\n");
380
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Mission Library — Phase S (v1.8.0)
3
+ *
4
+ * Named, repeatable job types that make recurring work boringly reliable.
5
+ * Each mission declares:
6
+ * - Default pack and role chain
7
+ * - Expected artifact flow
8
+ * - Common escalation branches
9
+ * - What "honest partial" looks like
10
+ * - Entry conditions and stop conditions
11
+ *
12
+ * Missions are NOT new abstractions — they are proven patterns
13
+ * extracted from real execution (trials G1–G10, pack comparisons,
14
+ * completion proof arc).
15
+ */
16
+
17
+ import { TEAM_PACKS } from "./packs.mjs";
18
+ import { ROLE_ARTIFACT_CONTRACTS } from "./artifacts.mjs";
19
+ import { ROLE_CATALOG } from "./route.mjs";
20
+
21
+ // ── Mission definitions ─────────────────────────────────────────────────────
22
+
23
+ export const MISSIONS = {
24
+ // ── Feature Shipment ────────────────────────────────────────────────────
25
+ "feature-ship": {
26
+ name: "Feature Shipment",
27
+ description: "Scope, spec, implement, test, and review a new feature end-to-end.",
28
+ pack: "feature",
29
+ entryPath: "Product Strategist frames scope → Spec Writer writes spec → implementation → test → review",
30
+ roleChain: [
31
+ "Product Strategist",
32
+ "Spec Writer",
33
+ "Backend Engineer",
34
+ "Test Engineer",
35
+ "Critic Reviewer",
36
+ ],
37
+ artifactFlow: [
38
+ { role: "Product Strategist", produces: "strategy-brief", consumedBy: "Spec Writer" },
39
+ { role: "Spec Writer", produces: "implementation-spec", consumedBy: "Backend Engineer" },
40
+ { role: "Backend Engineer", produces: "change-plan", consumedBy: "Test Engineer" },
41
+ { role: "Test Engineer", produces: "test-report", consumedBy: "Critic Reviewer" },
42
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
43
+ ],
44
+ escalationBranches: [
45
+ { trigger: "scope ambiguity", from: "Spec Writer", to: "Product Strategist", action: "clarify scope before proceeding" },
46
+ { trigger: "untestable spec", from: "Test Engineer", to: "Spec Writer", action: "revise interface spec for testability" },
47
+ { trigger: "review rejection", from: "Critic Reviewer", to: "Backend Engineer", action: "address findings, re-test, re-review" },
48
+ ],
49
+ honestPartial: "Scope + spec complete but implementation blocked. Artifact chain intact up to the blocking point. Handoff includes what was tried and why it stalled.",
50
+ stopConditions: [
51
+ "Critic Reviewer accepts",
52
+ "Escalation exhausts retry budget (3 loops max)",
53
+ "Scope change invalidates the mission — replan required",
54
+ ],
55
+ dispatchDefaults: { model: "sonnet", maxTurns: 30, maxBudgetUsd: 5.0 },
56
+ trialEvidence: "G1, G2 — 6/6 gold passes. Pack comparison: wins vs free routing.",
57
+ },
58
+
59
+ // ── Bugfix / Diagnosis ──────────────────────────────────────────────────
60
+ "bugfix": {
61
+ name: "Bugfix & Diagnosis",
62
+ description: "Diagnose root cause, fix, test, and verify a bug or regression.",
63
+ pack: "bugfix",
64
+ entryPath: "Repo Researcher diagnoses root cause → Backend Engineer fixes → Test Engineer verifies → Critic reviews",
65
+ roleChain: [
66
+ "Repo Researcher",
67
+ "Backend Engineer",
68
+ "Test Engineer",
69
+ "Critic Reviewer",
70
+ ],
71
+ artifactFlow: [
72
+ { role: "Repo Researcher", produces: "diagnosis-report", consumedBy: "Backend Engineer" },
73
+ { role: "Backend Engineer", produces: "change-plan", consumedBy: "Test Engineer" },
74
+ { role: "Test Engineer", produces: "test-report", consumedBy: "Critic Reviewer" },
75
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
76
+ ],
77
+ escalationBranches: [
78
+ { trigger: "root cause unclear", from: "Repo Researcher", to: "Repo Researcher", action: "gather more evidence, add reproduction steps" },
79
+ { trigger: "fix introduces regression", from: "Test Engineer", to: "Backend Engineer", action: "revise fix approach" },
80
+ { trigger: "diagnosis was wrong", from: "Backend Engineer", to: "Repo Researcher", action: "re-diagnose with new evidence from attempted fix" },
81
+ ],
82
+ honestPartial: "Root cause identified and documented but fix is non-trivial. Diagnosis report artifact is complete. Handoff includes attempted approaches and why they failed.",
83
+ stopConditions: [
84
+ "Critic Reviewer accepts fix",
85
+ "Root cause confirmed but fix requires scope expansion — replan as feature-ship",
86
+ "Bug is not reproducible — document findings and close",
87
+ ],
88
+ dispatchDefaults: { model: "sonnet", maxTurns: 20, maxBudgetUsd: 3.0 },
89
+ trialEvidence: "G3, G4 — 4/4 gold passes. Honest partial validated in R1-2 (README anomaly).",
90
+ },
91
+
92
+ // ── Treatment (Repo Polish) ─────────────────────────────────────────────
93
+ "treatment": {
94
+ name: "Treatment (Repo Polish)",
95
+ description: "Full treatment: shipcheck gates, polish, docs, translations, version bump, publish.",
96
+ pack: "treatment",
97
+ entryPath: "Shipcheck gates first → Security Reviewer audits → Docs Architect updates → Deployment Verifier verifies CI → Critic reviews",
98
+ roleChain: [
99
+ "Security Reviewer",
100
+ "Docs Architect",
101
+ "Deployment Verifier",
102
+ "Critic Reviewer",
103
+ ],
104
+ artifactFlow: [
105
+ { role: "Security Reviewer", produces: "security-audit", consumedBy: "Docs Architect" },
106
+ { role: "Docs Architect", produces: "docs-update", consumedBy: "Deployment Verifier" },
107
+ { role: "Deployment Verifier", produces: "ci-verification", consumedBy: "Critic Reviewer" },
108
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
109
+ ],
110
+ escalationBranches: [
111
+ { trigger: "security gate fails", from: "Security Reviewer", to: "Backend Engineer", action: "fix security issue before treatment continues" },
112
+ { trigger: "CI fails after changes", from: "Deployment Verifier", to: "Backend Engineer", action: "fix CI, re-verify" },
113
+ { trigger: "shipcheck hard gate blocks", from: "Critic Reviewer", to: "Security Reviewer", action: "address blocking gate, restart treatment" },
114
+ ],
115
+ honestPartial: "Shipcheck passed but polish incomplete. Security and docs artifacts exist. Remaining items documented in handoff.",
116
+ stopConditions: [
117
+ "All shipcheck hard gates pass + Critic accepts",
118
+ "Security gate A blocks — must fix before continuing",
119
+ "Scope expands to feature work — decompose into treatment + feature-ship",
120
+ ],
121
+ dispatchDefaults: { model: "sonnet", maxTurns: 25, maxBudgetUsd: 4.0 },
122
+ trialEvidence: "G5, G6 — treatment pack proven. Completion proof arc validated honest partial.",
123
+ },
124
+
125
+ // ── Docs / Release ──────────────────────────────────────────────────────
126
+ "docs-release": {
127
+ name: "Docs & Release",
128
+ description: "Write or update documentation, prepare release notes, publish.",
129
+ pack: "docs",
130
+ entryPath: "Docs Architect synthesizes upstream → writes docs → Critic reviews completeness",
131
+ roleChain: [
132
+ "Docs Architect",
133
+ "Critic Reviewer",
134
+ ],
135
+ artifactFlow: [
136
+ { role: "Docs Architect", produces: "docs-update", consumedBy: "Critic Reviewer" },
137
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
138
+ ],
139
+ escalationBranches: [
140
+ { trigger: "upstream source missing", from: "Docs Architect", to: "Product Strategist", action: "clarify what changed and why" },
141
+ { trigger: "docs conflict with code", from: "Critic Reviewer", to: "Docs Architect", action: "reconcile docs with actual implementation" },
142
+ ],
143
+ honestPartial: "Core docs updated but supplementary pages (handbook, translations) pending. Main artifact complete.",
144
+ stopConditions: [
145
+ "Critic accepts docs as accurate and complete",
146
+ "Upstream changes still in flight — park docs until code stabilizes",
147
+ ],
148
+ dispatchDefaults: { model: "sonnet", maxTurns: 15, maxBudgetUsd: 2.0 },
149
+ trialEvidence: "G7, G8 — docs pack proven. Upstream-synthesis gate prevents stale docs.",
150
+ },
151
+
152
+ // ── Security Hardening ──────────────────────────────────────────────────
153
+ "security-hardening": {
154
+ name: "Security Hardening",
155
+ description: "Threat model, audit, fix vulnerabilities, verify gates.",
156
+ pack: "security",
157
+ entryPath: "Security Reviewer threat-models → identifies issues → Backend Engineer fixes → re-audit",
158
+ roleChain: [
159
+ "Security Reviewer",
160
+ "Backend Engineer",
161
+ "Test Engineer",
162
+ "Critic Reviewer",
163
+ ],
164
+ artifactFlow: [
165
+ { role: "Security Reviewer", produces: "security-audit", consumedBy: "Backend Engineer" },
166
+ { role: "Backend Engineer", produces: "change-plan", consumedBy: "Test Engineer" },
167
+ { role: "Test Engineer", produces: "test-report", consumedBy: "Security Reviewer" },
168
+ { role: "Security Reviewer", produces: "security-audit", consumedBy: "Critic Reviewer" },
169
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
170
+ ],
171
+ escalationBranches: [
172
+ { trigger: "critical vulnerability found", from: "Security Reviewer", to: "Backend Engineer", action: "immediate fix, skip non-critical work" },
173
+ { trigger: "fix breaks functionality", from: "Test Engineer", to: "Backend Engineer", action: "find fix that preserves both security and function" },
174
+ { trigger: "threat model scope expands", from: "Security Reviewer", to: "Product Strategist", action: "reframe scope — is this a feature-ship or security mission?" },
175
+ ],
176
+ honestPartial: "Threat model complete, critical issues fixed, non-critical items documented as backlog. Audit artifact reflects actual state.",
177
+ stopConditions: [
178
+ "All critical findings fixed + Critic accepts",
179
+ "No critical findings — document clean audit",
180
+ "Scope expands beyond security — decompose into security + feature-ship",
181
+ ],
182
+ dispatchDefaults: { model: "sonnet", maxTurns: 25, maxBudgetUsd: 4.0 },
183
+ trialEvidence: "G9, G10 — security pack proven. Treatment includes Security Reviewer by default.",
184
+ },
185
+
186
+ // ── Research → Framing → Launch ─────────────────────────────────────────
187
+ "research-launch": {
188
+ name: "Research → Framing → Launch",
189
+ description: "Investigate a question, frame findings into a decision, prepare launch artifacts.",
190
+ pack: "research",
191
+ entryPath: "Product Strategist frames question → Competitive Analyst investigates → findings → launch decision",
192
+ roleChain: [
193
+ "Product Strategist",
194
+ "Competitive Analyst",
195
+ "Docs Architect",
196
+ "Critic Reviewer",
197
+ ],
198
+ artifactFlow: [
199
+ { role: "Product Strategist", produces: "strategy-brief", consumedBy: "Competitive Analyst" },
200
+ { role: "Competitive Analyst", produces: "research-findings", consumedBy: "Product Strategist" },
201
+ { role: "Docs Architect", produces: "docs-update", consumedBy: "Critic Reviewer" },
202
+ { role: "Critic Reviewer", produces: "review-verdict", consumedBy: null },
203
+ ],
204
+ escalationBranches: [
205
+ { trigger: "research inconclusive", from: "Competitive Analyst", to: "Product Strategist", action: "narrow the question or accept uncertainty" },
206
+ { trigger: "findings contradict assumption", from: "Competitive Analyst", to: "Product Strategist", action: "reframe strategy based on evidence" },
207
+ { trigger: "launch blocked by findings", from: "Critic Reviewer", to: "Product Strategist", action: "decide: proceed with caveats or pivot" },
208
+ ],
209
+ honestPartial: "Research complete with findings documented. Launch decision pending stakeholder input. Research artifact is self-contained.",
210
+ stopConditions: [
211
+ "Critic accepts research findings and launch plan",
212
+ "Research reveals the question is wrong — reframe mission",
213
+ "Findings are clear but launch is premature — park with documented rationale",
214
+ ],
215
+ dispatchDefaults: { model: "sonnet", maxTurns: 20, maxBudgetUsd: 3.0 },
216
+ trialEvidence: "Research pack opens with Product Strategist (framing before research). Validated in pack comparison.",
217
+ },
218
+ };
219
+
220
+ // ── Mission catalog ─────────────────────────────────────────────────────────
221
+
222
+ /**
223
+ * List all missions with summary info.
224
+ * @returns {Array<{key: string, name: string, description: string, pack: string, roleCount: number}>}
225
+ */
226
+ export function listMissions() {
227
+ return Object.entries(MISSIONS).map(([key, m]) => ({
228
+ key,
229
+ name: m.name,
230
+ description: m.description,
231
+ pack: m.pack,
232
+ roleCount: m.roleChain.length,
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * Get a mission by key.
238
+ * @param {string} key
239
+ * @returns {object|null}
240
+ */
241
+ export function getMission(key) {
242
+ return MISSIONS[key] || null;
243
+ }
244
+
245
+ /**
246
+ * Suggest a mission for a task description (signal matching).
247
+ * @param {string} taskDescription
248
+ * @returns {{mission: string, confidence: "high"|"medium"|"low", reason: string}|null}
249
+ */
250
+ export function suggestMission(taskDescription) {
251
+ const text = taskDescription.toLowerCase();
252
+
253
+ const MISSION_SIGNALS = {
254
+ "feature-ship": {
255
+ signals: ["add feature", "implement", "build", "create", "new command", "ship feature", "develop", "add command", "new feature", "add support"],
256
+ weight: 1,
257
+ },
258
+ "bugfix": {
259
+ signals: ["fix", "bug", "crash", "broken", "regression", "repair", "diagnose", "not working"],
260
+ weight: 1,
261
+ },
262
+ "treatment": {
263
+ signals: ["treatment", "polish", "shipcheck", "pre-release", "ship-ready", "cleanup", "housekeeping"],
264
+ weight: 1.2, // slightly prefer treatment when signals match (common task)
265
+ },
266
+ "docs-release": {
267
+ signals: ["documentation", "docs", "readme", "release notes", "changelog", "handbook", "write docs"],
268
+ weight: 1,
269
+ },
270
+ "security-hardening": {
271
+ signals: ["security", "vulnerability", "threat model", "audit", "CVE", "injection", "hardening"],
272
+ weight: 1.1,
273
+ },
274
+ "research-launch": {
275
+ signals: ["research", "investigate", "should we", "evaluate", "competitive", "launch plan", "strategy"],
276
+ weight: 1,
277
+ },
278
+ };
279
+
280
+ let bestKey = null;
281
+ let bestScore = 0;
282
+
283
+ for (const [key, def] of Object.entries(MISSION_SIGNALS)) {
284
+ let hits = 0;
285
+ for (const signal of def.signals) {
286
+ if (text.includes(signal)) hits++;
287
+ }
288
+ const score = hits * def.weight;
289
+ if (score > bestScore) {
290
+ bestScore = score;
291
+ bestKey = key;
292
+ }
293
+ }
294
+
295
+ if (!bestKey || bestScore === 0) return null;
296
+
297
+ const confidence = bestScore >= 3 ? "high" : bestScore >= 1.5 ? "medium" : "low";
298
+ const mission = MISSIONS[bestKey];
299
+
300
+ return {
301
+ mission: bestKey,
302
+ confidence,
303
+ reason: `Matched ${Math.floor(bestScore)} signal(s) for "${mission.name}"`,
304
+ };
305
+ }
306
+
307
+ // ── Mission validation ──────────────────────────────────────────────────────
308
+
309
+ /**
310
+ * Validate that a mission's pack and roles are properly wired.
311
+ * @param {string} key
312
+ * @returns {{valid: boolean, issues: string[]}}
313
+ */
314
+ export function validateMission(key) {
315
+ const mission = MISSIONS[key];
316
+ if (!mission) return { valid: false, issues: [`Mission "${key}" not found`] };
317
+
318
+ const issues = [];
319
+
320
+ // Check pack exists
321
+ if (!TEAM_PACKS[mission.pack]) {
322
+ issues.push(`Pack "${mission.pack}" not found in TEAM_PACKS`);
323
+ }
324
+
325
+ // S6-F1: Check all role names exist in ROLE_CATALOG
326
+ const catalogNames = new Set(ROLE_CATALOG.map((r) => r.name));
327
+ for (const roleName of mission.roleChain) {
328
+ if (!catalogNames.has(roleName)) {
329
+ issues.push(`Role "${roleName}" in roleChain not found in ROLE_CATALOG`);
330
+ }
331
+ }
332
+ for (const step of mission.artifactFlow) {
333
+ if (!catalogNames.has(step.role)) {
334
+ issues.push(`Role "${step.role}" in artifactFlow not found in ROLE_CATALOG`);
335
+ }
336
+ }
337
+
338
+ // Check artifact contracts exist for producing roles
339
+ for (const step of mission.artifactFlow) {
340
+ if (ROLE_ARTIFACT_CONTRACTS[step.role]) {
341
+ // Contract exists — good
342
+ }
343
+ // Not all roles need contracts (only chain-critical ones), so missing is a warning not error
344
+ }
345
+
346
+ // Check artifact flow continuity (each consumed artifact should be produced upstream)
347
+ const produced = new Set();
348
+ for (const step of mission.artifactFlow) {
349
+ produced.add(step.produces);
350
+ }
351
+
352
+ // Check escalation branches reference roles in the chain or known roles
353
+ for (const branch of mission.escalationBranches) {
354
+ if (!mission.roleChain.includes(branch.from) && branch.from !== branch.to) {
355
+ // from role should be in chain or self-referencing
356
+ issues.push(`Escalation "from" role "${branch.from}" not in role chain`);
357
+ }
358
+ }
359
+
360
+ // Check stop conditions exist
361
+ if (!mission.stopConditions || mission.stopConditions.length === 0) {
362
+ issues.push("Mission has no stop conditions");
363
+ }
364
+
365
+ // Check honest partial exists
366
+ if (!mission.honestPartial) {
367
+ issues.push("Mission has no honest-partial definition");
368
+ }
369
+
370
+ return { valid: issues.length === 0, issues };
371
+ }
372
+
373
+ /**
374
+ * Validate ALL missions.
375
+ * @returns {{allValid: boolean, results: Record<string, {valid: boolean, issues: string[]}>}}
376
+ */
377
+ export function validateAllMissions() {
378
+ const results = {};
379
+ let allValid = true;
380
+
381
+ for (const key of Object.keys(MISSIONS)) {
382
+ const result = validateMission(key);
383
+ results[key] = result;
384
+ if (!result.valid) allValid = false;
385
+ }
386
+
387
+ return { allValid, results };
388
+ }