@plainconceptsplatform/agent-harness 3.3.0 → 3.4.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 +454 -454
- 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/wizard.js +134 -134
- 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/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- 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/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +120 -120
- package/cli/steps/copy/fullstack-engineer.js +89 -89
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -307
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- 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 +81 -80
- 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 +10 -9
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -8
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -5
- package/harness/.opencode/commands/plan-explore.md +11 -9
- package/harness/.opencode/commands/plan-propose.md +11 -9
- package/harness/.opencode/commands/plan-quick.md +7 -5
- package/harness/.opencode/commands/plan-story.md +11 -9
- package/harness/.opencode/commands/repo-audit.md +7 -5
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/package.json +5 -5
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -354
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
- package/harness/.opencode/scripts/check-diff.mjs +244 -244
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- 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 +12 -14
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/template.md +36 -36
- 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-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +86 -86
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-propose/SKILL.md +125 -125
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-verify/SKILL.md +150 -150
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
-
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Normalizes CodeGraph's MCP command in the selected OpenCode config.
|
|
10
|
-
* Returns true if the config was successfully normalized (or no config exists).
|
|
11
|
-
*/
|
|
12
|
-
export async function fixCodegraphConfig(location = 'local') {
|
|
13
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim()
|
|
14
|
-
const configDir = location === 'global'
|
|
15
|
-
? path.join(xdgConfigHome || path.join(os.homedir(), '.config'), 'opencode')
|
|
16
|
-
: process.cwd()
|
|
17
|
-
const jsoncFile = path.join(configDir, 'opencode.jsonc')
|
|
18
|
-
const jsonFile = path.join(configDir, 'opencode.json')
|
|
19
|
-
const configFile = await fse.pathExists(jsoncFile)
|
|
20
|
-
? jsoncFile
|
|
21
|
-
: await fse.pathExists(jsonFile)
|
|
22
|
-
? jsonFile
|
|
23
|
-
: jsoncFile
|
|
24
|
-
|
|
25
|
-
if (!await fse.pathExists(configFile)) return true
|
|
26
|
-
|
|
27
|
-
let configText
|
|
28
|
-
let config
|
|
29
|
-
try {
|
|
30
|
-
configText = await fse.readFile(configFile, 'utf-8')
|
|
31
|
-
const errors = []
|
|
32
|
-
config = parseJsonc(configText, errors)
|
|
33
|
-
if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
|
|
34
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
35
|
-
throw new Error('unexpected structure')
|
|
36
|
-
}
|
|
37
|
-
} catch {
|
|
38
|
-
warn('Could not parse opencode.jsonc, leaving it untouched')
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const entry = config.mcp?.servers?.codegraph
|
|
43
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return true
|
|
44
|
-
|
|
45
|
-
const normalizedEntry = { ...entry }
|
|
46
|
-
let changed = false
|
|
47
|
-
|
|
48
|
-
if (normalizedEntry.type == null) {
|
|
49
|
-
normalizedEntry.type = 'local'
|
|
50
|
-
changed = true
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(normalizedEntry.command) && normalizedEntry.command[0] === 'codegraph') {
|
|
53
|
-
normalizedEntry.command = ['npx', '--yes', '@colbymchenry/codegraph', ...normalizedEntry.command.slice(1)]
|
|
54
|
-
changed = true
|
|
55
|
-
}
|
|
56
|
-
if (normalizedEntry.timeout == null) {
|
|
57
|
-
normalizedEntry.timeout = {
|
|
58
|
-
catalog: 120000,
|
|
59
|
-
execution: 120000,
|
|
60
|
-
}
|
|
61
|
-
changed = true
|
|
62
|
-
} else if (typeof normalizedEntry.timeout !== 'object' || Array.isArray(normalizedEntry.timeout)) {
|
|
63
|
-
const timeout = Number.isFinite(normalizedEntry.timeout) && normalizedEntry.timeout > 0
|
|
64
|
-
? normalizedEntry.timeout
|
|
65
|
-
: 120000
|
|
66
|
-
normalizedEntry.timeout = { catalog: timeout, execution: timeout }
|
|
67
|
-
changed = true
|
|
68
|
-
}
|
|
69
|
-
if (normalizedEntry.codemode !== false) {
|
|
70
|
-
normalizedEntry.codemode = false
|
|
71
|
-
changed = true
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (changed) {
|
|
75
|
-
configText = applyEdits(configText, modify(configText, ['mcp', 'servers', 'codegraph'], normalizedEntry, {
|
|
76
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
77
|
-
}))
|
|
78
|
-
await fse.writeFile(configFile, configText, 'utf-8')
|
|
79
|
-
warn('Normalized codegraph MCP config in opencode.jsonc')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return true
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function installCodegraph(options = {}) {
|
|
86
|
-
if (!options.skipHeader) header('Installing codegraph')
|
|
87
|
-
|
|
88
|
-
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
89
|
-
|
|
90
|
-
loading(`configuring codegraph for opencode (${location})...`)
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const installResult = await execa(
|
|
94
|
-
'npx',
|
|
95
|
-
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
96
|
-
{
|
|
97
|
-
cwd: process.cwd(),
|
|
98
|
-
reject: false,
|
|
99
|
-
stdio: 'pipe',
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if (installResult.exitCode !== 0) {
|
|
104
|
-
warn('codegraph install exited with non-zero code')
|
|
105
|
-
return { optedIn: true, installed: false }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const configFixed = await fixCodegraphConfig(location)
|
|
109
|
-
|
|
110
|
-
if (!configFixed) {
|
|
111
|
-
warn('codegraph config could not be merged: skipping init')
|
|
112
|
-
return { optedIn: true, installed: false }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
success(`codegraph configured for opencode (${location})`)
|
|
116
|
-
} catch (err) {
|
|
117
|
-
error(`Failed to install codegraph: ${err.message}`)
|
|
118
|
-
return { optedIn: true, installed: false }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
loading('initializing codegraph project index...')
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
125
|
-
cwd: process.cwd(),
|
|
126
|
-
reject: false,
|
|
127
|
-
stdio: 'pipe',
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (initResult.exitCode !== 0) {
|
|
131
|
-
warn('codegraph init exited with non-zero code')
|
|
132
|
-
return { optedIn: true, installed: false }
|
|
133
|
-
}
|
|
134
|
-
success('codegraph project index initialized')
|
|
135
|
-
} catch (err) {
|
|
136
|
-
error(`Failed to initialize codegraph: ${err.message}`)
|
|
137
|
-
return { optedIn: true, installed: false }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return { optedIn: true, installed: true }
|
|
141
|
-
}
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
+
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes CodeGraph's MCP command in the selected OpenCode config.
|
|
10
|
+
* Returns true if the config was successfully normalized (or no config exists).
|
|
11
|
+
*/
|
|
12
|
+
export async function fixCodegraphConfig(location = 'local') {
|
|
13
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim()
|
|
14
|
+
const configDir = location === 'global'
|
|
15
|
+
? path.join(xdgConfigHome || path.join(os.homedir(), '.config'), 'opencode')
|
|
16
|
+
: process.cwd()
|
|
17
|
+
const jsoncFile = path.join(configDir, 'opencode.jsonc')
|
|
18
|
+
const jsonFile = path.join(configDir, 'opencode.json')
|
|
19
|
+
const configFile = await fse.pathExists(jsoncFile)
|
|
20
|
+
? jsoncFile
|
|
21
|
+
: await fse.pathExists(jsonFile)
|
|
22
|
+
? jsonFile
|
|
23
|
+
: jsoncFile
|
|
24
|
+
|
|
25
|
+
if (!await fse.pathExists(configFile)) return true
|
|
26
|
+
|
|
27
|
+
let configText
|
|
28
|
+
let config
|
|
29
|
+
try {
|
|
30
|
+
configText = await fse.readFile(configFile, 'utf-8')
|
|
31
|
+
const errors = []
|
|
32
|
+
config = parseJsonc(configText, errors)
|
|
33
|
+
if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
|
|
34
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
35
|
+
throw new Error('unexpected structure')
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
warn('Could not parse opencode.jsonc, leaving it untouched')
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entry = config.mcp?.servers?.codegraph
|
|
43
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return true
|
|
44
|
+
|
|
45
|
+
const normalizedEntry = { ...entry }
|
|
46
|
+
let changed = false
|
|
47
|
+
|
|
48
|
+
if (normalizedEntry.type == null) {
|
|
49
|
+
normalizedEntry.type = 'local'
|
|
50
|
+
changed = true
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(normalizedEntry.command) && normalizedEntry.command[0] === 'codegraph') {
|
|
53
|
+
normalizedEntry.command = ['npx', '--yes', '@colbymchenry/codegraph', ...normalizedEntry.command.slice(1)]
|
|
54
|
+
changed = true
|
|
55
|
+
}
|
|
56
|
+
if (normalizedEntry.timeout == null) {
|
|
57
|
+
normalizedEntry.timeout = {
|
|
58
|
+
catalog: 120000,
|
|
59
|
+
execution: 120000,
|
|
60
|
+
}
|
|
61
|
+
changed = true
|
|
62
|
+
} else if (typeof normalizedEntry.timeout !== 'object' || Array.isArray(normalizedEntry.timeout)) {
|
|
63
|
+
const timeout = Number.isFinite(normalizedEntry.timeout) && normalizedEntry.timeout > 0
|
|
64
|
+
? normalizedEntry.timeout
|
|
65
|
+
: 120000
|
|
66
|
+
normalizedEntry.timeout = { catalog: timeout, execution: timeout }
|
|
67
|
+
changed = true
|
|
68
|
+
}
|
|
69
|
+
if (normalizedEntry.codemode !== false) {
|
|
70
|
+
normalizedEntry.codemode = false
|
|
71
|
+
changed = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (changed) {
|
|
75
|
+
configText = applyEdits(configText, modify(configText, ['mcp', 'servers', 'codegraph'], normalizedEntry, {
|
|
76
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
77
|
+
}))
|
|
78
|
+
await fse.writeFile(configFile, configText, 'utf-8')
|
|
79
|
+
warn('Normalized codegraph MCP config in opencode.jsonc')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function installCodegraph(options = {}) {
|
|
86
|
+
if (!options.skipHeader) header('Installing codegraph')
|
|
87
|
+
|
|
88
|
+
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
89
|
+
|
|
90
|
+
loading(`configuring codegraph for opencode (${location})...`)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const installResult = await execa(
|
|
94
|
+
'npx',
|
|
95
|
+
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
96
|
+
{
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
reject: false,
|
|
99
|
+
stdio: 'pipe',
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (installResult.exitCode !== 0) {
|
|
104
|
+
warn('codegraph install exited with non-zero code')
|
|
105
|
+
return { optedIn: true, installed: false }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const configFixed = await fixCodegraphConfig(location)
|
|
109
|
+
|
|
110
|
+
if (!configFixed) {
|
|
111
|
+
warn('codegraph config could not be merged: skipping init')
|
|
112
|
+
return { optedIn: true, installed: false }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
success(`codegraph configured for opencode (${location})`)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
error(`Failed to install codegraph: ${err.message}`)
|
|
118
|
+
return { optedIn: true, installed: false }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
loading('initializing codegraph project index...')
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
reject: false,
|
|
127
|
+
stdio: 'pipe',
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (initResult.exitCode !== 0) {
|
|
131
|
+
warn('codegraph init exited with non-zero code')
|
|
132
|
+
return { optedIn: true, installed: false }
|
|
133
|
+
}
|
|
134
|
+
success('codegraph project index initialized')
|
|
135
|
+
} catch (err) {
|
|
136
|
+
error(`Failed to initialize codegraph: ${err.message}`)
|
|
137
|
+
return { optedIn: true, installed: false }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { optedIn: true, installed: true }
|
|
141
|
+
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { parse } from 'jsonc-parser'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Work out which optional tools a project actually has, rather than trusting
|
|
7
|
-
* the flags in harness.json.
|
|
8
|
-
*
|
|
9
|
-
* Those flags drift. Numa and Odissey both had codegraph and agentmemory MCP
|
|
10
|
-
* servers enabled in opencode.jsonc and the humanizer and simple-english skills
|
|
11
|
-
* installed, while harness.json recorded `codegraph: false`, `memory: false` and
|
|
12
|
-
* no humanizer key at all. patchGuardrails believed the config and emptied those
|
|
13
|
-
* marker sections, so agents lost the guidance for tools the project was running.
|
|
14
|
-
*
|
|
15
|
-
* Detection only ever turns a tool ON. A flag that is already true is left
|
|
16
|
-
* alone, because some of these install globally and cannot be seen from inside
|
|
17
|
-
* the repository at all.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function mcpServers(cwd) {
|
|
21
|
-
const configPath = path.join(cwd, 'opencode.jsonc')
|
|
22
|
-
if (!fse.pathExistsSync(configPath)) return {}
|
|
23
|
-
const errors = []
|
|
24
|
-
const parsed = parse(fse.readFileSync(configPath, 'utf-8'), errors)
|
|
25
|
-
if (errors.length > 0 || typeof parsed !== 'object' || parsed === null) return {}
|
|
26
|
-
return parsed.mcp ?? {}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function hasSkill(cwd, name) {
|
|
30
|
-
return fse.pathExistsSync(path.join(cwd, '.agents', 'skills', name))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** An MCP server counts as present unless it is explicitly disabled. */
|
|
34
|
-
function hasMcp(servers, name) {
|
|
35
|
-
const server = servers[name]
|
|
36
|
-
if (!server) return false
|
|
37
|
-
return server.enabled !== false
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function detectInstalledTools(cwd = process.cwd()) {
|
|
41
|
-
const servers = mcpServers(cwd)
|
|
42
|
-
return {
|
|
43
|
-
// rtk is a global CLI with nothing to find in the repo, so it is not
|
|
44
|
-
// detectable here and the saved flag stands on its own.
|
|
45
|
-
codegraph: hasMcp(servers, 'codegraph') || fse.pathExistsSync(path.join(cwd, '.codegraph')),
|
|
46
|
-
memory: hasMcp(servers, 'agentmemory'),
|
|
47
|
-
simpleEnglish: hasSkill(cwd, 'simple-english'),
|
|
48
|
-
humanizer: hasSkill(cwd, 'humanizer'),
|
|
49
|
-
quota: fse.pathExistsSync(path.join(cwd, '.opencode', 'opencode-quota')),
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Saved flags merged with what is on disk. Detection can only add, so a tool
|
|
55
|
-
* recorded as enabled stays enabled even when nothing local proves it.
|
|
56
|
-
*/
|
|
57
|
-
export function reconcileTools(saved = {}, cwd = process.cwd()) {
|
|
58
|
-
const detected = detectInstalledTools(cwd)
|
|
59
|
-
const merged = { ...saved }
|
|
60
|
-
for (const [key, present] of Object.entries(detected)) {
|
|
61
|
-
if (present) merged[key] = true
|
|
62
|
-
}
|
|
63
|
-
// simpleEnglish replaced caveman; honour the old key one last time.
|
|
64
|
-
if (saved.caveman) merged.simpleEnglish = true
|
|
65
|
-
return merged
|
|
66
|
-
}
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { parse } from 'jsonc-parser'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Work out which optional tools a project actually has, rather than trusting
|
|
7
|
+
* the flags in harness.json.
|
|
8
|
+
*
|
|
9
|
+
* Those flags drift. Numa and Odissey both had codegraph and agentmemory MCP
|
|
10
|
+
* servers enabled in opencode.jsonc and the humanizer and simple-english skills
|
|
11
|
+
* installed, while harness.json recorded `codegraph: false`, `memory: false` and
|
|
12
|
+
* no humanizer key at all. patchGuardrails believed the config and emptied those
|
|
13
|
+
* marker sections, so agents lost the guidance for tools the project was running.
|
|
14
|
+
*
|
|
15
|
+
* Detection only ever turns a tool ON. A flag that is already true is left
|
|
16
|
+
* alone, because some of these install globally and cannot be seen from inside
|
|
17
|
+
* the repository at all.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function mcpServers(cwd) {
|
|
21
|
+
const configPath = path.join(cwd, 'opencode.jsonc')
|
|
22
|
+
if (!fse.pathExistsSync(configPath)) return {}
|
|
23
|
+
const errors = []
|
|
24
|
+
const parsed = parse(fse.readFileSync(configPath, 'utf-8'), errors)
|
|
25
|
+
if (errors.length > 0 || typeof parsed !== 'object' || parsed === null) return {}
|
|
26
|
+
return parsed.mcp ?? {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasSkill(cwd, name) {
|
|
30
|
+
return fse.pathExistsSync(path.join(cwd, '.agents', 'skills', name))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An MCP server counts as present unless it is explicitly disabled. */
|
|
34
|
+
function hasMcp(servers, name) {
|
|
35
|
+
const server = servers[name]
|
|
36
|
+
if (!server) return false
|
|
37
|
+
return server.enabled !== false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function detectInstalledTools(cwd = process.cwd()) {
|
|
41
|
+
const servers = mcpServers(cwd)
|
|
42
|
+
return {
|
|
43
|
+
// rtk is a global CLI with nothing to find in the repo, so it is not
|
|
44
|
+
// detectable here and the saved flag stands on its own.
|
|
45
|
+
codegraph: hasMcp(servers, 'codegraph') || fse.pathExistsSync(path.join(cwd, '.codegraph')),
|
|
46
|
+
memory: hasMcp(servers, 'agentmemory'),
|
|
47
|
+
simpleEnglish: hasSkill(cwd, 'simple-english'),
|
|
48
|
+
humanizer: hasSkill(cwd, 'humanizer'),
|
|
49
|
+
quota: fse.pathExistsSync(path.join(cwd, '.opencode', 'opencode-quota')),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Saved flags merged with what is on disk. Detection can only add, so a tool
|
|
55
|
+
* recorded as enabled stays enabled even when nothing local proves it.
|
|
56
|
+
*/
|
|
57
|
+
export function reconcileTools(saved = {}, cwd = process.cwd()) {
|
|
58
|
+
const detected = detectInstalledTools(cwd)
|
|
59
|
+
const merged = { ...saved }
|
|
60
|
+
for (const [key, present] of Object.entries(detected)) {
|
|
61
|
+
if (present) merged[key] = true
|
|
62
|
+
}
|
|
63
|
+
// simpleEnglish replaced caveman; honour the old key one last time.
|
|
64
|
+
if (saved.caveman) merged.simpleEnglish = true
|
|
65
|
+
return merged
|
|
66
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { addSkillToLock } from './skills-lock.js'
|
|
2
|
-
import { header, success, info } from '../../utils/exec.js'
|
|
3
|
-
|
|
4
|
-
const SKILL_ENTRY = {
|
|
5
|
-
source: 'blader/humanizer',
|
|
6
|
-
sourceType: 'github',
|
|
7
|
-
skillPath: 'SKILL.md',
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function installHumanizer(options = {}) {
|
|
11
|
-
if (!options.skipHeader) header('Installing humanizer')
|
|
12
|
-
|
|
13
|
-
info('Adding humanizer to skills-lock.json for batch install')
|
|
14
|
-
await addSkillToLock('humanizer', SKILL_ENTRY)
|
|
15
|
-
success('humanizer queued for installation')
|
|
16
|
-
return { optedIn: true, installed: true }
|
|
17
|
-
}
|
|
1
|
+
import { addSkillToLock } from './skills-lock.js'
|
|
2
|
+
import { header, success, info } from '../../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
const SKILL_ENTRY = {
|
|
5
|
+
source: 'blader/humanizer',
|
|
6
|
+
sourceType: 'github',
|
|
7
|
+
skillPath: 'SKILL.md',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function installHumanizer(options = {}) {
|
|
11
|
+
if (!options.skipHeader) header('Installing humanizer')
|
|
12
|
+
|
|
13
|
+
info('Adding humanizer to skills-lock.json for batch install')
|
|
14
|
+
await addSkillToLock('humanizer', SKILL_ENTRY)
|
|
15
|
+
success('humanizer queued for installation')
|
|
16
|
+
return { optedIn: true, installed: true }
|
|
17
|
+
}
|