@1aboveio/skills 1.0.15 → 1.0.16

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 (90) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +149 -145
  3. package/package.json +4 -4
  4. package/skills/app-dev/airflow/SKILL.md +1 -1
  5. package/skills/app-dev/android-development/SKILL.md +28 -1
  6. package/skills/app-dev/android-development/references/diagnosis.md +50 -0
  7. package/skills/app-dev/android-development/references/init.md +31 -2
  8. package/skills/app-dev/android-development/references/setup.md +8 -2
  9. package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
  10. package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
  11. package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
  12. package/skills/app-dev/android-development/scripts/init.mjs +129 -9
  13. package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
  14. package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
  15. package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
  16. package/skills/app-dev/app-debug/SKILL.md +1 -1
  17. package/skills/app-dev/better-auth/SKILL.md +1 -1
  18. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  19. package/skills/app-dev/data-quality/SKILL.md +1 -1
  20. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  21. package/skills/app-dev/monorepo/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/pyspark/SKILL.md +1 -1
  25. package/skills/app-dev/python-backend/SKILL.md +1 -1
  26. package/skills/app-dev/shadcn/SKILL.md +1 -1
  27. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  28. package/skills/cicd/cloud-build/SKILL.md +1 -1
  29. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  30. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  31. package/skills/cicd/docker/SKILL.md +1 -1
  32. package/skills/cicd/github-actions/SKILL.md +1 -1
  33. package/skills/cicd/github-flow/SKILL.md +1 -1
  34. package/skills/cicd/google-cloud/SKILL.md +1 -1
  35. package/skills/cicd/mergify/SKILL.md +1 -1
  36. package/skills/cicd/podman/SKILL.md +1 -1
  37. package/skills/content/article-review/SKILL.md +1 -1
  38. package/skills/content/article-to-image/SKILL.md +1 -1
  39. package/skills/content/flowchart-svg/SKILL.md +1 -1
  40. package/skills/content/image-gen/SKILL.md +1 -1
  41. package/skills/content/mermaid/SKILL.md +1 -1
  42. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  43. package/skills/content/wechat-publisher/SKILL.md +1 -1
  44. package/skills/content/wechat-search/SKILL.md +1 -1
  45. package/skills/content/xai-search/SKILL.md +1 -1
  46. package/skills/core/distill-writing/SKILL.md +1 -1
  47. package/skills/core/doc-to-md/SKILL.md +1 -1
  48. package/skills/core/editorial-review/SKILL.md +1 -1
  49. package/skills/core/house-style/SKILL.md +1 -1
  50. package/skills/core/plain-language/SKILL.md +1 -1
  51. package/skills/core/polish-cn/SKILL.md +1 -1
  52. package/skills/core/reg-parser/SKILL.md +1 -1
  53. package/skills/core/terminology/SKILL.md +1 -1
  54. package/skills/core/write-doc/SKILL.md +1 -1
  55. package/skills/core/write-report/SKILL.md +1 -1
  56. package/skills/engineering/better-review/SKILL.md +1 -1
  57. package/skills/engineering/e2e-test/SKILL.md +1 -1
  58. package/skills/engineering/engineering/SKILL.md +1 -1
  59. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  60. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  61. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  62. package/skills/engineering/golden-path/SKILL.md +1 -1
  63. package/skills/engineering/harness-runtime/SKILL.md +4 -3
  64. package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
  65. package/skills/engineering/harness-runtime/contracts.md +2 -0
  66. package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
  67. package/skills/engineering/resolve-issues/SKILL.md +9 -6
  68. package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
  69. package/skills/engineering/resolve-issues/references/expire.md +6 -3
  70. package/skills/engineering/resolve-issues/references/implementation.md +6 -1
  71. package/skills/engineering/resolve-issues/references/preflight.md +26 -17
  72. package/skills/engineering/resolve-issues/references/review.md +21 -9
  73. package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
  74. package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
  75. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
  76. package/skills/engineering/resolve-release/SKILL.md +1 -1
  77. package/skills/engineering/rush-issues/SKILL.md +4 -4
  78. package/skills/engineering/rush-release/SKILL.md +1 -1
  79. package/skills/engineering/test-quality/SKILL.md +1 -1
  80. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  81. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  82. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  83. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  84. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  85. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  86. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  87. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  88. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  89. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  90. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
@@ -1,185 +1,105 @@
1
1
  #!/usr/bin/env node
2
- // resolve-issues fast path for model selection.
2
+ // resolve-issues preflight for model slots.
3
3
  //
4
- // One command: discover → tier (shared harness-runtime catalog) → recommend
5
- // implementer + fill explorer + fill diagnoser + fill reviewer ≠ implementer. Prints a ready
6
- // ask payload the orchestrator hands straight to the harness's native question
7
- // tool (see references/shared-modules.md) without reading longer workflow
8
- // doctrine before model selection.
4
+ // A role request, not a model ladder. The orchestrator names the four worker
5
+ // roles; the harness-owned agent catalog decides which model fills each one and
6
+ // how that role is launched on this host. This script is the workflow-shaped
7
+ // adapter over `harness-runtime/agent-catalog.mjs`: it builds the capabilities
8
+ // request, resolves it, and prints the resolved fills (see
9
+ // references/shared-modules.md).
9
10
  //
10
- // Maintenance (sync / bank) is deferred until AFTER the human answers — never
11
- // block the picker on catalog writes.
11
+ // The fills are run metadata, never a question. This script resolves and
12
+ // validates the roles; the human approves the target branch, merge mode,
13
+ // scheduler mode and plan, and names an implementer only as an explicit
14
+ // override (`--implementer`).
15
+ //
16
+ // The skill owns no ranking and no classification. Nothing here reads
17
+ // `model-catalog.mjs`, ranks capability bands, or copies a resolved default into
18
+ // a Pi launch: on Pi a role is launched by role, and only something that
19
+ // explicitly asked for a model or thinking level names one — the human's task
20
+ // override (`--implementer`, or an override in the capabilities file) or the
21
+ // host's own preference, which the catalog reports as an explicit selection.
22
+ // A Codex slot must carry the adapter's native model and effort, and the host
23
+ // must advertise the launch controls, or the run is blocked — the tool never
24
+ // claims it can set a model it has no payload for.
25
+ //
26
+ // A missing resolver, a missing capabilities document, or a declined request is
27
+ // an explicit blocked result. There is no fallback to the old picker.
12
28
  //
13
29
  // Usage:
14
- // node preflight-models.mjs --harness <pi|codex|claude-code> [--risk routine|high]
15
- // [--available id,id,...] [--human-named id,id,...] [--implementer id[@effort]] [--ask-only]
30
+ // node preflight-models.mjs --harness <pi|codex|claude-code> --capabilities <capabilities.json>
31
+ // [--available id,id,...] [--foreground-model id] [--risk routine|high]
32
+ // [--implementer id[@effort]] [--cwd <dir>]
33
+ //
34
+ // --capabilities is the preferred input: it carries this turn's host evidence
35
+ // ({ available: [{ model, nativeModel?, supportedEfforts? }], foregroundModel,
36
+ // fallbackToForeground, launchCapabilities, hostPreferences, overrides, tiers,
37
+ // userRoot, projectTrusted }), including the providerless native ids a Codex slot
38
+ // needs and the controls this host actually advertises. --available is the
39
+ // shorthand for canonical ids alone.
40
+ //
41
+ // A native host (Codex, Claude Code) must advertise `launchCapabilities` with
42
+ // `model`, `instructions`, and — when the catalog resolves an effort — `effort`.
43
+ // A native id and an effort say what the catalog chose, never that the adapter
44
+ // can set it, so a missing control blocks the run instead of launching a slot
45
+ // this workflow cannot control.
16
46
  //
