@plainconceptsplatform/agent-harness 3.0.1 → 3.0.3

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.
@@ -50,11 +50,27 @@ export async function runJoin() {
50
50
  header('Step 2, Installing OpenCode plugins')
51
51
  const pkgPath = path.join(opencodeDir, 'package.json')
52
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
+ )
53
60
  try {
54
- await execa('npm', ['install'], { cwd: opencodeDir, reject: false, stdio: 'pipe' })
55
- success('OpenCode plugins installed')
56
- } catch {
61
+ await execa('npm', ['install'], { cwd: opencodeDir, stdio: 'pipe' })
62
+ } catch (err) {
57
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.')
58
74
  }
59
75
  } else {
60
76
  warn('No .opencode/package.json found: skipping plugin install')
@@ -19,7 +19,7 @@
19
19
  // generation is the mechanism: writing the files in setup, before agents resolve,
20
20
  // is what makes them visible.
21
21
 
22
- import { Plugin } from "@opencode/plugin"
22
+ import { Plugin, Model } from "@opencode/plugin"
23
23
  import fs from "node:fs/promises"
24
24
  import path from "node:path"
25
25
 
@@ -143,10 +143,16 @@ function stripLegacyFields(frontmatter) {
143
143
 
144
144
  function setScalar(frontmatter, key, value) {
145
145
  const line = `${key}: ${value}`
146
- if (new RegExp(`^${key}:`, "m").test(frontmatter)) {
147
- return frontmatter.replace(new RegExp(`^${key}:.*$`, "m"), () => line)
146
+ // String operations rather than a built RegExp: `key` is one of this module's
147
+ // own constants, so the pattern would be safe, but a dynamic regexp here is a
148
+ // ReDoS-shaped pattern that static analysis rightly refuses to clear.
149
+ const lines = frontmatter.split(/\r?\n/)
150
+ const index = lines.findIndex(current => current.startsWith(`${key}:`))
151
+ if (index === -1) {
152
+ return frontmatter ? `${line}\n${frontmatter}` : line
148
153
  }
149
- return frontmatter ? `${line}\n${frontmatter}` : line
154
+ lines[index] = line
155
+ return lines.join("\n")
150
156
  }
151
157
 
152
158
  function permissionsYaml(permissions) {
@@ -189,6 +195,9 @@ function buildVariant(templateContent, model) {
189
195
  let fm = stripLegacyFields(frontmatter ?? "")
190
196
  fm = setScalar(fm, "mode", "subagent")
191
197
  fm = setScalar(fm, "model", model)
198
+ // Hidden by default: a tier is chosen by whoever writes the task, not picked
199
+ // from the @ menu. Without this the menu lists every specialist once per tier.
200
+ fm = setScalar(fm, "hidden", "true")
192
201
  if (!/^color:/m.test(fm)) {
193
202
  const name = ""
194
203
  fm = setScalar(fm, "color", name ? yamlColor(agentColor(name)) : yamlColor(agentColor("engineer")))
@@ -324,7 +333,13 @@ export default Plugin.define({
324
333
  const existing = editor.get(id)
325
334
  if (!existing) continue
326
335
  const model = models[spec.tier]
327
- if (model) existing.model = model
336
+ if (model) {
337
+ try {
338
+ existing.model = Model.Ref.parse(model)
339
+ } catch {
340
+ // invalid model ref; leave undefined so session model applies
341
+ }
342
+ }
328
343
  existing.mode = "primary"
329
344
  existing.description = spec.description
330
345
  if (spec.permissions) existing.permissions = spec.permissions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
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",
@@ -1,30 +1,39 @@
1
- # Branching procedure
2
-
3
- Resolve and record the branch names:
4
-
5
- ```bash
6
- START_BRANCH="$(git branch --show-current)"
7
- DEFAULT_BRANCH="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')"
8
- [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH="main"
9
- ```
10
-
11
- Stash uncommitted work when needed and record that the goal stash exists:
12
-
13
- ```bash
14
- git stash push -u -m "goal-wip"
15
- ```
16
-
17
- Create the feature branch from the synchronized default branch:
18
-
19
- ```bash
20
- git switch "$DEFAULT_BRANCH"
21
-
22
- if git remote get-url origin >/dev/null 2>&1; then
23
- git pull origin "$DEFAULT_BRANCH"
24
- fi
25
-
26
- git switch -c "feature/{slug}"
27
- BRANCH="$(git branch --show-current)"
28
- ```
29
-
30
- Everything through archive happens on `$BRANCH`.
1
+ # Branching procedure
2
+
3
+ Resolve and record the branch names. If the invoking workflow already placed you on a feature branch (`feature/*`, `fix/*`, etc.), it owns the branch: do not stash, switch, pull, or rename. The sandbox hosting the workflow has no git credentials, so those commands would either corrupt the tree or hang, and the workflow has already cut the branch from the default branch. Treat its result as given.
4
+
5
+ ```bash
6
+ BRANCH="$(git branch --show-current)"
7
+ case "$BRANCH" in
8
+ feature/*|fix/*|chore/*|docs/*|refactor/*)
9
+ START_BRANCH="$BRANCH"
10
+ DEFAULT_BRANCH="$BRANCH"
11
+ GOAL_STASH="none"
12
+ ;;
13
+ *)
14
+ START_BRANCH="$BRANCH"
15
+ DEFAULT_BRANCH="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')"
16
+ [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH="main"
17
+
18
+ # Stash uncommitted work when needed and record that the goal stash exists.
19
+ git stash push -u -m "goal-wip"
20
+ GOAL_STASH="goal-wip"
21
+
22
+ # Create the feature branch from the synchronized default branch.
23
+ git switch "$DEFAULT_BRANCH"
24
+
25
+ if git remote get-url origin >/dev/null 2>&1; then
26
+ git pull origin "$DEFAULT_BRANCH"
27
+ fi
28
+
29
+ git switch -c "feature/{slug}"
30
+ BRANCH="$(git branch --show-current)"
31
+ ;;
32
+ esac
33
+ ```
34
+
35
+ Record the branch name and that `feature/{slug}` was applied unconditionally, so downstream phases can restore if they move off it.
36
+
37
+ Everything through archive happens on `$BRANCH`.
38
+
39
+ When a later phase renames `$BRANCH` to match the canonical change slug (Phase 3 of `pc-plan-goal`), skip the rename if the workflow already fixed the name. Renaming would call `git branch -m`, which a credentialless sandbox refuses, and the workflow's `create_pull_request` is told to use the branch as named. Detect this by checking that `$BRANCH` already starts with `feature/`; if it does, keep it.