@plainconceptsplatform/agent-harness 2.2.0 → 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.
@@ -1,108 +1,109 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { info, success } from '../../utils/exec.js'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
-
9
- const MARKER_SECTIONS = {
10
- rtk: 'RTK',
11
- codegraph: 'CODEGRAPH',
12
- memory: 'MEMORY',
13
- simpleEnglish: 'SIMPLE-ENGLISH',
14
- humanizer: 'HUMANIZER',
15
- }
16
-
17
- const OPTIONAL_TOOL_GUIDANCE = {
18
- codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
- memory: 'Use Agentmemory MCP tools for cross-session context and task-result notes.',
20
- }
21
-
22
- function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
- if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
- const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
- const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
- return content.replace(pattern, `${startMarker}\n${replacement}\n${endMarker}`)
27
- }
28
-
29
- async function patchOptionalToolGuidance(destSkillsDir, selections) {
30
- const files = []
31
-
32
- async function collect(dir) {
33
- for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
34
- const entryPath = path.join(dir, entry.name)
35
- if (entry.isDirectory()) await collect(entryPath)
36
- else if (entry.name.endsWith('.md')) files.push(entryPath)
37
- }
38
- }
39
-
40
- if (!await fse.pathExists(destSkillsDir)) return 0
41
- await collect(destSkillsDir)
42
-
43
- let patched = 0
44
- for (const filePath of files) {
45
- let content = await fse.readFile(filePath, 'utf-8')
46
- const original = content
47
- for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
48
- const suffix = key.toUpperCase()
49
- content = replaceMarkerSection(
50
- content,
51
- `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
52
- `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
53
- selections[key] ? guidance : '',
54
- )
55
- }
56
- if (content !== original) {
57
- await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
58
- patched++
59
- }
60
- }
61
- return patched
62
- }
63
-
64
- export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
65
- const destSkillsDir = path.join(cwd, '.agents', 'skills')
66
- const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
67
- let patchedCount = 0
68
-
69
- if (await fse.pathExists(guardrailsPath)) {
70
- let content = await fse.readFile(guardrailsPath, 'utf-8')
71
- content = replaceMarkerSection(
72
- content,
73
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
74
- '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
75
- '',
76
- ).replace(
77
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
78
- '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
79
- ).replace(
80
- '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
81
- '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
82
- )
83
- for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
84
- let sectionContent = ''
85
- if (selections[key]) {
86
- const presetName = key === 'simpleEnglish' ? 'simple-english' : key
87
- const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
88
- if (await fse.pathExists(presetPath)) {
89
- sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
90
- }
91
- }
92
- content = replaceMarkerSection(
93
- content,
94
- `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
95
- `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
96
- sectionContent,
97
- )
98
- if (selections[key]) patchedCount++
99
- }
100
- await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
101
- } else {
102
- info('pc-guardrails-generic not found, skipping guardrails patching')
103
- }
104
-
105
- const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
106
- success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
107
- return { patched: true, count: patchedCount, optionalPatched }
108
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { info, success } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
+
9
+ const MARKER_SECTIONS = {
10
+ rtk: 'RTK',
11
+ codegraph: 'CODEGRAPH',
12
+ memory: 'MEMORY',
13
+ simpleEnglish: 'SIMPLE-ENGLISH',
14
+ humanizer: 'HUMANIZER',
15
+ }
16
+
17
+ const OPTIONAL_TOOL_GUIDANCE = {
18
+ codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
+ memory: 'Use Agentmemory MCP tools for cross-session context and task-result notes.',
20
+ }
21
+
22
+ function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
+ if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
+ const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
+ const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
+ // Replacer function so $&, $' and $` in injected content stay literal.
27
+ return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
+ }
29
+
30
+ async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
+ const files = []
32
+
33
+ async function collect(dir) {
34
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
+ const entryPath = path.join(dir, entry.name)
36
+ if (entry.isDirectory()) await collect(entryPath)
37
+ else if (entry.name.endsWith('.md')) files.push(entryPath)
38
+ }
39
+ }
40
+
41
+ if (!await fse.pathExists(destSkillsDir)) return 0
42
+ await collect(destSkillsDir)
43
+
44
+ let patched = 0
45
+ for (const filePath of files) {
46
+ let content = await fse.readFile(filePath, 'utf-8')
47
+ const original = content
48
+ for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
+ const suffix = key.toUpperCase()
50
+ content = replaceMarkerSection(
51
+ content,
52
+ `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
+ `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
+ selections[key] ? guidance : '',
55
+ )
56
+ }
57
+ if (content !== original) {
58
+ await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
+ patched++
60
+ }
61
+ }
62
+ return patched
63
+ }
64
+
65
+ export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
+ const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
+ const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
+ let patchedCount = 0
69
+
70
+ if (await fse.pathExists(guardrailsPath)) {
71
+ let content = await fse.readFile(guardrailsPath, 'utf-8')
72
+ content = replaceMarkerSection(
73
+ content,
74
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
+ '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
+ '',
77
+ ).replace(
78
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
+ '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
+ ).replace(
81
+ '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
82
+ '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
83
+ )
84
+ for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
85
+ let sectionContent = ''
86
+ if (selections[key]) {
87
+ const presetName = key === 'simpleEnglish' ? 'simple-english' : key
88
+ const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
89
+ if (await fse.pathExists(presetPath)) {
90
+ sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
91
+ }
92
+ }
93
+ content = replaceMarkerSection(
94
+ content,
95
+ `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
96
+ `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
97
+ sectionContent,
98
+ )
99
+ if (selections[key]) patchedCount++
100
+ }
101
+ await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
102
+ } else {
103
+ info('pc-guardrails-generic not found, skipping guardrails patching')
104
+ }
105
+
106
+ const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
107
+ success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
108
+ return { patched: true, count: patchedCount, optionalPatched }
109
+ }
@@ -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
+ }