@plainconceptsplatform/agent-harness 3.3.1 → 3.4.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.
Files changed (95) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +454 -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/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/copy/skills.js +316 -316
  27. package/cli/steps/models/format.js +88 -88
  28. package/cli/steps/models/index.js +64 -64
  29. package/cli/steps/openspec/index.js +136 -136
  30. package/cli/steps/optimization/codegraph.js +141 -141
  31. package/cli/steps/optimization/detect.js +66 -66
  32. package/cli/steps/optimization/humanizer.js +17 -17
  33. package/cli/steps/optimization/index.js +158 -158
  34. package/cli/steps/optimization/memory.js +91 -91
  35. package/cli/steps/optimization/patch-guardrails.js +111 -111
  36. package/cli/steps/optimization/simple-english.js +17 -17
  37. package/cli/steps/optimization/skills-lock.js +30 -30
  38. package/cli/steps/platform/index.js +109 -109
  39. package/cli/steps/source/index.js +123 -123
  40. package/cli/utils/agent-color.js +111 -111
  41. package/cli/utils/copy.js +134 -134
  42. package/cli/utils/exec-spinner.js +47 -47
  43. package/cli/utils/exec.js +134 -134
  44. package/cli/utils/legacy-check.js +30 -30
  45. package/cli/utils/models-cache.js +58 -58
  46. package/cli/utils/models-pricing.js +42 -42
  47. package/cli/utils/paths.js +81 -80
  48. package/cli/utils/process.js +3 -3
  49. package/cli/utils/terminal.js +6 -6
  50. package/cli/utils/update-manifest.js +75 -75
  51. package/harness/.opencode/_gitignore +10 -9
  52. package/harness/.opencode/commands/ops-backlog.md +10 -10
  53. package/harness/.opencode/commands/ops-review.md +10 -8
  54. package/harness/.opencode/commands/ops-ship.md +9 -9
  55. package/harness/.opencode/commands/plan-archive.md +7 -5
  56. package/harness/.opencode/commands/plan-explore.md +11 -9
  57. package/harness/.opencode/commands/plan-propose.md +11 -9
  58. package/harness/.opencode/commands/plan-quick.md +7 -5
  59. package/harness/.opencode/commands/plan-story.md +11 -9
  60. package/harness/.opencode/commands/repo-audit.md +7 -5
  61. package/harness/.opencode/commands/repo-help.md +5 -5
  62. package/harness/.opencode/package.json +5 -5
  63. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  64. package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -354
  65. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  66. package/harness/.opencode/scripts/check-diff.mjs +244 -244
  67. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  68. package/harness/ARCHITECTURE.md +13 -13
  69. package/harness/DESIGN.md +13 -13
  70. package/harness/opencode.jsonc +36 -36
  71. package/harness/openspec/config.yaml +20 -20
  72. package/harness/skills-lock.json +17 -17
  73. package/package.json +12 -14
  74. package/skills/agent-harness-cli/SKILL.md +86 -86
  75. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  76. package/skills/pc-guardrails-project/SKILL.md +8 -8
  77. package/skills/pc-make-architecture/structure-template.md +64 -64
  78. package/skills/pc-make-engineer/SKILL.md +59 -59
  79. package/skills/pc-make-engineer/template.md +36 -36
  80. package/skills/pc-make-guardrails/category-reference.md +112 -112
  81. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  82. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  83. package/skills/pc-ops-ship/SKILL.md +22 -22
  84. package/skills/pc-plan-apply/SKILL.md +86 -86
  85. package/skills/pc-plan-apply/simple-mode.md +21 -21
  86. package/skills/pc-plan-explore/SKILL.md +21 -21
  87. package/skills/pc-plan-goal/SKILL.md +91 -91
  88. package/skills/pc-plan-goal/failure-policy.md +30 -30
  89. package/skills/pc-plan-goal/output-mode.md +10 -10
  90. package/skills/pc-plan-propose/SKILL.md +125 -125
  91. package/skills/pc-plan-propose/task-annotation.md +39 -39
  92. package/skills/pc-plan-quick/SKILL.md +46 -46
  93. package/skills/pc-repo-audit/SKILL.md +44 -44
  94. package/skills/pc-repo-help/SKILL.md +87 -87
  95. package/skills/pc-repo-verify/SKILL.md +150 -150
