@mayurdluffy/ccwf 0.0.0-stage → 0.1.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/bin/ccwf.js +7 -0
  4. package/lib/index.js +312 -0
  5. package/lib/installer.js +127 -0
  6. package/lib/machine.js +167 -0
  7. package/lib/merge.js +92 -0
  8. package/lib/modules.js +199 -0
  9. package/package.json +45 -4
  10. package/templates/agent-memory/MEMORY.md +4 -0
  11. package/templates/blocks/browser.md +9 -0
  12. package/templates/blocks/cbm.md +9 -0
  13. package/templates/blocks/core.md +35 -0
  14. package/templates/blocks/memory.md +7 -0
  15. package/templates/blocks/tracking.md +11 -0
  16. package/templates/blocks/workers.md +3 -0
  17. package/templates/cursor-cli.json +17 -0
  18. package/templates/cursor-rule.mdc +6 -0
  19. package/templates/docs/BACKLOG.md +4 -0
  20. package/templates/docs/DECISIONS.md +4 -0
  21. package/templates/docs/FEATURES.md +8 -0
  22. package/templates/docs/GLOSSARY.md +3 -0
  23. package/templates/docs/adr/.gitkeep +0 -0
  24. package/templates/docs/agents/domain.md +52 -0
  25. package/templates/docs/agents/issue-tracker.md +30 -0
  26. package/templates/docs/agents/setup.md +48 -0
  27. package/templates/docs/agents/triage-labels.md +15 -0
  28. package/templates/docs/agents/workflow.md +58 -0
  29. package/templates/machine.json +47 -0
  30. package/templates/skills/agent-browser/LICENSE +202 -0
  31. package/templates/skills/agent-browser/SKILL.md +51 -0
  32. package/templates/skills/commit/SKILL.md +77 -0
  33. package/templates/skills/orchestrate-workers/HC.md +10 -0
  34. package/templates/skills/orchestrate-workers/SKILL.md +138 -0
  35. package/templates/skills/orchestrate-workers/approve.sh +7 -0
  36. package/templates/skills/orchestrate-workers/claude-worker-settings.json +14 -0
  37. package/templates/skills/orchestrate-workers/end.sh +13 -0
  38. package/templates/skills/orchestrate-workers/grid.sh +24 -0
  39. package/templates/skills/orchestrate-workers/pane.sh +51 -0
  40. package/templates/skills/orchestrate-workers/watch.sh +42 -0
  41. package/templates/skills/orchestrate-workers/workers.conf +8 -0
  42. package/templates/skills/verify-ui/PLAYBOOK.md +81 -0
  43. package/templates/skills/verify-ui/SKILL.md +88 -0
  44. package/templates/skills/verify-ui/lib.sh +63 -0
  45. package/templates/skills/verify-ui/probe.sh +78 -0
  46. package/templates/skills/verify-ui/run.sh +113 -0
