compound-workflow 1.7.3 → 1.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 (92) hide show
  1. package/README.md +61 -69
  2. package/package.json +2 -8
  3. package/scripts/check-pack-readme.mjs +1 -16
  4. package/scripts/check-workflow-contracts.mjs +34 -44
  5. package/scripts/install-cli.mjs +273 -417
  6. package/src/AGENTS.md +59 -192
  7. package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
  8. package/src/{.agents/commands → commands}/assess.md +4 -4
  9. package/src/commands/install.md +43 -0
  10. package/src/{.agents/commands → commands}/metrics.md +1 -1
  11. package/src/commands/workflow-agents.md +101 -0
  12. package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
  13. package/src/{.agents/commands → commands}/workflow-plan.md +24 -33
  14. package/src/commands/workflow-work.md +836 -0
  15. package/src/{.agents/references → references}/README.md +1 -1
  16. package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
  17. package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
  18. package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
  19. package/src/skills/setup-agents/SKILL.md +250 -0
  20. package/src/skills/standards/SKILL.md +79 -0
  21. package/src/skills/standards/references/architecture.md +228 -0
  22. package/src/skills/standards/references/code-quality.md +192 -0
  23. package/src/skills/standards/references/presentation.md +515 -0
  24. package/src/skills/standards/references/services.md +172 -0
  25. package/src/skills/standards/references/state-management.md +936 -0
  26. package/.claude-plugin/plugin.json +0 -7
  27. package/.cursor-plugin/plugin.json +0 -20
  28. package/.cursor-plugin/registration.json +0 -5
  29. package/scripts/check-version-parity.mjs +0 -36
  30. package/scripts/generate-platform-artifacts.mjs +0 -230
  31. package/src/.agents/commands/install.md +0 -51
  32. package/src/.agents/commands/workflow-work.md +0 -690
  33. package/src/.agents/registry.json +0 -48
  34. package/src/.agents/scripts/self-check.mjs +0 -227
  35. package/src/.agents/scripts/sync-opencode.mjs +0 -362
  36. package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
  37. package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
  38. package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
  39. package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
  40. package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
  41. package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
  42. package/src/.agents/skills/standards/SKILL.md +0 -747
  43. package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
  44. package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
  45. package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
  46. package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
  47. package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
  48. package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
  49. package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
  50. package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
  51. package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
  52. package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
  53. package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
  54. package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
  55. package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
  56. package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
  57. package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
  58. package/src/generated/opencode.managed.json +0 -115
  59. /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
  60. /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
  61. /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
  62. /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
  63. /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
  64. /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
  65. /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
  66. /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
  67. /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
  68. /package/src/{.agents/commands → commands}/test-browser.md +0 -0
  69. /package/src/{.agents/commands → commands}/workflow-brainstorm.md +0 -0
  70. /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
  71. /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
  72. /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
  73. /package/src/{.agents/references → references}/standards/README.md +0 -0
  74. /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
  75. /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
  76. /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
  77. /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
  78. /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
  79. /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
  80. /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
  81. /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
  82. /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
  83. /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
  84. /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
  85. /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
  86. /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
  87. /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
  88. /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
  89. /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
  90. /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
  91. /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
  92. /package/src/{.agents/skills → skills}/technical-review/SKILL.md +0 -0
