@plainconceptsplatform/agent-harness 2.4.0 → 2.5.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 (81) hide show
  1. package/README.md +435 -437
  2. package/cli/fragments/archive/az.md +97 -95
  3. package/cli/fragments/archive/gh.md +96 -94
  4. package/cli/fragments/archive/gl.md +96 -94
  5. package/cli/fragments/archive/none.md +75 -73
  6. package/cli/fragments/guardrails/codegraph.md +5 -7
  7. package/cli/fragments/guardrails/humanizer.md +4 -4
  8. package/cli/fragments/guardrails/memory.md +4 -4
  9. package/cli/fragments/guardrails/rtk.md +3 -3
  10. package/cli/fragments/guardrails/simple-english.md +4 -4
  11. package/cli/fragments/ops-backlog/az.md +1 -1
  12. package/cli/fragments/ops-backlog/gh.md +1 -1
  13. package/cli/fragments/ops-backlog/jira.md +1 -1
  14. package/cli/fragments/ops-evidence/az.md +44 -41
  15. package/cli/fragments/ops-evidence/gh.md +54 -53
  16. package/cli/fragments/ops-evidence/jira.md +42 -38
  17. package/cli/fragments/ops-review/az.md +1 -1
  18. package/cli/fragments/ops-review/gh.md +1 -1
  19. package/cli/fragments/ops-review/gl.md +1 -1
  20. package/cli/fragments/ops-ship/az.md +81 -80
  21. package/cli/fragments/ops-ship/gh.md +68 -68
  22. package/cli/fragments/ops-ship/gl.md +85 -85
  23. package/cli/presets/agents-content.json +34 -53
  24. package/cli/steps/copy/agents.js +18 -17
  25. package/cli/steps/copy/opencode-json.js +5 -1
  26. package/cli/steps/copy/skills.js +98 -5
  27. package/cli/steps/optimization/patch-guardrails.js +5 -3
  28. package/cli/utils/copy.js +8 -3
  29. package/cli/utils/update-manifest.js +28 -2
  30. package/harness/.agents/skills/pc-guardrails-generic/SKILL.md +47 -68
  31. package/harness/.agents/skills/pc-make-architecture/SKILL.md +31 -51
  32. package/harness/.agents/skills/pc-make-design/SKILL.md +45 -68
  33. package/harness/.agents/skills/pc-make-engineer/SKILL.md +59 -219
  34. package/harness/.agents/skills/pc-make-engineer/signal-mapping.md +53 -68
  35. package/harness/.agents/skills/pc-make-engineer/template.md +42 -80
  36. package/harness/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
  37. package/harness/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
  38. package/harness/.agents/skills/pc-make-guardrails/SKILL.md +43 -74
  39. package/harness/.agents/skills/pc-make-guardrails/category-reference.md +10 -5
  40. package/harness/.agents/skills/pc-make-merge-risk-assess/category-reference.md +26 -7
  41. package/harness/.agents/skills/pc-make-user-model/SKILL.md +56 -66
  42. package/harness/.agents/skills/pc-ops-evidence/SKILL.md +133 -127
  43. package/harness/.agents/skills/pc-plan-apply/SKILL.md +14 -5
  44. package/harness/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
  45. package/harness/.agents/skills/pc-plan-archive/SKILL.md +66 -66
  46. package/harness/.agents/skills/pc-plan-explore/SKILL.md +19 -2
  47. package/harness/.agents/skills/pc-plan-goal/SKILL.md +11 -7
  48. package/harness/.agents/skills/pc-plan-goal/output-mode.md +1 -0
  49. package/harness/.agents/skills/pc-plan-goal/output.md +71 -65
  50. package/harness/.agents/skills/pc-plan-propose/SKILL.md +1 -1
  51. package/harness/.agents/skills/pc-plan-quick/SKILL.md +46 -62
  52. package/harness/.agents/skills/pc-plan-story/SKILL.md +48 -149
  53. package/harness/.agents/skills/pc-repo-help/SKILL.md +89 -91
  54. package/harness/.agents/skills/pc-repo-initialize/SKILL.md +112 -130
  55. package/harness/.agents/skills/pc-repo-onboard/SKILL.md +32 -87
  56. package/harness/.agents/skills/pc-repo-verify/SKILL.md +2 -0
  57. package/harness/.agents/skills/pc-userstory-az/SKILL.md +71 -157
  58. package/harness/.agents/skills/pc-userstory-browser/SKILL.md +50 -122
  59. package/harness/.agents/skills/pc-userstory-gh/SKILL.md +63 -120
  60. package/harness/.agents/skills/pc-userstory-jira/SKILL.md +74 -131
  61. package/harness/.opencode/commands/init.md +5 -5
  62. package/harness/.opencode/commands/make-architecture.md +5 -5
  63. package/harness/.opencode/commands/make-design.md +5 -5
  64. package/harness/.opencode/commands/make-engineer.md +5 -5
  65. package/harness/.opencode/commands/make-evidence-scaffold.md +5 -5
  66. package/harness/.opencode/commands/make-guardrails.md +5 -5
  67. package/harness/.opencode/commands/make-user-model.md +5 -5
  68. package/harness/.opencode/commands/plan-apply.md +9 -9
  69. package/harness/.opencode/commands/plan-goal.md +5 -5
  70. package/harness/.opencode/commands/plan-quick.md +5 -5
  71. package/harness/.opencode/commands/plan-story.md +9 -9
  72. package/harness/.opencode/commands/repo-audit.md +5 -5
  73. package/harness/.opencode/commands/repo-initialize.md +5 -5
  74. package/harness/.opencode/commands/repo-onboard.md +5 -5
  75. package/harness/.opencode/commands/repo-verify.md +5 -5
  76. package/harness/.opencode/plugins/pc-subagent-monitor.js +82 -2
  77. package/harness/.opencode/plugins/pc-subagent-tiers.js +9 -6
  78. package/harness/.opencode/plugins/pc-system-reminders.js +312 -3
  79. package/harness/AGENTS.md +49 -71
  80. package/harness/opencode.jsonc +1 -1
  81. package/package.json +1 -1