package/lib/machine.js ADDED
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ // Machine setup: things installed once per computer. plan() only inspects; run() installs.
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+ const MACHINE = require("../templates/machine.json");
8
+
9
+ const SKILLS_AGENT_ID = { claude: "claude-code", codex: "codex", cursor: "cursor" };
10
+ const SKILL_DIR = { claude: ".claude/skills", codex: ".agents/skills", cursor: ".cursor/skills" };
11
+ const AGENT_CLI = {
12
+ claude: { bin: "claude", cmd: "curl -fsSL https://claude.ai/install.sh | bash", login: "claude" },
13
+ codex: { bin: "codex", cmd: "curl -fsSL https://chatgpt.com/codex/install.sh | sh", login: "codex login" },
14
+ cursor: { bin: "cursor-agent", cmd: "curl https://cursor.com/install -fsS | bash", login: "cursor-agent login" },
15
+ };
16
+
17
+ function realEnv() {
18
+ return {
19
+ platform: process.platform,
20
+ home: os.homedir(),
21
+ have: (bin) => spawnSync("which", [bin], { stdio: "ignore" }).status === 0,
22
+ exists: (p) => fs.existsSync(p),
23
+ read: (p) => {
24
+ try {
25
+ return fs.readFileSync(p, "utf8");
26
+ } catch {
27
+ return null;
28
+ }
29
+ },
30
+ };
31
+ }
32
+
33
+ function tomlQuoted(value) {
34
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
35
+ }
36
+
37
+ function projectHeader(repoRoot) {
38
+ return `[projects.${tomlQuoted(repoRoot)}]`;
39
+ }
40
+
41
+ function repoTrusted(text, repoRoot) {
42
+ const header = projectHeader(repoRoot);
43
+ let inTable = false;
44
+ for (const line of String(text).split(/\r?\n/)) {
45
+ const trimmed = line.trim();
46
+ if (trimmed.startsWith("[")) {
47
+ inTable = trimmed === header;
48
+ continue;
49
+ }
50
+ if (inTable && trimmed === 'trust_level = "trusted"') return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function skillVisible(exists, home, agent, name) {
56
+ return exists(path.join(home, SKILL_DIR[agent], name));
57
+ }
58
+
59
+ // items: { id, label, status: "ok" | "missing", kind: "run" | "edit" | "manual", cmd?, hint?, apply?, detect? }
60
+ function plan({ agents, modules, repoRoot, env = realEnv() }) {
61
+ const { platform, home, have, exists, read } = env;
62
+ if (platform !== "darwin" && platform !== "linux") {
63
+ throw new Error(`machine setup supports macOS and Linux only (this is ${platform})`);
64
+ }
65
+ const items = [];
66
+ const item = (id, label, ok, rest) => items.push({ id, label, status: ok ? "ok" : "missing", ...rest });
67
+
68
+ // User-level skills (pinned lists): installed once for all selected agents.
69
+ const skillAgents = agents.map((a) => SKILLS_AGENT_ID[a]).join(" ");
70
+ for (const [source, names] of Object.entries(MACHINE.skills)) {
71
+ const unseen = () => names.filter((n) => agents.some((a) => !skillVisible(exists, home, a, n)));
72
+ const missing = unseen();
73
+ item(`skills:${source}`, `${source} skills (${names.length - missing.length}/${names.length} installed)`, missing.length === 0, {
74
+ kind: "run",
75
+ summary: `install ${missing.length} skill${missing.length === 1 ? "" : "s"} from ${source} with \`npx skills add\` (full command: ccwf init --machine-only --dry-run)`,
76
+ cmd: `npx -y skills add ${source} -g -a ${skillAgents} -s ${missing.join(" ")} -y`,
77
+ detect: () => unseen().length === 0,
78
+ });
79
+ }
80
+
81
+ const brewOr = (pkg, hint) => (platform === "darwin" && have("brew") ? { kind: "run", cmd: `brew install ${pkg}` } : { kind: "manual", hint });
82
+ item("gh", "gh (GitHub CLI)", have("gh"), {
83
+ ...brewOr("gh", "install gh with your package manager: https://cli.github.com"),
84
+ detect: () => have("gh"),
85
+ });
86
+
87
+ if (modules.includes("browser")) {
88
+ item("agent-browser", "agent-browser", have("agent-browser"), {
89
+ kind: "run",
90
+ cmd: `npm i -g agent-browser && agent-browser install${platform === "linux" ? " --with-deps" : ""}`,
91
+ detect: () => have("agent-browser"),
92
+ });
93
+ item("ffmpeg", "ffmpeg (bug recordings)", have("ffmpeg"), {
94
+ ...brewOr("ffmpeg", "install ffmpeg with your package manager"),
95
+ detect: () => have("ffmpeg"),
96
+ });
97
+ const chrome =
98
+ platform === "darwin"
99
+ ? exists("/Applications/Google Chrome.app")
100
+ : have("google-chrome") || have("google-chrome-stable") || have("chromium");
101
+ item("chrome", "Google Chrome (chrome-devtools MCP)", chrome, { kind: "manual", hint: "install Google Chrome: https://google.com/chrome" });
102
+ }
103
+
104
+ if (modules.includes("workers")) {
105
+ item("herdr", "herdr + hc (worker panes)", have("hc"), {
106
+ kind: "run",
107
+ cmd: "curl -fsSL https://releases.herdr.dev/app/install.sh | sh",
108
+ detect: () => have("hc"),
109
+ });
110
+ }
111
+
112
+ if (modules.includes("browser") || modules.includes("workers")) {
113
+ item("zsh", "zsh", have("zsh"), { kind: "manual", hint: "install zsh with your package manager" });
114
+ }
115
+ if (modules.includes("workers")) {
116
+ item("python3", "python3", have("python3"), { kind: "manual", hint: "install python3 with your package manager" });
117
+ }
118
+
119
+ for (const a of agents) {
120
+ const c = AGENT_CLI[a];
121
+ item(`cli:${a}`, `${c.bin} CLI`, have(c.bin), { kind: "run", cmd: c.cmd, detect: () => have(c.bin) });
122
+ }
123
+
124
+ if (agents.includes("codex") && repoRoot) {
125
+ const file = path.join(home, ".codex", "config.toml");
126
+ const header = projectHeader(repoRoot);
127
+ item("codex-trust", "Codex trusts this repo", repoTrusted(read(file) ?? "", repoRoot), {
128
+ kind: "edit",
129
+ hint: `add ${header} trust_level = "trusted" to ~/.codex/config.toml`,
130
+ apply: () => {
131
+ fs.mkdirSync(path.dirname(file), { recursive: true });
132
+ let cur = "";
133
+ if (fs.existsSync(file)) {
134
+ cur = fs.readFileSync(file, "utf8");
135
+ fs.copyFileSync(file, `${file}.bak`);
136
+ }
137
+ fs.writeFileSync(file, `${cur}${cur === "" || cur.endsWith("\n") ? "" : "\n"}\n${header}\ntrust_level = "trusted"\n`);
138
+ },
139
+ });
140
+ }
141
+ return items;
142
+ }
143
+
144
+ function loginHints(agents) {
145
+ return [...agents.map((a) => AGENT_CLI[a].login), "gh auth login"];
146
+ }
147
+
148
+ // Runs the given missing items; returns [{ id, ok, error? }]. `exec` is injectable for tests.
149
+ function run(items, exec = (cmd) => spawnSync("sh", ["-c", cmd], { stdio: "inherit" }).status === 0) {
150
+ const results = [];
151
+ for (const it of items) {
152
+ if (it.kind === "run") {
153
+ const ran = Boolean(exec(it.cmd));
154
+ results.push({ id: it.id, ok: ran && (it.detect ? it.detect() : true) });
155
+ } else if (it.kind === "edit") {
156
+ try {
157
+ it.apply();
158
+ results.push({ id: it.id, ok: true });
159
+ } catch (err) {
160
+ results.push({ id: it.id, ok: false, error: err.message });
161
+ }
162
+ }
163
+ }
164
+ return results;
165
+ }
166
+
167
+ module.exports = { plan, run, loginHints, realEnv };
package/lib/merge.js ADDED
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ // Pure text merges. Each returns the new text; calling twice with the same input is a no-op.
3
+
4
+ const STYLES = {
5
+ md: (id, edge) => `<!-- ccwf:${edge} ${id} -->`,
6
+ hash: (id, edge) => `# ccwf:${edge} ${id}`,
7
+ };
8
+
9
+ function escapeRe(s) {
10
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ }
12
+
13
+ // "end" or "begin" when that marker is missing and the other is present; null when the pair is usable.
14
+ function missingMarker(text, id, style = "md") {
15
+ const hasBegin = text.includes(STYLES[style](id, "begin"));
16
+ const hasEnd = text.includes(STYLES[style](id, "end"));
17
+ if (hasBegin && !hasEnd) return "end";
18
+ if (hasEnd && !hasBegin) return "begin";
19
+ return null;
20
+ }
21
+
22
+ // Insert or replace the block `id` between begin/end markers; text outside is untouched.
23
+ // A block with only one marker is left unchanged so text after the marker is not swallowed.
24
+ function upsertBlock(text, id, body, style = "md") {
25
+ if (missingMarker(text, id, style)) return text;
26
+ const begin = STYLES[style](id, "begin");
27
+ const end = STYLES[style](id, "end");
28
+ const block = `${begin}\n${body.trimEnd()}\n${end}`;
29
+ const re = new RegExp(`${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}`);
30
+ if (re.test(text)) return text.replace(re, () => block);
31
+ const sep = text === "" ? "" : text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n";
32
+ return `${text}${sep}${block}\n`;
33
+ }
34
+
35
+ function removeBlock(text, id, style = "md") {
36
+ const begin = STYLES[style](id, "begin");
37
+ const end = STYLES[style](id, "end");
38
+ return text.replace(new RegExp(`\\n?${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}\\n?`), "\n");
39
+ }
40
+
41
+ // True when a `[table]` header exists outside the managed block `id` (the user owns it).
42
+ function hasTableOutsideBlock(text, id, table) {
43
+ const outside = removeBlock(text, id, "hash");
44
+ return new RegExp(`^\\s*\\[${escapeRe(table)}\\]`, "m").test(outside);
45
+ }
46
+
47
+ function isPlainObject(v) {
48
+ return v !== null && typeof v === "object" && !Array.isArray(v);
49
+ }
50
+
51
+ function deepMerge(target, patch) {
52
+ for (const [k, v] of Object.entries(patch)) {
53
+ if (isPlainObject(v) && isPlainObject(target[k])) deepMerge(target[k], v);
54
+ else target[k] = v;
55
+ }
56
+ return target;
57
+ }
58
+
59
+ function getPath(obj, dotted) {
60
+ return dotted.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
61
+ }
62
+
63
+ function setPath(obj, dotted, value) {
64
+ const keys = dotted.split(".");
65
+ const last = keys.pop();
66
+ const box = keys.reduce((o, k) => (isPlainObject(o[k]) ? o[k] : (o[k] = {})), obj);
67
+ box[last] = value;
68
+ }
69
+
70
+ // Merge `patch` into JSON text, keeping every other key. Throws on invalid JSON (never overwrite).
71
+ // `union`: dotted paths whose arrays are merged as a set (existing entries first) instead of replaced.
72
+ function mergeJson(text, patch, label = "JSON file", union = []) {
73
+ let data = {};
74
+ if (text.trim() !== "") {
75
+ try {
76
+ data = JSON.parse(text);
77
+ } catch (err) {
78
+ throw new Error(`${label} is not valid JSON (${err.message}); fix it, then re-run`);
79
+ }
80
+ if (!isPlainObject(data)) throw new Error(`${label} must contain a JSON object`);
81
+ }
82
+ const p = JSON.parse(JSON.stringify(patch));
83
+ for (const path of union) {
84
+ const add = getPath(p, path);
85
+ if (!Array.isArray(add)) continue;
86
+ const have = getPath(data, path);
87
+ setPath(p, path, [...new Set([...(Array.isArray(have) ? have : []), ...add])]);
88
+ }
89
+ return JSON.stringify(deepMerge(data, p), null, 2) + "\n";
90
+ }
91
+
92
+ module.exports = { upsertBlock, removeBlock, hasTableOutsideBlock, mergeJson, missingMarker };
package/lib/modules.js ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ const TEMPLATES = path.join(__dirname, "..", "templates");
6
+ const AGENTS = ["claude", "codex", "cursor"];
7
+ const CHROME_ARGS = ["-y", "chrome-devtools-mcp@latest", "--isolated", "--no-usage-statistics", "--no-performance-crux"];
8
+
9
+ function render(text, params) {
10
+ return text.replace(/\{\{(\w+)\}\}/g, (m, k) => (k in params.vars ? params.vars[k] : m));
11
+ }
12
+
13
+ function walk(dir, base = dir) {
14
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
15
+ const p = path.join(dir, e.name);
16
+ return e.isDirectory() ? walk(p, base) : [path.relative(base, p).split(path.sep).join("/")];
17
+ });
18
+ }
19
+
20
+ function blockBody(name, params) {
21
+ return render(fs.readFileSync(path.join(TEMPLATES, "blocks", `${name}.md`), "utf8"), params);
22
+ }
23
+
24
+ // Rendered files of one skill: [{ rel, content, mode, seed }]
25
+ function skillFiles(name, params) {
26
+ const dir = path.join(TEMPLATES, "skills", name);
27
+ return walk(dir).map((rel) => {
28
+ const st = fs.statSync(path.join(dir, rel));
29
+ return {
30
+ rel,
31
+ content: render(fs.readFileSync(path.join(dir, rel), "utf8"), params),
32
+ mode: st.mode & 0o111 ? 0o755 : undefined,
33
+ seed: rel === "workers.conf",
34
+ };
35
+ });
36
+ }
37
+
38
+ function installSkill(ctx, name) {
39
+ const { ins, params, agents } = ctx;
40
+ for (const f of skillFiles(name, params)) {
41
+ const rel = `.agents/skills/${name}/${f.rel}`;
42
+ if (f.seed) ins.seed(rel, f.content, f.mode);
43
+ else ins.owned(rel, f.content, f.mode);
44
+ }
45
+ if (agents.includes("claude")) {
46
+ ins.link(`.claude/skills/${name}`, `../../.agents/skills/${name}`);
47
+ }
48
+ }
49
+
50
+ const tpl = (...parts) => fs.readFileSync(path.join(TEMPLATES, ...parts), "utf8");
51
+ const CONF = ".agents/skills/verify-ui/verify-ui.conf";
52
+
53
+ // CLAUDE.md is skipped when it is AGENTS.md (a symlink) or the import is already there.
54
+ function skipClaudeImport(ins) {
55
+ try {
56
+ if (fs.realpathSync(ins.abs("CLAUDE.md")) === fs.realpathSync(ins.abs("AGENTS.md"))) return true;
57
+ } catch {}
58
+ const cur = ins.read("CLAUDE.md");
59
+ return cur !== null && cur.includes("@AGENTS.md");
60
+ }
61
+
62
+ // Same MCP entry for Claude (.mcp.json) and Cursor (.cursor/mcp.json); Codex gets a TOML block.
63
+ function mcp(ctx, id, command, args, tomlBody) {
64
+ const { ins, agents } = ctx;
65
+ const entry = { mcpServers: { [id]: { command, args } } };
66
+ if (agents.includes("claude")) ins.json(".mcp.json", entry);
67
+ if (agents.includes("cursor")) ins.json(".cursor/mcp.json", entry);
68
+ if (agents.includes("codex")) ins.tomlBlock(".codex/config.toml", `mcp-${id}`, tomlBody, [`mcp_servers.${id}`]);
69
+ }
70
+
71
+ function unquote(value) {
72
+ if (value.length >= 2) {
73
+ const q = value[0];
74
+ if ((q === '"' || q === "'") && value[value.length - 1] === q) return value.slice(1, -1);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ // KEY=value lines. Spaces around = are optional, matching quotes are not part of the value,
80
+ // and the last line may lack a newline. Same rules as templates/skills/verify-ui/lib.sh.
81
+ function parseConf(text) {
82
+ const out = {};
83
+ for (const raw of (text || "").split("\n")) {
84
+ const line = raw.trim();
85
+ const eq = line.indexOf("=");
86
+ if (eq === -1) continue;
87
+ const key = line.slice(0, eq).trim();
88
+ if (!/^[A-Z_]+$/.test(key)) continue;
89
+ out[key] = unquote(line.slice(eq + 1).trim());
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // A site list is comma-separated domains: letters, digits, hyphens and dots.
95
+ function validSiteList(value) {
96
+ const label = "[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?";
97
+ return new RegExp(`^(?:${label})(?:\\.(?:${label}))*(?:,(?:${label})(?:\\.(?:${label}))*)*$`).test(value);
98
+ }
99
+
100
+ const MODULES = {
101
+ core: {
102
+ desc: "engineering discipline rules, commit skill, workflow + setup guides",
103
+ run(ctx) {
104
+ const { ins, agents, params } = ctx;
105
+ ins.block("AGENTS.md", "core", blockBody("core", params));
106
+ if (agents.includes("claude")) {
107
+ if (skipClaudeImport(ins)) ins.note("CLAUDE.md", "unchanged");
108
+ else ins.block("CLAUDE.md", "agents", "@AGENTS.md");
109
+ }
110
+ if (agents.includes("cursor")) ins.owned(".cursor/rules/ccwf.mdc", tpl("cursor-rule.mdc"));
111
+ installSkill(ctx, "commit");
112
+ for (const f of ["workflow", "setup"]) ins.owned(`docs/agents/${f}.md`, tpl("docs", "agents", `${f}.md`));
113
+ },
114
+ },
115
+ memory: {
116
+ desc: "shared .agent-memory/ for all three agents",
117
+ run(ctx) {
118
+ ctx.ins.seed(".agent-memory/MEMORY.md", tpl("agent-memory", "MEMORY.md"));
119
+ ctx.ins.block("AGENTS.md", "memory", blockBody("memory", ctx.params));
120
+ },
121
+ },
122
+ tracking: {
123
+ desc: "empty GLOSSARY, DECISIONS, FEATURES, BACKLOG, adr/ + the rules for using them",
124
+ run(ctx) {
125
+ const { ins } = ctx;
126
+ for (const f of ["GLOSSARY", "DECISIONS", "BACKLOG", "FEATURES"]) ins.seed(`docs/${f}.md`, tpl("docs", `${f}.md`));
127
+ ins.seed("docs/adr/.gitkeep", "");
128
+ for (const f of ["domain", "issue-tracker", "triage-labels"]) ins.seed(`docs/agents/${f}.md`, tpl("docs", "agents", `${f}.md`));
129
+ ins.block("AGENTS.md", "tracking", blockBody("tracking", ctx.params));
130
+ },
131
+ },
132
+ browser: {
133
+ desc: "agent-browser + verify-ui skills, chrome-devtools MCP, Cursor command allowlist",
134
+ run(ctx) {
135
+ const { ins, agents, params } = ctx;
136
+ installSkill(ctx, "agent-browser");
137
+ installSkill(ctx, "verify-ui");
138
+ mcp(ctx, "chrome-devtools", "npx", CHROME_ARGS, `[mcp_servers.chrome-devtools]\ncommand = "npx"\nargs = [${CHROME_ARGS.map((a) => `"${a}"`).join(", ")}]`);
139
+ if (agents.includes("claude")) ins.json(".claude/settings.json", { enabledMcpjsonServers: ["chrome-devtools"] }, ["enabledMcpjsonServers"]);
140
+ if (agents.includes("cursor")) ins.json(".cursor/cli.json", JSON.parse(tpl("cursor-cli.json")), ["permissions.allow", "permissions.deny"]);
141
+ ins.block("AGENTS.md", "browser", blockBody("browser", params));
142
+ },
143
+ },
144
+ workers: {
145
+ desc: "orchestrate-workers skill (needs herdr + hc)",
146
+ run(ctx) {
147
+ installSkill(ctx, "orchestrate-workers");
148
+ ctx.ins.block("AGENTS.md", "workers", blockBody("workers", ctx.params));
149
+ },
150
+ },
151
+ };
152
+
153
+ // Pushes per-project settings into the agent configs. Safe to run any time (idempotent).
154
+ function sync(ctx, modules) {
155
+ const { ins, agents, params } = ctx;
156
+ const conf = parseConf(ins.read(CONF));
157
+ if (conf.ALLOWED_DOMAINS && !validSiteList(conf.ALLOWED_DOMAINS)) {
158
+ throw new Error("verify-ui.conf: ALLOWED_DOMAINS contains characters that cannot be part of a site list");
159
+ }
160
+ const domains = conf.ALLOWED_DOMAINS || "localhost";
161
+ if (modules.includes("browser")) {
162
+ if (agents.includes("claude")) ins.json(".claude/settings.json", { env: { AGENT_BROWSER_ALLOWED_DOMAINS: domains } });
163
+ if (agents.includes("codex")) {
164
+ ins.tomlBlock(
165
+ ".codex/config.toml",
166
+ "browser-env",
167
+ `[shell_environment_policy]\ninherit = "all"\nset = { AGENT_BROWSER_ALLOWED_DOMAINS = "${domains}" }`,
168
+ ["shell_environment_policy"],
169
+ );
170
+ }
171
+ }
172
+ // cbm-cli owns the codebase-memory server; once it has set the repo up, ccwf adds the rules.
173
+ if (ins.read(".codebase-memory/cbm-server.sh") !== null) {
174
+ ins.block("AGENTS.md", "cbm", blockBody("cbm", params));
175
+ if (agents.includes("cursor") && modules.includes("browser")) {
176
+ ins.json(".cursor/cli.json", { permissions: { allow: ["Mcp(codebase-memory:*)"] } }, ["permissions.allow"]);
177
+ }
178
+ return { cbm: true, domains };
179
+ }
180
+ return { cbm: false, domains };
181
+ }
182
+
183
+ const ORDER = Object.keys(MODULES);
184
+
185
+ const WORKER_NAMES = ["alpha", "beta", "gamma", "theta"];
186
+ const DEFAULT_MODEL = { claude: "claude-sonnet-5-5", codex: "gpt-5-codex", cursor: "grok-4.7" };
187
+
188
+ function defaultRoster(agents) {
189
+ return WORKER_NAMES.map((n, i) => {
190
+ const a = agents[i % agents.length];
191
+ return `${n.padEnd(8)} ${a.padEnd(8)} ${DEFAULT_MODEL[a].padEnd(18)} auto`;
192
+ }).join("\n");
193
+ }
194
+
195
+ function makeParams({ agents }) {
196
+ return { agents, vars: { DATE: new Date().toISOString().slice(0, 10), ROSTER: defaultRoster(agents) } };
197
+ }
198
+
199
+ module.exports = { MODULES, ORDER, AGENTS, makeParams, sync, TEMPLATES, parseConf };
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@mayurdluffy/ccwf",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "One command to set up skills, MCP servers, shared memory and rules for Claude Code, Codex and Cursor in any repo",
5
+ "main": "bin/ccwf.js",
6
+ "bin": {
7
+ "ccwf": "bin/ccwf.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test",
11
+ "prepare": "node -e \"require('fs').chmodSync('bin/ccwf.js', 0o755)\""
12
+ },
13
+ "keywords": [
14
+ "claude-code",
15
+ "codex",
16
+ "cursor",
17
+ "agents",
18
+ "skills",
19
+ "mcp",
20
+ "codebase-memory-mcp",
21
+ "cli",
22
+ "developer-tools"
23
+ ],
24
+ "author": "mayurdluffy <mayurdluffy@gmail.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/mayurdluffy/ccwf.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/mayurdluffy/ccwf/issues"
32
+ },
33
+ "homepage": "https://github.com/mayurdluffy/ccwf#readme",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "files": [
41
+ "bin/",
42
+ "lib/",
43
+ "templates/",
44
+ "README.md",
45
+ "LICENSE"
46
+ ]
47
+ }
@@ -0,0 +1,4 @@
1
+ # Shared agent memory
2
+
3
+ Index for Claude Code, Codex and Cursor. One line per memory, alphabetical. Full facts live in the linked files.
4
+
@@ -0,0 +1,9 @@
1
+ ## Browser verification (Claude Code, Codex, Cursor)
2
+
3
+ Never screenshot to check that something exists or loaded; read the accessibility tree instead. Screenshot only for purely visual questions (layout, colors).
4
+
5
+ - **Start with the `verify-ui` skill** (`.agents/skills/verify-ui`): `probe.sh <path>` prints a page's health (uncaught errors, console, failed and API requests, vitals, interactive elements) in one call; `run.sh <dir>` runs ticket scenarios as a regression suite; `PLAYBOOK.md` maps each browser symptom to one command. Evidence is saved under `.scratch/.verify-out/` (git-ignored). Base URL and allowed sites are in `.agents/skills/verify-ui/verify-ui.conf`; the skill asks for them on first use, then run `npx @mayurdluffy/ccwf sync`.
6
+ - **Drive/inspect UI by hand → `agent-browser`** (CLI; skill in `.agents/skills/agent-browser`; run `agent-browser skills get core` once per session). Use your own session: `S=$(agent-browser session id --scope worktree --prefix task)` then `agent-browser --session $S ...`. Loop: `open <url>` → `snapshot -i -c` (add `--delta` on re-snapshots) → act on `@eN` refs → `console` for errors → `close`.
7
+ - **`chrome-devtools` MCP only for what agent-browser cannot do**: logpoints, CSS cascade, throttling, performance traces, heap snapshots, Lighthouse. Keep its output small.
8
+ - **Dev server**: start it as a background process and stop it by its ID; never `pkill`/`killall`.
9
+ - **Allowed sites** come from `ALLOWED_DOMAINS` in `verify-ui.conf` and are enforced through `AGENT_BROWSER_ALLOWED_DOMAINS` (Claude: `.claude/settings.json`; Codex: `.codex/config.toml`; Cursor: prefix agent-browser commands with it, the `verify-ui` scripts set it themselves). Never visit a site that is not listed; ask the user to add it.
@@ -0,0 +1,9 @@
1
+ ## Codebase navigation (Claude Code, Codex, Cursor)
2
+
3
+ Use the codebase-memory MCP (server `codebase-memory`) before Grep/Read. It is registered **per project** and is the only CBM server any agent runs here:
4
+
5
+ - Launcher: `.codebase-memory/cbm-server.sh`. Configs: `.mcp.json` (Claude), `.codex/config.toml` (Codex), `.cursor/mcp.json` (Cursor) all point at it.
6
+ - CBM allows one daemon and one cache root per user account at a time. Never set `CBM_CACHE_DIR` per repo and never register a global CBM server (`~/.claude.json`, `~/.codex/config.toml`, `~/.cursor/mcp.json`); either one makes servers fail with a "different cache directory" error. `npx @mayurdluffy/cbm-cli clean-global` removes stray global entries.
7
+ - Order: project docs → graph (`search_graph`, `trace_path`, `get_code_snippet`, `get_architecture`) → Grep/Read only on a graph miss or for non-code files.
8
+ - Exclusions go in the root `.cbmignore`; the indexer also honors `.gitignore`.
9
+ - Re-index after large changes: `npx @mayurdluffy/cbm-cli re-index` (refreshes the committed `.codebase-memory/graph.db.zst`).
@@ -0,0 +1,35 @@
1
+ ## Engineering discipline
2
+
3
+ Bias toward caution over speed; use judgment on trivial tasks. Lean by default.
4
+
5
+ **Before changing code**
6
+ - Read the task and the code it affects; trace the real end-to-end flow.
7
+ - Identify callers, tests, config, exports, and data the change could break.
8
+ - State assumptions. If the request is ambiguous or has several readings, present them and ask; don't pick silently or invent product behavior. Push back when a simpler approach exists.
9
+ - Define how success will be verified. For multi-step work, state a short plan with a verify check per step.
10
+
11
+ **Use the first option that fully solves it**
12
+ 1. Does it need to exist? Skip speculative features, options, flexibility; state the skipped scope in one line.
13
+ 2. Does the codebase already provide it? Reuse helpers, components, types, patterns.
14
+ 3. Standard library → 4. native platform feature → 5. already-installed dependency → 6. a clear one-liner.
15
+ 7. Otherwise, the minimum complete code. If 200 lines could be 50, rewrite.
16
+
17
+ **Keep diffs small and surgical**
18
+ - Every changed line traces to the request. No unrelated refactor, rename, or reformat; match surrounding style.
19
+ - No abstractions, wrappers, or config without a concrete current need. DRY only for true duplication.
20
+ - No new dependency for what the platform, stdlib, or existing code already does.
21
+ - Remove only orphans your change created; mention unrelated dead code instead of deleting it.
22
+ - Bugs: write a reproducing test first, fix the shared root cause, not the symptom.
23
+
24
+ **Never sacrifice correctness for brevity**
25
+ - Validate external input at trust boundaries; keep auth, data-integrity, and security controls.
26
+ - Preserve accessibility and error/loading/empty states; model failure paths explicitly.
27
+ - Add the smallest useful test for nontrivial logic; finish everything the change requires, including callers, fixtures, and tests it breaks.
28
+
29
+ **Before completion**: run the project's build, lint and tests; check user-visible behavior; report what changed, what was verified, and remaining risks.
30
+
31
+ ## Skills and commits
32
+
33
+ New here? Read `docs/agents/workflow.md` (how work flows) and `docs/agents/setup.md` (tools and skills to install).
34
+
35
+ Project skills live in `.agents/skills/` (Claude Code reaches them through symlinks in `.claude/skills/`). Read a skill's `SKILL.md` before the work it covers. For commits use the `commit` skill: atomic Conventional Commits, explicit paths when staging, no secrets, no `--no-verify`, never push or rewrite history unless asked. Commit only when the user asks.
@@ -0,0 +1,7 @@
1
+ ## Shared memory (Claude Code, Codex, Cursor)
2
+
3
+ Project memory lives in `.agent-memory/` (committed). At the start of a task read `.agent-memory/MEMORY.md` (short index) and open only the files relevant to the task. When the user asks you to remember something project-related, write it there instead of any tool-private memory:
4
+
5
+ - One fact per file with frontmatter (`name`, `description`, `metadata.type`: `user` | `feedback` | `project` | `reference`); body for feedback/project ends with **Why:** and **How to apply:**.
6
+ - Add one line to `MEMORY.md` (`- [Title](file.md) — hook`), keep it alphabetical, update an existing file instead of duplicating, delete memories that turn out wrong.
7
+ - This folder is pushed to GitHub: never store secrets, credentials, emails or personal details, and avoid machine-specific paths. Private notes stay in your tool's own memory.
@@ -0,0 +1,11 @@
1
+ ## Project tracking and domain docs (Claude Code, Codex, Cursor)
2
+
3
+ These files in `docs/` start empty and are kept current in **every** session:
4
+
5
+ - `docs/GLOSSARY.md` — the project's domain language, only terms, no implementation detail. Read it before exploring or naming things; use its terms exactly. Add or sharpen a term the moment it is resolved (the `domain-modeling` skill does this during `grill-with-docs`).
6
+ - `docs/adr/` — architecture decision records, one file per decision, only for decisions that are hard to reverse, surprising without context and a real trade-off. Read the ones touching the area you work in; surface a conflict instead of silently overriding an ADR.
7
+ - `docs/DECISIONS.md` — append every question you ask the user with your recommendation, the user's answer in their own words, and the resulting decision. Do it right after the user answers, before continuing. Append only: a reversal gets a new entry and the old one is marked `Superseded by <id>`.
8
+ - `docs/BACKLOG.md` — features deferred for later. Add an item the moment it is deferred; remove it when work starts.
9
+ - `docs/FEATURES.md` — every feature as Done / In progress / Planned. Update it in the same change that starts or finishes the work.
10
+
11
+ Keep the repo root clean: new project documents go under `docs/`. How the planning skills find these files: `docs/agents/domain.md`, `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`. The full workflow is in `docs/agents/workflow.md`; machine and tool setup is in `docs/agents/setup.md`.
@@ -0,0 +1,3 @@
1
+ ## Worker agents
2
+
3
+ The `orchestrate-workers` skill (`.agents/skills/orchestrate-workers`) runs a ticket set with other agent CLIs (Codex, Cursor, Claude) as workers in a herdr tab, driven with the `hc` CLI. Needs `herdr` and `hc` installed. The roster is `.agents/skills/orchestrate-workers/workers.conf`; confirm it with the user at the start of each session. Issues and specs live as markdown under `.scratch/<feature>/`. How to drive panes with `hc`: `.agents/skills/orchestrate-workers/HC.md`.
@@ -0,0 +1,17 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Shell(ls)", "Shell(cat)", "Shell(head)", "Shell(tail)", "Shell(wc)", "Shell(echo)",
5
+ "Shell(grep)", "Shell(rg)", "Shell(find)", "Shell(sed)", "Shell(diff)",
6
+ "Shell(npm)", "Shell(npx)", "Shell(node)",
7
+ "Shell(agent-browser)", "Shell(curl)",
8
+ "Shell(.agents/skills/verify-ui/run.sh)", "Shell(.agents/skills/verify-ui/probe.sh)",
9
+ "Mcp(chrome-devtools:*)",
10
+ "Shell(git status)", "Shell(git diff)", "Shell(git log)", "Shell(git show)"
11
+ ],
12
+ "deny": [
13
+ "Shell(git commit)", "Shell(git push)", "Shell(git reset)", "Shell(git checkout)",
14
+ "Shell(git stash)", "Shell(git clean)", "Shell(rm -rf)"
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Shared agent rules (discipline, memory, tracking, skills); see AGENTS.md
3
+ alwaysApply: true
4
+ ---
5
+
6
+ Follow [AGENTS.md](/AGENTS.md): the same rules apply to Claude Code, Codex and Cursor. Project skills are folders in `.agents/skills/`; read a skill's `SKILL.md` before the work it covers.
@@ -0,0 +1,4 @@
1
+ # Backlog
2
+
3
+ Features deferred for later. Add the moment something is deferred; remove when work starts.
4
+
@@ -0,0 +1,4 @@
1
+ # Decisions
2
+
3
+ Every question asked of the user, the recommendation, the answer in their words, and the decision. Append only; a reversal gets a new entry and the old one is marked `Superseded by <id>`.
4
+
@@ -0,0 +1,8 @@
1
+ # Features
2
+
3
+ ## Done
4
+
5
+ ## In progress
6
+
7
+ ## Planned
8
+
@@ -0,0 +1,3 @@
1
+ # Glossary
2
+
3
+ The project's domain language. Add a term the moment it is resolved; terms only, no implementation detail. See `docs/agents/domain.md`.
File without changes