@@ -1,8 +1,10 @@
1
- ---
2
- description: Read and triage PR review feedback. Reports what needs fixing, does not implement fixes.
3
- ---
4
-
5
- Repo platform is set in `.opencode/harness.json` → `platform.repo`. The platform-specific content below is injected by the CLI during onboarding.
6
-
7
- <!-- PC-PLATFORM-REVIEW-START -->
8
- <!-- PC-PLATFORM-REVIEW-END -->
1
+ ---
2
+ description: Read and triage PR review feedback. Reports what needs fixing, does not implement fixes.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Repo platform is set in `.opencode/harness.json` → `platform.repo`. The platform-specific content below is injected by the CLI during onboarding.
8
+
9
+ <!-- PC-PLATFORM-REVIEW-START -->
10
+ <!-- PC-PLATFORM-REVIEW-END -->
@@ -1,9 +1,9 @@
1
- ---
2
- description: Create a pull request for the current feature branch with screenshots if UI changed.
3
- ---
4
-
5
- Load the `pc-ops-ship` skill and execute it.
6
-
7
- Input:
8
-
9
- $ARGUMENTS
1
+ ---
2
+ description: Create a pull request for the current feature branch with screenshots if UI changed.
3
+ ---
4
+
5
+ Load the `pc-ops-ship` skill and execute it.
6
+
7
+ Input:
8
+
9
+ $ARGUMENTS
@@ -1,5 +1,7 @@
1
- ---
2
- description: Archive the oldest merged unarchived OpenSpec change and update documentation.
3
- ---
4
-
5
- Load the `pc-plan-archive` skill and execute it in **interactive mode**. No input required.
1
+ ---
2
+ description: Archive the oldest merged unarchived OpenSpec change and update documentation.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-plan-archive` skill and execute it in **interactive mode**. No input required.
@@ -1,9 +1,11 @@
1
- ---
2
- description: Think through an idea, investigate a problem, or clarify requirements before creating a change.
3
- ---
4
-
5
- Load the `pc-plan-explore` skill and execute it in **interactive mode**.
6
-
7
- Input:
8
-
9
- $ARGUMENTS
1
+ ---
2
+ description: Think through an idea, investigate a problem, or clarify requirements before creating a change.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-plan-explore` skill and execute it in **interactive mode**.
8
+
9
+ Input:
10
+
11
+ $ARGUMENTS
@@ -1,9 +1,11 @@
1
- ---
2
- description: Parse a work item or idea and propose a change plan with enriched task assignments.
3
- ---
4
-
5
- Load the `pc-plan-propose` skill and execute it in **interactive mode**.
6
-
7
- Input:
8
-
9
- $ARGUMENTS
1
+ ---
2
+ description: Parse a work item or idea and propose a change plan with enriched task assignments.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-plan-propose` skill and execute it in **interactive mode**.
8
+
9
+ Input:
10
+
11
+ $ARGUMENTS
@@ -1,5 +1,7 @@
1
- ---
2
- description: "Quick plan: analyze the codebase and create a task checklist using the Todo pane. No files, no OpenSpec."
3
- ---
4
-
5
- Load the `pc-plan-quick` skill.
1
+ ---
2
+ description: "Quick plan: analyze the codebase and create a task checklist using the Todo pane. No files, no OpenSpec."
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-plan-quick` skill.
@@ -1,9 +1,11 @@
1
- ---
2
- description: Write a detailed, repo-aware user story from a feature idea or need.
3
- ---
4
-
5
- Load the `pc-plan-story` skill.
6
-
7
- Input:
8
-
9
- $ARGUMENTS
1
+ ---
2
+ description: Write a detailed, repo-aware user story from a feature idea or need.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-plan-story` skill.
8
+
9
+ Input:
10
+
11
+ $ARGUMENTS
@@ -1,5 +1,7 @@
1
- ---
2
- description: Audit every configured source root against the fullstack engineer's guardrails and abilities. Read-only.
3
- ---
4
-
5
- Load the `pc-repo-audit` skill.
1
+ ---
2
+ description: Audit every configured source root against the fullstack engineer's guardrails and abilities. Read-only.
3
+ agent: planner
4
+ subagent: false
5
+ ---
6
+
7
+ Load the `pc-repo-audit` skill.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Show all available commands and when to use each one.
3
- ---
4
-
5
- Load the `pc-repo-help` skill and display its reference to the user exactly as written. Do not summarize.
1
+ ---
2
+ description: Show all available commands and when to use each one.
3
+ ---
4
+
5
+ Load the `pc-repo-help` skill and display its reference to the user exactly as written. Do not summarize.
@@ -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
+ }
@@ -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
+ })