@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.
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +149 -145
- package/package.json +4 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +28 -1
- package/skills/app-dev/android-development/references/diagnosis.md +50 -0
- package/skills/app-dev/android-development/references/init.md +31 -2
- package/skills/app-dev/android-development/references/setup.md +8 -2
- package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
- package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
- package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
- package/skills/app-dev/android-development/scripts/init.mjs +129 -9
- package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
- package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-report/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +4 -3
- package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
- package/skills/engineering/harness-runtime/contracts.md +2 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
- package/skills/engineering/resolve-issues/SKILL.md +9 -6
- package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
- package/skills/engineering/resolve-issues/references/expire.md +6 -3
- package/skills/engineering/resolve-issues/references/implementation.md +6 -1
- package/skills/engineering/resolve-issues/references/preflight.md +26 -17
- package/skills/engineering/resolve-issues/references/review.md +21 -9
- package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
- package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +4 -4
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
|
@@ -1,185 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// resolve-issues
|
|
2
|
+
// resolve-issues preflight for model slots.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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>
|
|
15
|
-
// [--available id,id,...] [--
|
|
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
|
-
// --
|
|
18
|
-
//
|
|
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`
|
|
21
|
-
//
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 —
|
|
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>
|
|
174
|
-
[--available <id,id,...>] [--
|
|
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,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 === '--
|
|
206
|
-
else if (a === '--
|
|
207
|
-
else if (a === '--
|
|
208
|
-
else if (a === '--available') out.available = parseList(
|
|
209
|
-
else if (a === '--human-named') out.humanNamed = parseList(
|
|
210
|
-
else if (a === '--
|
|
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()))
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
164
|
+
return { model: text }
|
|
278
165
|
}
|
|
279
166
|
|
|
280
|
-
function
|
|
281
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* the
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
241
|
+
ok: false,
|
|
242
|
+
code,
|
|
243
|
+
...(pin ? { pinned: pin } : {}),
|
|
244
|
+
harness,
|
|
245
|
+
risk,
|
|
458
246
|
reason,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
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
|
-
|
|
583
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
}
|