17
- // --implementer pins the implementer: it is the only option, and the run never
18
- // falls back to a recommended model when the pinned one is not available.
47
+ // --risk is recorded for the board; the catalog, not the risk word, chooses the
48
+ // model. --implementer pins the implementer through the catalog's explicit
49
+ // override; when the catalog declines it the run ends, and no other model is
50
+ // substituted.
19
51
  //
20
- // Exit: 0 whenever it ran (a result with `ok: false` and a `code` of `no-slot-eligible` or
21
- // `pinned-implementer-unavailable` is the answer, not a failure) · 2 usage or crash
52
+ // Exit: 0 whenever it ran (a result with `ok: false` is the answer, not a
53
+ // failure) · 2 usage or crash.
22
54
  import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
55
+ import { detectHarness } from '../../harness-runtime/discover-models.mjs'
23
56
  import {
24
- detectHarness,
25
- discover,
26
- } from '../../harness-runtime/discover-models.mjs'
27
- import {
28
- tierProposals,
29
- pointsFromTiers,
30
- isSlotEligible,
31
- DEFAULT_EFFORT,
32
- readCatalog,
33
- pathForHarness,
34
- readSeed,
35
- defaultSeedPath,
36
- findSeedEntry,
37
- } from '../../harness-runtime/model-catalog.mjs'
57
+ AGENT_CATALOG_BLOCK_CODES,
58
+ CATALOG_EFFORTS,
59
+ CATALOG_HARNESSES,
60
+ CATALOG_SLOT_ROLES,
61
+ CATALOG_SLOTS,
62
+ buildCatalogRequest,
63
+ normalizeCapabilities,
64
+ readCapabilitiesFile,
65
+ resolveAgentCatalog,
66
+ } from '../../harness-runtime/agent-catalog.mjs'
38
67
 
39
68
  export const RISK = Object.freeze(['routine', 'high'])
40
69
 
