harness-everything 0.30.5 → 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 (67) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +23 -0
  4. package/ci/fable-mode-test.js +112 -51
  5. package/ci/mechanism-2t-workspace-identity.test.js +3 -1
  6. package/ci/mechanism-2z-claude-plugin-manifest.test.js +12 -0
  7. package/ci/router-workflow-plan.test.js +92 -7
  8. package/docs/platform-capabilities.md +8 -0
  9. package/docs/workflows/fable-mode.md +10 -9
  10. package/evals/fable-mode/tasks/negative-tier2-no-model.yaml +3 -3
  11. package/evals/fable-mode/tasks/positive-haiku-mode.yaml +1 -1
  12. package/evals/fable-mode/tasks/positive-opus-mode.yaml +1 -1
  13. package/evals/fable-mode/tasks/positive-sonnet-mode.yaml +1 -1
  14. package/evals/fable-mode/tasks/positive-trigger.yaml +1 -1
  15. package/fable-mode/SKILL.md +18 -12
  16. package/fable-mode/agents/fable-orchestrator.md +7 -6
  17. package/fable-mode/agents/fable-verifier.md +1 -1
  18. package/fable-mode/agents/fable-worker-haiku.md +5 -4
  19. package/fable-mode/agents/fable-worker-sonnet.md +5 -4
  20. package/fable-mode/behavior-profile-matrix.json +36 -0
  21. package/fable-mode/execution-guardrails/SKILL.md +3 -6
  22. package/fable-mode/execution-guardrails/references/guardrail-rules.md +8 -9
  23. package/fable-mode/fable-haiku/SKILL.md +7 -5
  24. package/fable-mode/fable-haiku/references/routing-notes.md +2 -1
  25. package/fable-mode/fable-opus/SKILL.md +8 -6
  26. package/fable-mode/fable-sonnet/SKILL.md +8 -6
  27. package/fable-mode/model-matrix.json +7 -30
  28. package/fable-mode/references/model-matrix.md +35 -30
  29. package/fable-mode/references/profile-lookup.md +19 -0
  30. package/fable-mode/runtime-model-floor-matrix.json +47 -0
  31. package/fable-mode/scripts/model-selector.js +106 -60
  32. package/fable-mode/scripts/workflow-plan-consumer.js +1 -1
  33. package/harness-everything/SKILL.md +1 -1
  34. package/harness-everything/references/router-workflow-plan.md +2 -2
  35. package/harness-everything/schemas/router-task-shape.schema.json +3 -2
  36. package/harness-everything/schemas/router-workflow-plan.schema.json +6 -5
  37. package/harness-everything/scripts/router-contract-core.js +19 -12
  38. package/harness-everything/scripts/tier-router.js +100 -12
  39. package/opencode-plugin/plugin.json +1 -1
  40. package/package.json +1 -1
  41. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  42. package/plugins/harness-everything/plugin.json +1 -1
  43. package/plugins/harness-everything/skills/fable-mode/SKILL.md +18 -12
  44. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +7 -6
  45. package/plugins/harness-everything/skills/fable-mode/agents/fable-verifier.md +1 -1
  46. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-haiku.md +5 -4
  47. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-sonnet.md +5 -4
  48. package/plugins/harness-everything/skills/fable-mode/behavior-profile-matrix.json +36 -0
  49. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +3 -6
  50. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/references/guardrail-rules.md +8 -9
  51. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +7 -5
  52. package/plugins/harness-everything/skills/fable-mode/fable-haiku/references/routing-notes.md +2 -1
  53. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +8 -6
  54. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +8 -6
  55. package/plugins/harness-everything/skills/fable-mode/model-matrix.json +7 -30
  56. package/plugins/harness-everything/skills/fable-mode/references/model-matrix.md +35 -30
  57. package/plugins/harness-everything/skills/fable-mode/references/profile-lookup.md +19 -0
  58. package/plugins/harness-everything/skills/fable-mode/runtime-model-floor-matrix.json +47 -0
  59. package/plugins/harness-everything/skills/fable-mode/scripts/model-selector.js +106 -60
  60. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +1 -1
  61. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  62. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +2 -2
  63. package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +3 -2
  64. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +6 -5
  65. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +19 -12
  66. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +100 -12
  67. package/sbom.cdx.json +7 -7
