@plainconceptsplatform/agent-harness 2.2.1 → 2.3.0
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/package.json +1 -1
- package/src/commands/update.js +13 -1
- package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
- package/src/content/.agents/skills/pc-make-engineer/template.md +80 -81
- package/src/content/.opencode/plugins/pc-subagent-tiers.js +337 -281
- package/src/content/opencode.jsonc +41 -39
- package/src/steps/copy/fullstack-engineer.js +88 -85
- package/src/steps/copy/opencode-json.js +161 -147
- package/src/steps/metadata/index.js +4 -0
- package/src/steps/optimization/detect.js +66 -0
- package/src/utils/agent-color.js +83 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic colour for an agent, derived from its name.
|
|
3
|
+
*
|
|
4
|
+
* opencode accepts either a hex value or one of its theme keywords. Two of
|
|
5
|
+
* those keywords are reserved for the primaries so they stay recognisable
|
|
6
|
+
* across every project: build is `primary`, plan is `warning`. Every other
|
|
7
|
+
* agent gets a hex derived from its name, so a `butterfly-engineer` is the
|
|
8
|
+
* same colour in every repository that has one, with no central registry and
|
|
9
|
+
* no chance of two agents in one project colliding by accident.
|
|
10
|
+
*
|
|
11
|
+
* Saturation and lightness are fixed rather than hashed. That is what keeps
|
|
12
|
+
* the result usable: hashing them too would eventually produce near-grey,
|
|
13
|
+
* near-black or near-white agents that are invisible against one theme or the
|
|
14
|
+
* other. Only the hue varies.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** opencode's theme keywords. Treated as unset so a name-derived hex wins. */
|
|
18
|
+
export const THEME_COLORS = new Set([
|
|
19
|
+
'primary',
|
|
20
|
+
'secondary',
|
|
21
|
+
'accent',
|
|
22
|
+
'success',
|
|
23
|
+
'warning',
|
|
24
|
+
'error',
|
|
25
|
+
'info',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
/** Reserved: the two primaries, which never take a derived colour. */
|
|
29
|
+
export const RESERVED_COLORS = { build: 'primary', plan: 'warning' }
|
|
30
|
+
|
|
31
|
+
const SATURATION = 68
|
|
32
|
+
const LIGHTNESS = 52
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* FNV-1a, 32 bit. Chosen because it is short enough to duplicate verbatim in
|
|
36
|
+
* the shipped plugin and produces the same hue everywhere: the same name must
|
|
37
|
+
* resolve to the same colour on every machine and in every project.
|
|
38
|
+
*/
|
|
39
|
+
function hash(value) {
|
|
40
|
+
let h = 0x811c9dc5
|
|
41
|
+
for (let i = 0; i < value.length; i++) {
|
|
42
|
+
h ^= value.charCodeAt(i)
|
|
43
|
+
h = Math.imul(h, 0x01000193) >>> 0
|
|
44
|
+
}
|
|
45
|
+
return h >>> 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hslToHex(h, s, l) {
|
|
49
|
+
const sat = s / 100
|
|
50
|
+
const light = l / 100
|
|
51
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat
|
|
52
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
53
|
+
const m = light - c / 2
|
|
54
|
+
|
|
55
|
+
const [r, g, b] =
|
|
56
|
+
h < 60 ? [c, x, 0] :
|
|
57
|
+
h < 120 ? [x, c, 0] :
|
|
58
|
+
h < 180 ? [0, c, x] :
|
|
59
|
+
h < 240 ? [0, x, c] :
|
|
60
|
+
h < 300 ? [x, 0, c] :
|
|
61
|
+
[c, 0, x]
|
|
62
|
+
|
|
63
|
+
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, '0')
|
|
64
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The colour for an agent, honouring the two reserved primaries. */
|
|
68
|
+
export function agentColor(name) {
|
|
69
|
+
if (name in RESERVED_COLORS) return RESERVED_COLORS[name]
|
|
70
|
+
return hslToHex(hash(name) % 360, SATURATION, LIGHTNESS)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether a frontmatter colour should be replaced by the derived one.
|
|
75
|
+
*
|
|
76
|
+
* A theme keyword is replaced: those came from the old template, which asked
|
|
77
|
+
* whoever ran /make-engineer to pick one and avoid collisions by hand. An
|
|
78
|
+
* explicit hex is left alone, because someone chose it deliberately.
|
|
79
|
+
*/
|
|
80
|
+
export function shouldDeriveColor(current) {
|
|
81
|
+
if (!current) return true
|
|
82
|
+
return THEME_COLORS.has(current.trim().toLowerCase())
|
|
83
|
+
}
|