@plainconceptsplatform/agent-harness 3.5.0 → 3.6.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 (143) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +455 -454
  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/update.js +121 -121
  8. package/cli/commands/wizard.js +134 -134
  9. package/cli/fragments/archive/az.md +97 -97
  10. package/cli/fragments/archive/gh.md +96 -96
  11. package/cli/fragments/archive/gl.md +96 -96
  12. package/cli/fragments/archive/none.md +75 -75
  13. package/cli/fragments/guardrails/codegraph.md +5 -5
  14. package/cli/fragments/guardrails/humanizer.md +4 -4
  15. package/cli/fragments/guardrails/memory.md +4 -4
  16. package/cli/fragments/guardrails/rtk.md +3 -3
  17. package/cli/fragments/guardrails/simple-english.md +4 -4
  18. package/cli/fragments/ops-backlog/az.md +28 -28
  19. package/cli/fragments/ops-backlog/gh.md +28 -28
  20. package/cli/fragments/ops-backlog/jira.md +28 -28
  21. package/cli/fragments/ops-review/az.md +62 -62
  22. package/cli/fragments/ops-review/gh.md +51 -51
  23. package/cli/fragments/ops-review/gl.md +55 -55
  24. package/cli/fragments/ops-ship/az.md +81 -81
  25. package/cli/fragments/ops-ship/gh.md +67 -67
  26. package/cli/fragments/ops-ship/gl.md +84 -84
  27. package/cli/index.js +116 -116
  28. package/cli/presets/agents-content.json +34 -34
  29. package/cli/presets/browser.json +10 -10
  30. package/cli/presets/clean.json +21 -21
  31. package/cli/presets/models.json +68 -68
  32. package/cli/presets/optimization.json +32 -32
  33. package/cli/presets/platforms.json +76 -76
  34. package/cli/presets/source.json +23 -23
  35. package/cli/steps/browser/index.js +48 -48
  36. package/cli/steps/clean/index.js +120 -120
  37. package/cli/steps/copy/agents.js +120 -120
  38. package/cli/steps/copy/commands.js +79 -79
  39. package/cli/steps/copy/fullstack-engineer.js +89 -89
  40. package/cli/steps/copy/index.js +88 -88
  41. package/cli/steps/copy/opencode-json.js +324 -324
  42. package/cli/steps/copy/skills.js +316 -316
  43. package/cli/steps/metadata/index.js +115 -115
  44. package/cli/steps/models/format.js +88 -88
  45. package/cli/steps/models/index.js +64 -64
  46. package/cli/steps/models/write.js +38 -38
  47. package/cli/steps/openspec/index.js +136 -136
  48. package/cli/steps/optimization/codegraph.js +141 -141
  49. package/cli/steps/optimization/detect.js +66 -66
  50. package/cli/steps/optimization/humanizer.js +17 -17
  51. package/cli/steps/optimization/index.js +158 -158
  52. package/cli/steps/optimization/memory.js +91 -91
  53. package/cli/steps/optimization/patch-guardrails.js +111 -111
  54. package/cli/steps/optimization/simple-english.js +17 -17
  55. package/cli/steps/optimization/skills-lock.js +30 -30
  56. package/cli/steps/platform/index.js +109 -109
  57. package/cli/steps/source/index.js +123 -123
  58. package/cli/utils/agent-color.js +111 -111
  59. package/cli/utils/copy.js +134 -134
  60. package/cli/utils/exec-spinner.js +47 -47
  61. package/cli/utils/exec.js +134 -134
  62. package/cli/utils/legacy-check.js +30 -30
  63. package/cli/utils/models-cache.js +58 -58
  64. package/cli/utils/models-pricing.js +42 -42
  65. package/cli/utils/paths.js +81 -81
  66. package/cli/utils/process.js +3 -3
  67. package/cli/utils/terminal.js +6 -6
  68. package/cli/utils/update-manifest.js +75 -75
  69. package/harness/.opencode/_gitignore +10 -10
  70. package/harness/.opencode/commands/init.md +5 -5
  71. package/harness/.opencode/commands/make-architecture.md +5 -5
  72. package/harness/.opencode/commands/make-design.md +5 -5
  73. package/harness/.opencode/commands/make-engineer.md +5 -5
  74. package/harness/.opencode/commands/make-guardrails.md +5 -5
  75. package/harness/.opencode/commands/make-user-model.md +5 -5
  76. package/harness/.opencode/commands/ops-backlog.md +10 -10
  77. package/harness/.opencode/commands/ops-review.md +10 -10
  78. package/harness/.opencode/commands/ops-ship.md +9 -9
  79. package/harness/.opencode/commands/plan-apply.md +9 -9
  80. package/harness/.opencode/commands/plan-archive.md +7 -7
  81. package/harness/.opencode/commands/plan-explore.md +11 -11
  82. package/harness/.opencode/commands/plan-goal.md +5 -5
  83. package/harness/.opencode/commands/plan-propose.md +11 -11
  84. package/harness/.opencode/commands/plan-quick.md +7 -7
  85. package/harness/.opencode/commands/plan-story.md +11 -11
  86. package/harness/.opencode/commands/repo-audit.md +7 -7
  87. package/harness/.opencode/commands/repo-help.md +5 -5
  88. package/harness/.opencode/commands/repo-initialize.md +5 -5
  89. package/harness/.opencode/commands/repo-onboard.md +5 -5
  90. package/harness/.opencode/commands/repo-verify.md +5 -5
  91. package/harness/.opencode/package.json +5 -5
  92. package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -0
  93. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  94. package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
  95. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  96. package/harness/.opencode/scripts/check-diff.mjs +252 -252
  97. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  98. package/harness/AGENTS.md +49 -49
  99. package/harness/ARCHITECTURE.md +13 -13
  100. package/harness/DESIGN.md +13 -13
  101. package/harness/opencode.jsonc +36 -36
  102. package/harness/openspec/config.yaml +20 -20
  103. package/harness/skills-lock.json +17 -17
  104. package/package.json +14 -12
  105. package/skills/README.md +18 -18
  106. package/skills/agent-harness-cli/SKILL.md +86 -86
  107. package/skills/browser-automation/SKILL.md +72 -72
  108. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  109. package/skills/pc-guardrails-project/SKILL.md +8 -8
  110. package/skills/pc-make-architecture/SKILL.md +32 -32
  111. package/skills/pc-make-architecture/structure-template.md +64 -64
  112. package/skills/pc-make-design/SKILL.md +45 -45
  113. package/skills/pc-make-engineer/SKILL.md +59 -59
  114. package/skills/pc-make-engineer/signal-mapping.md +53 -53
  115. package/skills/pc-make-engineer/template.md +36 -36
  116. package/skills/pc-make-guardrails/SKILL.md +44 -44
  117. package/skills/pc-make-guardrails/category-reference.md +112 -112
  118. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  119. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  120. package/skills/pc-make-user-model/SKILL.md +56 -56
  121. package/skills/pc-ops-ship/SKILL.md +22 -22
  122. package/skills/pc-plan-apply/SKILL.md +88 -88
  123. package/skills/pc-plan-apply/simple-mode.md +21 -21
  124. package/skills/pc-plan-archive/SKILL.md +66 -66
  125. package/skills/pc-plan-explore/SKILL.md +21 -21
  126. package/skills/pc-plan-goal/SKILL.md +91 -91
  127. package/skills/pc-plan-goal/branching.md +39 -39
  128. package/skills/pc-plan-goal/failure-policy.md +30 -30
  129. package/skills/pc-plan-goal/output-mode.md +10 -10
  130. package/skills/pc-plan-goal/output.md +71 -71
  131. package/skills/pc-plan-propose/SKILL.md +125 -125
  132. package/skills/pc-plan-propose/task-annotation.md +39 -39
  133. package/skills/pc-plan-quick/SKILL.md +46 -46
  134. package/skills/pc-plan-story/SKILL.md +93 -93
  135. package/skills/pc-repo-audit/SKILL.md +44 -44
  136. package/skills/pc-repo-help/SKILL.md +87 -87
  137. package/skills/pc-repo-initialize/SKILL.md +119 -119
  138. package/skills/pc-repo-onboard/SKILL.md +32 -32
  139. package/skills/pc-repo-verify/SKILL.md +150 -150
  140. package/skills/pc-userstory-az/SKILL.md +71 -71
  141. package/skills/pc-userstory-browser/SKILL.md +64 -64
  142. package/skills/pc-userstory-gh/SKILL.md +63 -63
  143. package/skills/pc-userstory-jira/SKILL.md +74 -74
