@plainconceptsplatform/agent-harness 3.8.3 → 3.9.0-alpha.1
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/LICENSE +21 -21
- package/README.md +551 -551
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +52 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -176
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +81 -89
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +12 -10
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/package.json +6 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers-core.js +337 -0
- package/harness/.opencode/plugins/pc-subagent-tiers.js +56 -367
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/harness-doctor.mjs +52 -0
- package/harness/.opencode/scripts/harness-doctor.test.mjs +58 -0
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/.opencode/scripts/sync-agents.mjs +29 -0
- package/harness/AGENTS.md +55 -55
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +100 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +73 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import { header, success, warn } from '../../utils/exec.js'
|
|
6
|
-
import { configPath } from '../../utils/paths.js'
|
|
7
|
-
|
|
8
|
-
const require = createRequire(import.meta.url)
|
|
9
|
-
const { version: harnessVersion } = require('../../../package.json')
|
|
10
|
-
|
|
11
|
-
function clampConcurrency(n) {
|
|
12
|
-
const v = Number(n)
|
|
13
|
-
if (!Number.isFinite(v)) return 3
|
|
14
|
-
return Math.min(5, Math.max(1, Math.round(v)))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function detectOpencodeVersion() {
|
|
18
|
-
try {
|
|
19
|
-
const result = await execa('opencode', ['--version'], { reject: false })
|
|
20
|
-
if (result.exitCode !== 0) return null
|
|
21
|
-
// `opencode --version` prints "opencode v2.0.22" on some builds and "2.0.22" on others;
|
|
22
|
-
// record the bare version so the field does not churn between machines.
|
|
23
|
-
const output = (result.stdout || result.stderr || '').trim()
|
|
24
|
-
return output.match(/\d+\.\d+\.\d+[\w.-]*/)?.[0] ?? (output || null)
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function writeHarnessConfig(data) {
|
|
31
|
-
header('Step 10, Writing onboarding metadata')
|
|
32
|
-
|
|
33
|
-
const opencodeVersion = await detectOpencodeVersion()
|
|
34
|
-
const cwd = data.cwd ?? process.cwd()
|
|
35
|
-
const target = configPath(cwd)
|
|
36
|
-
|
|
37
|
-
// Read the existing file so load-bearing config (models, maxConcurrent)
|
|
38
|
-
// is preserved across metadata refreshes when not explicitly provided.
|
|
39
|
-
const existing = await fse.readJson(target).catch(() => null)
|
|
40
|
-
|
|
41
|
-
const selectedModels = Object.fromEntries(
|
|
42
|
-
Object.entries({
|
|
43
|
-
plan: data.planModel,
|
|
44
|
-
build: data.buildModel,
|
|
45
|
-
fast: data.fastModel,
|
|
46
|
-
}).filter(([, value]) => value)
|
|
47
|
-
)
|
|
48
|
-
const models = Object.keys(selectedModels).length > 0
|
|
49
|
-
? { ...existing?.models, ...selectedModels }
|
|
50
|
-
: existing?.models ?? null
|
|
51
|
-
const maxConcurrent = clampConcurrency(data.maxConcurrentAgents ?? existing?.agents?.maxConcurrent ?? 3)
|
|
52
|
-
|
|
53
|
-
const optionalTools = data.optionalTools ?? existing?.tools ?? {}
|
|
54
|
-
const { caveman: _caveman, ...existingTools } = existing?.tools ?? {}
|
|
55
|
-
const tools = {
|
|
56
|
-
rtk: !!(optionalTools.rtk?.optedIn ?? optionalTools.rtk),
|
|
57
|
-
quota: !!(optionalTools.quota?.optedIn ?? optionalTools.quota),
|
|
58
|
-
simpleEnglish: !!(
|
|
59
|
-
optionalTools.simpleEnglish?.optedIn ?? optionalTools.simpleEnglish ??
|
|
60
|
-
optionalTools.caveman?.optedIn ?? optionalTools.caveman
|
|
61
|
-
),
|
|
62
|
-
codegraph: !!(optionalTools.codegraph?.optedIn ?? optionalTools.codegraph),
|
|
63
|
-
memory: !!(optionalTools.memory?.optedIn ?? optionalTools.memory),
|
|
64
|
-
// humanizer was missing here, so a selection never survived a metadata
|
|
65
|
-
// write and patchGuardrails always saw it as off, leaving its marker
|
|
66
|
-
// section empty however the project was actually set up.
|
|
67
|
-
humanizer: !!(optionalTools.humanizer?.optedIn ?? optionalTools.humanizer),
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const payload = {
|
|
71
|
-
...existing,
|
|
72
|
-
// Never lower a schema version a repository already records.
|
|
73
|
-
version: Math.max(2, Number(existing?.version) || 0),
|
|
74
|
-
generatedAt: new Date().toISOString(),
|
|
75
|
-
harnessVersion,
|
|
76
|
-
opencodeVersion,
|
|
77
|
-
|
|
78
|
-
platform: {
|
|
79
|
-
...existing?.platform,
|
|
80
|
-
backlog: data.backlogPlatform ?? 'none',
|
|
81
|
-
repo: data.repoPlatform ?? 'none',
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
...(models ? { models } : {}),
|
|
85
|
-
|
|
86
|
-
agents: {
|
|
87
|
-
...existing?.agents,
|
|
88
|
-
maxConcurrent,
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
source: {
|
|
92
|
-
...existing?.source,
|
|
93
|
-
mode: data.sourceMode ?? 'current',
|
|
94
|
-
roots: data.sourceRoots ?? [],
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
tools: { ...existingTools, ...tools },
|
|
98
|
-
|
|
99
|
-
preexisting: {
|
|
100
|
-
...existing?.preexisting,
|
|
101
|
-
design: !!data.hasDesign,
|
|
102
|
-
architecture: !!data.hasArchitecture,
|
|
103
|
-
openspec: !!data.hasOpenspec,
|
|
104
|
-
},
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
await fse.ensureDir(path.dirname(target))
|
|
109
|
-
await fse.writeJson(target, payload, { spaces: 2 })
|
|
110
|
-
success('Wrote .opencode/harness.json')
|
|
111
|
-
if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
|
|
112
|
-
} catch (err) {
|
|
113
|
-
warn(`Could not write onboarding metadata: ${err.message}`)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { header, success, warn } from '../../utils/exec.js'
|
|
6
|
+
import { configPath } from '../../utils/paths.js'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const { version: harnessVersion } = require('../../../package.json')
|
|
10
|
+
|
|
11
|
+
function clampConcurrency(n) {
|
|
12
|
+
const v = Number(n)
|
|
13
|
+
if (!Number.isFinite(v)) return 3
|
|
14
|
+
return Math.min(5, Math.max(1, Math.round(v)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function detectOpencodeVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const result = await execa('opencode', ['--version'], { reject: false })
|
|
20
|
+
if (result.exitCode !== 0) return null
|
|
21
|
+
// `opencode --version` prints "opencode v2.0.22" on some builds and "2.0.22" on others;
|
|
22
|
+
// record the bare version so the field does not churn between machines.
|
|
23
|
+
const output = (result.stdout || result.stderr || '').trim()
|
|
24
|
+
return output.match(/\d+\.\d+\.\d+[\w.-]*/)?.[0] ?? (output || null)
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeHarnessConfig(data) {
|
|
31
|
+
header('Step 10, Writing onboarding metadata')
|
|
32
|
+
|
|
33
|
+
const opencodeVersion = await detectOpencodeVersion()
|
|
34
|
+
const cwd = data.cwd ?? process.cwd()
|
|
35
|
+
const target = configPath(cwd)
|
|
36
|
+
|
|
37
|
+
// Read the existing file so load-bearing config (models, maxConcurrent)
|
|
38
|
+
// is preserved across metadata refreshes when not explicitly provided.
|
|
39
|
+
const existing = await fse.readJson(target).catch(() => null)
|
|
40
|
+
|
|
41
|
+
const selectedModels = Object.fromEntries(
|
|
42
|
+
Object.entries({
|
|
43
|
+
plan: data.planModel,
|
|
44
|
+
build: data.buildModel,
|
|
45
|
+
fast: data.fastModel,
|
|
46
|
+
}).filter(([, value]) => value)
|
|
47
|
+
)
|
|
48
|
+
const models = Object.keys(selectedModels).length > 0
|
|
49
|
+
? { ...existing?.models, ...selectedModels }
|
|
50
|
+
: existing?.models ?? null
|
|
51
|
+
const maxConcurrent = clampConcurrency(data.maxConcurrentAgents ?? existing?.agents?.maxConcurrent ?? 3)
|
|
52
|
+
|
|
53
|
+
const optionalTools = data.optionalTools ?? existing?.tools ?? {}
|
|
54
|
+
const { caveman: _caveman, ...existingTools } = existing?.tools ?? {}
|
|
55
|
+
const tools = {
|
|
56
|
+
rtk: !!(optionalTools.rtk?.optedIn ?? optionalTools.rtk),
|
|
57
|
+
quota: !!(optionalTools.quota?.optedIn ?? optionalTools.quota),
|
|
58
|
+
simpleEnglish: !!(
|
|
59
|
+
optionalTools.simpleEnglish?.optedIn ?? optionalTools.simpleEnglish ??
|
|
60
|
+
optionalTools.caveman?.optedIn ?? optionalTools.caveman
|
|
61
|
+
),
|
|
62
|
+
codegraph: !!(optionalTools.codegraph?.optedIn ?? optionalTools.codegraph),
|
|
63
|
+
memory: !!(optionalTools.memory?.optedIn ?? optionalTools.memory),
|
|
64
|
+
// humanizer was missing here, so a selection never survived a metadata
|
|
65
|
+
// write and patchGuardrails always saw it as off, leaving its marker
|
|
66
|
+
// section empty however the project was actually set up.
|
|
67
|
+
humanizer: !!(optionalTools.humanizer?.optedIn ?? optionalTools.humanizer),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const payload = {
|
|
71
|
+
...existing,
|
|
72
|
+
// Never lower a schema version a repository already records.
|
|
73
|
+
version: Math.max(2, Number(existing?.version) || 0),
|
|
74
|
+
generatedAt: new Date().toISOString(),
|
|
75
|
+
harnessVersion,
|
|
76
|
+
opencodeVersion,
|
|
77
|
+
|
|
78
|
+
platform: {
|
|
79
|
+
...existing?.platform,
|
|
80
|
+
backlog: data.backlogPlatform ?? 'none',
|
|
81
|
+
repo: data.repoPlatform ?? 'none',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
...(models ? { models } : {}),
|
|
85
|
+
|
|
86
|
+
agents: {
|
|
87
|
+
...existing?.agents,
|
|
88
|
+
maxConcurrent,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
source: {
|
|
92
|
+
...existing?.source,
|
|
93
|
+
mode: data.sourceMode ?? 'current',
|
|
94
|
+
roots: data.sourceRoots ?? [],
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
tools: { ...existingTools, ...tools },
|
|
98
|
+
|
|
99
|
+
preexisting: {
|
|
100
|
+
...existing?.preexisting,
|
|
101
|
+
design: !!data.hasDesign,
|
|
102
|
+
architecture: !!data.hasArchitecture,
|
|
103
|
+
openspec: !!data.hasOpenspec,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await fse.ensureDir(path.dirname(target))
|
|
109
|
+
await fse.writeJson(target, payload, { spaces: 2 })
|
|
110
|
+
success('Wrote .opencode/harness.json')
|
|
111
|
+
if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
|
|
112
|
+
} catch (err) {
|
|
113
|
+
warn(`Could not write onboarding metadata: ${err.message}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { search, select } from '@inquirer/prompts'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
|
|
8
|
-
const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
|
|
9
|
-
|
|
10
|
-
function costTier(input) {
|
|
11
|
-
if (input === undefined || input === null) return '';
|
|
12
|
-
const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
|
|
13
|
-
return tier ? ` ${tier.label}` : '';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function costTierDisplay(cost, canonicalCost) {
|
|
17
|
-
return costTier(canonicalCost !== undefined ? canonicalCost : cost);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatPrice(price) {
|
|
21
|
-
if (price === undefined || price === null) return '?';
|
|
22
|
-
if (price === 0) return '$0 (subscription)';
|
|
23
|
-
return `$${price}/M`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildDisplayModels(rawModels) {
|
|
27
|
-
return rawModels.map(m => {
|
|
28
|
-
const priceStr = formatPrice(m.cost);
|
|
29
|
-
const canonicalNote = m.canonicalCost !== undefined
|
|
30
|
-
? ` · official price: ${formatPrice(m.canonicalCost)}/M`
|
|
31
|
-
: '';
|
|
32
|
-
const context = m.context ? `${m.context / 1000}k` : '?';
|
|
33
|
-
return {
|
|
34
|
-
...m,
|
|
35
|
-
label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
|
|
36
|
-
description: `${priceStr}${canonicalNote} · context: ${context}`,
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function buildModelChoices(input, models) {
|
|
42
|
-
const q = (input || '').toLowerCase();
|
|
43
|
-
const filtered = q
|
|
44
|
-
? models.filter(m =>
|
|
45
|
-
m.label.toLowerCase().includes(q) ||
|
|
46
|
-
m.id.toLowerCase().includes(q)
|
|
47
|
-
)
|
|
48
|
-
: models;
|
|
49
|
-
|
|
50
|
-
return [
|
|
51
|
-
{
|
|
52
|
-
name: 'None',
|
|
53
|
-
value: null,
|
|
54
|
-
description: 'Leave this model unset',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: '← Change provider',
|
|
58
|
-
value: '__back__',
|
|
59
|
-
description: 'Go back to provider selection',
|
|
60
|
-
},
|
|
61
|
-
...filtered.slice(0, 50).map(m => ({
|
|
62
|
-
name: m.label,
|
|
63
|
-
value: m.id,
|
|
64
|
-
description: m.description,
|
|
65
|
-
})),
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function pickModel(message, models) {
|
|
70
|
-
return search({
|
|
71
|
-
message,
|
|
72
|
-
source: input => buildModelChoices(input, models),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function filterModelsByProvider(models, provider) {
|
|
77
|
-
if (!provider) return models
|
|
78
|
-
return models.filter(m => m.id.startsWith(`${provider}/`))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function pickProvider(message) {
|
|
82
|
-
return select({
|
|
83
|
-
message,
|
|
84
|
-
choices: modelsPreset.providers.map(p => ({ value: p.value, name: p.name })),
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { modelsPreset };
|
|
1
|
+
import { search, select } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
|
|
8
|
+
const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
|
|
9
|
+
|
|
10
|
+
function costTier(input) {
|
|
11
|
+
if (input === undefined || input === null) return '';
|
|
12
|
+
const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
|
|
13
|
+
return tier ? ` ${tier.label}` : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function costTierDisplay(cost, canonicalCost) {
|
|
17
|
+
return costTier(canonicalCost !== undefined ? canonicalCost : cost);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatPrice(price) {
|
|
21
|
+
if (price === undefined || price === null) return '?';
|
|
22
|
+
if (price === 0) return '$0 (subscription)';
|
|
23
|
+
return `$${price}/M`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildDisplayModels(rawModels) {
|
|
27
|
+
return rawModels.map(m => {
|
|
28
|
+
const priceStr = formatPrice(m.cost);
|
|
29
|
+
const canonicalNote = m.canonicalCost !== undefined
|
|
30
|
+
? ` · official price: ${formatPrice(m.canonicalCost)}/M`
|
|
31
|
+
: '';
|
|
32
|
+
const context = m.context ? `${m.context / 1000}k` : '?';
|
|
33
|
+
return {
|
|
34
|
+
...m,
|
|
35
|
+
label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
|
|
36
|
+
description: `${priceStr}${canonicalNote} · context: ${context}`,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildModelChoices(input, models) {
|
|
42
|
+
const q = (input || '').toLowerCase();
|
|
43
|
+
const filtered = q
|
|
44
|
+
? models.filter(m =>
|
|
45
|
+
m.label.toLowerCase().includes(q) ||
|
|
46
|
+
m.id.toLowerCase().includes(q)
|
|
47
|
+
)
|
|
48
|
+
: models;
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
name: 'None',
|
|
53
|
+
value: null,
|
|
54
|
+
description: 'Leave this model unset',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '← Change provider',
|
|
58
|
+
value: '__back__',
|
|
59
|
+
description: 'Go back to provider selection',
|
|
60
|
+
},
|
|
61
|
+
...filtered.slice(0, 50).map(m => ({
|
|
62
|
+
name: m.label,
|
|
63
|
+
value: m.id,
|
|
64
|
+
description: m.description,
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function pickModel(message, models) {
|
|
70
|
+
return search({
|
|
71
|
+
message,
|
|
72
|
+
source: input => buildModelChoices(input, models),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function filterModelsByProvider(models, provider) {
|
|
77
|
+
if (!provider) return models
|
|
78
|
+
return models.filter(m => m.id.startsWith(`${provider}/`))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function pickProvider(message) {
|
|
82
|
+
return select({
|
|
83
|
+
message,
|
|
84
|
+
choices: modelsPreset.providers.map(p => ({ value: p.value, name: p.name })),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { modelsPreset };
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { header, info, prompt, success, warn } from '../../utils/exec.js'
|
|
2
|
-
import { fetchModels } from '../../utils/models-cache.js'
|
|
3
|
-
import { buildDisplayModels, filterModelsByProvider, modelsPreset, pickModel, pickProvider } from './format.js'
|
|
4
|
-
import { writeModelsToConfigs } from './write.js'
|
|
5
|
-
|
|
6
|
-
async function pickRoleModel(role, allModels) {
|
|
7
|
-
const rolePreset = modelsPreset.roles[role]
|
|
8
|
-
const [first, ...rest] = rolePreset.info
|
|
9
|
-
prompt(first)
|
|
10
|
-
for (const line of rest) info(line)
|
|
11
|
-
console.log()
|
|
12
|
-
|
|
13
|
-
while (true) {
|
|
14
|
-
const provider = await pickProvider('Provider:')
|
|
15
|
-
const filtered = filterModelsByProvider(allModels, provider)
|
|
16
|
-
const available = filtered.length > 0 ? filtered : allModels
|
|
17
|
-
|
|
18
|
-
const suggestion = rolePreset.suggestions?.[provider]
|
|
19
|
-
if (suggestion) info(` Suggested: ${suggestion}`)
|
|
20
|
-
if (filtered.length === 0 && provider) warn(` No ${provider} models found: showing all`)
|
|
21
|
-
|
|
22
|
-
const model = await pickModel(rolePreset.prompt, available)
|
|
23
|
-
if (model !== '__back__') return model
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function chooseModels() {
|
|
28
|
-
header('Step 7, Choose models');
|
|
29
|
-
|
|
30
|
-
info('Fetching available models from models.dev...');
|
|
31
|
-
const { models: rawModels, source } = await fetchModels();
|
|
32
|
-
|
|
33
|
-
if (!rawModels) {
|
|
34
|
-
warn('Could not fetch models (offline and no cache). Skipping model selection.');
|
|
35
|
-
warn('Set models later in opencode.jsonc');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (source === 'stale-cache') {
|
|
40
|
-
warn('Network unavailable, using cached model list (may be outdated).');
|
|
41
|
-
} else if (source === 'cache') {
|
|
42
|
-
info('Using cached model list (refreshes weekly).');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const models = buildDisplayModels(rawModels);
|
|
46
|
-
success(`${models.length} models available`);
|
|
47
|
-
console.log();
|
|
48
|
-
info('Three models: main session, engineers, and light commands.');
|
|
49
|
-
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive');
|
|
50
|
-
info('Pick provider first, then search by name. Change later in opencode.jsonc');
|
|
51
|
-
console.log();
|
|
52
|
-
|
|
53
|
-
const planModel = await pickRoleModel('plan', models); console.log();
|
|
54
|
-
const buildModel = await pickRoleModel('build', models); console.log();
|
|
55
|
-
const fastModel = await pickRoleModel('fast', models); console.log();
|
|
56
|
-
|
|
57
|
-
await writeModelsToConfigs({ planModel, buildModel, fastModel });
|
|
58
|
-
|
|
59
|
-
console.log();
|
|
60
|
-
warn('Make sure you have API access to the selected models.');
|
|
61
|
-
warn('Change them anytime in opencode.jsonc');
|
|
62
|
-
|
|
63
|
-
return { planModel, buildModel, fastModel };
|
|
64
|
-
}
|
|
1
|
+
import { header, info, prompt, success, warn } from '../../utils/exec.js'
|
|
2
|
+
import { fetchModels } from '../../utils/models-cache.js'
|
|
3
|
+
import { buildDisplayModels, filterModelsByProvider, modelsPreset, pickModel, pickProvider } from './format.js'
|
|
4
|
+
import { writeModelsToConfigs } from './write.js'
|
|
5
|
+
|
|
6
|
+
async function pickRoleModel(role, allModels) {
|
|
7
|
+
const rolePreset = modelsPreset.roles[role]
|
|
8
|
+
const [first, ...rest] = rolePreset.info
|
|
9
|
+
prompt(first)
|
|
10
|
+
for (const line of rest) info(line)
|
|
11
|
+
console.log()
|
|
12
|
+
|
|
13
|
+
while (true) {
|
|
14
|
+
const provider = await pickProvider('Provider:')
|
|
15
|
+
const filtered = filterModelsByProvider(allModels, provider)
|
|
16
|
+
const available = filtered.length > 0 ? filtered : allModels
|
|
17
|
+
|
|
18
|
+
const suggestion = rolePreset.suggestions?.[provider]
|
|
19
|
+
if (suggestion) info(` Suggested: ${suggestion}`)
|
|
20
|
+
if (filtered.length === 0 && provider) warn(` No ${provider} models found: showing all`)
|
|
21
|
+
|
|
22
|
+
const model = await pickModel(rolePreset.prompt, available)
|
|
23
|
+
if (model !== '__back__') return model
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function chooseModels() {
|
|
28
|
+
header('Step 7, Choose models');
|
|
29
|
+
|
|
30
|
+
info('Fetching available models from models.dev...');
|
|
31
|
+
const { models: rawModels, source } = await fetchModels();
|
|
32
|
+
|
|
33
|
+
if (!rawModels) {
|
|
34
|
+
warn('Could not fetch models (offline and no cache). Skipping model selection.');
|
|
35
|
+
warn('Set models later in opencode.jsonc');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (source === 'stale-cache') {
|
|
40
|
+
warn('Network unavailable, using cached model list (may be outdated).');
|
|
41
|
+
} else if (source === 'cache') {
|
|
42
|
+
info('Using cached model list (refreshes weekly).');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const models = buildDisplayModels(rawModels);
|
|
46
|
+
success(`${models.length} models available`);
|
|
47
|
+
console.log();
|
|
48
|
+
info('Three models: main session, engineers, and light commands.');
|
|
49
|
+
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive');
|
|
50
|
+
info('Pick provider first, then search by name. Change later in opencode.jsonc');
|
|
51
|
+
console.log();
|
|
52
|
+
|
|
53
|
+
const planModel = await pickRoleModel('plan', models); console.log();
|
|
54
|
+
const buildModel = await pickRoleModel('build', models); console.log();
|
|
55
|
+
const fastModel = await pickRoleModel('fast', models); console.log();
|
|
56
|
+
|
|
57
|
+
await writeModelsToConfigs({ planModel, buildModel, fastModel });
|
|
58
|
+
|
|
59
|
+
console.log();
|
|
60
|
+
warn('Make sure you have API access to the selected models.');
|
|
61
|
+
warn('Change them anytime in opencode.jsonc');
|
|
62
|
+
|
|
63
|
+
return { planModel, buildModel, fastModel };
|
|
64
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import { applyEdits, modify, parse } from 'jsonc-parser'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { success, warn } from '../../utils/exec.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Writes the selected build model as the default model in opencode.jsonc.
|
|
8
|
-
* Per-tier models (plan/build/fast) are recorded in harness.json (models)
|
|
9
|
-
* by the metadata step, then consumed by the agent-variant generator and /plan-apply.
|
|
10
|
-
*
|
|
11
|
-
* opencode tolerates JSONC (comments, trailing commas) in its config, so this
|
|
12
|
-
* must not use readJson/writeJson: a strict parse crashes on valid configs and
|
|
13
|
-
* a strict rewrite deletes user comments. jsonc-parser edits only the `model`
|
|
14
|
-
* key and preserves the rest of the file byte-for-byte.
|
|
15
|
-
*/
|
|
16
|
-
export async function writeModelsToConfigs({ buildModel, keepExisting = false, cwd = process.cwd() }) {
|
|
17
|
-
const opencodeJsonPath = path.join(cwd, 'opencode.jsonc')
|
|
18
|
-
if (!await fse.pathExists(opencodeJsonPath)) return
|
|
19
|
-
|
|
20
|
-
const text = await fse.readFile(opencodeJsonPath, 'utf-8')
|
|
21
|
-
const errors = []
|
|
22
|
-
parse(text, errors)
|
|
23
|
-
if (errors.length > 0) {
|
|
24
|
-
warn('opencode.jsonc could not be parsed: leaving it untouched. Set "model" manually.')
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// A model the repository already chose is its decision, not the wizard's: `update` used to
|
|
29
|
-
// reset a deliberately pinned default to the build tier in every consumer it ran in.
|
|
30
|
-
if (keepExisting && typeof parse(text)?.model === 'string') return
|
|
31
|
-
|
|
32
|
-
// `undefined` removes the key; a string value sets it.
|
|
33
|
-
const edits = modify(text, ['model'], buildModel || undefined, {
|
|
34
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
35
|
-
})
|
|
36
|
-
await fse.writeFile(opencodeJsonPath, applyEdits(text, edits), 'utf-8')
|
|
37
|
-
if (buildModel) success(`default model -> ${buildModel} (written to opencode.jsonc)`)
|
|
38
|
-
}
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import { applyEdits, modify, parse } from 'jsonc-parser'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { success, warn } from '../../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Writes the selected build model as the default model in opencode.jsonc.
|
|
8
|
+
* Per-tier models (plan/build/fast) are recorded in harness.json (models)
|
|
9
|
+
* by the metadata step, then consumed by the agent-variant generator and /plan-apply.
|
|
10
|
+
*
|
|
11
|
+
* opencode tolerates JSONC (comments, trailing commas) in its config, so this
|
|
12
|
+
* must not use readJson/writeJson: a strict parse crashes on valid configs and
|
|
13
|
+
* a strict rewrite deletes user comments. jsonc-parser edits only the `model`
|
|
14
|
+
* key and preserves the rest of the file byte-for-byte.
|
|
15
|
+
*/
|
|
16
|
+
export async function writeModelsToConfigs({ buildModel, keepExisting = false, cwd = process.cwd() }) {
|
|
17
|
+
const opencodeJsonPath = path.join(cwd, 'opencode.jsonc')
|
|
18
|
+
if (!await fse.pathExists(opencodeJsonPath)) return
|
|
19
|
+
|
|
20
|
+
const text = await fse.readFile(opencodeJsonPath, 'utf-8')
|
|
21
|
+
const errors = []
|
|
22
|
+
parse(text, errors)
|
|
23
|
+
if (errors.length > 0) {
|
|
24
|
+
warn('opencode.jsonc could not be parsed: leaving it untouched. Set "model" manually.')
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A model the repository already chose is its decision, not the wizard's: `update` used to
|
|
29
|
+
// reset a deliberately pinned default to the build tier in every consumer it ran in.
|
|
30
|
+
if (keepExisting && typeof parse(text)?.model === 'string') return
|
|
31
|
+
|
|
32
|
+
// `undefined` removes the key; a string value sets it.
|
|
33
|
+
const edits = modify(text, ['model'], buildModel || undefined, {
|
|
34
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
35
|
+
})
|
|
36
|
+
await fse.writeFile(opencodeJsonPath, applyEdits(text, edits), 'utf-8')
|
|
37
|
+
if (buildModel) success(`default model -> ${buildModel} (written to opencode.jsonc)`)
|
|
38
|
+
}
|