@@ -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,7 +4,7 @@ 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.5
7
+ version: 0.30.6
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -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,
@@ -46,11 +46,88 @@ function matchKeyword(prompt, keyword) {
46
46
  return new RegExp(`\\b${escaped}\\b`, 'i').test(prompt);
47
47
  }
48
48
 
49
+ const FABLE_PROFILE_INVOCATION = /\bfable(?:[- ]mode)?\s+(?:on|with)\s+(haiku|sonnet|sonnect|opus)\b|\bfable-(haiku|sonnet|opus)\b/i;
50
+ const PROFILE_LOOKUP_FIELDS = /\b(?:requestedProfile|effectiveProfile|profileAlias|assignedRole|runtimeModel|runtimeEffort)\b/gi;
51
+
49
52
  function detectFableModel(prompt) {
50
- const match = prompt.match(/\bfable(?:[- ]mode)?\s+(?:on|with)\s+(haiku|sonnet|sonnect|opus)\b|\bfable-(haiku|sonnet|opus)\b/i);
53
+ const match = prompt.match(FABLE_PROFILE_INVOCATION);
51
54
  if (!match) return null;
52
- const requested = (match[1] || match[2]).toLowerCase();
53
- return requested === 'sonnect' ? 'sonnet' : requested;
55
+ return (match[1] || match[2]).toLowerCase();
56
+ }
57
+
58
+ // A profile mention is data in a lookup, not an instruction to start Fable.
59
+ // Validate the whole clause, not just its prefix. Unknown prose conservatively
60
+ // retains execution routing; format field lists are recognized separately.
61
+ function containsOnlyLookupTerms(text, operation) {
62
+ const terms = {
63
+ explanation: /\b(?:please|help|me|explain|resolve|define|what|is|does|mean|means|meaning|the|a|an|of|to|into|in|for|behavior|behaviour|profile|profiles|selection|json|record|alias|role|runtime|model|effort|floor|fableprofile)\b/gi,
64
+ output: /\b(?:return|respond|output|show|provide|only|a|an|small|compact|json|record|selection|profile|result|with|fields|containing|include|including)\b/gi,
65
+ };
66
+ const chineseTerms = {
67
+ explanation: /(?:請|幫我|解釋|說明|解析|的意思|意思|含義|意義|是什麼|設定檔|行為|角色)/g,
68
+ output: /(?:只|僅|回傳|返回|輸出|顯示|提供|包含|含|的|欄位|記錄|紀錄|結果|設定檔)/g,
69
+ };
70
+ return text.replace(PROFILE_LOOKUP_FIELDS, '').replace(terms[operation], '')
71
+ .replace(chineseTerms[operation], '').replace(/[\s"'`“”‘’?::?]/g, '') === '';
72
+ }
73
+
74
+ // A negation scopes only its own noun/verb list. Vocabulary-only matching would
75
+ // let "no delegation, do execution" pass, so English negations follow a grammar
76
+ // and an un-negated continuation may only extend the noun list. Chinese nouns
77
+ // and verbs share forms, so Chinese lists must stay inside the negated clause.
78
+ const negatedList = unit => `${unit}(?:\\s+or\\s+${unit})*`;
79
+ const NEGATED_ITEM = '(?:orchestration|delegation|stages?|staging|execution|model[- ]switch(?:ing)?)';
80
+ const NEGATED_ACTION = '(?:switch(?:ing)?\\s+(?:the\\s+)?models?|delegate|orchestrate|stage|execute)';
81
+ const ENGLISH_NEGATION = new RegExp(`^(?:(?:no|without)\\s+${negatedList(NEGATED_ITEM)}|(?:do not|don't)\\s+${negatedList(NEGATED_ACTION)})$`, 'i');
82
+ const ENGLISH_NEGATION_CONTINUATION = new RegExp(`^(?:or\\s+)?${negatedList(NEGATED_ITEM)}$`, 'i');
83
+ const CHINESE_NEGATION_TERMS = /(?:不要|不需要|無需|不|或|編排|委派|階段|執行|切換模型)/g;
84
+
85
+ function isBoundedNegation(clause, continuation) {
86
+ const text = clause.replace(/’/g, "'").replace(/[“”"`?::?]/g, '').replace(/\s+/g, ' ').trim();
87
+ if (/^(?:不要|不需要|無需|不)/.test(text)) {
88
+ return !continuation && text.replace(CHINESE_NEGATION_TERMS, '').replace(/\s/g, '') === '';
89
+ }
90
+ return (continuation ? ENGLISH_NEGATION_CONTINUATION : ENGLISH_NEGATION).test(text);
91
+ }
92
+
93
+ function isFableProfileLookup(prompt, hasMacroSignal) {
94
+ if (!detectFableModel(prompt) || hasMacroSignal) return false;
95
+ if (/\b(?:direct[- ]single|iterative[- ]single|fable[- ](?:staged|parallel|multi[- ]agent[- ]workspace))\b/i.test(prompt)) return false;
96
+
97
+ const clauses = prompt
98
+ .split(/(?:[;,.,;。、]|\b(?:and|then|also|plus|afterwards|additionally)\b|(?:並且?|以及|接著|然後|另外))/i)
99
+ .map(part => part.trim())
100
+ .filter(Boolean);
101
+ if (clauses.length === 0) return false;
102
+
103
+ const [lookupClause, ...continuations] = clauses;
104
+ const quotedInvocation = new RegExp(`(["'\x60])(?:run|use|enter)\\s+(?:${FABLE_PROFILE_INVOCATION.source})\\1`, 'gi');
105
+ const normalizedLookup = lookupClause.replace(/[“”]/g, '"').replace(/[‘’]/g, "'")
106
+ .replace(quotedInvocation, 'fableprofile')
107
+ .replace(new RegExp(FABLE_PROFILE_INVOCATION.source, 'gi'), 'fableprofile');
108
+ const isLookupLead = /^(?:(?:please|help me)\s+|(?:請|幫我)\s*)?(?:explain|resolve|define|what (?:is|does)|解釋|說明|解析)/i.test(lookupClause)
109
+ && Boolean(detectFableModel(lookupClause));
110
+ if (!isLookupLead || !containsOnlyLookupTerms(normalizedLookup, 'explanation')) return false;
111
+
112
+ let continuationOperation = null;
113
+ for (const clause of continuations) {
114
+ const output = /^(?:please\s+)?(?:return|respond|output|show|provide)\b|^(?:請\s*)?(?:只|僅)?(?:回傳|返回|輸出|顯示|提供)/i.test(clause);
115
+ const negative = /^(?:please\s+)?(?:no|without|do not|don't)\b|^(?:請\s*)?(?:不要|不需要|無需|不)/i.test(clause);
116
+ if (output || negative) {
117
+ continuationOperation = output ? 'output' : 'negative';
118
+ const normalizedContinuation = clause.replace(/^(?:please\s+|請\s*)/i, '');
119
+ const bounded = output ? containsOnlyLookupTerms(normalizedContinuation, 'output')
120
+ : isBoundedNegation(normalizedContinuation, false);
121
+ if (!bounded) return false;
122
+ } else if (continuationOperation === 'output') {
123
+ // Only named canonical fields may continue a comma/and-separated list.
124
+ const remainder = clause.replace(PROFILE_LOOKUP_FIELDS, '').replace(/^(?:包含|欄位)/, '').replace(/[\s"'`“”‘’]/g, '');
125
+ if (remainder || !new RegExp(PROFILE_LOOKUP_FIELDS.source, 'i').test(clause)) return false;
126
+ } else if (continuationOperation !== 'negative' || !isBoundedNegation(clause, true)) {
127
+ return false;
128
+ }
129
+ }
130
+ return true;
54
131
  }
55
132
 
56
133
  function detectExplicitStrategy(prompt, requestedFableModel) {
@@ -250,6 +327,7 @@ function run(userPrompt, context, options = {}) {
250
327
  /(?:評估|稽核|基準|壓力測試|比較).*(?:每個|所有|全部|整個|全套|技能|skill|檔案|版本)/i,
251
328
  ];
252
329
  const hasMacroSignal = macroSignals.some(signal => signal.test(userPrompt));
330
+ const profileLookup = isFableProfileLookup(userPrompt, hasMacroSignal);
253
331
  const hasTrivialEditVerb = /^(?:(?:please|help me)\s+)?(?:fix|update|correct|change)\b/i.test(userPrompt.trim());
254
332
  const hasDocsTarget = /\b(?:readme|documentation|docs?)\b/i.test(userPrompt);
255
333
  const hasTinyEditSignal = /\b(?:typo|spelling|wording|one line|single line)\b/i.test(userPrompt);
@@ -262,7 +340,12 @@ function run(userPrompt, context, options = {}) {
262
340
  let rationale = 'No structural/testing signals matched; unclassified is not equivalent to trivial.';
263
341
  const reasonCodes = [...loadedConfig.reasonCodes, 'no-classification-signal'];
264
342
 
265
- if (memoryPersistenceRequested && !hasMacroSignal && !hasTier3Keyword && !hasTier2Keyword) {
343
+ if (profileLookup) {
344
+ recommendedTier = 'Tier 1 (Trivial)';
345
+ rationale = 'Bounded behavior-profile lookup; select compact reference before execution.';
346
+ reasonCodes.splice(loadedConfig.reasonCodes.length);
347
+ addReason(reasonCodes, 'fable-profile-lookup');
348
+ } else if (memoryPersistenceRequested && !hasMacroSignal && !hasTier3Keyword && !hasTier2Keyword) {
266
349
  recommendedTier = 'Tier 1 (Trivial)';
267
350
  rationale = 'Explicit bounded self-evolve memory persistence request.';
268
351
  reasonCodes.splice(loadedConfig.reasonCodes.length);
@@ -292,7 +375,7 @@ function run(userPrompt, context, options = {}) {
292
375
  const hasQuestionMarks = /\?|?/.test(userPrompt);
293
376
  const hasMultipleSentences = sentenceCount > 2;
294
377
 
295
- if (!hasMacroSignal && !isTrivialDocsEdit && hasMultipleTasks && hasMultipleSentences) {
378
+ if (!profileLookup && !hasMacroSignal && !isTrivialDocsEdit && hasMultipleTasks && hasMultipleSentences) {
296
379
  if (recommendedTier === 'Unclassified' || recommendedTier.startsWith('Tier 1')) {
297
380
  recommendedTier = 'Tier 2 (Standard Task)';
298
381
  rationale = 'Multiple tasks detected with structural complexity - upgraded to Tier 2 for TDD validation.';
@@ -320,7 +403,7 @@ function run(userPrompt, context, options = {}) {
320
403
  const structuralFloor = hasMacroSignal ? 'tier3'
321
404
  : !isTrivialDocsEdit && hasMultipleTasks && hasMultipleSentences ? 'tier2' : null;
322
405
  const semantic = selectTier({ prompt: userPrompt, tier: recommendedTier, floor: structuralFloor,
323
- explicit: Boolean(detectExplicitStrategy(userPrompt, detectFableModel(userPrompt))) }, process.env, options.systemOneScorer || undefined);
406
+ explicit: profileLookup || Boolean(detectExplicitStrategy(userPrompt, detectFableModel(userPrompt))) }, process.env, options.systemOneScorer || undefined);
324
407
  if (semantic.diagnostic) console.log(`\n=> SYSTEM ONE: ${JSON.stringify(semantic.diagnostic)}`);
325
408
  if (semantic.diagnostic?.applied) {
326
409
  recommendedTier = semantic.tier;
@@ -334,13 +417,18 @@ function run(userPrompt, context, options = {}) {
334
417
 
335
418
  const requestedFableModel = detectFableModel(promptLower);
336
419
  if (requestedFableModel) {
337
- console.log(`\n=> REQUESTED FABLE MODEL MODE: ${requestedFableModel}`);
338
- console.log(`=> ROUTE: fable-mode/fable-${requestedFableModel}/SKILL.md`);
339
- console.log(` Resolve availability and record fallback status with fable-mode/scripts/model-selector.js.`);
420
+ console.log(`\n=> REQUESTED FABLE PROFILE: ${requestedFableModel}`);
421
+ console.log(`=> ROUTE: fable-mode/SKILL.md`);
422
+ if (profileLookup) {
423
+ console.log('=> FABLE OPERATION: profile-lookup');
424
+ console.log('=> REFERENCES: fable-mode/references/profile-lookup.md only; no stage state or delegation.');
425
+ } else {
426
+ console.log(` Profile alias: ${requestedFableModel}; resolve behavior role plus advisory host runtime floor with fable-mode/scripts/model-selector.js.`);
427
+ }
340
428
  }
341
429
 
342
430
  const allRecommendedGuides = [];
343
- for (const group of routingConfig.guideGroups) {
431
+ for (const group of profileLookup ? [] : routingConfig.guideGroups) {
344
432
  let matched = false;
345
433
  if (typeof group.regex === 'string') {
346
434
  matched = new RegExp(group.regex, 'i').test(promptLower);
@@ -366,7 +454,7 @@ function run(userPrompt, context, options = {}) {
366
454
  });
367
455
  }
368
456
 
369
- emitDynamicSkills(promptLower, context, recommendedGuides);
457
+ if (!profileLookup) emitDynamicSkills(promptLower, context, recommendedGuides);
370
458
 
371
459
  const externalClaimTriggers = routingConfig.factAudit.externalClaim || [];
372
460
  const estimateTriggers = routingConfig.factAudit.estimate || [];
@@ -397,7 +485,7 @@ function run(userPrompt, context, options = {}) {
397
485
  const actionGateReasonCodes = detectActionGateReasons(userPrompt);
398
486
  const irreversibleAction = actionGateReasonCodes.includes('irreversible-action');
399
487
  const externalSideEffect = actionGateReasonCodes.includes('external-side-effect');
400
- const requestedStrategy = detectExplicitStrategy(userPrompt, requestedFableModel);
488
+ const requestedStrategy = profileLookup ? null : detectExplicitStrategy(userPrompt, requestedFableModel);
401
489
  const prohibitions = detectProhibitions(userPrompt);
402
490
  const plannerInputs = plannerInputsFromContext(context, promptLower);
403
491
 
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:681ee313-d384-5851-8f3a-9ec7af56fc28",
5
+ "serialNumber": "urn:uuid:284476ec-2aa6-5bc2-883b-7a9196027356",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-06T03:46:28.900Z",
8
+ "timestamp": "2026-10-06T16:15:39.747Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.30.5"
13
+ "version": "0.30.6"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.30.5",
18
+ "bom-ref": "pkg:npm/harness-everything@0.30.6",
19
19
  "name": "harness-everything",
20
- "version": "0.30.5",
21
- "purl": "pkg:npm/harness-everything@0.30.5"
20
+ "version": "0.30.6",
21
+ "purl": "pkg:npm/harness-everything@0.30.6"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -1999,7 +1999,7 @@
1999
1999
  ]
2000
2000
  },
2001
2001
  {
2002
- "ref": "pkg:npm/harness-everything@0.30.5",
2002
+ "ref": "pkg:npm/harness-everything@0.30.6",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",