@@ -1,5 +1,5 @@
1
- ---
2
- description: Initialize the project. Presents a single form with all setup questions, then executes selected steps.
3
- ---
4
-
5
- Load the `pc-repo-initialize` skill.
1
+ ---
2
+ description: Initialize the project. Presents a single form with all setup questions, then executes selected steps.
3
+ ---
4
+
5
+ Load the `pc-repo-initialize` skill.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Walk the user through the project and its agentic infrastructure. Explains what exists, how agents work, and how to use the system.
3
- ---
4
-
5
- Load the `pc-repo-onboard` skill.
1
+ ---
2
+ description: Walk the user through the project and its agentic infrastructure. Explains what exists, how agents work, and how to use the system.
3
+ ---
4
+
5
+ Load the `pc-repo-onboard` skill.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Verify current-branch changes, plus immutable dependency installs, builds, and tests for every discovered project.
3
- ---
4
-
5
- Load the `pc-repo-verify` skill.
1
+ ---
2
+ description: Verify current-branch changes, plus immutable dependency installs, builds, and tests for every discovered project.
3
+ ---
4
+
5
+ Load the `pc-repo-verify` skill.
@@ -1,5 +1,5 @@
1
- {
2
- "dependencies": {
3
- "@opencode/plugin": "2.0.22"
4
- }
5
- }
1
+ {
2
+ "dependencies": {
3
+ "@opencode/plugin": "2.0.22"
4
+ }
5
+ }
@@ -0,0 +1,41 @@
1
+ // pc-commit-coauthor: every commit an OpenCode agent makes names OpenCode as a
2
+ // co-author, so agent work stays identifiable in history and in the metrics
3
+ // built from it.
4
+ //
5
+ // The rule is enforced, not requested. `tool.execute.before` rewrites the shell
6
+ // tool's command before it runs (and before the permission check sees it):
7
+ // each `git commit` gains a `--trailer`, which git folds into the message
8
+ // however it was written (`-m`, `-F`, an editor, `--amend`). It fails open: a
9
+ // bug here leaves the command as the agent wrote it.
10
+
11
+ import { Plugin } from "@opencode/plugin"
12
+
13
+ export const TRAILER = "Co-Authored-By: opencode <noreply@opencode.ai>"
14
+
15
+ const SHELL_TOOL = "shell"
16
+
17
+ // `git commit` at the start of the command or of any segment, after git's own
18
+ // `-C <path>` / `-c <key=value>` options. `commit-tree` and `commit-graph` are
19
+ // other commands and do not match.
20
+ const GIT_COMMIT = /(^|[;&|\n({`])(\s*git(?:\s+-[Cc]\s+(?:"[^"]*"|'[^']*'|[^\s"']+)+)*\s+commit)(?![\w-])/g
21
+
22
+ export function withTrailer(command) {
23
+ if (typeof command !== "string" || command.includes("noreply@opencode.ai")) return command
24
+ return command.replace(GIT_COMMIT, (_, before, commit) => `${before}${commit} --trailer "${TRAILER}"`)
25
+ }
26
+
27
+ export default Plugin.define({
28
+ id: "pc.commit-coauthor",
29
+ async setup(ctx) {
30
+ await ctx.tool.hook("execute.before", event => {
31
+ try {
32
+ if (event.tool !== SHELL_TOOL) return
33
+ const command = event.input?.command
34
+ const next = withTrailer(command)
35
+ if (next !== command) event.input = { ...event.input, command: next }
36
+ } catch (error) {
37
+ console.error(`[harness] commit co-author hook failed open: ${error?.message}`)
38
+ }
39
+ })
40
+ },
41
+ })
@@ -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
+ })