harness-everything 0.30.4 → 0.30.6

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.
Files changed (71) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +38 -0
  4. package/README.md +17 -7
  5. package/bin/cli.js +5 -0
  6. package/ci/fable-mode-test.js +112 -51
  7. package/ci/mechanism-2t-workspace-identity.test.js +3 -1
  8. package/ci/mechanism-2w-platform-compatibility.test.js +39 -0
  9. package/ci/mechanism-2z-claude-plugin-manifest.test.js +12 -0
  10. package/ci/router-workflow-plan.test.js +92 -7
  11. package/docs/platform-capabilities.md +14 -1
  12. package/docs/troubleshooting.md +31 -1
  13. package/docs/workflows/fable-mode.md +10 -9
  14. package/evals/fable-mode/tasks/negative-tier2-no-model.yaml +3 -3
  15. package/evals/fable-mode/tasks/positive-haiku-mode.yaml +1 -1
  16. package/evals/fable-mode/tasks/positive-opus-mode.yaml +1 -1
  17. package/evals/fable-mode/tasks/positive-sonnet-mode.yaml +1 -1
  18. package/evals/fable-mode/tasks/positive-trigger.yaml +1 -1
  19. package/fable-mode/SKILL.md +18 -12
  20. package/fable-mode/agents/fable-orchestrator.md +7 -6
  21. package/fable-mode/agents/fable-verifier.md +1 -1
  22. package/fable-mode/agents/fable-worker-haiku.md +5 -4
  23. package/fable-mode/agents/fable-worker-sonnet.md +5 -4
  24. package/fable-mode/behavior-profile-matrix.json +36 -0
  25. package/fable-mode/execution-guardrails/SKILL.md +3 -6
  26. package/fable-mode/execution-guardrails/references/guardrail-rules.md +8 -9
  27. package/fable-mode/fable-haiku/SKILL.md +7 -5
  28. package/fable-mode/fable-haiku/references/routing-notes.md +2 -1
  29. package/fable-mode/fable-opus/SKILL.md +8 -6
  30. package/fable-mode/fable-sonnet/SKILL.md +8 -6
  31. package/fable-mode/model-matrix.json +7 -30
  32. package/fable-mode/references/model-matrix.md +35 -30
  33. package/fable-mode/references/profile-lookup.md +19 -0
  34. package/fable-mode/runtime-model-floor-matrix.json +47 -0
  35. package/fable-mode/scripts/model-selector.js +106 -60
  36. package/fable-mode/scripts/workflow-plan-consumer.js +1 -1
  37. package/harness-everything/SKILL.md +11 -14
  38. package/harness-everything/references/router-workflow-plan.md +2 -2
  39. package/harness-everything/schemas/router-task-shape.schema.json +3 -2
  40. package/harness-everything/schemas/router-workflow-plan.schema.json +6 -5
  41. package/harness-everything/scripts/router-contract-core.js +19 -12
  42. package/harness-everything/scripts/tier-router.js +100 -12
  43. package/opencode-plugin/plugin.json +1 -1
  44. package/package.json +1 -1
  45. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  46. package/plugins/harness-everything/plugin.json +1 -1
  47. package/plugins/harness-everything/skills/fable-mode/SKILL.md +18 -12
  48. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +7 -6
  49. package/plugins/harness-everything/skills/fable-mode/agents/fable-verifier.md +1 -1
  50. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-haiku.md +5 -4
  51. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-sonnet.md +5 -4
  52. package/plugins/harness-everything/skills/fable-mode/behavior-profile-matrix.json +36 -0
  53. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +3 -6
  54. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/references/guardrail-rules.md +8 -9
  55. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +7 -5
  56. package/plugins/harness-everything/skills/fable-mode/fable-haiku/references/routing-notes.md +2 -1
  57. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +8 -6
  58. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +8 -6
  59. package/plugins/harness-everything/skills/fable-mode/model-matrix.json +7 -30
  60. package/plugins/harness-everything/skills/fable-mode/references/model-matrix.md +35 -30
  61. package/plugins/harness-everything/skills/fable-mode/references/profile-lookup.md +19 -0
  62. package/plugins/harness-everything/skills/fable-mode/runtime-model-floor-matrix.json +47 -0
  63. package/plugins/harness-everything/skills/fable-mode/scripts/model-selector.js +106 -60
  64. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +1 -1
  65. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -14
  66. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +2 -2
  67. package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +3 -2
  68. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +6 -5
  69. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +19 -12
  70. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +100 -12
  71. package/sbom.cdx.json +7 -7
@@ -1,41 +1,46 @@
1
- # Fable model matrix and execution audit
1
+ # Fable behavior profiles and runtime model floors
2
2
 
3
- Issue #27 defines three explicit execution modes. The mode selector is the
4
- machine-readable gate; this document is the human-facing contract.
3
+ Fable profiles define **how work is performed and which role owns it**. They do not select or require a branded runtime model. The historical names `haiku`, `sonnet`/`sonnect`, and `opus` remain accepted aliases so existing prompts keep working.
5
4
 
