@plainconceptsplatform/agent-harness 3.1.1 → 3.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.
Files changed (87) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +24 -4
  3. package/cli/commands/join.js +259 -259
  4. package/cli/commands/migrate.js +310 -310
  5. package/cli/commands/shared.js +27 -27
  6. package/cli/commands/single.js +79 -79
  7. package/cli/commands/wizard.js +134 -134
  8. package/cli/fragments/ops-backlog/az.md +28 -28
  9. package/cli/fragments/ops-backlog/gh.md +28 -28
  10. package/cli/fragments/ops-backlog/jira.md +28 -28
  11. package/cli/fragments/ops-review/az.md +62 -62
  12. package/cli/fragments/ops-review/gh.md +51 -51
  13. package/cli/fragments/ops-review/gl.md +55 -55
  14. package/cli/fragments/ops-ship/gh.md +67 -67
  15. package/cli/fragments/ops-ship/gl.md +84 -84
  16. package/cli/index.js +116 -116
  17. package/cli/presets/clean.json +21 -21
  18. package/cli/presets/models.json +68 -68
  19. package/cli/presets/optimization.json +32 -32
  20. package/cli/presets/platforms.json +76 -76
  21. package/cli/presets/source.json +23 -23
  22. package/cli/steps/clean/index.js +120 -120
  23. package/cli/steps/copy/agents.js +120 -120
  24. package/cli/steps/copy/fullstack-engineer.js +89 -89
  25. package/cli/steps/copy/opencode-json.js +324 -324
  26. package/cli/steps/models/format.js +88 -88
  27. package/cli/steps/models/index.js +64 -64
  28. package/cli/steps/openspec/index.js +136 -136
  29. package/cli/steps/optimization/codegraph.js +141 -127
  30. package/cli/steps/optimization/detect.js +66 -66
  31. package/cli/steps/optimization/humanizer.js +17 -17
  32. package/cli/steps/optimization/index.js +158 -158
  33. package/cli/steps/optimization/memory.js +91 -88
  34. package/cli/steps/optimization/patch-guardrails.js +111 -111
  35. package/cli/steps/optimization/simple-english.js +17 -17
  36. package/cli/steps/optimization/skills-lock.js +30 -30
  37. package/cli/steps/platform/index.js +109 -109
  38. package/cli/steps/source/index.js +123 -123
  39. package/cli/utils/agent-color.js +111 -111
  40. package/cli/utils/copy.js +134 -131
  41. package/cli/utils/exec-spinner.js +47 -47
  42. package/cli/utils/exec.js +134 -134
  43. package/cli/utils/legacy-check.js +30 -30
  44. package/cli/utils/models-cache.js +58 -58
  45. package/cli/utils/models-pricing.js +42 -42
  46. package/cli/utils/process.js +3 -3
  47. package/cli/utils/terminal.js +6 -6
  48. package/cli/utils/update-manifest.js +75 -75
  49. package/harness/.opencode/commands/ops-backlog.md +10 -10
  50. package/harness/.opencode/commands/ops-review.md +8 -8
  51. package/harness/.opencode/commands/ops-ship.md +9 -9
  52. package/harness/.opencode/commands/plan-archive.md +5 -5
  53. package/harness/.opencode/commands/plan-explore.md +9 -9
  54. package/harness/.opencode/commands/plan-propose.md +9 -9
  55. package/harness/.opencode/commands/repo-help.md +5 -5
  56. package/harness/.opencode/package.json +5 -5
  57. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  58. package/harness/.opencode/plugins/pc-subagent-tiers.js +354 -354
  59. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  60. package/harness/.opencode/scripts/check-diff.mjs +244 -0
  61. package/harness/.opencode/scripts/code-rules.mjs +288 -0
  62. package/harness/ARCHITECTURE.md +13 -13
  63. package/harness/DESIGN.md +13 -13
  64. package/harness/opencode.jsonc +36 -36
  65. package/harness/openspec/config.yaml +20 -20
  66. package/harness/skills-lock.json +17 -17
  67. package/package.json +14 -12
  68. package/skills/agent-harness-cli/SKILL.md +86 -86
  69. package/skills/pc-guardrails-generic/SKILL.md +2 -1
  70. package/skills/pc-guardrails-project/SKILL.md +8 -8
  71. package/skills/pc-make-architecture/structure-template.md +64 -64
  72. package/skills/pc-make-engineer/SKILL.md +59 -59
  73. package/skills/pc-make-engineer/template.md +36 -36
  74. package/skills/pc-make-guardrails/category-reference.md +112 -112
  75. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  76. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  77. package/skills/pc-ops-ship/SKILL.md +22 -18
  78. package/skills/pc-plan-apply/SKILL.md +86 -86
  79. package/skills/pc-plan-apply/simple-mode.md +1 -1
  80. package/skills/pc-plan-explore/SKILL.md +21 -21
  81. package/skills/pc-plan-goal/SKILL.md +91 -91
  82. package/skills/pc-plan-goal/failure-policy.md +30 -30
  83. package/skills/pc-plan-goal/output-mode.md +10 -10
  84. package/skills/pc-plan-propose/SKILL.md +125 -125
  85. package/skills/pc-plan-propose/task-annotation.md +39 -39
  86. package/skills/pc-plan-quick/SKILL.md +1 -1
  87. package/skills/pc-repo-audit/SKILL.md +44 -44
