@plainconceptsplatform/agent-harness 3.8.2 → 3.9.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 (148) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +551 -549
  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 +176 -160
  38. package/cli/steps/copy/commands.js +79 -79
  39. package/cli/steps/copy/fullstack-engineer.js +81 -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 +82 -82
  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 +12 -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 +2 -1
  92. package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
  93. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  94. package/harness/.opencode/plugins/pc-subagent-tiers-core.js +337 -0
  95. package/harness/.opencode/plugins/pc-subagent-tiers.js +34 -345
  96. package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
  97. package/harness/.opencode/scripts/check-diff.mjs +305 -305
  98. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  99. package/harness/.opencode/scripts/harness-doctor.mjs +52 -0
  100. package/harness/.opencode/scripts/harness-doctor.test.mjs +58 -0
  101. package/harness/.opencode/scripts/platform-contract.mjs +366 -366
  102. package/harness/.opencode/scripts/sync-agents.mjs +29 -0
  103. package/harness/AGENTS.md +55 -49
  104. package/harness/ARCHITECTURE.md +13 -13
  105. package/harness/DESIGN.md +13 -13
  106. package/harness/opencode.jsonc +36 -36
  107. package/harness/openspec/config.yaml +20 -20
  108. package/harness/skills-lock.json +17 -17
  109. package/package.json +12 -14
  110. package/skills/README.md +18 -18
  111. package/skills/agent-harness-cli/SKILL.md +86 -86
  112. package/skills/browser-automation/SKILL.md +72 -72
  113. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  114. package/skills/pc-guardrails-project/SKILL.md +8 -8
  115. package/skills/pc-make-architecture/SKILL.md +33 -33
  116. package/skills/pc-make-architecture/structure-template.md +64 -64
  117. package/skills/pc-make-design/SKILL.md +45 -45
  118. package/skills/pc-make-engineer/SKILL.md +59 -59
  119. package/skills/pc-make-engineer/signal-mapping.md +53 -53
  120. package/skills/pc-make-engineer/template.md +36 -36
  121. package/skills/pc-make-guardrails/SKILL.md +44 -44
  122. package/skills/pc-make-guardrails/category-reference.md +112 -112
  123. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  124. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  125. package/skills/pc-make-user-model/SKILL.md +56 -56
  126. package/skills/pc-ops-ship/SKILL.md +22 -22
  127. package/skills/pc-plan-apply/SKILL.md +90 -90
  128. package/skills/pc-plan-apply/simple-mode.md +21 -21
  129. package/skills/pc-plan-archive/SKILL.md +66 -66
  130. package/skills/pc-plan-explore/SKILL.md +21 -21
  131. package/skills/pc-plan-goal/SKILL.md +91 -91
  132. package/skills/pc-plan-goal/branching.md +39 -39
  133. package/skills/pc-plan-goal/failure-policy.md +30 -30
  134. package/skills/pc-plan-goal/output-mode.md +10 -10
  135. package/skills/pc-plan-goal/output.md +71 -71
  136. package/skills/pc-plan-propose/SKILL.md +127 -127
  137. package/skills/pc-plan-propose/task-annotation.md +39 -39
  138. package/skills/pc-plan-quick/SKILL.md +46 -46
  139. package/skills/pc-plan-story/SKILL.md +93 -93
  140. package/skills/pc-repo-audit/SKILL.md +44 -44
  141. package/skills/pc-repo-help/SKILL.md +87 -87
  142. package/skills/pc-repo-initialize/SKILL.md +119 -119
  143. package/skills/pc-repo-onboard/SKILL.md +32 -32
  144. package/skills/pc-repo-verify/SKILL.md +150 -150
  145. package/skills/pc-userstory-az/SKILL.md +71 -71
  146. package/skills/pc-userstory-browser/SKILL.md +64 -64
  147. package/skills/pc-userstory-gh/SKILL.md +63 -63
  148. package/skills/pc-userstory-jira/SKILL.md +74 -74