6
- | Mode | Use for | Named agent |
7
- |---|---|---|
8
- | Haiku | Bulk mechanical work and format conversion | `fable-worker-haiku` |
9
- | Sonnet | Non-trivial implementation, research synthesis, bounded reasoning | `fable-worker-sonnet` |
10
- | Opus | Macro orchestration, cross-stage synthesis, high-stakes architecture and final decisions | `fable-orchestrator` |
5
+ ## Behavior Profile Matrix
6
+
7
+ | Canonical profile | Legacy alias | Role | Typical work | Named agent |
8
+ |---|---|---|---|---|
9
+ | `mechanical` | `haiku` | Mechanical worker | Bulk edits, format conversion, boilerplate, structured extraction | `fable-worker-haiku` |
10
+ | `reasoning` | `sonnet` / `sonnect` | Reasoning worker | Non-trivial implementation, research synthesis, analysis, bounded design | `fable-worker-sonnet` |
11
+ | `orchestrator` | `opus` | Orchestrator | Stage decomposition, dependency management, cross-stage synthesis, architecture/final decisions | `fable-orchestrator` |
12
+ | verifier | — | Cold reviewer | Spec-versus-artifact checks and independent re-verification | `fable-verifier` |
13
+
14
+ The machine-readable source is `fable-mode/behavior-profile-matrix.json`.
15
+
16
+ ## Runtime Model Floor Matrix
11
17
 
12
- Use the explicit entrypoints `fable-haiku`, `fable-sonnet`, and `fable-opus`.
13
- The accepted input `sonnect` normalizes to Sonnet. A normal Tier 2 prompt does
14
- not select a fable model.
18
+ Runtime floors are **host-specific recommendations**, not Fable identity and not an execution gate. A newer compatible model/effort may be used. If the active runtime is below or cannot be compared to the floor, Fable continues with the same behavior profile and records the status visibly.
15
19
 
16
- ## Deterministic selection
20
+ | Fable profile | Claude recommended floor | Codex recommended floor |
21
+ |---|---|---|
22
+ | `orchestrator` | Opus 5.5+ / medium+ | GPT-6 Sol+ / xhigh+ |
23
+ | `reasoning` | Sonnet 5.5+ / medium+ | GPT-6 Sol+ / medium+ |
24
+ | `mechanical` | Haiku 5.5+ | GPT-6 Luna 6+ / xhigh+ |
25
+
26
+ The machine-readable source is `fable-mode/runtime-model-floor-matrix.json`. These are Harness support baselines, not claims that every host currently exposes every listed model.
17
27
 
18
- Before each stage, run `fable-mode/scripts/model-selector.js` with the requested
19
- mode, the runtime's available modes, the host model, and the stage contract:
28
+ ## Resolution
20
29
 
21
30
  ```text
22
- node fable-mode/scripts/model-selector.js --requested opus --available opus,sonnet,haiku --available-agents fable-orchestrator,fable-worker-sonnet,fable-worker-haiku --fallback stop --host-model opus --stage-brief "Synthesize the architecture" --pass-condition "ADR and dependency map exist" --verification-command "npm test" --verifier-result pending
31
+ task
32
+ -> behavior profile
33
+ -> named role / agent
34
+ -> host adapter
35
+ -> advisory runtime floor
36
+ -> actual host-selected model
37
+ -> execute the SAME behavior contract
23
38
  ```
24
39
 
25
- The JSON result always records `requestedModel`, `effectiveModel`,
26
- `fallbackReason`, `stageBrief`, `passCondition`, `verificationCommand`, and
27
- `verifierResult`, plus available agents, status, agent, and escalation state. The
28
- CLI appends the same record as one JSON line to
29
- `.claude/harness-everything/state/fable-mode/audit.jsonl` (or the path supplied by
30
- `--audit-file`).
31
-
32
- If the requested model is unavailable, `--fallback inline` records the declared
33
- host model and a visible reason. `--fallback stop` emits `status: blocked`, a
34
- null effective model, and exit code 2. Neither path silently downgrades.
35
-
36
- The orchestrator still owns scope lock, at most two full replans, stage
37
- contracts, worker non-recursion, cold verification, and escalation. The native
38
- host TODO tracker or a Markdown checklist records stage progress; no CLI TODO
39
- state machine is part of this contract.
40
+ `fable on haiku` normalizes to `mechanical`; `fable on sonnect` normalizes to `reasoning`; `fable on opus` normalizes to `orchestrator`. None of these aliases means switching the host to that branded model.
41
+
42
+ The audit record uses `requestedProfile`, `effectiveProfile`, `profileAlias`, `assignedRole`, `runtimeModel`, `runtimeEffort`, `recommendedRuntimeFloor`, `runtimeFloorStatus`, and `runtimeFloorReason`, plus stage verification fields.
43
+
44
+ A `below-recommended` or `unknown` runtime-floor result is advisory and does not block Fable. Missing named-agent capability is separate: the existing `inline` or `stop` fallback policy decides whether the same profile runs inline or the stage is blocked.
40
45
 
