@plainconceptsplatform/agent-harness 3.1.0 → 3.1.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/README.md +1 -1
- package/cli/commands/update.js +121 -121
- package/cli/index.js +0 -0
- package/cli/steps/metadata/index.js +115 -112
- package/cli/steps/models/write.js +38 -34
- package/package.json +12 -14
- package/skills/pc-repo-help/SKILL.md +1 -1
- package/skills/pc-repo-verify/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ Commands that other commands (or agents) need to execute are thin wrappers aroun
|
|
|
145
145
|
| `/make-architecture` | Generate or regenerate `ARCHITECTURE.md` from the codebase. |
|
|
146
146
|
| `/make-design` | Generate or regenerate `DESIGN.md` from the design system. |
|
|
147
147
|
| `/make-guardrails` | Generate a `pc-guardrails-project` skill from `ARCHITECTURE.md` and project config files. Extracts architecture boundaries, naming, code style, testing, and git workflow rules. Updates all `*-engineer.md` to load the skill. |
|
|
148
|
-
| `/repo-verify` | Write the verification plan for the current branch's change: a journey through the new functionality as a fenced YAML block in `verification-plan.md`, stored with the change. The evidence workflow from `@plainconceptsplatform/workflows` replays it in headless Chromium when the pull request merges and archives one screenshot per waypoint
|
|
148
|
+
| `/repo-verify` | Write the verification plan for the current branch's change: a journey through the new functionality as a fenced YAML block in `verification-plan.md`, stored with the change. The evidence workflow from `@plainconceptsplatform/workflows` replays it in headless Chromium when the pull request merges and archives one screenshot per waypoint in an `evidence/` folder beside the plan. Backend-only changes are traced through to the frontend when their contract is consumed there; a `not-applicable` stub is written when no UI surface is reachable. Does not run checks or take screenshots. Runs automatically in `/plan-goal`. |
|
|
149
149
|
| `/make-user-model [user] <tier> <model>` | Set the model for a tier (`plan`, `build`, `fast`). Writes to `harness.json` (team) or `harness.user.json` (user override, gitignored) when `user` prefix is used. Restart to pick up changes: the `pc-subagent-tiers` plugin rebuilds tier agents at startup. Pass a model id or `current` for the active session model. |
|
|
150
150
|
|
|
151
151
|
---
|
package/cli/commands/update.js
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { execa } from 'execa'
|
|
3
|
-
import fse from 'fs-extra'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
import { readHarnessConfig } from './shared.js'
|
|
7
|
-
import { copyContentStep } from '../steps/copy/index.js'
|
|
8
|
-
import { writeModelsToConfigs } from '../steps/models/write.js'
|
|
9
|
-
import { patchGuardrails } from '../steps/optimization/patch-guardrails.js'
|
|
10
|
-
import { reconcileTools } from '../steps/optimization/detect.js'
|
|
11
|
-
import { writeHarnessConfig } from '../steps/metadata/index.js'
|
|
12
|
-
import { info } from '../utils/exec.js'
|
|
13
|
-
import { exit } from '../utils/process.js'
|
|
14
|
-
|
|
15
|
-
const SIMPLE_ENGLISH_ENTRY = {
|
|
16
|
-
source: 'AminBlg/SimpleEnglish',
|
|
17
|
-
sourceType: 'github',
|
|
18
|
-
skillPath: 'skills/simple-english/SKILL.md',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
22
|
-
const platformsPreset = await fse.readJson(path.resolve(__dirname, '../presets/platforms.json'))
|
|
23
|
-
const VALID_PLATFORMS = new Set(platformsPreset.map(p => p.value))
|
|
24
|
-
|
|
25
|
-
function resolvePlatform(value, fallback = 'github') {
|
|
26
|
-
return VALID_PLATFORMS.has(value) ? value : fallback
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function runUpdate() {
|
|
30
|
-
const saved = await readHarnessConfig()
|
|
31
|
-
if (!saved?.platform) {
|
|
32
|
-
console.log(chalk.red('No agent-harness config found. Run the wizard first.'))
|
|
33
|
-
exit(1)
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const backlogPlatform = resolvePlatform(saved.platform.backlog)
|
|
38
|
-
const repoPlatform = resolvePlatform(saved.platform.repo)
|
|
39
|
-
|
|
40
|
-
const ctx = {
|
|
41
|
-
hasDesign: !!saved.preexisting?.design,
|
|
42
|
-
hasArchitecture: !!saved.preexisting?.architecture,
|
|
43
|
-
hasOpenspec: !!saved.preexisting?.openspec,
|
|
44
|
-
sourceMode: saved.source?.mode ?? 'current',
|
|
45
|
-
sourceRoots: Array.isArray(saved.source?.roots) ? saved.source.roots : [],
|
|
46
|
-
maxConcurrentAgents: saved.agents?.maxConcurrent ?? 3,
|
|
47
|
-
installScope: 'local',
|
|
48
|
-
skipSkills: false,
|
|
49
|
-
updateMode: true,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log()
|
|
53
|
-
console.log(chalk.bold('Updating project from saved config'))
|
|
54
|
-
console.log(chalk.dim(` backlog: ${backlogPlatform} repo: ${repoPlatform} agents: ${ctx.maxConcurrentAgents}`))
|
|
55
|
-
console.log()
|
|
56
|
-
|
|
57
|
-
await copyContentStep({ backlogPlatform, repoPlatform }, ctx)
|
|
58
|
-
|
|
59
|
-
if (saved.models) {
|
|
60
|
-
await writeModelsToConfigs({ buildModel: saved.models.build })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Reconcile against what is actually installed before patching guardrails.
|
|
64
|
-
// The saved flags drift: a project can be running the codegraph and
|
|
65
|
-
// agentmemory MCP servers with harness.json still recording them as false,
|
|
66
|
-
// and patchGuardrails would then empty those marker sections and strip the
|
|
67
|
-
// guidance for tools the project genuinely uses.
|
|
68
|
-
const tools = reconcileTools(saved.tools ?? {})
|
|
69
|
-
const gained = Object.keys(tools).filter(k => tools[k] && !saved.tools?.[k])
|
|
70
|
-
if (gained.length > 0) {
|
|
71
|
-
info(`Detected installed tools missing from harness.json: ${gained.join(', ')}`)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
await migrateCavemanSkill(tools)
|
|
75
|
-
await patchGuardrails({
|
|
76
|
-
rtk: !!tools.rtk,
|
|
77
|
-
simpleEnglish: !!(tools.simpleEnglish || tools.caveman),
|
|
78
|
-
codegraph: !!tools.codegraph,
|
|
79
|
-
memory: !!tools.memory,
|
|
80
|
-
humanizer: !!tools.humanizer,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
await writeHarnessConfig({
|
|
84
|
-
...ctx,
|
|
85
|
-
backlogPlatform,
|
|
86
|
-
repoPlatform,
|
|
87
|
-
maxConcurrentAgents: ctx.maxConcurrentAgents,
|
|
88
|
-
planModel: saved.models?.plan ?? null,
|
|
89
|
-
buildModel: saved.models?.build ?? null,
|
|
90
|
-
fastModel: saved.models?.fast ?? null,
|
|
91
|
-
optionalTools: tools,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
console.log()
|
|
95
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
96
|
-
console.log(chalk.bold.green(' Update complete!'))
|
|
97
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
98
|
-
console.log()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function migrateCavemanSkill(tools) {
|
|
102
|
-
if (!tools.caveman || tools.simpleEnglish) return
|
|
103
|
-
|
|
104
|
-
const lockPath = path.join(process.cwd(), 'skills-lock.json')
|
|
105
|
-
const lock = await fse.readJson(lockPath).catch(() => ({ version: 1, skills: {} }))
|
|
106
|
-
lock.skills ??= {}
|
|
107
|
-
delete lock.skills.caveman
|
|
108
|
-
lock.skills['simple-english'] = SIMPLE_ENGLISH_ENTRY
|
|
109
|
-
await fse.writeJson(lockPath, lock, { spaces: 2 })
|
|
110
|
-
await fse.remove(path.join(process.cwd(), '.agents', 'skills', 'caveman'))
|
|
111
|
-
|
|
112
|
-
const result = await execa('npx', ['skills', 'experimental_install', '--yes'], {
|
|
113
|
-
cwd: process.cwd(),
|
|
114
|
-
reject: false,
|
|
115
|
-
stdio: 'pipe',
|
|
116
|
-
timeout: 600000,
|
|
117
|
-
})
|
|
118
|
-
if (result.exitCode !== 0) {
|
|
119
|
-
console.log(chalk.yellow('Simple English was added to skills-lock.json but could not be installed. Run npx skills experimental_install --yes.'))
|
|
120
|
-
}
|
|
121
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { readHarnessConfig } from './shared.js'
|
|
7
|
+
import { copyContentStep } from '../steps/copy/index.js'
|
|
8
|
+
import { writeModelsToConfigs } from '../steps/models/write.js'
|
|
9
|
+
import { patchGuardrails } from '../steps/optimization/patch-guardrails.js'
|
|
10
|
+
import { reconcileTools } from '../steps/optimization/detect.js'
|
|
11
|
+
import { writeHarnessConfig } from '../steps/metadata/index.js'
|
|
12
|
+
import { info } from '../utils/exec.js'
|
|
13
|
+
import { exit } from '../utils/process.js'
|
|
14
|
+
|
|
15
|
+
const SIMPLE_ENGLISH_ENTRY = {
|
|
16
|
+
source: 'AminBlg/SimpleEnglish',
|
|
17
|
+
sourceType: 'github',
|
|
18
|
+
skillPath: 'skills/simple-english/SKILL.md',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const platformsPreset = await fse.readJson(path.resolve(__dirname, '../presets/platforms.json'))
|
|
23
|
+
const VALID_PLATFORMS = new Set(platformsPreset.map(p => p.value))
|
|
24
|
+
|
|
25
|
+
function resolvePlatform(value, fallback = 'github') {
|
|
26
|
+
return VALID_PLATFORMS.has(value) ? value : fallback
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runUpdate() {
|
|
30
|
+
const saved = await readHarnessConfig()
|
|
31
|
+
if (!saved?.platform) {
|
|
32
|
+
console.log(chalk.red('No agent-harness config found. Run the wizard first.'))
|
|
33
|
+
exit(1)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const backlogPlatform = resolvePlatform(saved.platform.backlog)
|
|
38
|
+
const repoPlatform = resolvePlatform(saved.platform.repo)
|
|
39
|
+
|
|
40
|
+
const ctx = {
|
|
41
|
+
hasDesign: !!saved.preexisting?.design,
|
|
42
|
+
hasArchitecture: !!saved.preexisting?.architecture,
|
|
43
|
+
hasOpenspec: !!saved.preexisting?.openspec,
|
|
44
|
+
sourceMode: saved.source?.mode ?? 'current',
|
|
45
|
+
sourceRoots: Array.isArray(saved.source?.roots) ? saved.source.roots : [],
|
|
46
|
+
maxConcurrentAgents: saved.agents?.maxConcurrent ?? 3,
|
|
47
|
+
installScope: 'local',
|
|
48
|
+
skipSkills: false,
|
|
49
|
+
updateMode: true,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log()
|
|
53
|
+
console.log(chalk.bold('Updating project from saved config'))
|
|
54
|
+
console.log(chalk.dim(` backlog: ${backlogPlatform} repo: ${repoPlatform} agents: ${ctx.maxConcurrentAgents}`))
|
|
55
|
+
console.log()
|
|
56
|
+
|
|
57
|
+
await copyContentStep({ backlogPlatform, repoPlatform }, ctx)
|
|
58
|
+
|
|
59
|
+
if (saved.models) {
|
|
60
|
+
await writeModelsToConfigs({ buildModel: saved.models.build, keepExisting: true })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reconcile against what is actually installed before patching guardrails.
|
|
64
|
+
// The saved flags drift: a project can be running the codegraph and
|
|
65
|
+
// agentmemory MCP servers with harness.json still recording them as false,
|
|
66
|
+
// and patchGuardrails would then empty those marker sections and strip the
|
|
67
|
+
// guidance for tools the project genuinely uses.
|
|
68
|
+
const tools = reconcileTools(saved.tools ?? {})
|
|
69
|
+
const gained = Object.keys(tools).filter(k => tools[k] && !saved.tools?.[k])
|
|
70
|
+
if (gained.length > 0) {
|
|
71
|
+
info(`Detected installed tools missing from harness.json: ${gained.join(', ')}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await migrateCavemanSkill(tools)
|
|
75
|
+
await patchGuardrails({
|
|
76
|
+
rtk: !!tools.rtk,
|
|
77
|
+
simpleEnglish: !!(tools.simpleEnglish || tools.caveman),
|
|
78
|
+
codegraph: !!tools.codegraph,
|
|
79
|
+
memory: !!tools.memory,
|
|
80
|
+
humanizer: !!tools.humanizer,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await writeHarnessConfig({
|
|
84
|
+
...ctx,
|
|
85
|
+
backlogPlatform,
|
|
86
|
+
repoPlatform,
|
|
87
|
+
maxConcurrentAgents: ctx.maxConcurrentAgents,
|
|
88
|
+
planModel: saved.models?.plan ?? null,
|
|
89
|
+
buildModel: saved.models?.build ?? null,
|
|
90
|
+
fastModel: saved.models?.fast ?? null,
|
|
91
|
+
optionalTools: tools,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
console.log()
|
|
95
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
96
|
+
console.log(chalk.bold.green(' Update complete!'))
|
|
97
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
98
|
+
console.log()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function migrateCavemanSkill(tools) {
|
|
102
|
+
if (!tools.caveman || tools.simpleEnglish) return
|
|
103
|
+
|
|
104
|
+
const lockPath = path.join(process.cwd(), 'skills-lock.json')
|
|
105
|
+
const lock = await fse.readJson(lockPath).catch(() => ({ version: 1, skills: {} }))
|
|
106
|
+
lock.skills ??= {}
|
|
107
|
+
delete lock.skills.caveman
|
|
108
|
+
lock.skills['simple-english'] = SIMPLE_ENGLISH_ENTRY
|
|
109
|
+
await fse.writeJson(lockPath, lock, { spaces: 2 })
|
|
110
|
+
await fse.remove(path.join(process.cwd(), '.agents', 'skills', 'caveman'))
|
|
111
|
+
|
|
112
|
+
const result = await execa('npx', ['skills', 'experimental_install', '--yes'], {
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
reject: false,
|
|
115
|
+
stdio: 'pipe',
|
|
116
|
+
timeout: 600000,
|
|
117
|
+
})
|
|
118
|
+
if (result.exitCode !== 0) {
|
|
119
|
+
console.log(chalk.yellow('Simple English was added to skills-lock.json but could not be installed. Run npx skills experimental_install --yes.'))
|
|
120
|
+
}
|
|
121
|
+
}
|
package/cli/index.js
CHANGED
|
File without changes
|
|
@@ -1,112 +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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
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,34 +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, 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
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plainconceptsplatform/agent-harness",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Installs the Plain Concepts Platform Harness into any codebase, and keeps it up to date. Wires OpenCode, OpenSpec, codegraph, and agentmemory into a multi-agent workflow that runs on native parallel subagents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
|
-
"packageManager": "pnpm@11.10.0",
|
|
46
45
|
"dependencies": {
|
|
47
46
|
"@inquirer/prompts": "^7.0.0",
|
|
48
47
|
"chalk": "^5.0.0",
|
|
@@ -51,17 +50,6 @@
|
|
|
51
50
|
"jsonc-parser": "^3.3.1",
|
|
52
51
|
"ora": "^8.0.0"
|
|
53
52
|
},
|
|
54
|
-
"scripts": {
|
|
55
|
-
"lint": "eslint .",
|
|
56
|
-
"lint:fix": "eslint . --fix",
|
|
57
|
-
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
58
|
-
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\"",
|
|
59
|
-
"test": "vitest run",
|
|
60
|
-
"test:watch": "vitest",
|
|
61
|
-
"prepublishOnly": "npm run lint && npm run lint:md && npm run test",
|
|
62
|
-
"release:dry": "pnpm publish --dry-run --no-git-checks --access public",
|
|
63
|
-
"release": "pnpm publish --no-git-checks --access public"
|
|
64
|
-
},
|
|
65
53
|
"devDependencies": {
|
|
66
54
|
"@eslint/js": "^9.0.0",
|
|
67
55
|
"eslint": "^9.0.0",
|
|
@@ -71,5 +59,15 @@
|
|
|
71
59
|
},
|
|
72
60
|
"vitest": {
|
|
73
61
|
"environment": "node"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"lint:fix": "eslint . --fix",
|
|
66
|
+
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
67
|
+
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\"",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"release:dry": "pnpm publish --dry-run --no-git-checks --access public",
|
|
71
|
+
"release": "pnpm publish --no-git-checks --access public"
|
|
74
72
|
}
|
|
75
|
-
}
|
|
73
|
+
}
|
|
@@ -58,7 +58,7 @@ Display the following reference to the user exactly as written. Do not summarize
|
|
|
58
58
|
|
|
59
59
|
**`/make-guardrails`**: Generate a `pc-guardrails-project` skill from `ARCHITECTURE.md` and project config files. Extracts concrete rules (architecture boundaries, naming, code style, testing, git workflow) that all agents must follow. Updates every `*-engineer.md` to load the skill.
|
|
60
60
|
|
|
61
|
-
**`/repo-verify`**: Write the verification plan for the current branch's change: a journey through the new functionality as a fenced YAML block in `verification-plan.md`, stored with the change. The evidence workflow from `@plainconceptsplatform/workflows` replays it in headless Chromium when the pull request merges and archives one screenshot per waypoint
|
|
61
|
+
**`/repo-verify`**: Write the verification plan for the current branch's change: a journey through the new functionality as a fenced YAML block in `verification-plan.md`, stored with the change. The evidence workflow from `@plainconceptsplatform/workflows` replays it in headless Chromium when the pull request merges and archives one screenshot per waypoint in an `evidence/` folder beside the plan. Backend-only changes are traced through to the frontend when their contract is consumed there; a `not-applicable` stub is written when no UI surface is reachable. Does not run checks or take screenshots. Runs automatically in `/plan-goal`.
|
|
62
62
|
|
|
63
63
|
**`/make-user-model <tier> <model>`**: Set the model for a tier (`plan`, `build`, or `fast`). Writes to `.opencode/harness.json` (`models`). Use `user` prefix for a personal override: `/make-user-model user fast opencode/big-pickle`. Use a model id or `current` for the active session model. Restart opencode for the `pc-subagent-tiers` plugin to rebuild tier agents.
|
|
64
64
|
|
|
@@ -8,11 +8,11 @@ license: MIT
|
|
|
8
8
|
|
|
9
9
|
Write the verification plan for the current branch's change. The checks gate lives in `pc-plan-apply` step 10 (lint, typecheck, test, build). This skill produces the reproduction plan instead: a journey through the new functionality with one screenshot per waypoint.
|
|
10
10
|
|
|
11
|
-
The plan is executed by a machine, not read by one. When the pull request merges, the evidence workflow (`evidence.yml` from `@plainconceptsplatform/workflows`) checks out the pull request head, starts the app, replays every step below in headless Chromium with no model in the loop, and archives one screenshot per waypoint on the pull request. A step it cannot follow is a screenshot of the wrong screen. Write every step so it can be followed literally from a freshly started app.
|
|
11
|
+
The plan is executed by a machine, not read by one. When the pull request merges, the evidence workflow (`evidence.yml` from `@plainconceptsplatform/workflows`) checks out the pull request head, starts the app, replays every step below in headless Chromium with no model in the loop, and archives one screenshot per waypoint in an `evidence/` folder beside this plan on the default branch, shown in one comment on the pull request. A step it cannot follow is a screenshot of the wrong screen. Write every step so it can be followed literally from a freshly started app.
|
|
12
12
|
|
|
13
13
|
This skill never launches a browser, never takes a screenshot, never starts a server. It only reads the change and writes a plan file.
|
|
14
14
|
|
|
15
|
-
Work only on the current branch: do not switch branches, push, create pull requests, or contact external platforms. Write the plan into the change directory; it is carried into the archive by `pc-plan-archive`.
|
|
15
|
+
Work only on the current branch: do not switch branches, push, create pull requests, or contact external platforms. Write the plan into the change directory; it is carried into the archive by `pc-plan-archive`. Never create or edit an `evidence/` folder: the workflow writes it beside the archived plan after the merge.
|
|
16
16
|
|
|
17
17
|
## Project environment
|
|
18
18
|
|