@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.
- package/LICENSE +21 -21
- package/README.md +24 -4
- 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/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 -127
- 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 -88
- 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 -131
- 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/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +8 -8
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +5 -5
- package/harness/.opencode/commands/plan-explore.md +9 -9
- package/harness/.opencode/commands/plan-propose.md +9 -9
- 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 +354 -354
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
- package/harness/.opencode/scripts/check-diff.mjs +244 -0
- package/harness/.opencode/scripts/code-rules.mjs +288 -0
- 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 +14 -12
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/pc-guardrails-generic/SKILL.md +2 -1
- 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 -18
- package/skills/pc-plan-apply/SKILL.md +86 -86
- package/skills/pc-plan-apply/simple-mode.md +1 -1
- 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 +1 -1
- package/skills/pc-repo-audit/SKILL.md +44 -44
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Plain Concepts
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plain Concepts
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -288,10 +288,13 @@ your-project/
|
|
|
288
288
|
│ ├── harness-managed.json ← hashes of every managed file, so "update" spares your edits
|
|
289
289
|
│ ├── agents/ ← build.md + plan.md (generated primaries), fullstack-engineer,
|
|
290
290
|
│ │ and user-created *-engineer files (all subagents)
|
|
291
|
-
│
|
|
292
|
-
│
|
|
293
|
-
│
|
|
294
|
-
│
|
|
291
|
+
│ ├── plugins/
|
|
292
|
+
│ │ ├── pc-subagent-monitor.js ← server plugin: subagent state → .opencode/harness-run.json, and the wave cap
|
|
293
|
+
│ │ ├── pc-subagent-tiers.js ← server plugin: derives tier agents and the two primaries
|
|
294
|
+
│ │ └── pc-system-reminders.js ← server plugin: gates work on the agent's abilities, denies the never-rules
|
|
295
|
+
│ └── scripts/
|
|
296
|
+
│ ├── check-diff.mjs ← the comments and tests gate on the current diff (see below)
|
|
297
|
+
│ └── code-rules.mjs ← its rules, the same ones Project-Health scores a repository with
|
|
295
298
|
└── .agents/
|
|
296
299
|
└── skills/
|
|
297
300
|
├── pc-guardrails-generic/ ← foundation for user guardrails
|
|
@@ -306,6 +309,23 @@ Platform skills ship as suffixed variants (`pc-userstory-gh`, `pc-userstory-az`,
|
|
|
306
309
|
|
|
307
310
|
---
|
|
308
311
|
|
|
312
|
+
## Comments and tests gate
|
|
313
|
+
|
|
314
|
+
`node .opencode/scripts/check-diff.mjs [--base <ref>] [--json]` reads the diff against the merge base with `origin/HEAD` (committed, uncommitted and untracked) and exits 1 when it:
|
|
315
|
+
|
|
316
|
+
- adds more comment lines than 10% of the code lines it adds (2 are always allowed);
|
|
317
|
+
- adds a slop shape (banner, region, untracked TODO, a doc comment that repeats the name, commented-out code, an apology), a comment block of five lines or more, an incident story, or a doc comment on an internal symbol;
|
|
318
|
+
- touches a file and leaves slop in it, or leaves it with more comments per code line than before;
|
|
319
|
+
- grows the tests more than the code: test lines beyond the code lines added (40 always allowed), more than 40 cases per 1,000 code lines (3 always allowed), or any new skipped, focused, snapshot, assertion-less or mock-only test.
|
|
320
|
+
|
|
321
|
+
A tool directive (`nosemgrep`, `biome-ignore`, `eslint-disable`, `@ts-expect-error`, coverage ignores) is not a comment and costs nothing. Code added to a file that has methods in the complexity baseline (`scripts/complexity-baseline.json`, keys `path::method`) counts twice toward the test budget: that gate fails a method whose coverage drops, so its new branches need tests.
|
|
322
|
+
|
|
323
|
+
`pc-plan-apply` runs it with the other checks, and the workflows run it before a bot pull request can merge. A project changes the limits in `.opencode/harness.json`:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
"codeRules": { "commentRatio": 0.1, "commentAllowance": 2, "testRatio": 1, "testAllowance": 40, "casesPerKloc": 40, "caseAllowance": 3, "complexityBaseline": "scripts/complexity-baseline.json" }
|
|
327
|
+
```
|
|
328
|
+
|
|
309
329
|
## The bootstrap sequence
|
|
310
330
|
|
|
311
331
|
The first time you type `init` in OpenCode after onboarding, the agent asks whether this is a **greenfield** or **brownfield** project:
|
package/cli/commands/join.js
CHANGED
|
@@ -1,259 +1,259 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { execa } from 'execa'
|
|
3
|
-
import fse from 'fs-extra'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
-
import { installBrowser } from '../steps/browser/index.js'
|
|
7
|
-
import { checkRtk } from '../steps/optimization/index.js'
|
|
8
|
-
import { checkPlatform } from '../steps/platform/index.js'
|
|
9
|
-
import { commandExists, header, info, loading, success, warn } from '../utils/exec.js'
|
|
10
|
-
import { IGNORED_ENTRIES, USER_CONFIG_FILE } from '../utils/paths.js'
|
|
11
|
-
import { readHarnessConfig } from './shared.js'
|
|
12
|
-
|
|
13
|
-
export async function runJoin() {
|
|
14
|
-
const logo = chalk.hex('#fe3d57')
|
|
15
|
-
console.log()
|
|
16
|
-
console.log(logo(' 🤝 agent-harness join'))
|
|
17
|
-
console.log(chalk.dim(' New team member setup: checks & local installs only.'))
|
|
18
|
-
console.log(chalk.dim(' Does not modify committed project files.'))
|
|
19
|
-
console.log()
|
|
20
|
-
|
|
21
|
-
const saved = await readHarnessConfig()
|
|
22
|
-
|
|
23
|
-
if (!saved) {
|
|
24
|
-
warn('No .opencode/harness.json found.')
|
|
25
|
-
warn('This project may not have been onboarded yet. Run `npx @plainconceptsplatform/agent-harness` first.')
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const backlogPlatform = saved?.platform?.backlog
|
|
30
|
-
const repoPlatform = saved?.platform?.repo
|
|
31
|
-
const teamModels = saved?.models ?? {}
|
|
32
|
-
|
|
33
|
-
const opencodeDir = path.join(process.cwd(), '.opencode')
|
|
34
|
-
const opencodeJsonPath = path.join(process.cwd(), 'opencode.jsonc')
|
|
35
|
-
|
|
36
|
-
// Step 1: Platform CLI check
|
|
37
|
-
header('Step 1, Platform CLI check')
|
|
38
|
-
const platformsToCheck = [...new Set([backlogPlatform, repoPlatform])].filter(p => p && p !== 'none')
|
|
39
|
-
if (platformsToCheck.length === 0) {
|
|
40
|
-
info('No platform integration selected, skipping CLI checks.')
|
|
41
|
-
} else {
|
|
42
|
-
for (const p of platformsToCheck) {
|
|
43
|
-
const display = p === 'github' ? 'GitHub' : p === 'azure' ? 'Azure DevOps' : p
|
|
44
|
-
info(`Checking platform: ${display}`)
|
|
45
|
-
await checkPlatform(p)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Step 2: Install OpenCode plugins
|
|
50
|
-
header('Step 2, Installing OpenCode plugins')
|
|
51
|
-
const pkgPath = path.join(opencodeDir, 'package.json')
|
|
52
|
-
if (await fse.pathExists(pkgPath)) {
|
|
53
|
-
// `reject: false` made the catch below unreachable: a failed install exited
|
|
54
|
-
// non-zero without throwing, so this reported success while the plugin's
|
|
55
|
-
// dependency was missing and every plugin silently refused to load. Let the
|
|
56
|
-
// call reject, and confirm the dependency is actually on disk before saying so.
|
|
57
|
-
const declared = Object.keys(
|
|
58
|
-
parseJsonc(await fse.readFile(pkgPath, 'utf8'))?.dependencies ?? {},
|
|
59
|
-
)
|
|
60
|
-
try {
|
|
61
|
-
await execa('npm', ['install'], { cwd: opencodeDir, stdio: 'pipe' })
|
|
62
|
-
} catch (err) {
|
|
63
|
-
warn('npm install failed in .opencode/: plugins may not load correctly')
|
|
64
|
-
if (err?.stderr) info(String(err.stderr).trim().split('\n').slice(-3).join('\n'))
|
|
65
|
-
}
|
|
66
|
-
const missing = declared.filter(
|
|
67
|
-
name => !fse.pathExistsSync(path.join(opencodeDir, 'node_modules', ...name.split('/'))),
|
|
68
|
-
)
|
|
69
|
-
if (missing.length === 0) {
|
|
70
|
-
success('OpenCode plugins installed')
|
|
71
|
-
} else {
|
|
72
|
-
warn(`These dependencies are still missing from .opencode/node_modules: ${missing.join(', ')}`)
|
|
73
|
-
warn('Plugins that import them will not load. Re-run `npm install` there once the registry is reachable.')
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
warn('No .opencode/package.json found: skipping plugin install')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Step 3: Install/update skills
|
|
80
|
-
header('Step 3, Installing skills')
|
|
81
|
-
try {
|
|
82
|
-
await execa('npx', ['skills'], { cwd: process.cwd(), reject: false, stdio: 'pipe' })
|
|
83
|
-
success('Skills installed/updated')
|
|
84
|
-
} catch {
|
|
85
|
-
warn('npx skills failed: skills may be missing')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Step 4: OpenSpec CLI check
|
|
89
|
-
header('Step 4, Checking OpenSpec')
|
|
90
|
-
const openspecAvailable = await commandExists('openspec')
|
|
91
|
-
if (openspecAvailable) {
|
|
92
|
-
success('OpenSpec CLI is available')
|
|
93
|
-
} else {
|
|
94
|
-
info('OpenSpec not found on PATH: installing...')
|
|
95
|
-
try {
|
|
96
|
-
const result = await execa('npm', ['install', '@fission-ai/openspec', '--global'], {
|
|
97
|
-
cwd: process.cwd(),
|
|
98
|
-
reject: false,
|
|
99
|
-
stdio: 'pipe',
|
|
100
|
-
})
|
|
101
|
-
if (result.exitCode === 0) {
|
|
102
|
-
success('OpenSpec installed')
|
|
103
|
-
} else {
|
|
104
|
-
warn('OpenSpec install failed: run `npm install -g @fission-ai/openspec` manually')
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
warn('OpenSpec install failed: run `npm install -g @fission-ai/openspec` manually')
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Step 5: agentmemory (if project uses it)
|
|
112
|
-
header('Step 5, Checking agentmemory')
|
|
113
|
-
let cfg = {}
|
|
114
|
-
if (await fse.pathExists(opencodeJsonPath)) {
|
|
115
|
-
try {
|
|
116
|
-
const errors = []
|
|
117
|
-
cfg = parseJsonc(await fse.readFile(opencodeJsonPath, 'utf-8'), errors)
|
|
118
|
-
if (errors.length > 0) cfg = {}
|
|
119
|
-
} catch {
|
|
120
|
-
// ignore config read errors
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (cfg?.mcp?.['agentmemory']) {
|
|
125
|
-
info('Project uses agentmemory MCP')
|
|
126
|
-
loading('checking agentmemory server...')
|
|
127
|
-
|
|
128
|
-
// Check if agentmemory is installed
|
|
129
|
-
const agentmemoryAvailable = await commandExists('agentmemory')
|
|
130
|
-
if (agentmemoryAvailable) {
|
|
131
|
-
success('agentmemory CLI is available')
|
|
132
|
-
} else {
|
|
133
|
-
info('agentmemory not found on PATH, installing...')
|
|
134
|
-
try {
|
|
135
|
-
const installResult = await execa('npm', ['install', '-g', '@agentmemory/agentmemory'], {
|
|
136
|
-
reject: false,
|
|
137
|
-
timeout: 300000,
|
|
138
|
-
stdio: 'pipe',
|
|
139
|
-
})
|
|
140
|
-
if (installResult.exitCode === 0) {
|
|
141
|
-
success('agentmemory installed')
|
|
142
|
-
} else {
|
|
143
|
-
warn('agentmemory install failed, run `npm install -g @agentmemory/agentmemory` manually')
|
|
144
|
-
}
|
|
145
|
-
} catch (err) {
|
|
146
|
-
warn(`agentmemory install failed: ${err.message}`)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Health-check the server
|
|
151
|
-
try {
|
|
152
|
-
const healthResult = await execa('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:3111/agentmemory/health'], {
|
|
153
|
-
reject: false,
|
|
154
|
-
timeout: 5000,
|
|
155
|
-
})
|
|
156
|
-
if (healthResult.stdout.trim() === '200') {
|
|
157
|
-
success('agentmemory server is running')
|
|
158
|
-
} else {
|
|
159
|
-
info('agentmemory server not running, start it with `agentmemory` in a separate terminal')
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
info('agentmemory server not running, start it with `agentmemory` in a separate terminal')
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
info('agentmemory not configured for this project, skipping')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Step 6: Codegraph (if project uses it)
|
|
169
|
-
header('Step 6, Checking codegraph')
|
|
170
|
-
if (cfg?.mcp?.['codegraph']) {
|
|
171
|
-
info('Project uses codegraph MCP: building index...')
|
|
172
|
-
try {
|
|
173
|
-
const result = await execa('npx', ['--yes', '@colbymchenry/codegraph', 'init', '-i'], {
|
|
174
|
-
cwd: process.cwd(),
|
|
175
|
-
reject: false,
|
|
176
|
-
stdio: 'pipe',
|
|
177
|
-
timeout: 120000,
|
|
178
|
-
})
|
|
179
|
-
if (result.exitCode === 0) {
|
|
180
|
-
success('codegraph index initialized')
|
|
181
|
-
} else {
|
|
182
|
-
warn('codegraph init failed: codegraph_search may be slow or unavailable')
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
warn('codegraph init failed: codegraph_search may be slow or unavailable')
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
info('codegraph not configured for this project: skipping')
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Step 7: RTK check
|
|
192
|
-
header('Step 7, Checking rtk')
|
|
193
|
-
await checkRtk({ skipHeader: true, skipPrompt: true })
|
|
194
|
-
|
|
195
|
-
// Step 8: Browser automation
|
|
196
|
-
header('Step 8, Installing agent-browser')
|
|
197
|
-
await installBrowser()
|
|
198
|
-
|
|
199
|
-
// Step 9: Model tier guidance
|
|
200
|
-
header('Step 9, Model tiers')
|
|
201
|
-
const userConfigPath = path.join(opencodeDir, USER_CONFIG_FILE)
|
|
202
|
-
const hasUserOverride = await fse.pathExists(userConfigPath)
|
|
203
|
-
|
|
204
|
-
if (teamModels && Object.keys(teamModels).length > 0) {
|
|
205
|
-
info('Team model configuration:')
|
|
206
|
-
for (const [tier, model] of Object.entries(teamModels)) {
|
|
207
|
-
info(` ${tier}: ${model}`)
|
|
208
|
-
}
|
|
209
|
-
console.log()
|
|
210
|
-
|
|
211
|
-
if (hasUserOverride) {
|
|
212
|
-
info('You have a local model override (harness.user.json).')
|
|
213
|
-
info('To change: /make-user-model user <tier> <model>')
|
|
214
|
-
} else {
|
|
215
|
-
info('No local override: using team defaults.')
|
|
216
|
-
info('To override a tier for your machine: /make-user-model user <tier> <model>')
|
|
217
|
-
info(' e.g. /make-user-model user build current')
|
|
218
|
-
}
|
|
219
|
-
console.log()
|
|
220
|
-
info('The pc-subagent-tiers plugin will generate *-engineer.<tier>.md variants')
|
|
221
|
-
info('on opencode startup from these model configs.')
|
|
222
|
-
} else {
|
|
223
|
-
warn('No model tiers configured in harness.json.')
|
|
224
|
-
warn('Run /make-user-model <tier> <model> for plan, build, and fast.')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Step 10: Ensure .opencode/.gitignore exists
|
|
228
|
-
header('Step 10, Checking .opencode/.gitignore')
|
|
229
|
-
const gitignorePath = path.join(opencodeDir, '.gitignore')
|
|
230
|
-
const requiredEntries = IGNORED_ENTRIES
|
|
231
|
-
|
|
232
|
-
if (await fse.pathExists(gitignorePath)) {
|
|
233
|
-
const content = await fse.readFile(gitignorePath, 'utf-8')
|
|
234
|
-
const existing = content.split('\n').map(l => l.trim()).filter(Boolean)
|
|
235
|
-
const missing = requiredEntries.filter(e => !existing.includes(e))
|
|
236
|
-
if (missing.length > 0) {
|
|
237
|
-
const merged = `${[...existing, ...missing].join('\n')}\n`
|
|
238
|
-
await fse.writeFile(gitignorePath, merged, 'utf-8')
|
|
239
|
-
success(`Merged ${missing.length} missing .gitignore entries`)
|
|
240
|
-
} else {
|
|
241
|
-
success('.gitignore is up to date')
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
await fse.writeFile(gitignorePath, `${requiredEntries.join('\n')}\n`, 'utf-8')
|
|
245
|
-
success('Created .opencode/.gitignore')
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
console.log()
|
|
249
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
250
|
-
console.log(chalk.bold.green(' Join setup complete!'))
|
|
251
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
252
|
-
console.log()
|
|
253
|
-
console.log(' Your local environment is ready.')
|
|
254
|
-
if (teamModels && Object.keys(teamModels).length > 0 && !hasUserOverride) {
|
|
255
|
-
console.log(chalk.dim(' Tip: Run /make-user-model user <tier> current to override team models locally.'))
|
|
256
|
-
}
|
|
257
|
-
console.log(chalk.dim(' Restart opencode to pick up tier agent variants.'))
|
|
258
|
-
console.log()
|
|
259
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
+
import { installBrowser } from '../steps/browser/index.js'
|
|
7
|
+
import { checkRtk } from '../steps/optimization/index.js'
|
|
8
|
+
import { checkPlatform } from '../steps/platform/index.js'
|
|
9
|
+
import { commandExists, header, info, loading, success, warn } from '../utils/exec.js'
|
|
10
|
+
import { IGNORED_ENTRIES, USER_CONFIG_FILE } from '../utils/paths.js'
|
|
11
|
+
import { readHarnessConfig } from './shared.js'
|
|
12
|
+
|
|
13
|
+
export async function runJoin() {
|
|
14
|
+
const logo = chalk.hex('#fe3d57')
|
|
15
|
+
console.log()
|
|
16
|
+
console.log(logo(' 🤝 agent-harness join'))
|
|
17
|
+
console.log(chalk.dim(' New team member setup: checks & local installs only.'))
|
|
18
|
+
console.log(chalk.dim(' Does not modify committed project files.'))
|
|
19
|
+
console.log()
|
|
20
|
+
|
|
21
|
+
const saved = await readHarnessConfig()
|
|
22
|
+
|
|
23
|
+
if (!saved) {
|
|
24
|
+
warn('No .opencode/harness.json found.')
|
|
25
|
+
warn('This project may not have been onboarded yet. Run `npx @plainconceptsplatform/agent-harness` first.')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const backlogPlatform = saved?.platform?.backlog
|
|
30
|
+
const repoPlatform = saved?.platform?.repo
|
|
31
|
+
const teamModels = saved?.models ?? {}
|
|
32
|
+
|
|
33
|
+
const opencodeDir = path.join(process.cwd(), '.opencode')
|
|
34
|
+
const opencodeJsonPath = path.join(process.cwd(), 'opencode.jsonc')
|
|
35
|
+
|
|
36
|
+
// Step 1: Platform CLI check
|
|
37
|
+
header('Step 1, Platform CLI check')
|
|
38
|
+
const platformsToCheck = [...new Set([backlogPlatform, repoPlatform])].filter(p => p && p !== 'none')
|
|
39
|
+
if (platformsToCheck.length === 0) {
|
|
40
|
+
info('No platform integration selected, skipping CLI checks.')
|
|
41
|
+
} else {
|
|
42
|
+
for (const p of platformsToCheck) {
|
|
43
|
+
const display = p === 'github' ? 'GitHub' : p === 'azure' ? 'Azure DevOps' : p
|
|
44
|
+
info(`Checking platform: ${display}`)
|
|
45
|
+
await checkPlatform(p)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 2: Install OpenCode plugins
|
|
50
|
+
header('Step 2, Installing OpenCode plugins')
|
|
51
|
+
const pkgPath = path.join(opencodeDir, 'package.json')
|
|
52
|
+
if (await fse.pathExists(pkgPath)) {
|
|
53
|
+
// `reject: false` made the catch below unreachable: a failed install exited
|
|
54
|
+
// non-zero without throwing, so this reported success while the plugin's
|
|
55
|
+
// dependency was missing and every plugin silently refused to load. Let the
|
|
56
|
+
// call reject, and confirm the dependency is actually on disk before saying so.
|
|
57
|
+
const declared = Object.keys(
|
|
58
|
+
parseJsonc(await fse.readFile(pkgPath, 'utf8'))?.dependencies ?? {},
|
|
59
|
+
)
|
|
60
|
+
try {
|
|
61
|
+
await execa('npm', ['install'], { cwd: opencodeDir, stdio: 'pipe' })
|
|
62
|
+
} catch (err) {
|
|
63
|
+
warn('npm install failed in .opencode/: plugins may not load correctly')
|
|
64
|
+
if (err?.stderr) info(String(err.stderr).trim().split('\n').slice(-3).join('\n'))
|
|
65
|
+
}
|
|
66
|
+
const missing = declared.filter(
|
|
67
|
+
name => !fse.pathExistsSync(path.join(opencodeDir, 'node_modules', ...name.split('/'))),
|
|
68
|
+
)
|
|
69
|
+
if (missing.length === 0) {
|
|
70
|
+
success('OpenCode plugins installed')
|
|
71
|
+
} else {
|
|
72
|
+
warn(`These dependencies are still missing from .opencode/node_modules: ${missing.join(', ')}`)
|
|
73
|
+
warn('Plugins that import them will not load. Re-run `npm install` there once the registry is reachable.')
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
warn('No .opencode/package.json found: skipping plugin install')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 3: Install/update skills
|
|
80
|
+
header('Step 3, Installing skills')
|
|
81
|
+
try {
|
|
82
|
+
await execa('npx', ['skills'], { cwd: process.cwd(), reject: false, stdio: 'pipe' })
|
|
83
|
+
success('Skills installed/updated')
|
|
84
|
+
} catch {
|
|
85
|
+
warn('npx skills failed: skills may be missing')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Step 4: OpenSpec CLI check
|
|
89
|
+
header('Step 4, Checking OpenSpec')
|
|
90
|
+
const openspecAvailable = await commandExists('openspec')
|
|
91
|
+
if (openspecAvailable) {
|
|
92
|
+
success('OpenSpec CLI is available')
|
|
93
|
+
} else {
|
|
94
|
+
info('OpenSpec not found on PATH: installing...')
|
|
95
|
+
try {
|
|
96
|
+
const result = await execa('npm', ['install', '@fission-ai/openspec', '--global'], {
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
reject: false,
|
|
99
|
+
stdio: 'pipe',
|
|
100
|
+
})
|
|
101
|
+
if (result.exitCode === 0) {
|
|
102
|
+
success('OpenSpec installed')
|
|
103
|
+
} else {
|
|
104
|
+
warn('OpenSpec install failed: run `npm install -g @fission-ai/openspec` manually')
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
warn('OpenSpec install failed: run `npm install -g @fission-ai/openspec` manually')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 5: agentmemory (if project uses it)
|
|
112
|
+
header('Step 5, Checking agentmemory')
|
|
113
|
+
let cfg = {}
|
|
114
|
+
if (await fse.pathExists(opencodeJsonPath)) {
|
|
115
|
+
try {
|
|
116
|
+
const errors = []
|
|
117
|
+
cfg = parseJsonc(await fse.readFile(opencodeJsonPath, 'utf-8'), errors)
|
|
118
|
+
if (errors.length > 0) cfg = {}
|
|
119
|
+
} catch {
|
|
120
|
+
// ignore config read errors
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (cfg?.mcp?.['agentmemory']) {
|
|
125
|
+
info('Project uses agentmemory MCP')
|
|
126
|
+
loading('checking agentmemory server...')
|
|
127
|
+
|
|
128
|
+
// Check if agentmemory is installed
|
|
129
|
+
const agentmemoryAvailable = await commandExists('agentmemory')
|
|
130
|
+
if (agentmemoryAvailable) {
|
|
131
|
+
success('agentmemory CLI is available')
|
|
132
|
+
} else {
|
|
133
|
+
info('agentmemory not found on PATH, installing...')
|
|
134
|
+
try {
|
|
135
|
+
const installResult = await execa('npm', ['install', '-g', '@agentmemory/agentmemory'], {
|
|
136
|
+
reject: false,
|
|
137
|
+
timeout: 300000,
|
|
138
|
+
stdio: 'pipe',
|
|
139
|
+
})
|
|
140
|
+
if (installResult.exitCode === 0) {
|
|
141
|
+
success('agentmemory installed')
|
|
142
|
+
} else {
|
|
143
|
+
warn('agentmemory install failed, run `npm install -g @agentmemory/agentmemory` manually')
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
warn(`agentmemory install failed: ${err.message}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Health-check the server
|
|
151
|
+
try {
|
|
152
|
+
const healthResult = await execa('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:3111/agentmemory/health'], {
|
|
153
|
+
reject: false,
|
|
154
|
+
timeout: 5000,
|
|
155
|
+
})
|
|
156
|
+
if (healthResult.stdout.trim() === '200') {
|
|
157
|
+
success('agentmemory server is running')
|
|
158
|
+
} else {
|
|
159
|
+
info('agentmemory server not running, start it with `agentmemory` in a separate terminal')
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
info('agentmemory server not running, start it with `agentmemory` in a separate terminal')
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
info('agentmemory not configured for this project, skipping')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 6: Codegraph (if project uses it)
|
|
169
|
+
header('Step 6, Checking codegraph')
|
|
170
|
+
if (cfg?.mcp?.['codegraph']) {
|
|
171
|
+
info('Project uses codegraph MCP: building index...')
|
|
172
|
+
try {
|
|
173
|
+
const result = await execa('npx', ['--yes', '@colbymchenry/codegraph', 'init', '-i'], {
|
|
174
|
+
cwd: process.cwd(),
|
|
175
|
+
reject: false,
|
|
176
|
+
stdio: 'pipe',
|
|
177
|
+
timeout: 120000,
|
|
178
|
+
})
|
|
179
|
+
if (result.exitCode === 0) {
|
|
180
|
+
success('codegraph index initialized')
|
|
181
|
+
} else {
|
|
182
|
+
warn('codegraph init failed: codegraph_search may be slow or unavailable')
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
warn('codegraph init failed: codegraph_search may be slow or unavailable')
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
info('codegraph not configured for this project: skipping')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 7: RTK check
|
|
192
|
+
header('Step 7, Checking rtk')
|
|
193
|
+
await checkRtk({ skipHeader: true, skipPrompt: true })
|
|
194
|
+
|
|
195
|
+
// Step 8: Browser automation
|
|
196
|
+
header('Step 8, Installing agent-browser')
|
|
197
|
+
await installBrowser()
|
|
198
|
+
|
|
199
|
+
// Step 9: Model tier guidance
|
|
200
|
+
header('Step 9, Model tiers')
|
|
201
|
+
const userConfigPath = path.join(opencodeDir, USER_CONFIG_FILE)
|
|
202
|
+
const hasUserOverride = await fse.pathExists(userConfigPath)
|
|
203
|
+
|
|
204
|
+
if (teamModels && Object.keys(teamModels).length > 0) {
|
|
205
|
+
info('Team model configuration:')
|
|
206
|
+
for (const [tier, model] of Object.entries(teamModels)) {
|
|
207
|
+
info(` ${tier}: ${model}`)
|
|
208
|
+
}
|
|
209
|
+
console.log()
|
|
210
|
+
|
|
211
|
+
if (hasUserOverride) {
|
|
212
|
+
info('You have a local model override (harness.user.json).')
|
|
213
|
+
info('To change: /make-user-model user <tier> <model>')
|
|
214
|
+
} else {
|
|
215
|
+
info('No local override: using team defaults.')
|
|
216
|
+
info('To override a tier for your machine: /make-user-model user <tier> <model>')
|
|
217
|
+
info(' e.g. /make-user-model user build current')
|
|
218
|
+
}
|
|
219
|
+
console.log()
|
|
220
|
+
info('The pc-subagent-tiers plugin will generate *-engineer.<tier>.md variants')
|
|
221
|
+
info('on opencode startup from these model configs.')
|
|
222
|
+
} else {
|
|
223
|
+
warn('No model tiers configured in harness.json.')
|
|
224
|
+
warn('Run /make-user-model <tier> <model> for plan, build, and fast.')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Step 10: Ensure .opencode/.gitignore exists
|
|
228
|
+
header('Step 10, Checking .opencode/.gitignore')
|
|
229
|
+
const gitignorePath = path.join(opencodeDir, '.gitignore')
|
|
230
|
+
const requiredEntries = IGNORED_ENTRIES
|
|
231
|
+
|
|
232
|
+
if (await fse.pathExists(gitignorePath)) {
|
|
233
|
+
const content = await fse.readFile(gitignorePath, 'utf-8')
|
|
234
|
+
const existing = content.split('\n').map(l => l.trim()).filter(Boolean)
|
|
235
|
+
const missing = requiredEntries.filter(e => !existing.includes(e))
|
|
236
|
+
if (missing.length > 0) {
|
|
237
|
+
const merged = `${[...existing, ...missing].join('\n')}\n`
|
|
238
|
+
await fse.writeFile(gitignorePath, merged, 'utf-8')
|
|
239
|
+
success(`Merged ${missing.length} missing .gitignore entries`)
|
|
240
|
+
} else {
|
|
241
|
+
success('.gitignore is up to date')
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
await fse.writeFile(gitignorePath, `${requiredEntries.join('\n')}\n`, 'utf-8')
|
|
245
|
+
success('Created .opencode/.gitignore')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log()
|
|
249
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
250
|
+
console.log(chalk.bold.green(' Join setup complete!'))
|
|
251
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
252
|
+
console.log()
|
|
253
|
+
console.log(' Your local environment is ready.')
|
|
254
|
+
if (teamModels && Object.keys(teamModels).length > 0 && !hasUserOverride) {
|
|
255
|
+
console.log(chalk.dim(' Tip: Run /make-user-model user <tier> current to override team models locally.'))
|
|
256
|
+
}
|
|
257
|
+
console.log(chalk.dim(' Restart opencode to pick up tier agent variants.'))
|
|
258
|
+
console.log()
|
|
259
|
+
}
|