@@ -0,0 +1,337 @@
1
+ // pc-subagent-tiers-core: pure Node agent generation. The plugin and the
2
+ // standalone sync script both call this, so a CI pre-start step can write the
3
+ // agents before OpenCode builds its task enum. No @opencode/plugin import here.
4
+ //
5
+ // Agent topology:
6
+ // build.md / plan.md mode: primary the only agents a human selects.
7
+ // planner.md mode: all fullstack body on the plan tier;
8
+ // commands run on it, it cannot spawn.
9
+ // planner-sub.md mode: subagent spawnable plan-tier alias for the
10
+ // task tool, same body, cannot spawn.
11
+ // fullstack-engineer.md mode: subagent shared body and fallback worker.
12
+ // *-engineer.md mode: subagent specialists.
13
+ // *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
14
+
15
+ import fs from "node:fs/promises";
16
+ import path from "node:path";
17
+
18
+ export const TIERS = ["build", "fast", "plan"];
19
+ export const MANIFEST_FILE = "harness-models.json";
20
+ export const FULLSTACK_NAME = "fullstack-engineer";
21
+ export const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`;
22
+
23
+ export const PRIMARIES = {
24
+ build: {
25
+ tier: "build",
26
+ color: "primary",
27
+ description:
28
+ "Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
29
+ permissions: null,
30
+ },
31
+ plan: {
32
+ tier: "plan",
33
+ color: "warning",
34
+ description:
35
+ "Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
36
+ permissions: [
37
+ { action: "edit", resource: "*", effect: "deny" },
38
+ { action: "subagent", resource: "*", effect: "deny" },
39
+ ],
40
+ },
41
+ planner: {
42
+ tier: "plan",
43
+ mode: "all",
44
+ color: "info",
45
+ description:
46
+ "Write plans, proposals, stories, reviews and archives on the plan model. Edits the tree, never spawns workers.",
47
+ permissions: [{ action: "subagent", resource: "*", effect: "deny" }],
48
+ },
49
+ };
50
+
51
+ const THEME_COLORS = new Set(["primary", "secondary", "accent", "success", "warning", "error", "info"]);
52
+
53
+ const LEGACY_FIELDS = [
54
+ "model",
55
+ "permission",
56
+ "prompt",
57
+ "disable",
58
+ "disabled",
59
+ "temperature",
60
+ "top_p",
61
+ "maxSteps",
62
+ "tools",
63
+ ];
64
+
65
+ function agentColor(name) {
66
+ let h = 0x811c9dc5;
67
+ for (let i = 0; i < name.length; i++) {
68
+ h ^= name.charCodeAt(i);
69
+ h = Math.imul(h, 0x01000193) >>> 0;
70
+ }
71
+ const hue = (h >>> 0) % 360;
72
+ const sat = 0.68;
73
+ const light = 0.52;
74
+ const c = (1 - Math.abs(2 * light - 1)) * sat;
75
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
76
+ const m = light - c / 2;
77
+ const [r, g, b] =
78
+ hue < 60
79
+ ? [c, x, 0]
80
+ : hue < 120
81
+ ? [x, c, 0]
82
+ : hue < 180
83
+ ? [0, c, x]
84
+ : hue < 240
85
+ ? [0, x, c]
86
+ : hue < 300
87
+ ? [x, 0, c]
88
+ : [c, 0, x];
89
+ const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
90
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
91
+ }
92
+
93
+ function shouldDeriveColor(current) {
94
+ if (!current) return true;
95
+ return THEME_COLORS.has(current.trim().toLowerCase());
96
+ }
97
+
98
+ function yamlColor(value) {
99
+ return value.startsWith("#") ? `"${value}"` : value;
100
+ }
101
+
102
+ function unquoteColor(raw) {
103
+ return (raw ?? "").trim().replace(/^["']/, "").replace(/["']$/, "");
104
+ }
105
+
106
+ function splitFrontmatter(content) {
107
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
108
+ if (!match) return { frontmatter: null, body: content };
109
+ return { frontmatter: match[1], body: content.slice(match[0].length) };
110
+ }
111
+
112
+ function stripLegacyFields(frontmatter) {
113
+ const lines = frontmatter.split(/\r?\n/);
114
+ const kept = [];
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const key = lines[i].match(/^([A-Za-z0-9_-]+):/)?.[1];
117
+ if (key && LEGACY_FIELDS.includes(key)) {
118
+ i++;
119
+ while (i < lines.length && /^\s+\S/.test(lines[i])) i++;
120
+ i--;
121
+ continue;
122
+ }
123
+ kept.push(lines[i]);
124
+ }
125
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
126
+ }
127
+
128
+ function setScalar(frontmatter, key, value) {
129
+ const line = `${key}: ${value}`;
130
+ const lines = frontmatter.split(/\r?\n/);
131
+ const index = lines.findIndex((current) => current.startsWith(`${key}:`));
132
+ if (index === -1) return frontmatter ? `${line}\n${frontmatter}` : line;
133
+ lines[index] = line;
134
+ return lines.join("\n");
135
+ }
136
+
137
+ function permissionsYaml(permissions) {
138
+ return [
139
+ "permissions:",
140
+ ...permissions.flatMap((p) => [
141
+ ` - action: ${p.action}`,
142
+ ` resource: ${p.resource}`,
143
+ ` effect: ${p.effect}`,
144
+ ]),
145
+ ].join("\n");
146
+ }
147
+
148
+ function colorLineFor(name, rawCurrent) {
149
+ const current = unquoteColor(rawCurrent);
150
+ const target = shouldDeriveColor(current) ? agentColor(name) : current;
151
+ const rendered = yamlColor(target);
152
+ return (rawCurrent ?? "").trim() === rendered ? null : `color: ${rendered}`;
153
+ }
154
+
155
+ function normalizeTemplate(content, name) {
156
+ const { frontmatter, body } = splitFrontmatter(content);
157
+ if (frontmatter === null) {
158
+ return `---\nmode: subagent\ncolor: ${yamlColor(agentColor(name))}\n---\n\n${content}`;
159
+ }
160
+ let fm = stripLegacyFields(frontmatter);
161
+ fm = setScalar(fm, "mode", "subagent");
162
+ const colorLine = colorLineFor(name, fm.match(/^color:\s*(.+)$/m)?.[1]);
163
+ if (colorLine) fm = setScalar(fm, "color", colorLine.slice("color: ".length));
164
+ return `---\n${fm}\n---\n${body}`;
165
+ }
166
+
167
+ function buildVariant(templateContent, model) {
168
+ const { frontmatter, body } = splitFrontmatter(templateContent);
169
+ let fm = stripLegacyFields(frontmatter ?? "");
170
+ fm = setScalar(fm, "mode", "subagent");
171
+ fm = setScalar(fm, "model", model);
172
+ fm = setScalar(fm, "hidden", "true");
173
+ if (!/^color:/m.test(fm)) {
174
+ fm = setScalar(fm, "color", yamlColor(agentColor("engineer")));
175
+ }
176
+ return `---\n${fm}\n---\n${body}`;
177
+ }
178
+
179
+ function buildPrimary(spec, fullstackContent, model) {
180
+ const { body } = splitFrontmatter(fullstackContent);
181
+ const lines = ["---", `description: ${spec.description}`, `mode: ${spec.mode ?? "primary"}`];
182
+ if (model) lines.push(`model: ${model}`);
183
+ lines.push(`color: ${yamlColor(spec.color)}`);
184
+ if (spec.permissions) lines.push(permissionsYaml(spec.permissions));
185
+ lines.push("---");
186
+ return `${lines.join("\n")}\n\n${body.trim()}\n`;
187
+ }
188
+
189
+ function buildPlannerSub(fullstackContent, model) {
190
+ const { body } = splitFrontmatter(fullstackContent);
191
+ const lines = [
192
+ "---",
193
+ "description: Spawnable planner alias for the task tool. Uses the plan model and cannot spawn workers.",
194
+ "mode: subagent",
195
+ ];
196
+ if (model) lines.push(`model: ${model}`);
197
+ lines.push(`color: ${yamlColor(agentColor("planner-sub"))}`);
198
+ lines.push("hidden: true");
199
+ lines.push(permissionsYaml([{ action: "subagent", resource: "*", effect: "deny" }]));
200
+ lines.push("---");
201
+ return `${lines.join("\n")}\n\n${body.trim()}\n`;
202
+ }
203
+
204
+ async function readJson(filePath) {
205
+ try {
206
+ return JSON.parse(await fs.readFile(filePath, "utf-8"));
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ async function scanEngineers(agentsDir) {
213
+ try {
214
+ const entries = await fs.readdir(agentsDir);
215
+ return {
216
+ templates: entries
217
+ .filter((f) => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE)
218
+ .map((f) => f.replace(/\.md$/, "")),
219
+ variantFiles: entries.filter((f) => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
220
+ hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
221
+ };
222
+ } catch {
223
+ return { templates: [], variantFiles: [], hasFullstack: false };
224
+ }
225
+ }
226
+
227
+ async function writeIfChanged(filePath, content) {
228
+ try {
229
+ if ((await fs.readFile(filePath, "utf-8")) === content) return false;
230
+ } catch {
231
+ // file missing, write it
232
+ }
233
+ const tmpPath = `${filePath}.tmp`;
234
+ await fs.writeFile(tmpPath, content, "utf-8");
235
+ await fs.rename(tmpPath, filePath);
236
+ return true;
237
+ }
238
+
239
+ function isModelId(value) {
240
+ return typeof value === "string" && /^[^/\s]+\/[^/\s]+$/.test(value);
241
+ }
242
+
243
+ export async function generateAgents({ root, writeManifest = true } = {}) {
244
+ if (!root) throw new Error("generateAgents requires a root directory");
245
+ const agentsDir = path.join(root, ".opencode", "agents");
246
+ const [user, team] = await Promise.all([
247
+ readJson(path.join(root, ".opencode", "harness.user.json")),
248
+ readJson(path.join(root, ".opencode", "harness.json")),
249
+ ]);
250
+
251
+ const models = {};
252
+ const sources = {};
253
+ const warnings = [];
254
+ for (const tier of TIERS) {
255
+ const fromUser = user?.models?.[tier];
256
+ const fromTeam = team?.models?.[tier];
257
+ models[tier] = fromUser ?? fromTeam ?? null;
258
+ sources[tier] = fromUser ? "user" : fromTeam ? "team" : "missing";
259
+ if (!models[tier]) {
260
+ warnings.push(`tier ${tier} has no model; its agents inherit the session model`);
261
+ } else if (!isModelId(models[tier])) {
262
+ warnings.push(`tier ${tier} model "${models[tier]}" is not provider/model`);
263
+ }
264
+ }
265
+
266
+ const available = TIERS.filter((tier) => models[tier]);
267
+ const { templates, variantFiles, hasFullstack } = await scanEngineers(agentsDir);
268
+ const manifest = {
269
+ generatedAt: new Date().toISOString(),
270
+ models,
271
+ source: sources,
272
+ agents: {},
273
+ warnings,
274
+ };
275
+
276
+ if (hasFullstack) {
277
+ const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE);
278
+ const rawFullstack = await fs.readFile(fullstackPath, "utf-8");
279
+ const fullstack = normalizeTemplate(rawFullstack, FULLSTACK_NAME);
280
+ if (fullstack !== rawFullstack) await writeIfChanged(fullstackPath, fullstack);
281
+
282
+ for (const [name, spec] of Object.entries(PRIMARIES)) {
283
+ await writeIfChanged(path.join(agentsDir, `${name}.md`), buildPrimary(spec, fullstack, models[spec.tier]));
284
+ manifest.agents[name] = {
285
+ file: `${name}.md`,
286
+ mode: spec.mode ?? "primary",
287
+ model: models[spec.tier] ?? null,
288
+ };
289
+ }
290
+ await writeIfChanged(path.join(agentsDir, "planner-sub.md"), buildPlannerSub(fullstack, models.plan));
291
+ manifest.agents["planner-sub"] = {
292
+ file: "planner-sub.md",
293
+ mode: "subagent",
294
+ model: models.plan ?? null,
295
+ hidden: true,
296
+ };
297
+ } else {
298
+ warnings.push(`${FULLSTACK_TEMPLATE} missing: build, plan, planner and planner-sub not generated`);
299
+ }
300
+
301
+ const keepSet = new Set();
302
+ const variants = [];
303
+ for (const name of templates) {
304
+ const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8");
305
+ const content = normalizeTemplate(rawContent, name);
306
+ if (content !== rawContent) await writeIfChanged(path.join(agentsDir, `${name}.md`), content);
307
+ for (const tier of available) {
308
+ const file = `${name}.${tier}.md`;
309
+ variants.push({ file, path: path.join(agentsDir, file), content: buildVariant(content, models[tier]) });
310
+ keepSet.add(file);
311
+ manifest.agents[name] ??= {};
312
+ manifest.agents[name][tier] = { file, model: models[tier], hidden: true };
313
+ }
314
+ }
315
+
316
+ await Promise.all(variants.map((variant) => writeIfChanged(variant.path, variant.content)));
317
+ await Promise.all(
318
+ variantFiles.filter((file) => !keepSet.has(file)).map((file) => fs.unlink(path.join(agentsDir, file)).catch(() => {})),
319
+ );
320
+
321
+ if (templates.length === 0) warnings.push("no *-engineer.md templates found; no tier variants generated");
322
+
323
+ if (writeManifest) {
324
+ await fs.mkdir(path.join(root, ".opencode"), { recursive: true });
325
+ await writeIfChanged(path.join(root, ".opencode", MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
326
+ }
327
+
328
+ return {
329
+ models,
330
+ sources,
331
+ warnings,
332
+ manifest,
333
+ templates,
334
+ variants: variants.map((variant) => variant.file),
335
+ hasFullstack,
336
+ };
337
+ }