41
46
  See [execution phases](execution-phases.md) for the staged run contract.
@@ -0,0 +1,19 @@
1
+ # Profile lookup
2
+
3
+ Use for explanation or a profile-only JSON record. No stage, audit-log mutation, delegation, or model switch is implied. Execution requests retain their selected workflow obligations.
4
+
5
+ | Alias | Profile | Role | Named agent |
6
+ |---|---|---|---|
7
+ | haiku | mechanical | mechanical-worker | fable-worker-haiku |
8
+ | sonnet / sonnect | reasoning | reasoning-worker | fable-worker-sonnet |
9
+ | opus | orchestrator | orchestrator | fable-orchestrator |
10
+
11
+ Preserve the input alias. Profiles describe behavior; the host chooses the actual runtime model. Report runtime fields as unknown when they were not supplied; never infer a model from an alias. Runtime floors are advisory, including unknown or below-recommended results.
12
+
13
+ For a lookup, these fields suffice: `requestedProfile`, `effectiveProfile`, `profileAlias`, `assignedRole`, `runtimeModel`, `runtimeEffort`. If the user requests a computed floor comparison, read the relevant entry in `runtime-model-floor-matrix.json` and the resolution rules in [runtime-floor rules](model-matrix.md); use `scripts/model-selector.js` for a stage audit, not by reading its implementation for a lookup.
14
+
15
+ Example for `fable on sonnect` with no supplied runtime:
16
+
17
+ ```json
18
+ {"requestedProfile":"reasoning","effectiveProfile":"reasoning","profileAlias":"sonnect","assignedRole":"reasoning-worker","runtimeModel":null,"runtimeEffort":null}
19
+ ```
@@ -0,0 +1,47 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policy": "advisory-floor",
4
+ "belowFloorDisposition": "continue-with-visible-status",
5
+ "hosts": {
6
+ "claude": {
7
+ "orchestrator": {
8
+ "family": "opus",
9
+ "minVersion": "5.5",
10
+ "minEffort": "medium",
11
+ "display": "Opus 5.5+ / medium+"
12
+ },
13
+ "reasoning": {
14
+ "family": "sonnet",
15
+ "minVersion": "5.5",
16
+ "minEffort": "medium",
17
+ "display": "Sonnet 5.5+ / medium+"
18
+ },
19
+ "mechanical": {
20
+ "family": "haiku",
21
+ "minVersion": "5.5",
22
+ "minEffort": null,
23
+ "display": "Haiku 5.5+"
24
+ }
25
+ },
26
+ "codex": {
27
+ "orchestrator": {
28
+ "family": "sol",
29
+ "minGeneration": 6,
30
+ "minEffort": "xhigh",
31
+ "display": "GPT-6 Sol+ / xhigh+"
32
+ },
33
+ "reasoning": {
34
+ "family": "sol",
35
+ "minGeneration": 6,
36
+ "minEffort": "medium",
37
+ "display": "GPT-6 Sol+ / medium+"
38
+ },
39
+ "mechanical": {
40
+ "family": "luna",
41
+ "minGeneration": 6,
42
+ "minEffort": "xhigh",
43
+ "display": "GPT-6 Luna 6+ / xhigh+"
44
+ }
45
+ }
46
+ }
47
+ }
@@ -5,7 +5,6 @@ const os = require('os');
5
5
  const path = require('path');
6
6
  const crypto = require('crypto');
7
7
 
8
- const MATRIX_PATH = path.join(__dirname, '..', 'model-matrix.json');
9
8
  const SESSION_REGISTRY_DIR = 'session-workspaces';
10
9
  const SESSION_REGISTRY_VERSION = 1;
11
10
  const UNBOUND_WORKSPACE_KEY = 'unbound-workspace';
@@ -136,89 +135,140 @@ function getWorkspaceStateDir(root) {
136
135
  }
137
136
  const REQUIRED = ['stageBrief', 'passCondition', 'verificationCommand', 'verifierResult'];
138
137
  const VALID_VERIFIER_RESULTS = new Set(['pending', 'pass', 'fail', 'not-run', 'blocked']);
138
+ const PROFILE_MATRIX_PATH = path.join(__dirname, '..', 'behavior-profile-matrix.json');
139
+ const RUNTIME_FLOOR_PATH = path.join(__dirname, '..', 'runtime-model-floor-matrix.json');
140
+ const EFFORT_ORDER = ['low', 'medium', 'high', 'xhigh', 'max'];
139
141
 
