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.
- package/README.md +61 -69
- package/package.json +2 -8
- package/scripts/check-pack-readme.mjs +1 -16
- package/scripts/check-workflow-contracts.mjs +34 -44
- package/scripts/install-cli.mjs +273 -417
- package/src/AGENTS.md +59 -192
- package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
- package/src/{.agents/commands → commands}/assess.md +4 -4
- package/src/commands/install.md +43 -0
- package/src/{.agents/commands → commands}/metrics.md +1 -1
- package/src/commands/workflow-agents.md +101 -0
- package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
- package/src/{.agents/commands → commands}/workflow-plan.md +24 -33
- package/src/commands/workflow-work.md +836 -0
- package/src/{.agents/references → references}/README.md +1 -1
- package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
- package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
- package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
- package/src/skills/setup-agents/SKILL.md +250 -0
- package/src/skills/standards/SKILL.md +79 -0
- package/src/skills/standards/references/architecture.md +228 -0
- package/src/skills/standards/references/code-quality.md +192 -0
- package/src/skills/standards/references/presentation.md +515 -0
- package/src/skills/standards/references/services.md +172 -0
- package/src/skills/standards/references/state-management.md +936 -0
- package/.claude-plugin/plugin.json +0 -7
- package/.cursor-plugin/plugin.json +0 -20
- package/.cursor-plugin/registration.json +0 -5
- package/scripts/check-version-parity.mjs +0 -36
- package/scripts/generate-platform-artifacts.mjs +0 -230
- package/src/.agents/commands/install.md +0 -51
- package/src/.agents/commands/workflow-work.md +0 -690
- package/src/.agents/registry.json +0 -48
- package/src/.agents/scripts/self-check.mjs +0 -227
- package/src/.agents/scripts/sync-opencode.mjs +0 -362
- package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
- package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
- package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
- package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
- package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
- package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
- package/src/.agents/skills/standards/SKILL.md +0 -747
- package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
- package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
- package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
- package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
- package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
- package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
- package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
- package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
- package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
- package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
- package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
- package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
- package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
- package/src/generated/opencode.managed.json +0 -115
- /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
- /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
- /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
- /package/src/{.agents/commands → commands}/test-browser.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-brainstorm.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
- /package/src/{.agents/references → references}/standards/README.md +0 -0
- /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
- /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
- /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
- /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
|