@@ -1,9 +1,224 @@
1
+ import { execFile } from "node:child_process"
1
2
  import fs from "node:fs/promises"
2
3
  import path from "node:path"
4
+ import { promisify } from "node:util"
3
5
 
4
6
  const GUARDRAILS_SKILL = "pc-guardrails-generic"
5
7
  const TIER_SUFFIX = /\.(?:build|fast|plan)$/
6
8
 
9
+ const run = promisify(execFile)
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Enforcement
13
+ //
14
+ // The harness states a number of rules as "never". Prose is a weak enforcer
15
+ // when nobody is watching, so the ones expressible as a predicate over tool
16
+ // arguments are denied here instead.
17
+ //
18
+ // A denial throws. opencode aborts the call and hands the message back to the
19
+ // model as the tool result, and because a plugin throw is not a permission
20
+ // rejection the turn continues, so the model can read the rule and adapt.
21
+ //
22
+ // Two invariants for everything below. It fails CLOSED only on a deliberate
23
+ // deny, and it fails OPEN on any bug of its own: a guard that breaks a session
24
+ // because of its own exception is worse than the rule it enforces. And the
25
+ // first throwing hook aborts the rest of the chain, so this file must never
26
+ // throw anything except its own denial.
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const DENIED = Symbol("pc-denied")
30
+
31
+ function deny(rule, fix) {
32
+ const error = new Error(`[harness] ${rule}\n${fix}`)
33
+ error[DENIED] = true
34
+ throw error
35
+ }
36
+
37
+ // Work tools. Held until the session's required skills are loaded; `skill`,
38
+ // `read`, `grep` and `glob` are never gated, or the agent could not load what
39
+ // it is being told to load, nor find out what that is.
40
+ const GATED_TOOLS = new Set(["edit", "write", "apply_patch", "bash", "task"])
41
+ const WRITE_TOOLS = new Set(["edit", "write", "apply_patch"])
42
+
43
+ // Backlog and repository hosts whose data must come from their CLI. The prose
44
+ // version of this rule sat at the top of twelve fragments.
45
+ const CLI_ONLY_HOSTS = [
46
+ "github.com",
47
+ "dev.azure.com",
48
+ "visualstudio.com",
49
+ "atlassian.net",
50
+ "gitlab.com",
51
+ ]
52
+
53
+ // A plan session reads; it does not change the tree. `edit` and `task` are
54
+ // denied by config, which leaves the shell. Only these roots are inspection.
55
+ const PLAN_ALLOWED = [
56
+ /^git\s+(status|log|diff|show|rev-parse|symbolic-ref|remote|ls-files|blame|describe|shortlog)\b/,
57
+ /^git\s+(branch|stash)\s+(--show-current|list)\b/,
58
+ /^openspec\s+(list|status|show|validate|diff)\b/,
59
+ /^(ls|cat|head|tail|wc|pwd|tree|find|grep|rg|which|echo|date|node\s+-p|jq)\b/,
60
+ ]
61
+
62
+ function splitCommand(command) {
63
+ // Every segment of a compound command has to pass on its own: one allowed
64
+ // read followed by `&& rm -rf` is not a read.
65
+ return command.split(/\|\||&&|;|\||\n/).map(part => part.trim()).filter(Boolean)
66
+ }
67
+
68
+ function checkGit(command) {
69
+ for (const segment of splitCommand(command)) {
70
+ if (/^git\s+add\s+(-A\b|--all\b|\.(\s|$))/.test(segment)) {
71
+ deny(
72
+ "`git add -A` and `git add .` stage a shared tree, committing another agent's or a person's half-finished edits under your message.",
73
+ "Stage the paths you changed: `git add <path> <path>`.",
74
+ )
75
+ }
76
+ if (/^git\s+commit\b.*\s-a\b/.test(segment) || /^git\s+commit\s+-[a-z]*a[a-z]*\b/.test(segment)) {
77
+ deny(
78
+ "`git commit -a` stages every tracked change, including work that is not yours.",
79
+ "Stage named paths first, then commit without `-a`.",
80
+ )
81
+ }
82
+ if (/^git\s+clean\b/.test(segment) && !/\s--\s+\S/.test(segment)) {
83
+ deny(
84
+ "`git clean` without `-- <paths>` deletes untracked files anywhere in the tree, including work nobody has committed yet.",
85
+ "Scope it: `git clean -f -- <path> <path>`.",
86
+ )
87
+ }
88
+ if (/^git\s+reset\s+--hard\b/.test(segment)) {
89
+ deny(
90
+ "`git reset --hard` discards uncommitted work in a tree you may be sharing.",
91
+ "Revert the paths you touched: `git checkout -- <path>`.",
92
+ )
93
+ }
94
+ if (/^git\s+stash\s+(drop|clear)\b/.test(segment)) {
95
+ deny(
96
+ "Dropping a stash destroys work that was set aside, possibly not yours.",
97
+ "Leave the stash and report its `git stash list` reference.",
98
+ )
99
+ }
100
+ if (/^git\s+(checkout|restore)\s+\.(\s|$)/.test(segment)) {
101
+ deny(
102
+ "`git checkout .` and `git restore .` discard every uncommitted change in the tree.",
103
+ "Name the paths to revert: `git checkout -- <path>`.",
104
+ )
105
+ }
106
+ if (/^git\s+push\b/.test(segment) && /--force\b/.test(segment) && !/--force-with-lease\b/.test(segment)) {
107
+ deny(
108
+ "`git push --force` can overwrite commits someone else pushed.",
109
+ "Use `--force-with-lease`, which refuses when the remote moved.",
110
+ )
111
+ }
112
+ }
113
+ }
114
+
115
+ async function checkPush(command, root, defaultBranch) {
116
+ if (!defaultBranch) return
117
+ for (const segment of splitCommand(command)) {
118
+ if (!/^git\s+push\b/.test(segment)) continue
119
+
120
+ const named = new RegExp(`(^|[\\s:])${defaultBranch}($|[\\s:])`).test(segment)
121
+ if (named) {
122
+ deny(
123
+ `This pushes \`${defaultBranch}\`, the default branch. The harness ships work on a branch and lets a human merge it.`,
124
+ "Push your work branch instead, or open a pull request.",
125
+ )
126
+ }
127
+ // A bare `git push` while standing on the default branch is the same act.
128
+ if (!/\s(origin|upstream)\b/.test(segment) || /^git\s+push\s*$/.test(segment)) {
129
+ const current = await run("git", ["branch", "--show-current"], { cwd: root })
130
+ .then(result => result.stdout.trim())
131
+ .catch(() => "")
132
+ if (current && current === defaultBranch) {
133
+ deny(
134
+ `You are on \`${defaultBranch}\`, the default branch, and this pushes it.`,
135
+ "Move the work to a branch first: `git switch -c feature/<slug>`.",
136
+ )
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ function checkScratch(command) {
143
+ const writesSomewhere = /(>>?|\btee\b|\bcp\b|\bmv\b|\bmkdir\b|\btouch\b|\bdd\b)/.test(command)
144
+ const outsideRepo = /(\/tmp\/|\$TMPDIR|\$TEMP\b|%TEMP%|\bmktemp\b)/.test(command)
145
+ if (writesSomewhere && outsideRepo) {
146
+ deny(
147
+ "Scratch files belong inside the repository, where the next step and the next agent can still find them.",
148
+ "Write under `$REPO_ROOT/.opencode/.tmp/`.",
149
+ )
150
+ }
151
+ }
152
+
153
+ function checkWritePath(args, root) {
154
+ const target = args?.filePath ?? args?.path
155
+ if (typeof target !== "string" || !target) return
156
+ if (!path.isAbsolute(target)) return
157
+
158
+ // Windows hands back mixed drive-letter case, so compare case-insensitively
159
+ // there and exactly everywhere else.
160
+ const normalize = value => (process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value))
161
+ const resolved = normalize(target)
162
+ const base = normalize(root)
163
+ if (resolved === base || resolved.startsWith(base + path.sep)) return
164
+
165
+ deny(
166
+ `\`${target}\` is outside the repository, so nothing else in the run can see it and nobody will clean it up.`,
167
+ "Write under `$REPO_ROOT/.opencode/.tmp/` instead.",
168
+ )
169
+ }
170
+
171
+ function checkHost(tool, args, backlogPlatform) {
172
+ const url = args?.url
173
+ if (typeof url !== "string" || !url) return
174
+
175
+ let host = ""
176
+ try {
177
+ host = new URL(url).hostname.toLowerCase()
178
+ } catch {
179
+ return
180
+ }
181
+
182
+ const isBrowserNav = /^agent-browser_/.test(tool)
183
+ if (tool === "webfetch" || isBrowserNav) {
184
+ const cliOnly = CLI_ONLY_HOSTS.find(candidate => host === candidate || host.endsWith(`.${candidate}`))
185
+ if (cliOnly) {
186
+ deny(
187
+ `${cliOnly} data must come from its CLI (\`gh\`, \`az\`, \`glab\`, \`acli\`), which is authenticated and returns structured output. A page fetch returns whatever HTML the browser would see.`,
188
+ "Use the platform CLI. If it is unavailable, report that as a blocker.",
189
+ )
190
+ }
191
+ }
192
+
193
+ if (isBrowserNav && backlogPlatform !== "browser") {
194
+ const local = host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host.endsWith(".localhost")
195
+ if (!local) {
196
+ deny(
197
+ `Browser tools are for this project's own app on localhost, not for ${host}.`,
198
+ "Use the platform CLI for external services.",
199
+ )
200
+ }
201
+ }
202
+ }
203
+
204
+ function checkPlanReadOnly(command) {
205
+ for (const segment of splitCommand(command)) {
206
+ // `echo` and `cat` are inspection right up to the point a redirect turns
207
+ // them into a write, so the redirect is checked before the allowlist.
208
+ if (/(^|\s)>>?\s*\S/.test(segment) || /\btee\b/.test(segment)) {
209
+ deny(
210
+ "The plan agent reads; a redirect writes to the tree.",
211
+ "Switch to the build agent to make changes.",
212
+ )
213
+ }
214
+ if (PLAN_ALLOWED.some(pattern => pattern.test(segment))) continue
215
+ deny(
216
+ `The plan agent reads; it does not change the tree, and \`${segment.split(/\s+/)[0]}\` is not one of its inspection commands.`,
217
+ "Switch to the build agent to make changes.",
218
+ )
219
+ }
220
+ }
221
+
7
222
  function skillNames(content) {
8
223
  const abilities = content.match(/^## Abilities\s*\n([\s\S]*?)(?=^## |\s*$)/m)?.[1] ?? ""
9
224
  return [...abilities.matchAll(/@([a-z0-9][a-z0-9-]*)/gi)].map(match => match[1])
@@ -37,31 +252,77 @@ async function requiredSkills(directory, agent) {
37
252
  ])
38
253
  }
39
254
 
255
+ // Only skills that exist on disk can gate work. An `@skill` reference to
256
+ // something uninstalled would otherwise be unloadable and deadlock the worker,
257
+ // which is a worse failure than the dangling reference itself.
258
+ async function installedSkills(directory, names) {
259
+ const present = new Set()
260
+ await Promise.all([...names].map(async name => {
261
+ const skillPath = path.join(directory, ".agents", "skills", name, "SKILL.md")
262
+ try {
263
+ await fs.access(skillPath)
264
+ present.add(name)
265
+ } catch {
266
+ // not installed; the reminder still names it, the gate ignores it
267
+ }
268
+ }))
269
+ return present
270
+ }
271
+
272
+ async function detectDefaultBranch(root) {
273
+ const fromRemote = await run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd: root })
274
+ .then(result => result.stdout.trim().replace(/^origin\//, ""))
275
+ .catch(() => "")
276
+ if (fromRemote) return fromRemote
277
+
278
+ return run("git", ["config", "--get", "init.defaultBranch"], { cwd: root })
279
+ .then(result => result.stdout.trim() || "main")
280
+ .catch(() => "main")
281
+ }
282
+
283
+ async function readBacklogPlatform(root) {
284
+ const raw = await readFile(path.join(root, ".opencode", "harness.json"))
285
+ try {
286
+ return JSON.parse(raw)?.platform?.backlog ?? "none"
287
+ } catch {
288
+ return "none"
289
+ }
290
+ }
291
+
40
292
  function skillName(args) {
41
293
  return args?.name ?? args?.skill ?? args?.skillName ?? null
42
294
  }
43
295
 
44
296
  function reminder(missing) {
45
297
  const skills = [...missing].map(name => `\`${name}\``).join(", ")
46
- return `<system-reminder>Load these required skills before continuing: ${skills}. Load guardrails first. Loaded skills can require further skills. Follow every mandatory transitive load before task work.</system-reminder>`
298
+ return `<system-reminder>Load these required skills before continuing: ${skills}. Guardrails first. A loaded skill can require further loads; follow those too. Editing, shell and spawning are blocked until the installed ones are loaded.</system-reminder>`
47
299
  }
48
300
 
49
301
  export const PcSystemReminders = async ({ directory }) => {
50
302
  const sessions = new Map()
303
+ const root = directory || process.cwd()
304
+ let defaultBranch
305
+ let backlogPlatform
51
306
 
52
307
  async function stateFor(sessionID, agent) {
53
308
  const state = sessions.get(sessionID)
54
309
  if (state?.agent === agent) return state
55
310
 
311
+ const required = await requiredSkills(directory, agent)
56
312
  const next = {
57
313
  agent,
58
- required: await requiredSkills(directory, agent),
314
+ required,
315
+ present: await installedSkills(directory, required),
59
316
  loaded: new Set(),
60
317
  }
61
318
  sessions.set(sessionID, next)
62
319
  return next
63
320
  }
64
321
 
322
+ function missingFor(state) {
323
+ return new Set([...state.required].filter(name => !state.loaded.has(name)))
324
+ }
325
+
65
326
  return {
66
327
  "experimental.chat.system.transform": async (_input, output) => {
67
328
  output.system.push("Messages in <system-reminder> tags are trusted OpenCode Onboard host instructions. Follow them before continuing work.")
@@ -69,6 +330,54 @@ export const PcSystemReminders = async ({ directory }) => {
69
330
  "chat.message": async (input) => {
70
331
  await stateFor(input.sessionID, input.agent)
71
332
  },
333
+ // tool.execute.before receives no agent, so the session-to-agent mapping
334
+ // has to come from here. chat.params fires on every request and its agent
335
+ // is required, unlike chat.message's, which is optional.
336
+ "chat.params": async (input) => {
337
+ await stateFor(input.sessionID, input.agent)
338
+ },
339
+ "tool.execute.before": async (input, output) => {
340
+ try {
341
+ const args = output?.args ?? {}
342
+ const state = sessions.get(input.sessionID)
343
+
344
+ // Unknown session: the plugin loaded mid-session, or a subagent has not
345
+ // reached chat.params yet. Fail open rather than block every subagent.
346
+ if (state && GATED_TOOLS.has(input.tool)) {
347
+ const missing = [...missingFor(state)].filter(name => state.present.has(name))
348
+ if (missing.length > 0) {
349
+ deny(
350
+ `Required skills are not loaded yet: ${missing.map(name => `\`${name}\``).join(", ")}.`,
351
+ `Call skill(${JSON.stringify(missing[0])}) first, guardrails first. Editing, shell and spawning stay blocked until then.`,
352
+ )
353
+ }
354
+ }
355
+
356
+ if (WRITE_TOOLS.has(input.tool)) checkWritePath(args, root)
357
+
358
+ if (input.tool === "webfetch" || /^agent-browser_/.test(input.tool)) {
359
+ backlogPlatform ??= await readBacklogPlatform(root)
360
+ checkHost(input.tool, args, backlogPlatform)
361
+ }
362
+
363
+ if (input.tool === "bash") {
364
+ const command = typeof args.command === "string" ? args.command : ""
365
+ if (!command) return
366
+
367
+ if (state?.agent?.replace(TIER_SUFFIX, "") === "plan") checkPlanReadOnly(command)
368
+ checkGit(command)
369
+ checkScratch(command)
370
+ if (/\bgit\s+push\b/.test(command)) {
371
+ defaultBranch ??= await detectDefaultBranch(root)
372
+ await checkPush(command, root, defaultBranch)
373
+ }
374
+ }
375
+ } catch (error) {
376
+ if (error?.[DENIED]) throw error
377
+ // A bug in this guard must never break the session it is guarding.
378
+ console.error(`[harness] guard failed open on ${input.tool}: ${error?.message}`)
379
+ }
380
+ },
72
381
  "tool.execute.after": async (input) => {
73
382
  if (input.tool !== "skill") return
74
383
  const state = sessions.get(input.sessionID)
@@ -86,7 +395,7 @@ export const PcSystemReminders = async ({ directory }) => {
86
395
  if (!userMessage) return
87
396
 
88
397
  const state = await stateFor(userMessage.info.sessionID, userMessage.info.agent)
89
- const missing = new Set([...state.required].filter(name => !state.loaded.has(name)))
398
+ const missing = missingFor(state)
90
399
  if (missing.size === 0) return
91
400
 
92
401
  const textPart = userMessage.parts.find(part => part.type === "text")
package/harness/AGENTS.md CHANGED
@@ -1,71 +1,49 @@
1
- # AGENTS.md
2
-
3
- <!-- PC-NOT-INITIALIZED -->
4
-
5
- # Agent operating guide
6
-
7
- This guide defines the common operating contract for AI agents in this repository.
8
- It is agent-agnostic and works with OpenCode, Claude Code, Codex, Gemini, and other agents.
9
-
10
- ## Purpose and scope
11
-
12
- Use this file for repository-wide workflow rules. Keep product architecture, security constraints, and design rules in their source documents rather than duplicating them here.
13
-
14
- ## Session context
15
-
16
- Before a non-trivial change, read these documents in order:
17
-
18
- 1. `AGENTS.md` for workflow and repository rules.
19
- 2. `ARCHITECTURE.md` for boundaries, dependencies, and component interactions.
20
- 3. `DESIGN.md` for UI and design-system work.
21
- 4. The active OpenSpec change or the relevant specification for the area being changed.
22
-
23
- Read each document once per session unless it changes or the task moves into a different area.
24
-
25
- Command aliases: OpenSpec skills may reference `/opsx-propose`, `/opsx-apply`, `/opsx-archive`, or `/opsx-explore`. Always substitute them with the `pc-plan-propose`, `pc-plan-apply`, `pc-plan-archive`, and `pc-plan-explore` skills respectively. User-facing command names are `/plan-propose`, `/plan-apply`, `/plan-archive`, and `/plan-explore`. Never mention the `opsx-` names to the user.
26
-
27
- ## Workflow ownership
28
-
29
- <!-- PC-PLATFORM-WORKFLOW-START -->
30
- <!-- PC-PLATFORM-WORKFLOW-END -->
31
-
32
- ## Planning and execution
33
-
34
- - Plan before delegating work. Use OpenSpec when the change needs explicit scope, decisions, or sequenced tasks.
35
- - Keep changes focused. Do not combine unrelated refactors with requested work.
36
- - Do not guess when requirements, architecture, or security constraints are unclear. Ask before proceeding.
37
- - Prefer the project's established patterns and source documents over introducing new conventions.
38
-
39
- ## Engineer selection
40
-
41
- Inspect `.opencode/agents/*.md` before spawning. Prefer the most specialized custom engineer. `build` and `plan` are the only primaries and are never spawned; `fullstack-engineer` is the body they share and the fallback worker, so prefer a specialist over it. If no specialist matches, tell the user to create one with `/make-engineer`. Spawn only engineers present in that directory.
42
-
43
- The `pc-plan-apply` skill is authoritative for subagent waves, dependency ordering, retries, and concurrency. Read `agents.maxConcurrent` from `.opencode/harness.json` before spawning workers.
44
-
45
- ## Tool and repository safety
46
-
47
- - Never expose or commit secrets, credentials, tokens, or production data.
48
- - Read before editing. Respect repository ownership, generated files, and existing local changes.
49
- - Run only commands appropriate to the task. Do not bypass checks, weaken tests, or silence lint rules to get a green result.
50
- - Commit, push, create pull requests, alter dependencies, or change deployment configuration only with the user's explicit approval and the repository's stated process.
51
-
52
- ## Verification and completion
53
-
54
- - Run the applicable tests, lint, typecheck, and build before reporting completion.
55
- - A bug fix needs a test that would have caught the defect when practical.
56
- - Update specifications, architecture, or design documentation when the change makes their current statements inaccurate.
57
- - Report changed files, checks run, and any remaining risk or follow-up work.
58
-
59
- ## Communication
60
-
61
- - Keep updates concise and factual.
62
- - State blockers early and explain the decision needed.
63
- - Use the repository's language and writing conventions for source, documentation, issues, commits, and pull requests.
64
- - Comments explain non-obvious reasons, constraints, or invariants. Do not add comments that restate code.
65
-
66
- ## Skills
67
-
68
- Skills live in `.agents/skills/`. Always installed: `@pc-guardrails-generic`, `@pc-guardrails-project`, and `@browser-automation`. The always-installed `pc-system-reminders` plugin loads each agent's `## Abilities` before work, guardrails first. Skills can require mandatory transitive loads. Keep `## Abilities` complete and do not treat entries as passive references.
69
-
70
- <!-- PC-PLATFORM-SKILLS-GUIDE-START -->
71
- <!-- PC-PLATFORM-SKILLS-GUIDE-END -->
1
+ # AGENTS.md
2
+
3
+ <!-- PC-NOT-INITIALIZED -->
4
+
5
+ # Agent operating guide
6
+
7
+ The operating contract for AI agents in this repository. Agent-agnostic: OpenCode, Claude Code, Codex, Gemini and others.
8
+
9
+ ## Session context
10
+
11
+ Before a non-trivial change, read `AGENTS.md` for workflow rules, `ARCHITECTURE.md` for boundaries and component interactions, `DESIGN.md` for UI and design-system work, and the active OpenSpec change or the specification covering the area you are changing.
12
+
13
+ Command aliases: OpenSpec skills may reference `/opsx-propose`, `/opsx-apply`, `/opsx-archive`, or `/opsx-explore`. Always substitute them with the `pc-plan-propose`, `pc-plan-apply`, `pc-plan-archive`, and `pc-plan-explore` skills respectively. User-facing command names are `/plan-propose`, `/plan-apply`, `/plan-archive`, and `/plan-explore`. Never mention the `opsx-` names to the user.
14
+
15
+ ## Workflow ownership
16
+
17
+ <!-- PC-PLATFORM-WORKFLOW-START -->
18
+ <!-- PC-PLATFORM-WORKFLOW-END -->
19
+
20
+ ## Planning and execution
21
+
22
+ - Never combine an unrelated refactor with the work you were asked to do. It arrives under a message that does not mention it, and the reviewer approves both.
23
+ - Never introduce a new convention where the repository already has one, and never guess when requirements, architecture or a security constraint are unclear. Ask.
24
+
25
+ ## Engineer selection
26
+
27
+ Inspect `.opencode/agents/*.md` before spawning. Prefer the most specialized custom engineer. `build` and `plan` are the only primaries and are never spawned; `fullstack-engineer` is the body they share and the fallback worker, so prefer a specialist over it. If no specialist matches, tell the user to create one with `/make-engineer`. Spawn only engineers present in that directory.
28
+
29
+ The `pc-plan-apply` skill is authoritative for subagent waves, dependency ordering, retries, and concurrency. A spawn past `agents.maxConcurrent` is denied (`pc-subagent-monitor`); a denied spawn is not a failed task, so re-issue it in the next wave.
30
+
31
+ ## Tool and repository safety
32
+
33
+ - Never expose or commit secrets, credentials, tokens, or production data.
34
+ - Never overwrite a generated file, or uncommitted changes you did not make. The tree may be shared with a person and another agent.
35
+ - Never bypass a check, weaken a test, or silence a lint rule to reach a green result. A green run that was arranged is worse than a red one, because nobody looks again.
36
+ - Commit, push, open a pull request, change dependencies, or touch deployment configuration only with the user's explicit approval and the repository's stated process.
37
+
38
+ ## Verification and completion
39
+
40
+ - Never call a bug fixed without a test that would have caught it, where one is practical.
41
+ - Never leave a specification, `ARCHITECTURE.md` or `DESIGN.md` asserting something this change made false.
42
+ - Never end on a blocker without naming it and the decision it needs. An unattended run that stops quietly looks like one that finished.
43
+
44
+ ## Skills
45
+
46
+ Skills live in `.agents/skills/`. Always installed: `@pc-guardrails-generic`, `@pc-guardrails-project`, and `@browser-automation`. An entry under `## Abilities` is not a passive reference: call the `skill` tool once per `@skill-name`, guardrails first. Editing, shell and spawning stay blocked until you have (`pc-system-reminders`), and a loaded skill can require further loads.
47
+
48
+ <!-- PC-PLATFORM-SKILLS-GUIDE-START -->
49
+ <!-- PC-PLATFORM-SKILLS-GUIDE-END -->
@@ -41,7 +41,7 @@
41
41
  "build": { "mode": "primary" },
42
42
  "plan": {
43
43
  "mode": "primary",
44
- "permission": { "edit": "deny" }
44
+ "permission": { "edit": "deny", "task": "deny" }
45
45
  }
46
46
  }
47
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
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",