@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,184 +1,184 @@
1
- // pc-subagent-monitor: holds a spawn wave to agents.maxConcurrent and keeps a
2
- // live record of child sessions in .opencode/harness-run.json.
3
- //
4
- // OpenCode 2 denies through the permission system, not by throwing from a tool
5
- // hook, so the cap is a `subagent` permission rule: an `evaluate` hook counts
6
- // the children of the asking session and denies the launch over the limit.
7
- //
8
- // Child sessions are observed from the event stream. V2 does not document the
9
- // event type strings (the payload schema is an opaque `string`), so the handler
10
- // is deliberately tolerant and, with PC_HARNESS_DEBUG set, prints every event
11
- // type once so the names can be pinned from a real run. Counting via the event
12
- // stream is the iteration-one mechanism; `ctx.session` in the plugin context is
13
- // not documented to expose `list`, so it is not used here.
14
-
15
- import { Plugin } from "@opencode/plugin"
16
- import fs from "node:fs/promises"
17
- import path from "node:path"
18
-
19
- const PENDING_TTL_MS = 15_000
20
-
21
- export default Plugin.define({
22
- id: "pc.subagent-monitor",
23
- async setup(ctx) {
24
- const root = ctx.location.directory || process.cwd()
25
- const statePath = path.join(root, ".opencode", "harness-run.json")
26
- const state = { updatedAt: null, agents: {} }
27
- const pending = []
28
- const seenTypes = new Set()
29
- let capCache = null
30
-
31
- try {
32
- const prev = JSON.parse(await fs.readFile(statePath, "utf-8"))
33
- if (prev?.agents && typeof prev.agents === "object") {
34
- for (const [id, entry] of Object.entries(prev.agents)) {
35
- if (entry?.status === "running") entry.stale = true
36
- state.agents[id] = entry
37
- }
38
- }
39
- } catch {
40
- // fresh start
41
- }
42
-
43
- let persistScheduled = false
44
- function persist() {
45
- if (persistScheduled) return
46
- persistScheduled = true
47
- queueMicrotask(async () => {
48
- persistScheduled = false
49
- state.updatedAt = new Date().toISOString()
50
- try {
51
- await fs.mkdir(path.dirname(statePath), { recursive: true })
52
- const tmpPath = `${statePath}.tmp`
53
- await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8")
54
- await fs.rename(tmpPath, statePath)
55
- } catch {
56
- // best-effort
57
- }
58
- })
59
- }
60
-
61
- function pruneStale() {
62
- for (const [id, entry] of Object.entries(state.agents)) {
63
- if (entry?.stale) delete state.agents[id]
64
- }
65
- }
66
-
67
- async function maxConcurrent() {
68
- if (capCache) return capCache
69
- let configured = 3
70
- try {
71
- const raw = await fs.readFile(path.join(root, ".opencode", "harness.json"), "utf-8")
72
- const value = JSON.parse(raw)?.agents?.maxConcurrent
73
- if (Number.isFinite(value)) configured = value
74
- } catch {
75
- // no config yet; the CLI's own default is 3
76
- }
77
- capCache = Math.min(5, Math.max(1, Math.trunc(configured)))
78
- return capCache
79
- }
80
-
81
- function runningFor(parentID) {
82
- return Object.values(state.agents)
83
- .filter(entry => entry?.status === "running" && !entry.stale && entry.parentID === parentID)
84
- .length
85
- }
86
-
87
- function livePending(parentID) {
88
- const now = Date.now()
89
- for (let i = pending.length - 1; i >= 0; i--) {
90
- if (now - pending[i].at > PENDING_TTL_MS) pending.splice(i, 1)
91
- }
92
- return pending.filter(entry => entry.parentID === parentID).length
93
- }
94
-
95
- function releasePending(parentID) {
96
- const index = pending.findIndex(entry => entry.parentID === parentID)
97
- if (index !== -1) pending.splice(index, 1)
98
- }
99
-
100
- // The cap was prose in pc-plan-apply ("you enforce the cap"), which asked the
101
- // lead to count its own parallel calls. Six disjoint groups and a cap of
102
- // three is exactly the moment a model stops counting.
103
- await ctx.permission.hook("evaluate", async event => {
104
- if (event.action !== "subagent") return
105
- let cap
106
- let live
107
- try {
108
- pruneStale()
109
- cap = await maxConcurrent()
110
- live = runningFor(event.sessionID) + livePending(event.sessionID)
111
- if (live < cap) {
112
- pending.push({ parentID: event.sessionID, at: Date.now() })
113
- return
114
- }
115
- } catch (error) {
116
- console.error(`[harness] concurrency check failed open: ${error?.message}`)
117
- return
118
- }
119
- deny(event, live, cap)
120
- })
121
-
122
- function deny(event, live, cap) {
123
- event.effect = "deny"
124
- event.message =
125
- `[harness] ${live} subagents are already in flight and agents.maxConcurrent is ${cap}.\n` +
126
- "Collect a running worker before spawning another. A denied spawn is not a failed task: re-issue it in the next wave."
127
- }
128
-
129
- function extractSession(event) {
130
- const data = event?.data ?? event?.properties ?? event ?? {}
131
- const info = data?.info ?? data?.session ?? data ?? {}
132
- const id = info.id ?? info.sessionID ?? data.sessionID
133
- const parentID = info.parentID ?? info.parentId ?? data.parentID
134
- const status = info.status ?? data.status
135
- return { id, parentID, agent: info.agent ?? null, title: info.title ?? null, status }
136
- }
137
-
138
- const controller = new AbortController()
139
- void (async () => {
140
- try {
141
- for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
142
- try {
143
- const type = String(event?.type ?? "")
144
- if (process.env.PC_HARNESS_DEBUG && type && !seenTypes.has(type)) {
145
- seenTypes.add(type)
146
- console.error(`[pc-subagent-monitor] event: ${type}`)
147
- }
148
- const { id, parentID, agent, title, status } = extractSession(event)
149
- if (!id || !parentID) continue
150
-
151
- if (status === "running" || /created|started/.test(type)) {
152
- pruneStale()
153
- releasePending(parentID)
154
- state.agents[id] = {
155
- agent,
156
- parentID,
157
- title,
158
- status: "running",
159
- startedAt: new Date().toISOString(),
160
- endedAt: null,
161
- }
162
- await persist()
163
- continue
164
- }
165
-
166
- const entry = state.agents[id]
167
- if (!entry) continue
168
- if (status === "idle" || /idle|completed|succeeded|failed/.test(type)) {
169
- entry.status = "done"
170
- entry.endedAt = new Date().toISOString()
171
- await persist()
172
- }
173
- } catch {
174
- // monitoring must never disrupt the run
175
- }
176
- }
177
- } catch {
178
- // stream aborted on unload
179
- }
180
- })()
181
-
182
- return () => controller.abort()
183
- },
184
- })
1
+ // pc-subagent-monitor: holds a spawn wave to agents.maxConcurrent and keeps a
2
+ // live record of child sessions in .opencode/harness-run.json.
3
+ //
4
+ // OpenCode 2 denies through the permission system, not by throwing from a tool
5
+ // hook, so the cap is a `subagent` permission rule: an `evaluate` hook counts
6
+ // the children of the asking session and denies the launch over the limit.
7
+ //
8
+ // Child sessions are observed from the event stream. V2 does not document the
9
+ // event type strings (the payload schema is an opaque `string`), so the handler
10
+ // is deliberately tolerant and, with PC_HARNESS_DEBUG set, prints every event
11
+ // type once so the names can be pinned from a real run. Counting via the event
12
+ // stream is the iteration-one mechanism; `ctx.session` in the plugin context is
13
+ // not documented to expose `list`, so it is not used here.
14
+
15
+ import { Plugin } from "@opencode/plugin"
16
+ import fs from "node:fs/promises"
17
+ import path from "node:path"
18
+
19
+ const PENDING_TTL_MS = 15_000
20
+
21
+ export default Plugin.define({
22
+ id: "pc.subagent-monitor",
23
+ async setup(ctx) {
24
+ const root = ctx.location.directory || process.cwd()
25
+ const statePath = path.join(root, ".opencode", "harness-run.json")
26
+ const state = { updatedAt: null, agents: {} }
27
+ const pending = []
28
+ const seenTypes = new Set()
29
+ let capCache = null
30
+
31
+ try {
32
+ const prev = JSON.parse(await fs.readFile(statePath, "utf-8"))
33
+ if (prev?.agents && typeof prev.agents === "object") {
34
+ for (const [id, entry] of Object.entries(prev.agents)) {
35
+ if (entry?.status === "running") entry.stale = true
36
+ state.agents[id] = entry
37
+ }
38
+ }
39
+ } catch {
40
+ // fresh start
41
+ }
42
+
43
+ let persistScheduled = false
44
+ function persist() {
45
+ if (persistScheduled) return
46
+ persistScheduled = true
47
+ queueMicrotask(async () => {
48
+ persistScheduled = false
49
+ state.updatedAt = new Date().toISOString()
50
+ try {
51
+ await fs.mkdir(path.dirname(statePath), { recursive: true })
52
+ const tmpPath = `${statePath}.tmp`
53
+ await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8")
54
+ await fs.rename(tmpPath, statePath)
55
+ } catch {
56
+ // best-effort
57
+ }
58
+ })
59
+ }
60
+
61
+ function pruneStale() {
62
+ for (const [id, entry] of Object.entries(state.agents)) {
63
+ if (entry?.stale) delete state.agents[id]
64
+ }
65
+ }
66
+
67
+ async function maxConcurrent() {
68
+ if (capCache) return capCache
69
+ let configured = 3
70
+ try {
71
+ const raw = await fs.readFile(path.join(root, ".opencode", "harness.json"), "utf-8")
72
+ const value = JSON.parse(raw)?.agents?.maxConcurrent
73
+ if (Number.isFinite(value)) configured = value
74
+ } catch {
75
+ // no config yet; the CLI's own default is 3
76
+ }
77
+ capCache = Math.min(5, Math.max(1, Math.trunc(configured)))
78
+ return capCache
79
+ }
80
+
81
+ function runningFor(parentID) {
82
+ return Object.values(state.agents)
83
+ .filter(entry => entry?.status === "running" && !entry.stale && entry.parentID === parentID)
84
+ .length
85
+ }
86
+
87
+ function livePending(parentID) {
88
+ const now = Date.now()
89
+ for (let i = pending.length - 1; i >= 0; i--) {
90
+ if (now - pending[i].at > PENDING_TTL_MS) pending.splice(i, 1)
91
+ }
92
+ return pending.filter(entry => entry.parentID === parentID).length
93
+ }
94
+
95
+ function releasePending(parentID) {
96
+ const index = pending.findIndex(entry => entry.parentID === parentID)
97
+ if (index !== -1) pending.splice(index, 1)
98
+ }
99
+
100
+ // The cap was prose in pc-plan-apply ("you enforce the cap"), which asked the
101
+ // lead to count its own parallel calls. Six disjoint groups and a cap of
102
+ // three is exactly the moment a model stops counting.
103
+ await ctx.permission.hook("evaluate", async event => {
104
+ if (event.action !== "subagent") return
105
+ let cap
106
+ let live
107
+ try {
108
+ pruneStale()
109
+ cap = await maxConcurrent()
110
+ live = runningFor(event.sessionID) + livePending(event.sessionID)
111
+ if (live < cap) {
112
+ pending.push({ parentID: event.sessionID, at: Date.now() })
113
+ return
114
+ }
115
+ } catch (error) {
116
+ console.error(`[harness] concurrency check failed open: ${error?.message}`)
117
+ return
118
+ }
119
+ deny(event, live, cap)
120
+ })
121
+
122
+ function deny(event, live, cap) {
123
+ event.effect = "deny"
124
+ event.message =
125
+ `[harness] ${live} subagents are already in flight and agents.maxConcurrent is ${cap}.\n` +
126
+ "Collect a running worker before spawning another. A denied spawn is not a failed task: re-issue it in the next wave."
127
+ }
128
+
129
+ function extractSession(event) {
130
+ const data = event?.data ?? event?.properties ?? event ?? {}
131
+ const info = data?.info ?? data?.session ?? data ?? {}
132
+ const id = info.id ?? info.sessionID ?? data.sessionID
133
+ const parentID = info.parentID ?? info.parentId ?? data.parentID
134
+ const status = info.status ?? data.status
135
+ return { id, parentID, agent: info.agent ?? null, title: info.title ?? null, status }
136
+ }
137
+
138
+ const controller = new AbortController()
139
+ void (async () => {
140
+ try {
141
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
142
+ try {
143
+ const type = String(event?.type ?? "")
144
+ if (process.env.PC_HARNESS_DEBUG && type && !seenTypes.has(type)) {
145
+ seenTypes.add(type)
146
+ console.error(`[pc-subagent-monitor] event: ${type}`)
147
+ }
148
+ const { id, parentID, agent, title, status } = extractSession(event)
149
+ if (!id || !parentID) continue
150
+
151
+ if (status === "running" || /created|started/.test(type)) {
152
+ pruneStale()
153
+ releasePending(parentID)
154
+ state.agents[id] = {
155
+ agent,
156
+ parentID,
157
+ title,
158
+ status: "running",
159
+ startedAt: new Date().toISOString(),
160
+ endedAt: null,
161
+ }
162
+ await persist()
163
+ continue
164
+ }
165
+
166
+ const entry = state.agents[id]
167
+ if (!entry) continue
168
+ if (status === "idle" || /idle|completed|succeeded|failed/.test(type)) {
169
+ entry.status = "done"
170
+ entry.endedAt = new Date().toISOString()
171
+ await persist()
172
+ }
173
+ } catch {
174
+ // monitoring must never disrupt the run
175
+ }
176
+ }
177
+ } catch {
178
+ // stream aborted on unload
179
+ }
180
+ })()
181
+
182
+ return () => controller.abort()
183
+ },
184
+ })