@@ -1,48 +0,0 @@
1
- {
2
- "$schema": "https://compound-workflow.dev/registry.schema.json",
3
- "roots": {
4
- "package": {
5
- "commands": "src/.agents/commands",
6
- "agents": "src/.agents/agents",
7
- "skills": "src/.agents/skills",
8
- "references": "src/.agents/references"
9
- },
10
- "consumer": {
11
- "commands": "node_modules/compound-workflow/src/.agents/commands",
12
- "agents": "node_modules/compound-workflow/src/.agents/agents",
13
- "skills": "node_modules/compound-workflow/src/.agents/skills",
14
- "references": "node_modules/compound-workflow/src/.agents/references"
15
- }
16
- },
17
- "assetTypes": {
18
- "command": {
19
- "dir": "commands",
20
- "glob": "**/*.md",
21
- "idFrom": ["invocation", "name"],
22
- "idFallback": "basename",
23
- "descriptionFrom": "description",
24
- "descriptionFallback": "id",
25
- "output": ["opencode.command", "plugin.commands"]
26
- },
27
- "agent": {
28
- "dir": "agents",
29
- "glob": "**/*.md",
30
- "idFrom": ["name"],
31
- "idFallback": "basename",
32
- "descriptionFrom": "description",
33
- "descriptionFallback": "id",
34
- "output": ["opencode.agent", "plugin.agents"]
35
- },
36
- "skill": {
37
- "dir": "skills",
38
- "glob": "*/SKILL.md",
39
- "idFrom": ["dirname"],
40
- "output": ["opencode.skillsPath", "plugin.skills"]
41
- },
42
- "reference": {
43
- "dir": "references",
44
- "glob": "**/*.md",
45
- "output": ["path"]
46
- }
47
- }
48
- }
@@ -1,227 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage(exitCode = 0) {
6
- const msg = `
7
- Usage:
8
- node .agents/scripts/self-check.mjs [--root <repoRoot>]
9
-
10
- Checks:
11
- - Every .agents/commands/**/*.md and .agents/agents/**/*.md is registered in opencode.json
12
- - Managed entries in opencode.json point to existing source files
13
- - Flags missing required frontmatter fields:
14
- - commands: description (all)
15
- - commands/workflow-*.md: invocation (recommended/expected)
16
- - agents: description (all)
17
- `;
18
- (exitCode === 0 ? console.log : console.error)(msg.trimStart());
19
- process.exit(exitCode);
20
- }
21
-
22
- function parseArgs(argv) {
23
- const out = { root: process.cwd() };
24
- for (let i = 2; i < argv.length; i++) {
25
- const a = argv[i];
26
- if (a === "--root") {
27
- const v = argv[i + 1];
28
- if (!v) usage(1);
29
- out.root = v;
30
- i++;
31
- } else if (a === "-h" || a === "--help") usage(0);
32
- else usage(1);
33
- }
34
- return out;
35
- }
36
-
37
- function realpathSafe(p) {
38
- try {
39
- return fs.realpathSync(p);
40
- } catch {
41
- return path.resolve(p);
42
- }
43
- }
44
-
45
- function walkFiles(dirAbs, predicate) {
46
- const out = [];
47
- const stack = [dirAbs];
48
- while (stack.length) {
49
- const cur = stack.pop();
50
- let entries;
51
- try {
52
- entries = fs.readdirSync(cur, { withFileTypes: true });
53
- } catch {
54
- continue;
55
- }
56
- for (const e of entries) {
57
- const p = path.join(cur, e.name);
58
- if (e.isDirectory()) stack.push(p);
59
- else if (e.isFile() && predicate(p)) out.push(p);
60
- }
61
- }
62
- out.sort();
63
- return out;
64
- }
65
-
66
- function stripJsonc(input) {
67
- let out = "";
68
- let i = 0;
69
- let inStr = false;
70
- let strQuote = "";
71
- let escape = false;
72
- while (i < input.length) {
73
- const c = input[i];
74
- const n = input[i + 1];
75
- if (inStr) {
76
- out += c;
77
- if (escape) escape = false;
78
- else if (c === "\\") escape = true;
79
- else if (c === strQuote) inStr = false;
80
- i++;
81
- continue;
82
- }
83
- if (c === '"' || c === "'") {
84
- inStr = true;
85
- strQuote = c;
86
- out += c;
87
- i++;
88
- continue;
89
- }
90
- if (c === "/" && n === "/") {
91
- while (i < input.length && input[i] !== "\n") i++;
92
- continue;
93
- }
94
- if (c === "/" && n === "*") {
95
- i += 2;
96
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
97
- i += 2;
98
- continue;
99
- }
100
- out += c;
101
- i++;
102
- }
103
- return out;
104
- }
105
-
106
- function parseFrontmatter(md) {
107
- if (!md.startsWith("---\n") && !md.startsWith("---\r\n")) return {};
108
- const end = md.indexOf("\n---", 4);
109
- if (end === -1) return {};
110
- const block = md.slice(4, end + 1);
111
- const out = {};
112
- for (const line of block.split(/\r?\n/)) {
113
- const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/);
114
- if (!m) continue;
115
- const k = m[1];
116
- let v = m[2] ?? "";
117
- v = v.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
118
- out[k] = v;
119
- }
120
- return out;
121
- }
122
-
123
- function discoverCommands(rootAbs) {
124
- const dir = path.join(rootAbs, ".agents", "commands");
125
- const files = walkFiles(dir, (p) => p.endsWith(".md"));
126
- const out = [];
127
- for (const fileAbs of files) {
128
- const rel = path.relative(rootAbs, fileAbs).replaceAll(path.sep, "/");
129
- const md = fs.readFileSync(fileAbs, "utf8");
130
- const fm = parseFrontmatter(md);
131
- const id = (fm.invocation || fm.name || path.basename(fileAbs, ".md")).trim();
132
- out.push({ fileAbs, rel, fm, id });
133
- }
134
- return out;
135
- }
136
-
137
- function discoverAgents(rootAbs) {
138
- const dir = path.join(rootAbs, ".agents", "agents");
139
- const files = walkFiles(dir, (p) => p.endsWith(".md"));
140
- const out = [];
141
- for (const fileAbs of files) {
142
- const rel = path.relative(rootAbs, fileAbs).replaceAll(path.sep, "/");
143
- const md = fs.readFileSync(fileAbs, "utf8");
144
- const fm = parseFrontmatter(md);
145
- const id = (fm.name || path.basename(fileAbs, ".md")).trim();
146
- out.push({ fileAbs, rel, fm, id });
147
- }
148
- return out;
149
- }
150
-
151
- function fileExists(rootAbs, rel) {
152
- return fs.existsSync(path.join(rootAbs, rel));
153
- }
154
-
155
- function main() {
156
- const args = parseArgs(process.argv);
157
- const rootAbs = realpathSafe(args.root);
158
- const opencodeAbs = path.join(rootAbs, "opencode.json");
159
-
160
- console.log(`Resolved root: ${rootAbs}`);
161
- console.log(`Checking opencode.json: ${opencodeAbs}`);
162
-
163
- if (!fs.existsSync(opencodeAbs)) {
164
- console.error("Error: missing opencode.json");
165
- process.exit(2);
166
- }
167
-
168
- const opencode = JSON.parse(stripJsonc(fs.readFileSync(opencodeAbs, "utf8")));
169
- const commandsReg = opencode.command ?? {};
170
- const agentsReg = opencode.agent ?? {};
171
-
172
- const errors = [];
173
- const warnings = [];
174
-
175
- const cmds = discoverCommands(rootAbs);
176
- const ags = discoverAgents(rootAbs);
177
-
178
- for (const c of cmds) {
179
- if (!c.id) errors.push(`Command missing id (name/invocation): ${c.rel}`);
180
- if (!c.fm.description) errors.push(`Command missing frontmatter description: ${c.rel} (${c.id || "?"})`);
181
- if (c.rel.includes("commands/workflow-") && !c.fm.invocation) {
182
- errors.push(`Workflow command missing frontmatter invocation: ${c.rel} (${c.id || "?"})`);
183
- }
184
- if (c.id && !commandsReg[c.id]) errors.push(`Command not registered in opencode.json: ${c.id} (source: ${c.rel})`);
185
- }
186
-
187
- for (const a of ags) {
188
- if (!a.id) errors.push(`Agent missing id (name): ${a.rel}`);
189
- if (!a.fm.description) errors.push(`Agent missing frontmatter description: ${a.rel} (${a.id || "?"})`);
190
- if (a.id && !agentsReg[a.id]) errors.push(`Agent not registered in opencode.json: ${a.id} (source: ${a.rel})`);
191
- }
192
-
193
- // Validate managed template/prompt pointers exist
194
- for (const [id, entry] of Object.entries(commandsReg)) {
195
- const tpl = entry?.template;
196
- if (typeof tpl !== "string") continue;
197
- if (!tpl.includes("@.agents/commands/")) continue;
198
- const m = tpl.match(/@(\.agents\/commands\/[^\n\r]+)/);
199
- const rel = m?.[1];
200
- if (rel && !fileExists(rootAbs, rel)) errors.push(`Managed command '${id}' points to missing file: ${rel}`);
201
- }
202
-
203
- for (const [id, entry] of Object.entries(agentsReg)) {
204
- const p = entry?.prompt;
205
- if (typeof p !== "string") continue;
206
- if (!p.includes("{file:.agents/agents/")) continue;
207
- const m = p.match(/\{file:(\.agents\/agents\/[^}]+)\}/);
208
- const rel = m?.[1];
209
- if (rel && !fileExists(rootAbs, rel)) errors.push(`Managed agent '${id}' points to missing file: ${rel}`);
210
- }
211
-
212
- if (warnings.length) {
213
- console.log("\nWarnings:");
214
- for (const w of warnings) console.log(`- ${w}`);
215
- }
216
-
217
- if (errors.length) {
218
- console.error("\nSelf-check failed:");
219
- for (const e of errors) console.error(`- ${e}`);
220
- process.exit(2);
221
- }
222
-
223
- console.log("Self-check passed.");
224
- }
225
-
226
- main();
227
-
@@ -1,362 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage(exitCode = 0) {
6
- const msg = `
7
- Usage:
8
- node .agents/scripts/sync-opencode.mjs [--root <repoRoot>] [--dry-run]
9
-
10
- What it does:
11
- - Discovers commands from .agents/commands/**/*.md
12
- - id: frontmatter 'invocation' (preferred) else 'name' else filename
13
- - Discovers agents from .agents/agents/**/*.md
14
- - id: frontmatter 'name' else filename
15
- - Creates/updates repo-root opencode.json with managed command/agent entries
16
- - Prunes managed entries whose source files no longer exist
17
-
18
- Output:
19
- - Always prints resolved root and absolute modified paths
20
- - Prints idempotency summary (0 changes needed vs updated N managed entries)
21
- `;
22
- (exitCode === 0 ? console.log : console.error)(msg.trimStart());
23
- process.exit(exitCode);
24
- }
25
-
26
- function parseArgs(argv) {
27
- const out = { root: process.cwd(), dryRun: false };
28
- for (let i = 2; i < argv.length; i++) {
29
- const a = argv[i];
30
- if (a === "--dry-run") out.dryRun = true;
31
- else if (a === "--root") {
32
- const v = argv[i + 1];
33
- if (!v) usage(1);
34
- out.root = v;
35
- i++;
36
- } else if (a === "-h" || a === "--help") usage(0);
37
- else usage(1);
38
- }
39
- return out;
40
- }
41
-
42
- function realpathSafe(p) {
43
- try {
44
- return fs.realpathSync(p);
45
- } catch {
46
- return path.resolve(p);
47
- }
48
- }
49
-
50
- function walkFiles(dirAbs, predicate) {
51
- const out = [];
52
- const stack = [dirAbs];
53
- while (stack.length) {
54
- const cur = stack.pop();
55
- let entries;
56
- try {
57
- entries = fs.readdirSync(cur, { withFileTypes: true });
58
- } catch {
59
- continue;
60
- }
61
- for (const e of entries) {
62
- const p = path.join(cur, e.name);
63
- if (e.isDirectory()) stack.push(p);
64
- else if (e.isFile() && predicate(p)) out.push(p);
65
- }
66
- }
67
- out.sort();
68
- return out;
69
- }
70
-
71
- function stripJsonc(input) {
72
- // Removes // and /* */ comments while preserving strings.
73
- let out = "";
74
- let i = 0;
75
- let inStr = false;
76
- let strQuote = "";
77
- let escape = false;
78
- while (i < input.length) {
79
- const c = input[i];
80
- const n = input[i + 1];
81
- if (inStr) {
82
- out += c;
83
- if (escape) escape = false;
84
- else if (c === "\\") escape = true;
85
- else if (c === strQuote) inStr = false;
86
- i++;
87
- continue;
88
- }
89
-
90
- if (c === '"' || c === "'") {
91
- inStr = true;
92
- strQuote = c;
93
- out += c;
94
- i++;
95
- continue;
96
- }
97
-
98
- if (c === "/" && n === "/") {
99
- while (i < input.length && input[i] !== "\n") i++;
100
- continue;
101
- }
102
- if (c === "/" && n === "*") {
103
- i += 2;
104
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
105
- i += 2;
106
- continue;
107
- }
108
-
109
- out += c;
110
- i++;
111
- }
112
- return out;
113
- }
114
-
115
- function readJsonMaybeJsonc(fileAbs) {
116
- if (!fs.existsSync(fileAbs)) return null;
117
- const raw = fs.readFileSync(fileAbs, "utf8");
118
- const stripped = stripJsonc(raw);
119
- return JSON.parse(stripped);
120
- }
121
-
122
- function parseFrontmatter(md) {
123
- // Only supports simple YAML key: value lines (enough for name/description/invocation).
124
- if (!md.startsWith("---\n") && !md.startsWith("---\r\n")) return {};
125
- const end = md.indexOf("\n---", 4);
126
- if (end === -1) return {};
127
- const block = md.slice(4, end + 1); // include trailing newline for simpler parsing
128
- const out = {};
129
- for (const line of block.split(/\r?\n/)) {
130
- const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/);
131
- if (!m) continue;
132
- const k = m[1];
133
- let v = m[2] ?? "";
134
- v = v.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
135
- out[k] = v;
136
- }
137
- return out;
138
- }
139
-
140
- function isUnder(parentAbs, childAbs) {
141
- const rel = path.relative(parentAbs, childAbs);
142
- return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
143
- }
144
-
145
- function discoverCommands(rootAbs) {
146
- const commandsDir = path.join(rootAbs, ".agents", "commands");
147
- const files = walkFiles(commandsDir, (p) => p.endsWith(".md"));
148
- const map = new Map();
149
- const warnings = [];
150
- for (const fileAbs of files) {
151
- const rel = path.relative(rootAbs, fileAbs).replaceAll(path.sep, "/");
152
- const md = fs.readFileSync(fileAbs, "utf8");
153
- const fm = parseFrontmatter(md);
154
- const id = (fm.invocation || fm.name || path.basename(fileAbs, ".md")).trim();
155
- const description = (fm.description || id).trim();
156
- if (!id) {
157
- warnings.push(`Command file missing id (name/invocation): ${rel}`);
158
- continue;
159
- }
160
- if (!fm.description) warnings.push(`Command missing frontmatter description: ${rel} (${id})`);
161
-
162
- // Prefer first occurrence to avoid silent overwrites.
163
- if (map.has(id)) {
164
- warnings.push(`Duplicate command id '${id}': ${rel} (already have ${map.get(id).rel})`);
165
- continue;
166
- }
167
- map.set(id, { id, rel, fileAbs, description });
168
- }
169
- return { map, warnings };
170
- }
171
-
172
- function discoverAgents(rootAbs) {
173
- const agentsDir = path.join(rootAbs, ".agents", "agents");
174
- const files = walkFiles(agentsDir, (p) => p.endsWith(".md"));
175
- const map = new Map();
176
- const warnings = [];
177
- for (const fileAbs of files) {
178
- const rel = path.relative(rootAbs, fileAbs).replaceAll(path.sep, "/");
179
- const md = fs.readFileSync(fileAbs, "utf8");
180
- const fm = parseFrontmatter(md);
181
- const id = (fm.name || path.basename(fileAbs, ".md")).trim();
182
- const description = (fm.description || id).trim();
183
- if (!id) {
184
- warnings.push(`Agent file missing id (name): ${rel}`);
185
- continue;
186
- }
187
- if (!fm.description) warnings.push(`Agent missing frontmatter description: ${rel} (${id})`);
188
- if (map.has(id)) {
189
- warnings.push(`Duplicate agent id '${id}': ${rel} (already have ${map.get(id).rel})`);
190
- continue;
191
- }
192
- map.set(id, { id, rel, fileAbs, description });
193
- }
194
- return { map, warnings };
195
- }
196
-
197
- function ensureObject(v) {
198
- return v && typeof v === "object" && !Array.isArray(v) ? v : {};
199
- }
200
-
201
- function stableStringify(obj) {
202
- return JSON.stringify(obj, null, 2) + "\n";
203
- }
204
-
205
- function buildManagedCommandTemplate(cmdRel) {
206
- // Keep simple + consistent with existing docs.
207
- return `@AGENTS.md\n@${cmdRel}\nArguments: $ARGUMENTS\n`;
208
- }
209
-
210
- function buildManagedAgentPrompt(agentRel) {
211
- return `{file:${agentRel}}`;
212
- }
213
-
214
- function isManagedCommand(entry) {
215
- const tpl = entry?.template;
216
- return typeof tpl === "string" && tpl.includes("@.agents/commands/");
217
- }
218
-
219
- function isManagedAgent(entry) {
220
- const p = entry?.prompt;
221
- return typeof p === "string" && p.includes("{file:.agents/agents/");
222
- }
223
-
224
- function fileExists(rootAbs, rel) {
225
- const abs = path.join(rootAbs, rel);
226
- return fs.existsSync(abs);
227
- }
228
-
229
- function main() {
230
- const args = parseArgs(process.argv);
231
- const rootAbs = realpathSafe(args.root);
232
- const agentsDir = path.join(rootAbs, ".agents");
233
- const commandsDir = path.join(agentsDir, "commands");
234
- const agentsMdAbs = path.join(rootAbs, "AGENTS.md");
235
- const opencodeAbs = path.join(rootAbs, "opencode.json");
236
-
237
- if (!fs.existsSync(commandsDir)) {
238
- console.error(`Error: missing .agents/commands in ${rootAbs}`);
239
- process.exit(2);
240
- }
241
- if (!fs.existsSync(path.join(agentsDir, "agents"))) {
242
- console.error(`Error: missing .agents/agents in ${rootAbs}`);
243
- process.exit(2);
244
- }
245
-
246
- const { map: commands, warnings: cmdWarn } = discoverCommands(rootAbs);
247
- const { map: agents, warnings: agentWarn } = discoverAgents(rootAbs);
248
- const warnings = [...cmdWarn, ...agentWarn];
249
-
250
- const existing = readJsonMaybeJsonc(opencodeAbs) ?? {};
251
- const next = structuredClone(existing);
252
-
253
- const SKILLS_COMPOUND_PATH = ".agents/compound-workflow-skills";
254
- next.$schema = next.$schema || "https://opencode.ai/config.json";
255
- next.skills = ensureObject(next.skills);
256
- next.skills.paths = Array.isArray(next.skills.paths) ? next.skills.paths : [".agents/skills"];
257
- const hasCompoundWorkflow =
258
- fs.existsSync(path.join(rootAbs, "node_modules", "compound-workflow")) ||
259
- fs.existsSync(path.join(rootAbs, SKILLS_COMPOUND_PATH));
260
- if (hasCompoundWorkflow && !next.skills.paths.includes(SKILLS_COMPOUND_PATH)) {
261
- next.skills.paths.unshift(SKILLS_COMPOUND_PATH);
262
- }
263
- next.command = ensureObject(next.command);
264
- next.agent = ensureObject(next.agent);
265
-
266
- let created = 0;
267
- let updated = 0;
268
- let pruned = 0;
269
-
270
- // Upsert commands
271
- for (const [id, cmd] of commands.entries()) {
272
- const entry = ensureObject(next.command[id]);
273
- const desired = {
274
- ...entry,
275
- description: cmd.description,
276
- agent: "build",
277
- template: buildManagedCommandTemplate(cmd.rel),
278
- };
279
- const before = JSON.stringify(next.command[id] ?? null);
280
- const after = JSON.stringify(desired);
281
- if (!next.command[id]) created++;
282
- else if (before !== after) updated++;
283
- next.command[id] = desired;
284
- }
285
-
286
- // Upsert agents
287
- for (const [id, ag] of agents.entries()) {
288
- const entry = ensureObject(next.agent[id]);
289
- const desired = {
290
- ...entry,
291
- description: ag.description,
292
- mode: "subagent",
293
- prompt: buildManagedAgentPrompt(ag.rel),
294
- permission: {
295
- ...(ensureObject(entry.permission)),
296
- edit: ensureObject(entry.permission).edit ?? "deny",
297
- },
298
- };
299
- const before = JSON.stringify(next.agent[id] ?? null);
300
- const after = JSON.stringify(desired);
301
- if (!next.agent[id]) created++;
302
- else if (before !== after) updated++;
303
- next.agent[id] = desired;
304
- }
305
-
306
- // Prune stale managed commands
307
- for (const [id, entry] of Object.entries(next.command)) {
308
- if (!isManagedCommand(entry)) continue;
309
- const tpl = entry.template;
310
- const m = tpl.match(/@(\.agents\/commands\/[^\n\r]+)/);
311
- const rel = m?.[1];
312
- if (!rel) continue;
313
- if (!fileExists(rootAbs, rel)) {
314
- delete next.command[id];
315
- pruned++;
316
- }
317
- }
318
-
319
- // Prune stale managed agents
320
- for (const [id, entry] of Object.entries(next.agent)) {
321
- if (!isManagedAgent(entry)) continue;
322
- const p = entry.prompt;
323
- const m = p.match(/\{file:(\.agents\/agents\/[^}]+)\}/);
324
- const rel = m?.[1];
325
- if (!rel) continue;
326
- if (!fileExists(rootAbs, rel)) {
327
- delete next.agent[id];
328
- pruned++;
329
- }
330
- }
331
-
332
- const afterText = stableStringify(next);
333
- // Semantic idempotency (ignore formatting / key order differences in the source file).
334
- const semanticChanged = JSON.stringify(existing) !== JSON.stringify(next);
335
-
336
- console.log(`Resolved root: ${rootAbs}`);
337
- console.log(`Target opencode.json: ${opencodeAbs}`);
338
- if (!fs.existsSync(agentsMdAbs)) console.log(`Note: missing AGENTS.md at ${agentsMdAbs} (commands may still run; templates reference it).`);
339
-
340
- if (warnings.length) {
341
- console.log("\nWarnings:");
342
- for (const w of warnings) console.log(`- ${w}`);
343
- }
344
-
345
- if (!semanticChanged) {
346
- console.log("\nIdempotency: 0 changes needed.");
347
- process.exit(0);
348
- }
349
-
350
- console.log(`\nPlanned managed changes: created=${created}, updated=${updated}, pruned=${pruned}`);
351
- if (args.dryRun) {
352
- console.log("Dry-run: no changes made.");
353
- process.exit(0);
354
- }
355
-
356
- fs.writeFileSync(opencodeAbs, afterText, "utf8");
357
- console.log(`Wrote: ${opencodeAbs}`);
358
- console.log(`Idempotency: updated ${created + updated + pruned} managed entries.`);
359
- }
360
-
361
- main();
362
-
@@ -1,72 +0,0 @@
1
- ---
2
- name: presentation-composability
3
- description: Enforce folder-per-component structure for tapesv3 presentation modules. Use when creating or refactoring components under src/features/tapesv3/presentation/.
4
- ---
5
-
6
- # Presentation Composability
7
-
8
- Enforce folder-per-component structure for tapesv3 presentation modules.
9
-
10
- ## When to Use
11
-
12
- - Adding new presentation components to tapesv3
13
- - Refactoring existing tapesv3 presentation into composable pieces
14
- - Aligning new UI with established tapesv3 patterns
15
-
16
- ## Structure Rules
17
-
18
- ### One folder per composable
19
-
20
- Each logical UI element gets its own subfolder (PascalCase):
21
-
22
- ```
23
- presentation/FeatureName/
24
- ├── index.ts # barrel
25
- ├── SubComponentA/
26
- │ ├── index.tsx
27
- │ └── styles.ts
28
- ├── SubComponentB/
29
- │ ├── index.tsx
30
- │ └── styles.ts
31
- └── SubComponentC/
32
- └── index.tsx # styles.ts optional if no styled components
33
- ```
34
-
35
- ### Barrel (`index.ts`)
36
-
37
- Re-export from subfolders. Two patterns:
38
-
39
- **Flat exports** (simple feature, independent components):
40
-
41
- ```typescript
42
- export { SubComponentA } from "./SubComponentA";
43
- export { SubComponentB } from "./SubComponentB";
44
- ```
45
-
46
- **Compound component** (Root + subcomponents used together):
47
-
48
- ```typescript
49
- export const FeatureName = Object.assign(RootComponent, {
50
- SubA: SubComponentA,
51
- SubB: SubComponentB,
52
- });
53
- ```
54
-
55
- ### Component + styles
56
-
57
- - `index.tsx`: component logic and JSX
58
- - `styles.ts`: styled-components (or emotion) only
59
- - Import: `import * as S from "./styles"` or `import { Root, Item } from "./styles"`
60
-
61
- ### Reference implementations
62
-
63
- - `SharedTabsHeader` – compound pattern (Root.Action, Root.BackgroundPicker)
64
- - `PanelClipsArtist`, `FolderSelector` – compound pattern
65
- - `MultiSourceThreads` – flat exports (FilterBar, Message, Input, List)
66
- - `TapesTabBarTabOptions` – flat exports
67
-
68
- ## Anti-patterns
69
-
70
- - Single file with multiple components and inline styles
71
- - `ComponentName.styles.ts` – use `styles.ts` in the component folder
72
- - Barrel re-exporting from sibling files instead of subfolders