@@ -1,127 +1,141 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import path from 'node:path'
4
- import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
5
- import { header, success, warn, error, loading } from '../../utils/exec.js'
6
-
7
- /**
8
- * Normalizes codegraph's MCP entry in the project-level OpenCode config.
9
- * Older codegraph versions write `mcpServers`; OpenCode expects `mcp`.
10
- * Returns true if the config was successfully normalized (or no config exists).
11
- */
12
- export async function fixCodegraphConfig() {
13
- const cwd = process.cwd()
14
- const configFile = path.join(cwd, 'opencode.jsonc')
15
-
16
- if (!await fse.pathExists(configFile)) return true
17
-
18
- let configText
19
- let config
20
- try {
21
- configText = await fse.readFile(configFile, 'utf-8')
22
- const errors = []
23
- config = parseJsonc(configText, errors)
24
- if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
25
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
26
- throw new Error('unexpected structure')
27
- }
28
- } catch {
29
- warn('Could not parse opencode.jsonc, leaving it untouched')
30
- return false
31
- }
32
-
33
- const legacyMcp = config.mcpServers
34
- const mcp = config.mcp || {}
35
- const entries = { ...legacyMcp, ...mcp }
36
- let changed = false
37
-
38
- if (Object.keys(entries).length > 0) {
39
- for (const entry of Object.values(entries)) {
40
- if (Array.isArray(entry.command) && entry.command[0] === 'codegraph') {
41
- entry.command = ['npx', '@colbymchenry/codegraph', ...entry.command.slice(1)]
42
- // codegraph defaults to a 30s MCP timeout, which fails under parallel
43
- // subagent load. Raise it unless the user has set one explicitly.
44
- if (entry.timeout == null) entry.timeout = 120000
45
- }
46
- }
47
- for (const [name, entry] of Object.entries(entries)) {
48
- const edits = modify(configText, ['mcp', name], entry, {
49
- formattingOptions: { insertSpaces: true, tabSize: 2 },
50
- })
51
- configText = applyEdits(configText, edits)
52
- changed = true
53
- }
54
- }
55
-
56
- if (legacyMcp) {
57
- configText = applyEdits(configText, modify(configText, ['mcpServers'], undefined, {
58
- formattingOptions: { insertSpaces: true, tabSize: 2 },
59
- }))
60
- changed = true
61
- }
62
-
63
- if (changed) {
64
- await fse.writeFile(configFile, configText, 'utf-8')
65
- warn('Normalized codegraph MCP config in opencode.jsonc')
66
- }
67
-
68
- return true
69
- }
70
-
71
- export async function installCodegraph(options = {}) {
72
- if (!options.skipHeader) header('Installing codegraph')
73
-
74
- const location = options.installScope === 'global' ? 'global' : 'local'
75
-
76
- loading(`configuring codegraph for opencode (${location})...`)
77
-
78
- try {
79
- const installResult = await execa(
80
- 'npx',
81
- ['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
82
- {
83
- cwd: process.cwd(),
84
- reject: false,
85
- stdio: 'pipe',
86
- }
87
- )
88
-
89
- if (installResult.exitCode !== 0) {
90
- warn('codegraph install exited with non-zero code')
91
- return { optedIn: true, installed: false }
92
- }
93
-
94
- const configFixed = await fixCodegraphConfig()
95
-
96
- if (!configFixed) {
97
- warn('codegraph config could not be merged: skipping init')
98
- return { optedIn: true, installed: false }
99
- }
100
-
101
- success(`codegraph configured for opencode (${location})`)
102
- } catch (err) {
103
- error(`Failed to install codegraph: ${err.message}`)
104
- return { optedIn: true, installed: false }
105
- }
106
-
107
- loading('initializing codegraph project index...')
108
-
109
- try {
110
- const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
111
- cwd: process.cwd(),
112
- reject: false,
113
- stdio: 'pipe',
114
- })
115
-
116
- if (initResult.exitCode !== 0) {
117
- warn('codegraph init exited with non-zero code')
118
- return { optedIn: true, installed: false }
119
- }
120
- success('codegraph project index initialized')
121
- } catch (err) {
122
- error(`Failed to initialize codegraph: ${err.message}`)
123
- return { optedIn: true, installed: false }
124
- }
125
-
126
- return { optedIn: true, installed: true }
127
- }
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
+ }