41
- // Preferred fills when discovery actually yielded the id (resolve-issues defaults).
42
- // Reviewer: the frontier band's home role is review (model-catalog.md). Since
43
- // 2026-09-23 the primary is gpt-6-astra@medium and the fallback (when the
44
- // implementer IS Astra — a human pick; resolve-issues never recommends frontier for
45
- // implementation) is gpt-5.6-sol@medium. Both are synthesized below-catalog
46
- // policy points (the catalog banks `high` per rule 6); the model ids are the
47
- // two frontier points by the official DeepSWE v1.1 board at high (73.2%±3.4,
48
- // 69.4%±1.4) and the *effort* comes from the local blinded review benchmark
49
- // (docs/research/2026-09-23-reviewer-benchmark-astra-vs-sol.md): Astra-medium
50
- // matched Sol-medium's useful-finding yield in under half the wall-clock time
51
- // and beat Astra-high on yield; Sol-high found three more at 3.5x the time.
52
- // Independence is by model id, never by effort.
53
- export const TEAM_REVIEWER_POLICY = Object.freeze({
54
- pi: {
55
- primary: { id: 'openai/gpt-6-astra', family: 'openai', effort: 'medium', tier: 'frontier' },
56
- fallback: { id: 'openai/gpt-5.6-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
57
- },
58
- codex: {
59
- primary: { id: 'gpt-6-astra', family: 'openai', effort: 'medium', tier: 'frontier' },
60
- fallback: { id: 'gpt-5.6-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
61
- },
62
- 'claude-code': {
63
- primary: { id: 'gpt-6-astra', family: 'openai', effort: 'medium', tier: 'frontier' },
64
- fallback: { id: 'gpt-5.6-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
65
- },
66
- })
67
-
68
- // Implementer ladders: ordered per risk and harness, first discovered point
69
- // wins. Order set by the team on 2026-09-23 over the DeepSWE v1.1 evidence in
70
- // the catalog notes (official board where a model has a run, vendor launch
71
- // figure otherwise). A frontier point reaches the recommendation only through
72
- // these ladders; the reviewer slot then switches to a distinct model id (an
73
- // Astra implementation is reviewed by gpt-5.6-sol@medium; every other rung by
74
- // gpt-6-astra@medium, a different family for the Grok/MiMo rungs).
75
- //
76
- // High risk (reordered 2026-09-30 by the team: gpt-6.1-sol@high first, 75.2
77
- // vendor at about a fifth of Astra's cost per task, then the rest unchanged):
78
- // gpt-6.1-sol@high → gpt-6-astra@high (73.2%±3.4 official, top of the board) →
79
- // gpt-5.6-sol@high (69.4%±1.4 official; slowest point in the local review
80
- // benchmark) → grok-4.7@high (71.0 vendor, $2/$6, not on the official board;
81
- // kept behind the two official frontier points until Datacurve runs it), over
82
- // gpt-5.5@high (64.4%±3.1, retiring from Codex 2026-10-14).
83
- //
84
- // Routine (reordered 2026-09-28 by the team: gpt-6-sol@high first, then
85
- // luna, grok-4.6, mimo; the evidence notes below are unchanged): gpt-6-luna@high (balanced-coder by human classification — OpenAI
86
- // published no high point, 66.6 vendor is at max; $0.10/$0.50. Pi smoke on one
87
- // small tool task: high 30s vs max 62s vs Terra@high 29s at ~1/20 the cost, so
88
- // high keeps Terra's latency at near-zero spend; max was too slow) →
89
- // grok-4.6@high (deep-reasoner; 65.2%±1.5 official, $2/$6) → mimo-v2.6-pro@high
90
- // (frontier; 71.9 vendor, $0.44/$0.87; MiMo's API has thinking on/off only, so
91
- // `high` labels that single point — behind grok-4.6 until an official run and
92
- // local trajectory evidence exist) → gpt-6-sol@high (deep-reasoner; 65.3 vendor;
93
- // replaced gpt-5.6-terra@high on 2026-09-27, ADR 0009). On 2026-09-30 every
94
- // gpt-6-sol rung became gpt-6.1-sol (75.2 vendor at high, frontier; Codex now
95
- // lists gpt-6-sol as previous generation), in the same positions. Only the OpenAI rungs
96
- // exist on Codex (it lists no Grok or MiMo ids). A rung discovery does not
97
- // yield is skipped; past the ladder the tier order applies.
98
- //
99
- // Escalation fix workers (test-first, last-try) always take the high-risk ladder
100
- // (gpt-6.1-sol@high first since 2026-09-30), whatever the run's risk (team
101
- // decision 2026-09-28).
102
- export const TEAM_IMPLEMENTER_PREFERENCE = Object.freeze({
103
- routine: {
104
- pi: [
105
- { id: 'openai/gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
106
- { id: 'openai/gpt-6-luna', family: 'openai', effort: 'high', tier: 'balanced-coder' },
107
- { id: 'xai/grok-4.6', family: 'xai', effort: 'high', tier: 'deep-reasoner' },
108
- { id: 'xiaomi/mimo-v2.6-pro', family: 'xiaomi', effort: 'high', tier: 'frontier' },
109
- ],
110
- codex: [
111
- { id: 'gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
112
- { id: 'gpt-6-luna', family: 'openai', effort: 'high', tier: 'balanced-coder' },
113
- ],
114
- 'claude-code': [
115
- { id: 'gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
116
- { id: 'gpt-6-luna', family: 'openai', effort: 'high', tier: 'balanced-coder' },
117
- { id: 'grok-4.6', family: 'xai', effort: 'high', tier: 'deep-reasoner' },
118
- { id: 'mimo-v2.6-pro', family: 'xiaomi', effort: 'high', tier: 'frontier' },
119
- ],
120
- },
121
- high: {
122
- pi: [
123
- { id: 'openai/gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
124
- { id: 'openai/gpt-6-astra', family: 'openai', effort: 'high', tier: 'frontier' },
125
- { id: 'openai/gpt-5.6-sol', family: 'openai', effort: 'high', tier: 'frontier' },
126
- { id: 'xai/grok-4.7', family: 'xai', effort: 'high', tier: 'frontier' },
127
- ],
128
- codex: [
129
- { id: 'gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
130
- { id: 'gpt-6-astra', family: 'openai', effort: 'high', tier: 'frontier' },
131
- { id: 'gpt-5.6-sol', family: 'openai', effort: 'high', tier: 'frontier' },
132
- ],
133
- 'claude-code': [
134
- { id: 'gpt-6.1-sol', family: 'openai', effort: 'high', tier: 'frontier' },
135
- { id: 'gpt-6-astra', family: 'openai', effort: 'high', tier: 'frontier' },
136
- { id: 'gpt-5.6-sol', family: 'openai', effort: 'high', tier: 'frontier' },
137
- { id: 'grok-4.7', family: 'xai', effort: 'high', tier: 'frontier' },
138
- ],
139
- },
140
- })
141
-
142
- // Explorer: gpt-6.1-sol@medium first (ADR 0009 named gpt-6-sol; replaced
143
- // 2026-09-30; 73 vendor at medium), then the routine implementer ladder without
144
- // its gpt-6.1-sol@high rung. The medium point is below the catalog's banked `high`, so it is a
145
- // policy point synthesized from a discovered id, like the reviewer's; when
146
- // discovery does not yield gpt-6.1-sol, the fill falls through to the routine
147
- // ladder.
148
- export const TEAM_EXPLORER_POLICY = Object.freeze({
149
- pi: { id: 'openai/gpt-6.1-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
150
- codex: { id: 'gpt-6.1-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
151
- 'claude-code': { id: 'gpt-6.1-sol', family: 'openai', effort: 'medium', tier: 'frontier' },
152
- })
153
-
154
- export const TEAM_EXPLORER_DEFAULT = Object.freeze(
155
- Object.fromEntries(
156
- Object.entries(TEAM_IMPLEMENTER_PREFERENCE.routine).map(([harness, ladder]) => [
157
- harness,
158
- [TEAM_EXPLORER_POLICY[harness], ...ladder.filter((rung) => rung.id !== TEAM_EXPLORER_POLICY[harness].id)],
159
- ]),
160
- ),
161
- )
162
-
163
- const TIER_RANK = Object.freeze({
164
- frontier: 0,
165
- 'deep-reasoner': 1,
166
- 'balanced-coder': 2,
167
- })
168
-
169
70
  function usage() {
170
- return `preflight-models — discover + recommend implementer, fill explorer + reviewer (resolve-issues fast path)
71
+ return `preflight-models — resolve implementer, explorer, diagnoser and reviewer through the harness agent catalog
171
72
 
172
73
  Usage:
173
- node preflight-models.mjs --harness <pi|codex|claude-code> [--risk routine|high]
174
- [--available <id,id,...>] [--human-named <id,id,...>] [--implementer <id[@effort]>] [--ask-only]
74
+ node preflight-models.mjs --harness <pi|codex|claude-code> --capabilities <capabilities.json>
75
+ [--available <id,id,...>] [--foreground-model <id>] [--risk routine|high]
76
+ [--implementer <id[@effort]>] [--cwd <dir>]
77
+
78
+ Capabilities file (the preferred input, written from this turn's advertised models):
79
+ { "available": [{ "model": "provider/model", "nativeModel": "host-id", "supportedEfforts": ["high"] }],
80
+ "foregroundModel": "provider/model", "fallbackToForeground": false,
81
+ "launchCapabilities": { "model": true, "effort": true, "instructions": true },
82
+ "hostPreferences": { "developer": { "model": "provider/model", "effort": "high" } },
83
+ "overrides": { "implementer": { "model": "...", "effort": "..." } },
84
+ "tiers": { "tiers": { "balanced": { "members": ["provider/model"], "candidates": [] } } },
85
+ "userRoot": "~/.pi/agent", "projectTrusted": true, "cwd": "/repo" }
86
+
87
+ A non-Pi host must set launchCapabilities.model and .instructions true, and
88
+ .effort true when a slot resolves an effort; otherwise the run blocks with
89
+ code launch-unsupported.
175
90
 
176
91
  Output JSON:
177
- { harness, risk, discovery, models, implementer, explorer, diagnoser, reviewer, ask, deferred }
178
-
179
- --ask-only prints only the ask block (for piping into a question UI).
180
- --implementer pins the implementer (the only option); exit 4 when it is not an
181
- available slot-eligible point. Never substitute another model.
182
- Deferred: run catalog sync/bank AFTER the human answers — never before the ask.`
92
+ { ok, harness, risk, catalogDigest, diagnostics, implementer, explorer, diagnoser,
93
+ reviewer, pinned?, blocked? }
94
+
95
+ implementer, explorer, diagnoser and reviewer are fills: { slot, role, id, key,
96
+ modelId, nativeModel, effort, launch, instructions, selection, catalogDigest,
97
+ diagnostics, reason }. Record the model and effort as run metadata; no role is a
98
+ question, and no option list is built here.
99
+ --implementer pins the implementer; the run ends when the catalog declines it and
100
+ no other model is ever substituted.
101
+ A blocked result (ok: false) ends the run: never reuse a previous pick or the
102
+ skill-owned catalog.`
183
103
  }
184
104
 
185
105
  function parseList(value) {
@@ -193,491 +113,362 @@ export function parseArgs(argv) {
193
113
  const out = {
194
114
  harness: null,
195
115
  risk: 'routine',
116
+ capabilities: null,
196
117
  available: [],
197
118
  humanNamed: [],
119
+ foregroundModel: null,
120
+ cwd: null,
198
121
  implementer: null,
199
- askOnly: false,
200
122
  help: false,
201
123
  }
124
+ const value = (index, flag) => {
125
+ const next = argv[index]
126
+ if (next === undefined || next.startsWith('--')) throw new Error(`${flag} needs a value`)
127
+ return next
128
+ }
202
129
  for (let i = 0; i < argv.length; i++) {
203
130
  const a = argv[i]
204
131
  if (a === '--help' || a === '-h' || a === 'help') out.help = true
205
- else if (a === '--ask-only') out.askOnly = true
206
- else if (a === '--harness') out.harness = argv[++i]
207
- else if (a === '--risk') out.risk = argv[++i]
208
- else if (a === '--available') out.available = parseList(argv[++i])
209
- else if (a === '--human-named') out.humanNamed = parseList(argv[++i])
210
- else if (a === '--implementer') out.implementer = argv[++i]
132
+ else if (a === '--harness') out.harness = value(++i, '--harness')
133
+ else if (a === '--risk') out.risk = value(++i, '--risk')
134
+ else if (a === '--capabilities') out.capabilities = value(++i, '--capabilities')
135
+ else if (a === '--available') out.available = parseList(value(++i, '--available'))
136
+ else if (a === '--human-named') out.humanNamed = parseList(value(++i, '--human-named'))
137
+ else if (a === '--foreground-model') out.foregroundModel = value(++i, '--foreground-model')
138
+ else if (a === '--cwd') out.cwd = value(++i, '--cwd')
139
+ else if (a === '--implementer') out.implementer = value(++i, '--implementer')
211
140
  else if (a.startsWith('--harness=')) out.harness = a.slice('--harness='.length)
212
141
  else if (a.startsWith('--risk=')) out.risk = a.slice('--risk='.length)
142
+ else if (a.startsWith('--capabilities=')) out.capabilities = a.slice('--capabilities='.length)
213
143
  else if (a.startsWith('--available=')) out.available = parseList(a.slice('--available='.length))
214
144
  else if (a.startsWith('--human-named=')) out.humanNamed = parseList(a.slice('--human-named='.length))
145
+ else if (a.startsWith('--foreground-model=')) out.foregroundModel = a.slice('--foreground-model='.length)
146
+ else if (a.startsWith('--cwd=')) out.cwd = a.slice('--cwd='.length)
215
147
  else if (a.startsWith('--implementer=')) out.implementer = a.slice('--implementer='.length)
216
148
  else throw new Error(`unknown argument: ${a}`)
217
149
  }
218
- if (out.implementer !== null && (typeof out.implementer !== 'string' || !out.implementer.trim())) throw new Error('--implementer needs a model id')
150
+ if (out.implementer !== null && (typeof out.implementer !== 'string' || !out.implementer.trim())) {
151
+ throw new Error('--implementer needs a model id')
152
+ }
219
153
  return out
220
154
  }
221
155
 
222
- /**
223
- * The point a pin names: `id@effort` matches that operating point exactly; a
224
- * bare `id` takes the floor effort when banked, else the id's first point.
225
- */
226
- export function findPinnedPoint(points, pin) {
227
- const text = String(pin || '').trim()
156
+ /** `id@effort` splits only on a suffix in the catalog's effort vocabulary. */
157
+ export function parsePin(pin) {
158
+ const text = String(pin ?? '').trim()
228
159
  if (!text) return null
229
160
  const at = text.lastIndexOf('@')
230
- const id = at > 0 ? text.slice(0, at) : text
231
- const effort = at > 0 ? text.slice(at + 1) : null
232
- const same = points.filter((p) => p.id === id)
233
- if (effort) return same.find((p) => p.effort === effort) || null
234
- return same.find((p) => p.effort === DEFAULT_EFFORT) || same[0] || null
235
- }
236
-
237
- function familyOf(id, hinted) {
238
- if (hinted) return hinted
239
- if (id.includes('/')) return id.slice(0, id.indexOf('/'))
240
- return 'unknown'
241
- }
242
-
243
- /** Prefer banked catalog family, then seed, then id prefix. */
244
- export function resolveFamily(harness, id, { catalog = null, seed = null } = {}) {
245
- const banked = (catalog?.models || []).find((m) => m && m.id === id)
246
- if (banked?.family) return banked.family
247
- const seeded = seed ? findSeedEntry(seed, harness, id) : null
248
- if (seeded?.family) return seeded.family
249
- return familyOf(id)
250
- }
251
-
252
- function pointKey(id, effort) {
253
- return `${id}@${effort || DEFAULT_EFFORT}`
254
- }
255
-
256
- /** Expand tierProposals().models into slot-eligible operating points. */
257
- export function slotPoints(tieredModels, { harness, catalog = null, seed = null } = {}) {
258
- const points = []
259
- for (const model of tieredModels) {
260
- const entries = Object.keys(model.tiers || {}).length
261
- ? pointsFromTiers(model.tiers)
262
- : []
263
- const family = resolveFamily(harness, model.id, { catalog, seed })
264
- for (const p of entries) {
265
- if (!isSlotEligible(p.tier)) continue
266
- points.push({
267
- id: model.id,
268
- effort: p.effort || DEFAULT_EFFORT,
269
- tier: p.tier,
270
- family,
271
- source: model.source,
272
- citation: model.citation || null,
273
- key: pointKey(model.id, p.effort || DEFAULT_EFFORT),
274
- })
275
- }
161
+ if (at > 0 && CATALOG_EFFORTS.includes(text.slice(at + 1))) {
162
+ return { model: text.slice(0, at), effort: text.slice(at + 1) }
276
163
  }
277
- return points
164
+ return { model: text }
278
165
  }
279
166
 
280
- function preferRisk(tier, risk) {
281
- if (risk === 'high') {
282
- if (tier === 'deep-reasoner') return 0
283
- if (tier === 'balanced-coder') return 1
284
- return 2 // frontier is never recommended by tier alone (only via the team ladder)
285
- }
286
- // routine
287
- if (tier === 'balanced-coder') return 0
288
- if (tier === 'deep-reasoner') return 1
289
- return 2
167
+ export function pointKey(model, effort) {
168
+ return effort ? `${model}@${effort}` : String(model ?? '')
290
169
  }
291
170
 
292
- /**
293
- * Recommend implementer. The team ladder for the risk/harness is consulted
294
- * first (high risk: gpt-6.1-sol, gpt-6-astra, gpt-5.6-sol, then grok-4.7, all
295
- * @high — the ladder is the only way a frontier point becomes the
296
- * recommendation; routine: gpt-6.1-sol@high, gpt-6-luna@high, grok-4.6@high,
297
- * then mimo-v2.6-pro@high). Without a ladder hit
298
- * the tier order applies: routine → balanced-coder; high → deep-reasoner;
299
- * frontier is listed but never recommended by tier alone.
300
- */
301
- export function recommendImplementer(points, risk = 'routine', harness = null) {
302
- if (!points.length) return null
303
- const ladder = harness ? TEAM_IMPLEMENTER_PREFERENCE[risk]?.[harness] || [] : []
304
- for (const preferred of ladder) {
305
- const preferredPoint = findPreferredPoint(points, preferred)
306
- if (preferredPoint && preferredPoint.tier === preferred.tier) return preferredPoint
307
- }
308
- const sorted = [...points].sort((a, b) => {
309
- const ra = preferRisk(a.tier, risk)
310
- const rb = preferRisk(b.tier, risk)
311
- if (ra !== rb) return ra - rb
312
- // Prefer the floor effort when tied; then stable id.
313
- if (a.effort !== b.effort) {
314
- if (a.effort === DEFAULT_EFFORT) return -1
315
- if (b.effort === DEFAULT_EFFORT) return 1
316
- return a.effort < b.effort ? -1 : 1
317
- }
318
- return a.id.localeCompare(b.id)
171
+ function normalizeAvailableInput(list) {
172
+ return list.map((entry) => {
173
+ if (typeof entry === 'string') return { model: entry }
174
+ if (entry && typeof entry === 'object' && typeof entry.model === 'string') return entry
175
+ throw new Error(`available entry must be a model id or { model } object: ${JSON.stringify(entry)}`)
319
176
  })
320
- // First non-frontier if any; else first point (single-model / only-frontier harness)
321
- return sorted.find((p) => p.tier !== 'frontier') || sorted[0]
322
177
  }
323
178
 
324
179
  /**
325
- * Escalation fix-worker fill (test-first, last-try): the high-risk implementer ladder
326
- * (gpt-6.1-sol, gpt-6-astra, gpt-5.6-sol, then grok-4.7, all @high), whatever
327
- * the run's risk.
180
+ * Pi launches carry the role, and nothing else unless something explicitly asked
181
+ * for a model or an effort. Two things can ask: the human's task override, and
182
+ * the host's own preference reported by the catalog (`selection.*.explicit*`).
183
+ * A resolved default is neither, so it must not be copied into the payload — the
184
+ * live Pi catalog keeps supplying its own default for a role-only launch.
185
+ *
186
+ * The reported canonical values win over the raw override text: the catalog has
187
+ * already resolved aliases, effort support, and precedence, so re-sending the
188
+ * raw string would assert a choice the catalog did not make.
328
189
  */
329
- export function fillDiagnoser(points, harness) {
330
- const fill = recommendImplementer(points, 'high', harness)
331
- return { fill: fill || null, reason: fill ? 'high-risk-ladder' : 'none' }
332
- }
333
-
334
- function findPreferredPoint(points, preferred) {
335
- if (!preferred) return null
336
- return points.find(
337
- (p) => p.id === preferred.id && (p.effort || DEFAULT_EFFORT) === (preferred.effort || DEFAULT_EFFORT),
338
- ) || null
339
- }
340
-
341
- // Reviewer and explorer policy may deliberately use a below-catalog effort. The
342
- // catalog continues to describe pushed capability points (high/xhigh/max);
343
- // discovery of the model id is still required before this role-specific point
344
- // exists.
345
- function findPolicyPoint(points, preferred, { source, citation }) {
346
- if (!preferred) return null
347
- const discovered = points.find((p) => p.id === preferred.id)
348
- if (!discovered) return null
349
- if ((preferred.effort || DEFAULT_EFFORT) === DEFAULT_EFFORT) {
350
- return findPreferredPoint(points, preferred)
351
- }
352
- return {
353
- ...discovered,
354
- effort: preferred.effort,
355
- tier: preferred.tier,
356
- source,
357
- citation,
358
- key: pointKey(preferred.id, preferred.effort),
190
+ function adaptLaunch(worker, { harness, slot, override, diagnostics }) {
191
+ if (harness !== 'pi') return worker.launch
192
+ const reported = worker.launch && typeof worker.launch === 'object' ? worker.launch : {}
193
+ const selection = worker.selection && typeof worker.selection === 'object' ? worker.selection : {}
194
+ const selectionModel = selection.model && typeof selection.model === 'object' ? selection.model : {}
195
+ const selectionEffort = selection.effort && typeof selection.effort === 'object' ? selection.effort : {}
196
+ const requestedModel = selectionModel.explicitRequest === true
197
+ ? reported.model ?? worker.model ?? null
198
+ : null
199
+ const requestedThinking = selectionEffort.explicit === true
200
+ ? reported.thinking ?? worker.effort ?? null
201
+ : null
202
+ const model = requestedModel ?? override?.model ?? null
203
+ const thinking = requestedThinking ?? override?.effort ?? null
204
+ const launch = { role: worker.role }
205
+ if (model) launch.model = model
206
+ if (thinking) launch.thinking = thinking
207
+ const carriedModel = reported.model !== undefined && reported.model !== model
208
+ const carriedThinking = reported.thinking !== undefined && reported.thinking !== thinking
209
+ const carriedRole = reported.role !== undefined && reported.role !== worker.role
210
+ if (carriedModel || carriedThinking || carriedRole) {
211
+ diagnostics.push(
212
+ `${slot}: ignored catalog-supplied Pi launch field(s) `
213
+ + `${[carriedRole ? 'role' : null, carriedModel ? 'model' : null, carriedThinking ? 'thinking' : null]
214
+ .filter(Boolean).join(', ')}; a Pi launch is role-based unless the task override or the `
215
+ + 'catalog reports that the model or effort was explicitly requested',
216
+ )
359
217
  }
218
+ return launch
360
219
  }
361
220
 
362
- const REVIEW_POLICY_SOURCE = Object.freeze({
363
- source: 'team-review-policy',
364
- citation: 'docs/research/2026-09-23-reviewer-benchmark-astra-vs-sol.md',
365
- })
366
- const EXPLORER_POLICY_SOURCE = Object.freeze({
367
- source: 'team-explorer-policy',
368
- citation: 'docs/adr/0009-orchestrator-coordinates-only.md',
369
- })
370
-
371
- function findReviewerPolicyPoint(points, preferred) {
372
- return findPolicyPoint(points, preferred, REVIEW_POLICY_SOURCE)
373
- }
374
-
375
- /**
376
- * Explorer fill: gpt-6.1-sol@medium when gpt-6.1-sol is discovered, then the
377
- * routine implementer ladder without its gpt-6.1-sol@high rung (gpt-6-luna@high,
378
- * grok-4.6@high, then mimo-v2.6-pro@high), first discovered rung wins;
379
- * without a rung, the balanced-coder tier.
380
- */
381
- export function fillExplorer(points, harness) {
382
- const ladder = TEAM_EXPLORER_DEFAULT[harness] || []
383
- const policy = TEAM_EXPLORER_POLICY[harness]
384
- let preferredPoint = null
385
- for (const preferred of ladder) {
386
- const candidate = preferred === policy
387
- ? findPolicyPoint(points, preferred, EXPLORER_POLICY_SOURCE)
388
- : findPreferredPoint(points, preferred)
389
- if (candidate) {
390
- preferredPoint = candidate
391
- if (candidate.tier === preferred.tier) return { fill: candidate, reason: 'team-default' }
392
- }
393
- }
394
- const balanced = points
395
- .filter((p) => p.tier === 'balanced-coder')
396
- .sort((a, b) => {
397
- if (a.effort === DEFAULT_EFFORT && b.effort !== DEFAULT_EFFORT) return -1
398
- if (b.effort === DEFAULT_EFFORT && a.effort !== DEFAULT_EFFORT) return 1
399
- return a.id.localeCompare(b.id)
400
- })
401
- const fill = balanced[0] || null
221
+ function slotFill(worker, { harness, slot, override, diagnostics }) {
402
222
  return {
403
- fill,
404
- reason: fill ? (preferredPoint ? 'ladder' : 'balanced-coder') : 'none',
223
+ slot,
224
+ role: worker.role,
225
+ id: worker.model,
226
+ key: pointKey(worker.model, worker.effort),
227
+ modelId: worker.model,
228
+ nativeModel: worker.nativeModel,
229
+ effort: worker.effort,
230
+ launch: adaptLaunch(worker, { harness, slot, override, diagnostics }),
231
+ instructions: worker.instructions,
232
+ selection: worker.selection,
233
+ catalogDigest: worker.catalogDigest,
234
+ diagnostics: Array.isArray(worker.diagnostics) ? worker.diagnostics : [],
235
+ reason: 'agent-catalog',
405
236
  }
406
237
  }
407
238
 
408
- /**
409
- * Reviewer fill is implementer-aware and must differ by model id:
410
- * Astra implementer -> gpt-5.6-sol@medium; otherwise -> gpt-6-astra@medium. If that
411
- * policy point is unavailable, the existing diversity-first ladder applies.
412
- */
413
- export function rankReviewers(points, implementer, harness) {
414
- const policy = TEAM_REVIEWER_POLICY[harness] || null
415
- const deepPool = points.filter((p) => p.tier === 'deep-reasoner' || p.tier === 'frontier')
416
- const pool = deepPool.length ? deepPool : points
417
- const scored = pool.map((p) => {
418
- const sameId = p.id === implementer.id
419
- const sameFamily = p.family && implementer.family && p.family === implementer.family
420
- let diversity
421
- if (sameId) diversity = 5
422
- else if (!sameFamily && p.tier === 'deep-reasoner') diversity = 0
423
- else if (!sameFamily && p.tier === 'frontier') diversity = 1
424
- else if (sameFamily && p.tier === 'deep-reasoner') diversity = 2
425
- else if (!sameFamily) diversity = 3
426
- else diversity = 4
427
- return { ...p, diversity, tierRank: TIER_RANK[p.tier] ?? 9 }
428
- })
429
- scored.sort((a, b) => {
430
- if (a.diversity !== b.diversity) return a.diversity - b.diversity
431
- if (a.tierRank !== b.tierRank) return a.tierRank - b.tierRank
432
- return a.id.localeCompare(b.id)
433
- })
434
-
435
- const primaryPoint = findReviewerPolicyPoint(points, policy?.primary)
436
- const fallbackPoint = findReviewerPolicyPoint(points, policy?.fallback)
437
-
438
- const ranking = []
439
- const rankedIds = new Set()
440
- for (const candidate of [primaryPoint, fallbackPoint, ...scored]) {
441
- if (!candidate || rankedIds.has(candidate.id)) continue
442
- rankedIds.add(candidate.id)
443
- ranking.push(candidate)
444
- }
445
-
446
- const implementerIsPrimary = implementer.id === policy?.primary?.id
447
- const policyFill = implementerIsPrimary ? fallbackPoint : primaryPoint
448
- const fill = policyFill && policyFill.id !== implementer.id
449
- ? policyFill
450
- : scored.find((p) => p.id !== implementer.id) || scored[0]
451
- let reason
452
- if (fill?.id === primaryPoint?.id) reason = 'team-default'
453
- else if (fill?.id === fallbackPoint?.id && implementerIsPrimary) reason = 'implementer-aware-default'
454
- else reason = fill?.id === implementer.id ? 'no-distinct-candidate' : 'ladder'
455
-
239
+ function blockedResult({ code, reason, harness, risk, pin, blocked, diagnostics }) {
456
240
  return {
457
- fill,
241
+ ok: false,
242
+ code,
243
+ ...(pin ? { pinned: pin } : {}),
244
+ harness,
245
+ risk,
458
246
  reason,
459
- ranking,
460
- teamDefaultApplied: reason === 'team-default' || reason === 'implementer-aware-default',
461
- reducedDiversity: !!(fill && implementer && fill.family && fill.family === implementer.family && fill.id !== implementer.id),
462
- notIndependent: !!(fill && implementer && fill.id === implementer.id),
463
- }
464
- }
465
-
466
- function optionFromPoint(p, { recommended = false, role, ladderRung = 0 }) {
467
- const why =
468
- role === 'implementer'
469
- ? ladderRung
470
- ? `team ladder rung ${ladderRung}${recommended ? '' : ' — next discovered rung'}`
471
- : p.tier === 'balanced-coder'
472
- ? 'routine implementer band'
473
- : p.tier === 'deep-reasoner'
474
- ? 'deep-reasoner for risk-heavy work'
475
- : p.tier === 'frontier'
476
- ? recommended
477
- ? 'frontier — team implementer ladder preference'
478
- : 'frontier (alternate only — not the recommendation)'
479
- : p.tier
480
- : p.tier
481
- return {
482
- id: p.key, // operating point id for the picker; parse back to model+effort
483
- modelId: p.id,
484
- label: `${p.id} @ ${p.effort}`,
485
- description: `${p.tier}${recommended ? ' · recommended' : ''} · ${why}`,
486
- family: p.family,
487
- effort: p.effort,
488
- tier: p.tier,
247
+ ...(blocked ? { blocked } : {}),
248
+ ...(diagnostics ? { diagnostics } : {}),
249
+ slots: null,
250
+ implementer: null,
251
+ explorer: null,
252
+ diagnoser: null,
253
+ reviewer: null,
489
254
  }
490
255
  }
491
256
 
492
- /** Native question UIs typically allow 2–4 options. Keep the rest as `alternates`. */
493
- export const ASK_OPTION_LIMIT = 4
494
-
495
- function slotFillPayload(slot) {
496
- if (!slot?.fill) return null
497
- return {
498
- id: slot.fill.key,
499
- modelId: slot.fill.id,
500
- effort: slot.fill.effort,
501
- family: slot.fill.family,
502
- tier: slot.fill.tier,
503
- reason: slot.reason,
504
- reducedDiversity: slot.reducedDiversity || false,
505
- notIndependent: slot.notIndependent || false,
506
- }
257
+ function blockedFromCatalog(resolved, { harness, risk, pin }) {
258
+ const blocked = resolved.blocked || {}
259
+ const code = pin && resolved.declined
260
+ ? 'pinned-implementer-unavailable'
261
+ : (blocked.code || AGENT_CATALOG_BLOCK_CODES.COMMAND_FAILED)
262
+ const reason = pin && resolved.declined
263
+ ? `${pin} is not available to the agent catalog here; block the run, never substitute another implementer`
264
+ : `the agent catalog did not resolve the workflow roles (${blocked.reason || code}); block the run, never fall back to a skill-owned picker`
265
+ return blockedResult({
266
+ code,
267
+ reason,
268
+ harness,
269
+ risk,
270
+ pin,
271
+ blocked,
272
+ diagnostics: Array.isArray(resolved.diagnostics) ? resolved.diagnostics : undefined,
273
+ })
507
274
  }
508
275
 
509
276
  /**
510
- * The ask's options are the team ladder for this risk/harness, in ladder order,
511
- * limited to the rungs discovery actually yielded, then the remaining points by
512
- * risk-matched band and id. The ladder is the policy; before 2026-09-23 the
513
- * options were band-then-id, which pushed the ladder rungs (gpt-5.6-sol,
514
- * grok-4.7, gpt-6-sol) into `alternates` behind whichever deep-reasoners sorted
515
- * first by id, and native pickers never show alternates. One frontier point is
516
- * kept visible only when none of the primary options is frontier already.
277
+ * Resolve the four worker roles through the harness agent catalog and adapt them
278
+ * to the workflow payload.
279
+ *
280
+ * Injectable: `catalogFn` replaces the bridge call, and `runner` replaces its
281
+ * child process, so focused tests can drive the protocol without installing it.
517
282
  */
518
- export function buildAsk(implementer, points, reviewer, { risk = 'routine', explorer = null, diagnoser = null, harness = null, pinned = false } = {}) {
519
- if (pinned) {
520
- return {
521
- header: 'Implementer',
522
- question: 'The implementer is pinned for this run; it is the only option. (Explorer + reviewer are filled automatically; reviewer ≠ implementer.)',
523
- recommended: implementer.key,
524
- pinned: true,
525
- options: [{ ...optionFromPoint(implementer, { recommended: true, role: 'implementer' }), description: `${implementer.tier} · pinned` }],
526
- alternates: [],
527
- explorerFill: slotFillPayload(explorer),
528
- diagnoserFill: slotFillPayload(diagnoser),
529
- reviewerFill: slotFillPayload(reviewer),
530
- }
531
- }
532
- const seen = new Set()
533
- const ordered = []
534
- const ladderRungByKey = new Map()
535
- const push = (p) => {
536
- if (!p || seen.has(p.key)) return
537
- seen.add(p.key)
538
- ordered.push(p)
539
- }
540
- push(implementer)
541
- const ladder = harness ? TEAM_IMPLEMENTER_PREFERENCE[risk]?.[harness] || [] : []
542
- ladder.forEach((preferred, index) => {
543
- const point = findPreferredPoint(points, preferred)
544
- if (point && point.tier === preferred.tier) {
545
- ladderRungByKey.set(point.key, index + 1)
546
- push(point)
547
- }
548
- })
549
- // Then the remaining points by risk-matched band, then id.
550
- const rest = [...points].sort((a, b) => preferRisk(a.tier, risk) - preferRisk(b.tier, risk) || a.id.localeCompare(b.id))
551
- for (const p of rest) push(p)
552
- let primary = ordered.slice(0, ASK_OPTION_LIMIT)
553
- const frontier = ordered.find((p) => p.tier === 'frontier' && p.key !== implementer?.key)
554
- if (frontier && !primary.some((p) => p.tier === 'frontier') && primary.length === ASK_OPTION_LIMIT) {
555
- primary = [...primary.slice(0, ASK_OPTION_LIMIT - 1), frontier]
556
- }
557
- const primaryKeys = new Set(primary.map((p) => p.key))
558
- const optionFor = (p, recommended) =>
559
- optionFromPoint(p, { recommended, role: 'implementer', ladderRung: ladderRungByKey.get(p.key) || 0 })
560
- const options = primary.map((p) => optionFor(p, p.key === implementer?.key))
561
- const alternates = ordered.filter((p) => !primaryKeys.has(p.key)).map((p) => optionFor(p, false))
562
-
563
- return {
564
- header: 'Implementer',
565
- question: 'Which model should implement every issue in this run? (Explorer + reviewer are filled automatically; reviewer ≠ implementer.)',
566
- recommended: implementer?.key || null,
567
- options,
568
- alternates,
569
- explorerFill: slotFillPayload(explorer),
570
- diagnoserFill: slotFillPayload(diagnoser),
571
- reviewerFill: slotFillPayload(reviewer),
572
- }
573
- }
574
-
575
283
  export function buildPreflightModels({
576
284
  harness,
577
285
  risk = 'routine',
286
+ cwd = process.cwd(),
578
287
  available = [],
579
288
  humanNamed = [],
289
+ capabilities = null,
290
+ capabilitiesPath = null,
291
+ foregroundModel = null,
292
+ overrides = null,
580
293
  implementer: pin = null,
581
294
  env = process.env,
582
- discoverFn = discover,
583
- tierFn = tierProposals,
295
+ catalogFn = resolveAgentCatalog,
296
+ runner,
584
297
  } = {}) {
585
- if (!harness) throw new Error('harness is required')
586
298
  if (!RISK.includes(risk)) throw new Error(`risk must be one of ${RISK.join('|')}`)
587
299
 
588
- const discovery = discoverFn(harness, env, {
589
- spawnModelIds: available,
590
- humanNamedModelIds: humanNamed,
591
- })
592
- const seenIds = discovery.discovered.map((d) => d.id)
593
- const tiered = tierFn(harness, seenIds)
594
- const catalog = readCatalog(pathForHarness(harness, env))
595
- const seed = readSeed(defaultSeedPath())
596
- const points = slotPoints(tiered.models, { harness, catalog, seed })
597
- const implementer = pin ? findPinnedPoint(points, pin) : recommendImplementer(points, risk, harness)
598
- if (pin && !implementer) {
599
- return {
600
- ok: false,
601
- code: 'pinned-implementer-unavailable',
602
- pinned: pin,
603
- harness,
300
+ let document = capabilities
301
+ if (capabilities && typeof capabilities === 'object') {
302
+ const checked = normalizeCapabilities(capabilities)
303
+ if (!checked.ok) {
304
+ return blockedResult({
305
+ code: checked.blocked.code,
306
+ reason: `${checked.blocked.reason}; block the run, never guess this turn's models`,
307
+ harness: harness ?? null,
308
+ risk,
309
+ pin,
310
+ blocked: checked.blocked,
311
+ })
312
+ }
313
+ document = checked.capabilities
314
+ }
315
+ if (capabilitiesPath) {
316
+ const read = readCapabilitiesFile(capabilitiesPath)
317
+ if (!read.ok) {
318
+ return blockedResult({
319
+ code: read.blocked.code,
320
+ reason: `${read.blocked.reason}; block the run, never guess this turn's models`,
321
+ harness: harness ?? null,
322
+ risk,
323
+ pin,
324
+ blocked: read.blocked,
325
+ })
326
+ }
327
+ document = read.capabilities
328
+ }
329
+
330
+ const effectiveHarness = harness || document?.harness || null
331
+ if (!effectiveHarness) {
332
+ return blockedResult({
333
+ code: AGENT_CATALOG_BLOCK_CODES.REQUEST_INVALID,
334
+ reason: 'no harness was given and the capabilities document names none',
335
+ harness: null,
604
336
  risk,
605
- discovery,
606
- models: tiered,
607
- points: points.map((p) => p.key),
608
- implementer: null,
609
- explorer: null,
610
- diagnoser: null,
611
- reviewer: null,
612
- ask: null,
613
- reason: `${pin} is not an available slot-eligible model here; block the run, never substitute another implementer`,
337
+ pin,
338
+ })
339
+ }
340
+
341
+ const explicitAvailable = normalizeAvailableInput([...available, ...humanNamed])
342
+ const requestAvailable = explicitAvailable.length > 0 ? explicitAvailable : (document?.available ?? [])
343
+ const pinOverride = pin ? parsePin(pin) : null
344
+ const mergedOverrides = { ...(document?.overrides ?? {}), ...(overrides ?? {}) }
345
+ if (pinOverride) mergedOverrides.implementer = pinOverride
346
+ const launchCapabilities = document?.launchCapabilities ?? null
347
+ if (effectiveHarness !== 'pi') {
348
+ const missingControls = ['model', 'instructions'].filter((field) => launchCapabilities?.[field] !== true)
349
+ if (missingControls.length > 0) {
350
+ return blockedResult({
351
+ code: 'launch-unsupported',
352
+ reason: `the capabilities document does not advertise ${missingControls.join(' and ')} launch control for `
353
+ + `${effectiveHarness}; a native model id and an effort say what the catalog chose, never that the adapter `
354
+ + 'can set it, so block the run instead of launching a slot this workflow cannot control',
355
+ harness: effectiveHarness,
356
+ risk,
357
+ pin,
358
+ blocked: { code: 'launch-unsupported', harness: effectiveHarness, missing: missingControls },
359
+ })
614
360
  }
615
361
  }
616
- if (!implementer) {
617
- return {
618
- ok: false,
619
- code: 'no-slot-eligible',
620
- harness,
362
+
363
+ const request = buildCatalogRequest({
364
+ harness: effectiveHarness,
365
+ cwd: document?.cwd || cwd,
366
+ available: requestAvailable,
367
+ foregroundModel: foregroundModel ?? document?.foregroundModel ?? null,
368
+ fallbackToForeground: document?.fallbackToForeground ?? null,
369
+ launchCapabilities,
370
+ hostPreferences: document?.hostPreferences ?? null,
371
+ overrides: mergedOverrides,
372
+ tiers: document?.tiers ?? null,
373
+ userRoot: document?.userRoot ?? null,
374
+ projectTrusted: document?.projectTrusted ?? null,
375
+ })
376
+ if (!request.ok) {
377
+ return blockedResult({
378
+ code: request.blocked.code,
379
+ reason: `${request.blocked.reason}; block the run, never guess this turn's models`,
380
+ harness: effectiveHarness,
621
381
  risk,
622
- discovery,
623
- models: tiered,
624
- points: [],
625
- implementer: null,
626
- explorer: null,
627
- diagnoser: null,
628
- reviewer: null,
629
- ask: null,
630
- deferred: {
631
- afterConfirm: [
632
- 'model-catalog.mjs sync --harness <h> --seen <ids>',
633
- 'model-catalog.mjs bank / bank-from-seed for any human confirmation',
634
- ],
635
- },
382
+ pin,
383
+ blocked: request.blocked,
384
+ })
385
+ }
386
+
387
+ const resolved = catalogFn({
388
+ request: request.request,
389
+ env,
390
+ ...(runner ? { runner } : {}),
391
+ })
392
+ if (!resolved?.ok) return blockedFromCatalog(resolved ?? {}, { harness: effectiveHarness, risk, pin })
393
+
394
+ const diagnostics = [...(resolved.diagnostics ?? [])]
395
+ for (const slot of CATALOG_SLOTS) {
396
+ const worker = resolved.slots?.[slot]
397
+ const expectedRole = CATALOG_SLOT_ROLES[slot]
398
+ if (worker.role !== expectedRole) {
399
+ return blockedResult({
400
+ code: 'catalog-role-mismatch',
401
+ reason: `slots.${slot} resolved role ${JSON.stringify(worker.role)}, expected ${expectedRole}; `
402
+ + 'block the run rather than launching a role the catalog did not describe',
403
+ harness: effectiveHarness,
404
+ risk,
405
+ pin,
406
+ blocked: { code: 'catalog-role-mismatch', slot, role: worker.role, expectedRole },
407
+ })
408
+ }
409
+ if (!worker.model) {
410
+ return blockedResult({
411
+ code: 'catalog-model-missing',
412
+ reason: `slots.${slot} has no canonical model; block the run rather than launching an unnamed model`,
413
+ harness: effectiveHarness,
414
+ risk,
415
+ pin,
416
+ blocked: { code: 'catalog-model-missing', slot },
417
+ })
418
+ }
419
+ if (effectiveHarness === 'codex' && (!worker.nativeModel || !worker.effort)) {
420
+ return blockedResult({
421
+ code: 'missing-native-selection',
422
+ reason: `slots.${slot} has no native model${worker.effort ? '' : ' or effort'}; the Codex adapter `
423
+ + 'cannot select a model without it, and this run will not claim a selection it cannot make',
424
+ harness: effectiveHarness,
425
+ risk,
426
+ pin,
427
+ blocked: {
428
+ code: 'missing-native-selection',
429
+ slot,
430
+ nativeModel: worker.nativeModel,
431
+ effort: worker.effort,
432
+ },
433
+ })
434
+ }
435
+ if (effectiveHarness !== 'pi' && worker.effort !== null && launchCapabilities?.effort !== true) {
436
+ return blockedResult({
437
+ code: 'launch-unsupported',
438
+ reason: `slots.${slot} resolves effort ${worker.effort}, but the capabilities document does not advertise `
439
+ + `effort launch control for ${effectiveHarness}; block the run instead of launching at an effort the `
440
+ + 'adapter cannot set',
441
+ harness: effectiveHarness,
442
+ risk,
443
+ pin,
444
+ blocked: { code: 'launch-unsupported', harness: effectiveHarness, slot, missing: ['effort'] },
445
+ })
636
446
  }
637
447
  }
638
- const explorer = fillExplorer(points, harness)
639
- const diagnoser = fillDiagnoser(points, harness)
640
- const reviewer = rankReviewers(points, implementer, harness)
641
- const ask = buildAsk(implementer, points, reviewer, { risk, explorer, diagnoser, harness, pinned: !!pin })
448
+
449
+ const fills = Object.fromEntries(CATALOG_SLOTS.map((slot) => [
450
+ slot,
451
+ slotFill(resolved.slots[slot], {
452
+ harness: effectiveHarness,
453
+ slot,
454
+ override: mergedOverrides[slot] ?? null,
455
+ diagnostics,
456
+ }),
457
+ ]))
458
+
642
459
  return {
643
460
  ok: true,
644
461
  ...(pin ? { pinned: pin } : {}),
645
- harness,
462
+ harness: effectiveHarness,
646
463
  risk,
647
- discovery: {
648
- completeness: discovery.completeness,
649
- discovered: discovery.discovered,
650
- notes: discovery.notes,
651
- },
652
- models: tiered,
653
- points,
654
- implementer,
655
- explorer: {
656
- fill: explorer.fill,
657
- reason: explorer.reason,
658
- },
659
- diagnoser: {
660
- fill: diagnoser.fill,
661
- reason: diagnoser.reason,
662
- },
663
- reviewer: {
664
- fill: reviewer.fill,
665
- reason: reviewer.reason,
666
- ranking: reviewer.ranking.map((p) => p.key),
667
- teamDefaultApplied: reviewer.teamDefaultApplied,
668
- reducedDiversity: reviewer.reducedDiversity,
669
- notIndependent: reviewer.notIndependent,
670
- },
671
- ask,
672
- deferred: {
673
- afterConfirm: [
674
- `model-catalog.mjs sync --harness ${harness} --seen ${seenIds.join(',')}`,
675
- 'bank confirmations only after the human answers — never before the ask',
676
- ],
677
- doNotReadBeforeAsk: [
678
- 'harness-runtime/references/model-catalog.md',
679
- ],
680
- },
464
+ catalogDigest: resolved.catalogDigest,
465
+ command: resolved.command ?? null,
466
+ diagnostics,
467
+ slots: resolved.slots,
468
+ implementer: fills.implementer,
469
+ explorer: fills.explorer,
470
+ diagnoser: fills.diagnoser,
471
+ reviewer: fills.reviewer,
681
472
  }
682
473
  }
683
474
 
@@ -703,14 +494,21 @@ export function runCli(argv, env = process.env) {
703
494
  console.error(`preflight-models: --risk must be ${RISK.join('|')}`)
704
495
  return 2
705
496
  }
497
+ if (!CATALOG_HARNESSES.includes(harness)) {
498
+ console.error(`preflight-models: --harness must be ${CATALOG_HARNESSES.join('|')}`)
499
+ return 2
500
+ }
706
501
 
707
502
  let result
708
503
  try {
709
504
  result = buildPreflightModels({
710
505
  harness,
711
506
  risk: args.risk,
507
+ capabilitiesPath: args.capabilities,
712
508
  available: args.available,
713
509
  humanNamed: args.humanNamed,
510
+ foregroundModel: args.foregroundModel,
511
+ cwd: args.cwd || process.cwd(),
714
512
  implementer: args.implementer,
715
513
  env,
716
514
  })
@@ -719,14 +517,6 @@ export function runCli(argv, env = process.env) {
719
517
  return 2
720
518
  }
721
519
 
722
- if (!result.ok) {
723
- console.log(JSON.stringify(result, null, 2))
724
- return 0
725
- }
726
- if (args.askOnly) {
727
- console.log(JSON.stringify(result.ask, null, 2))
728
- return 0
729
- }
730
520
  console.log(JSON.stringify(result, null, 2))
731
521
  return 0
732
522
  }