140
- function loadMatrix() {
141
- return JSON.parse(fs.readFileSync(MATRIX_PATH, 'utf8'));
142
+ function loadBehaviorMatrix() {
143
+ return JSON.parse(fs.readFileSync(PROFILE_MATRIX_PATH, 'utf8'));
142
144
  }
143
145
 
144
- function normalizeModel(value, matrix) {
146
+ function loadRuntimeFloors() {
147
+ return JSON.parse(fs.readFileSync(RUNTIME_FLOOR_PATH, 'utf8'));
148
+ }
149
+
150
+ function normalizeProfile(value, matrix = loadBehaviorMatrix()) {
145
151
  const input = String(value || '').trim().toLowerCase();
146
- for (const [model, definition] of Object.entries(matrix.modes)) {
147
- if (definition.aliases.includes(input)) return model;
152
+ for (const [profile, definition] of Object.entries(matrix.profiles)) {
153
+ if (profile === input || definition.aliases.includes(input)) return { profile, alias: input, definition };
148
154
  }
149
155
  return null;
150
156
  }
151
157
 
158
+ function normalizeHost(value) {
159
+ const input = String(value || '').trim().toLowerCase();
160
+ if (input === 'claude' || input === 'claude-code' || input === 'claude code') return 'claude';
161
+ if (input === 'codex' || input === 'openai-codex' || input === 'openai codex') return 'codex';
162
+ return input || 'unknown';
163
+ }
164
+
165
+ function normalizeEffort(value) {
166
+ const input = String(value || '').trim().toLowerCase().replace(/[_ -]/g, '');
167
+ if (!input) return null;
168
+ if (input === 'extrahigh' || input === 'xhigh') return 'xhigh';
169
+ return input;
170
+ }
171
+
172
+ function claudeVersion(model, family) {
173
+ const text = String(model || '').toLowerCase();
174
+ const match = text.match(new RegExp(family + '[^0-9]*(\\d+)(?:[.-](\\d+))?'));
175
+ if (!match) return null;
176
+ return Number(match[1] + '.' + (match[2] || '0'));
177
+ }
178
+
179
+ function codexGeneration(model) {
180
+ const match = String(model || '').toLowerCase().match(/\bgpt-(\d+(?:\.\d+)?)/);
181
+ return match ? Number(match[1]) : null;
182
+ }
183
+
184
+ function evaluateRuntimeFloor(profile, input, floors = loadRuntimeFloors()) {
185
+ const host = normalizeHost(input.host);
186
+ const floor = floors.hosts[host] && floors.hosts[host][profile] ? floors.hosts[host][profile] : null;
187
+ const runtimeModel = String(input.runtimeModel || input.hostModel || '').trim() || null;
188
+ const runtimeEffort = normalizeEffort(input.runtimeEffort || input.effort);
189
+ if (!floor) return { host, runtimeModel, runtimeEffort, floor: null, status: 'not-configured', reason: 'no runtime floor configured for host ' + host };
190
+ if (!runtimeModel) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime model was not reported by the host' };
191
+
192
+ const lower = runtimeModel.toLowerCase();
193
+ if (!lower.includes(floor.family)) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime model family is not comparable to recommended ' + floor.family };
194
+
195
+ if (floor.minVersion) {
196
+ const actual = claudeVersion(runtimeModel, floor.family);
197
+ if (actual === null) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime model version could not be parsed' };
198
+ if (actual < Number(floor.minVersion)) return { host, runtimeModel, runtimeEffort, floor, status: 'below-recommended', reason: 'runtime model version ' + actual + ' is below ' + floor.minVersion };
199
+ }
200
+
201
+ if (floor.minGeneration) {
202
+ const actual = codexGeneration(runtimeModel);
203
+ if (actual === null) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime model generation could not be parsed' };
204
+ if (actual < Number(floor.minGeneration)) return { host, runtimeModel, runtimeEffort, floor, status: 'below-recommended', reason: 'runtime model generation ' + actual + ' is below ' + floor.minGeneration };
205
+ }
206
+
207
+ if (floor.minEffort) {
208
+ if (!runtimeEffort) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime reasoning effort was not reported by the host' };
209
+ const actualRank = EFFORT_ORDER.indexOf(runtimeEffort);
210
+ const floorRank = EFFORT_ORDER.indexOf(normalizeEffort(floor.minEffort));
211
+ if (actualRank < 0 || floorRank < 0) return { host, runtimeModel, runtimeEffort, floor, status: 'unknown', reason: 'runtime effort is not comparable to configured floor' };
212
+ if (actualRank < floorRank) return { host, runtimeModel, runtimeEffort, floor, status: 'below-recommended', reason: 'runtime effort ' + runtimeEffort + ' is below ' + floor.minEffort };
213
+ }
214
+
215
+ return { host, runtimeModel, runtimeEffort, floor, status: 'meets-recommended', reason: 'runtime meets the configured advisory floor' };
216
+ }
217
+
152
218
  function parseArgs(argv) {
153
219
  const args = {};
154
220
  for (let i = 0; i < argv.length; i += 1) {
155
221
  const token = argv[i];
156
222
  if (token === '--help' || token === '-h') return { help: true };
157
- if (!token.startsWith('--')) throw new Error(`unknown argument: ${token}`);
223
+ if (!token.startsWith('--')) throw new Error('unknown argument: ' + token);
158
224
  const key = token.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
159
225
  const value = argv[i + 1];
160
- if (typeof value === 'undefined' || value.startsWith('--')) throw new Error(`missing value for --${key}`);
226
+ if (typeof value === 'undefined' || value.startsWith('--')) throw new Error('missing value for --' + key);
161
227
  args[key] = value;
162
228
  i += 1;
163
229
  }
164
230
  return args;
165
231
  }
166
232
 
167
- function resolveMode(input, matrix = loadMatrix()) {
168
- const requestedModel = normalizeModel(input.requested, matrix);
169
- if (!requestedModel) throw new Error('requested model must be one of haiku, sonnet, sonnect, or opus');
170
-
171
- const availableModels = String(input.available || '')
172
- .split(/[\s,]+/)
173
- .map(model => normalizeModel(model, matrix))
174
- .filter(Boolean);
175
- const availableAgents = String(input.availableAgents || '')
176
- .split(/[\s,]+/)
177
- .map(agent => agent.trim())
178
- .filter(Boolean);
179
- const fallback = input.fallback || 'stop';
180
- if (!Object.prototype.hasOwnProperty.call(matrix.fallbacks, fallback)) {
181
- throw new Error('fallback must be inline or stop');
182
- }
233
+ function resolveMode(input, matrix = loadBehaviorMatrix(), floors = loadRuntimeFloors()) {
234
+ const normalized = normalizeProfile(input.requested, matrix);
235
+ if (!normalized) throw new Error('requested profile must be one of mechanical, reasoning, orchestrator, haiku, sonnet, sonnect, or opus');
183
236
 
237
+ const availableAgents = String(input.availableAgents || '').split(/[\s,]+/).map(agent => agent.trim()).filter(Boolean);
238
+ const fallback = input.fallback || 'stop';
239
+ if (!['inline', 'stop'].includes(fallback)) throw new Error('fallback must be inline or stop');
184
240
  for (const field of REQUIRED) {
185
- if (!String(input[field] || '').trim()) throw new Error(`${field} is required for an auditable stage`);
186
- }
187
- if (!VALID_VERIFIER_RESULTS.has(input.verifierResult)) {
188
- throw new Error(`verifierResult must be one of ${[...VALID_VERIFIER_RESULTS].join(', ')}`);
241
+ if (!String(input[field] || '').trim()) throw new Error(field + ' is required for an auditable stage');
189
242
  }
243
+ if (!VALID_VERIFIER_RESULTS.has(input.verifierResult)) throw new Error('verifierResult must be one of ' + [...VALID_VERIFIER_RESULTS].join(', '));
190
244
 
191
- const requestedAgent = matrix.modes[requestedModel].agent;
192
- const modelAvailable = availableModels.includes(requestedModel);
245
+ const requestedProfile = normalized.profile;
246
+ const requestedAgent = normalized.definition.agent;
193
247
  const agentAvailable = availableAgents.includes(requestedAgent);
194
- const selected = modelAvailable && agentAvailable;
195
- const hostModel = String(input.hostModel || 'current-host').trim();
196
- const fallbackDefinition = matrix.fallbacks[fallback];
197
- const status = selected ? 'selected' : fallbackDefinition.status;
198
- const effectiveModel = selected
199
- ? requestedModel
200
- : fallback === 'inline'
201
- ? hostModel
202
- : fallbackDefinition.effectiveModel;
203
- const missing = [];
204
- if (!modelAvailable) missing.push(`${requestedModel} is not in availableModels`);
205
- if (!agentAvailable) missing.push(`${requestedAgent} is not in availableAgents`);
206
- const fallbackReason = selected
207
- ? 'none'
208
- : `${fallbackDefinition.reason}: ${missing.join('; ')}`;
248
+ const status = agentAvailable ? 'selected' : fallback === 'inline' ? 'fallback' : 'blocked';
249
+ const runtime = evaluateRuntimeFloor(requestedProfile, input, floors);
250
+ const fallbackReason = agentAvailable ? 'none' : fallback === 'inline'
251
+ ? requestedAgent + ' is not in availableAgents; execute the same ' + requestedProfile + ' behavior profile inline'
252
+ : requestedAgent + ' is not in availableAgents and inline fallback was not authorized';
209
253
 
210
254
  return {
211
255
  schemaVersion: matrix.schemaVersion,
212
256
  status,
213
- requestedModel,
257
+ requestedProfile,
258
+ effectiveProfile: status === 'blocked' ? null : requestedProfile,
259
+ profileAlias: normalized.alias,
214
260
  requestedInput: String(input.requested).trim(),
215
- effectiveModel,
216
- availableModels: [...new Set(availableModels)],
261
+ assignedRole: normalized.definition.role,
262
+ agent: agentAvailable ? requestedAgent : null,
217
263
  availableAgents: [...new Set(availableAgents)],
218
264
  fallbackPolicy: fallback,
219
265
  fallbackReason,
220
- agent: selected ? requestedAgent : null,
221
- modelRole: matrix.modes[requestedModel].role,
266
+ host: runtime.host,
267
+ runtimeModel: runtime.runtimeModel,
268
+ runtimeEffort: runtime.runtimeEffort,
269
+ recommendedRuntimeFloor: runtime.floor,
270
+ runtimeFloorStatus: runtime.status,
271
+ runtimeFloorReason: runtime.reason,
222
272
  stageBrief: String(input.stageBrief).trim(),
223
273
  passCondition: String(input.passCondition).trim(),
224
274
  verificationCommand: String(input.verificationCommand).trim(),
@@ -228,33 +278,29 @@ function resolveMode(input, matrix = loadMatrix()) {
228
278
  }
229
279
 
230
280
  function printHelp() {
231
- console.log('Usage: node fable-mode/scripts/model-selector.js --requested <haiku|sonnet|sonnect|opus> --available <models> --available-agents <agent names> --stage-brief <text> --pass-condition <text> --verification-command <command> --verifier-result <pending|pass|fail|not-run|blocked> [--fallback <inline|stop>] [--host-model <model>] [--audit-file <path>]');
281
+ console.log('Usage: node fable-mode/scripts/model-selector.js --requested <mechanical|reasoning|orchestrator|haiku|sonnet|sonnect|opus> --available-agents <agent names> --stage-brief <text> --pass-condition <text> --verification-command <command> --verifier-result <pending|pass|fail|not-run|blocked> [--host <claude|codex>] [--runtime-model <model>] [--runtime-effort <effort>] [--fallback <inline|stop>] [--audit-file <path>]');
232
282
  }
233
283
 
234
284
  function appendAuditRecord(record, auditFile, context) {
235
- const target = auditFile || process.env.FABLE_AUDIT_FILE ||
236
- path.join(getWorkspaceStateDir(getWorkspaceRoot(context)), 'state', 'fable-mode', 'audit.jsonl');
285
+ const target = auditFile || process.env.FABLE_AUDIT_FILE || path.join(getWorkspaceStateDir(getWorkspaceRoot(context)), 'state', 'fable-mode', 'audit.jsonl');
237
286
  fs.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
238
- fs.appendFileSync(target, `${JSON.stringify(record)}\n`, 'utf8');
287
+ fs.appendFileSync(target, JSON.stringify(record) + '\n', 'utf8');
239
288
  }
240
289
 
241
290
  function main(argv = process.argv.slice(2)) {
242
291
  try {
243
292
  const input = parseArgs(argv);
244
- if (input.help) {
245
- printHelp();
246
- return 0;
247
- }
293
+ if (input.help) { printHelp(); return 0; }
248
294
  const record = resolveMode(input);
249
295
  appendAuditRecord(record, input.auditFile, input);
250
296
  console.log(JSON.stringify(record, null, 2));
251
297
  return record.status === 'blocked' ? 2 : 0;
252
298
  } catch (error) {
253
- console.error(`[fable-mode/model-selector] ${error.message}`);
299
+ console.error('[fable-mode/model-selector] ' + error.message);
254
300
  return 2;
255
301
  }
256
302
  }
257
303
 
258
304
  if (require.main === module) process.exitCode = main();
259
305
 
260
- module.exports = { loadMatrix, normalizeModel, parseArgs, resolveMode, main };
306
+ module.exports = { loadBehaviorMatrix, loadMatrix: loadBehaviorMatrix, loadRuntimeFloors, normalizeProfile, normalizeModel: normalizeProfile, parseArgs, evaluateRuntimeFloor, resolveMode, main };
@@ -272,7 +272,7 @@ function prepareRun({ routerContract, stages, workspaceRoot, runId, sessionId =
272
272
  verification: plan.verification,
273
273
  workspace: plan.workspace,
274
274
  memory: plan.memory,
275
- modelSelection: plan.modelSelection,
275
+ profileSelection: plan.profileSelection,
276
276
  limits: plan.limits,
277
277
  },
278
278
  stageIds: normalizedStages.map(stage => stage.stageId),
@@ -4,35 +4,32 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.30.3
7
+ version: 0.30.6
8
8
  ---
9
9
 
10
10
  # Harness Everything
11
11
 
12
- Kernel routing entry.
13
-
14
12
  ## USE FOR:
15
13
  - Software/project triage, routing, re-routing, workflow selection.
16
- - Work that names or matches another skill.
14
+ - Work naming or matching another skill.
17
15
 
18
16
  ## DO NOT USE FOR:
19
17
  - General Q&A or non-software writing.
20
18
 
21
19
  ## Invariants
22
- 1. **Route (MUST)** — establish scope/tier and selected/deferred topology.
23
- 2. **Verify (MUST)** — completion claims need objective evidence; `verify-gate.js --json` distinguishes passed, failed, discovered-but-unrun, and absent project checks.
20
+ 1. **Route (MUST)** — scope/tier plus selected/deferred topology.
21
+ 2. **Verify (MUST)** — objective evidence via `verify-gate.js --json`.
24
22
  3. **Zoom out (MUST)** — after 3 same-signature failures, use `zoom-out`.
25
- 4. **Resolve suggestions (MUST)** — read/evaluate each suggested `SKILL.md`; applicable core contracts MUST run, otherwise retain a flow-grounded reason.
26
- 5. **Surface status (MUST)** — non-trivial work renders `### 🚦 Harness Status` with bold `Current`, `Read / Evidence`, `Next`; optional `Risk / Blocked`.
27
- 6. **Keep agency** — required obligations are MUST; tactics MAY adapt. Numeric planning values MAY guide, never hard-stop.
23
+ 4. **Resolve suggestions (MUST)** — evaluate each suggested `SKILL.md`; run applicable core contracts or retain a flow-grounded reason.
24
+ 5. **Surface status (MUST)** — non-trivial work renders `### 🚦 Harness Status` with bold `Current`, `Read / Evidence`, `Next`; `Risk / Blocked` optional.
25
+ 6. **Keep agency** — required obligations are MUST; tactics MAY adapt. Numeric planning MAY guide, never hard-stop; only Rule-of-3 reflection and explicit user/host permission boundaries may block.
26
+ 7. **Windows PowerShell (MUST)** — `Get-Content -Encoding UTF8 -Raw`; npm `.cmd` shims (`npx.cmd`); never weaken `ExecutionPolicy`.
28
27
 
29
28
  ## Workflow
30
29
  1. Reuse kernel output or run `node "<this-skill-dir>/scripts/kernel-router.js" "<prompt summary>"`.
31
30
  2. Keep the routing checkpoint internal.
32
- 3. Resolve each suggestion as `use`, `not-applicable`, or `unresolved`; `use` follows its core contract.
33
- 4. Resolve selected-topology required invariants/stages/checks before completion.
34
- 5. Tier-3/Fable broad mutation MUST resolve isolation: linked worktree or explicit degraded fallback.
35
-
36
- **Rule:** semantic MUST is not hard blocking; only Rule-of-3 reflection and explicit user/host permission boundaries may block.
31
+ 3. Resolve suggestions as `use`, `not-applicable`, or `unresolved`.
32
+ 4. Complete selected-topology invariants/stages/checks.
33
+ 5. Tier-3/Fable broad mutation MUST use a linked worktree or explicit degraded fallback.
37
34
 
38
35
  Deep dive: <this-skill-dir>/references/triage-and-tiers.md
@@ -68,7 +68,7 @@ The Phase 2 `iterative-single` plan exposes **8 iterations** as advisory plannin
68
68
  The task-shape contract records:
69
69
 
70
70
  - an explicit requested strategy, when named;
71
- - an explicit Fable model request, still delegated to `fable-mode/scripts/model-selector.js`;
71
+ - an explicit Fable behavior-profile request; legacy aliases `haiku`/`sonnet`/`opus` are delegated to `fable-mode/scripts/model-selector.js`;
72
72
  - prohibitions such as no Fable, no subagents, no parallel execution, or no workspace.
73
73
 
74
74
  Explicit choices override derived topology. Contradictory choice + prohibition is visible as a blocked fallback rather than silently guessing which instruction to ignore.
@@ -82,7 +82,7 @@ Examples:
82
82
  - missing parallel calls or `concurrency: 1` serializes `fable-parallel` to `fable-staged` with a `reduced/serialized` fallback;
83
83
  - unavailable subagents may reduce staged execution to visible inline execution;
84
84
  - missing durable state for a required multi-agent workspace is `blocked`, not silently downgraded;
85
- - a requested Fable model with known unavailable model capability is `blocked`; the router never substitutes another branded model;
85
+ - Fable profile choice is independent of runtime-model availability; host-specific model floors are advisory and recorded by Fable rather than blocking routing;
86
86
  - a required `actionGate` with known unavailable hook capability is `blocked`.
87
87
 
88
88
  Fallbacks carry deterministic reason codes.
@@ -56,10 +56,11 @@
56
56
  },
57
57
  "explicitRequest": {
58
58
  "type": "object",
59
- "required": ["fableModel", "strategy", "prohibitions"],
59
+ "required": ["fableProfile", "fableAlias", "strategy", "prohibitions"],
60
60
  "additionalProperties": false,
61
61
  "properties": {
62
- "fableModel": { "enum": ["haiku", "sonnet", "opus", null] },
62
+ "fableProfile": { "enum": ["mechanical", "reasoning", "orchestrator", null] },
63
+ "fableAlias": { "enum": ["mechanical", "reasoning", "orchestrator", "haiku", "sonnet", "sonnect", "opus", null] },
63
64
  "strategy": {
64
65
  "enum": ["direct-single", "iterative-single", "fable-staged", "fable-parallel", "fable-multi-agent-workspace", null]
65
66
  },
@@ -19,7 +19,7 @@
19
19
  "memory",
20
20
  "verification",
21
21
  "ensemble",
22
- "modelSelection",
22
+ "profileSelection",
23
23
  "fallback",
24
24
  "reasonCodes"
25
25
  ],
@@ -120,13 +120,14 @@
120
120
  }
121
121
  ]
122
122
  },
123
- "modelSelection": {
123
+ "profileSelection": {
124
124
  "type": "object",
125
- "required": ["requested", "policy"],
125
+ "required": ["requested", "alias", "policy"],
126
126
  "additionalProperties": false,
127
127
  "properties": {
128
- "requested": { "enum": ["haiku", "sonnet", "opus", null] },
129
- "policy": { "enum": ["defer-to-fable-selector"] }
128
+ "requested": { "enum": ["mechanical", "reasoning", "orchestrator", null] },
129
+ "alias": { "enum": ["mechanical", "reasoning", "orchestrator", "haiku", "sonnet", "sonnect", "opus", null] },
130
+ "policy": { "enum": ["behavior-profile-with-advisory-runtime-floor"] }
130
131
  }
131
132
  },
132
133
  "fallback": {
@@ -59,13 +59,23 @@ function normalizeConstraints(raw = {}) {
59
59
  };
60
60
  }
61
61
 
62
+ function normalizeFableProfile(value) {
63
+ const input = String(value || '').trim().toLowerCase();
64
+ if (input === 'mechanical' || input === 'haiku') return { profile: 'mechanical', alias: input };
65
+ if (input === 'reasoning' || input === 'sonnet' || input === 'sonnect') return { profile: 'reasoning', alias: input };
66
+ if (input === 'orchestrator' || input === 'opus') return { profile: 'orchestrator', alias: input };
67
+ return { profile: null, alias: null };
68
+ }
69
+
62
70
  function normalizeExplicitRequest(raw = {}) {
63
71
  const requestedStrategy = STRATEGIES.has(raw.strategy) ? raw.strategy : null;
64
72
  const prohibitions = Array.isArray(raw.prohibitions)
65
73
  ? raw.prohibitions.filter(value => PROHIBITIONS.has(value))
66
74
  : [];
75
+ const fable = normalizeFableProfile(raw.fableAlias || raw.fableProfile || raw.fableModel);
67
76
  return {
68
- fableModel: ['haiku', 'sonnet', 'opus'].includes(raw.fableModel) ? raw.fableModel : null,
77
+ fableProfile: fable.profile,
78
+ fableAlias: fable.alias,
69
79
  strategy: requestedStrategy,
70
80
  prohibitions: Array.from(new Set(prohibitions)),
71
81
  };
@@ -77,7 +87,7 @@ function buildTaskShape(input = {}) {
77
87
  const hostCapabilities = normalizeHostCapabilities(input.hostCapabilities);
78
88
  const constraints = normalizeConstraints(input.constraints);
79
89
  const explicitRequest = normalizeExplicitRequest(input.explicitRequest || {
80
- fableModel: input.requestedFableModel,
90
+ fableProfile: input.requestedFableProfile || input.requestedFableModel,
81
91
  });
82
92
 
83
93
  let scopeSize = 'unknown';
@@ -299,11 +309,6 @@ function selectWorkflowStrategy(input = {}) {
299
309
  fallback.reasonCodes.push(prohibitions.includes('subagents') ? 'user-prohibited-subagents' : 'subagents-unavailable');
300
310
  }
301
311
 
302
- if (explicit.fableModel && taskShape.hostCapabilities.modelAvailability === 'unavailable') {
303
- fallback.disposition = 'blocked';
304
- fallback.mode = 'blocked';
305
- fallback.reasonCodes.push('requested-model-capability-unavailable');
306
- }
307
312
  }
308
313
 
309
314
  return {
@@ -494,9 +499,10 @@ function buildWorkflowPlan(input = {}) {
494
499
  independent: meta.verificationIndependent,
495
500
  },
496
501
  ensemble: null,
497
- modelSelection: {
498
- requested: taskShape.explicitRequest.fableModel,
499
- policy: 'defer-to-fable-selector',
502
+ profileSelection: {
503
+ requested: taskShape.explicitRequest.fableProfile,
504
+ alias: taskShape.explicitRequest.fableAlias,
505
+ policy: 'behavior-profile-with-advisory-runtime-floor',
500
506
  },
501
507
  fallback,
502
508
  reasonCodes,
@@ -507,7 +513,7 @@ function buildRouterContract(input = {}) {
507
513
  const tier = normalizeTierLabel(input.recommendedTier);
508
514
  const reasonCodes = uniqueReasonCodes(input.reasonCodes);
509
515
  const explicitRequest = normalizeExplicitRequest(input.explicitRequest || {
510
- fableModel: input.requestedFableModel,
516
+ fableProfile: input.requestedFableProfile || input.requestedFableModel,
511
517
  });
512
518
  const taskShape = buildTaskShape({
513
519
  tier,
@@ -546,7 +552,7 @@ function createDegradedRouterContract(reasonCode, message) {
546
552
  rationale: message || 'Structured router contract unavailable.',
547
553
  reasonCodes: [reasonCode || 'router-contract-unavailable'],
548
554
  signals: {},
549
- requestedFableModel: null,
555
+ requestedFableProfile: null,
550
556
  });
551
557
  }
552
558
 
@@ -624,6 +630,7 @@ module.exports = {
624
630
  createDegradedRouterContract,
625
631
  normalizeConstraints,
626
632
  normalizeExplicitRequest,
633
+ normalizeFableProfile,
627
634
  normalizeHostCapabilities,
628
635
  normalizeTierLabel,
629
636
  selectWorkflowStrategy,