makdoong2-team 1.3.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/LICENSE +21 -0
- package/README.md +193 -0
- package/agents/makdoong2-analyzer.md +135 -0
- package/agents/makdoong2-engineer.md +165 -0
- package/agents/makdoong2-planner.md +267 -0
- package/agents/makdoong2-publisher.md +481 -0
- package/agents/makdoong2-team-leader.md +249 -0
- package/agents/makdoong2-verifier.md +353 -0
- package/assets/makdoong2-team.default.json +46 -0
- package/assets/makdoong2-team.schema.json +357 -0
- package/bin/cli.js +404 -0
- package/dist/agent-stage-config.d.ts +15 -0
- package/dist/agent-stage-config.js +119 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +96 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +131 -0
- package/dist/mcp-secret-injector.d.ts +56 -0
- package/dist/mcp-secret-injector.js +89 -0
- package/dist/model-chain-cli.d.ts +1 -0
- package/dist/model-chain-cli.js +21 -0
- package/dist/model-fallback-policy.d.ts +69 -0
- package/dist/model-fallback-policy.js +211 -0
- package/dist/opencode-plugin.d.ts +8 -0
- package/dist/opencode-plugin.js +2457 -0
- package/dist/poll-sub-session.d.ts +139 -0
- package/dist/poll-sub-session.js +494 -0
- package/dist/redact-secrets.d.ts +3 -0
- package/dist/redact-secrets.js +68 -0
- package/dist/session-index.d.ts +11 -0
- package/dist/session-index.js +71 -0
- package/dist/skill-mcp-registry.d.ts +59 -0
- package/dist/skill-mcp-registry.js +178 -0
- package/dist/stall-escalation.d.ts +1 -0
- package/dist/stall-escalation.js +22 -0
- package/dist/tmux-monitor.d.ts +193 -0
- package/dist/tmux-monitor.js +694 -0
- package/dist/verdict-hash.d.ts +1 -0
- package/dist/verdict-hash.js +62 -0
- package/gates/stage-analysis-verify.sh +84 -0
- package/gates/stage2-requirements-verify.sh +13 -0
- package/gates/stage3-scope-verify.sh +45 -0
- package/gates/stage4-dev-post-verify.sh +64 -0
- package/gates/stage4-dev-verify.sh +36 -0
- package/gates/stage5-coverage-verify.sh +36 -0
- package/gates/stage5-test-verify.sh +24 -0
- package/gates/stage6-commit-verify.sh +41 -0
- package/gates/stage6-post-commit-verify.sh +131 -0
- package/gates/stage7-post-pr-verify.sh +53 -0
- package/gates/stage7-pr-verify.sh +48 -0
- package/gates/stage8-post-review-verify.sh +84 -0
- package/gates/stage8-review-verify.sh +45 -0
- package/gates/verify.sh +44 -0
- package/opencode.json.example +40 -0
- package/package.json +84 -0
- package/postinstall.mjs +56 -0
- package/references/commit-convention.md +130 -0
- package/references/jira-issue-templates.md +203 -0
- package/references/pr-template.md +381 -0
- package/scripts/config.sh +46 -0
- package/scripts/coverage-record.sh +67 -0
- package/scripts/gate-policy-test.sh +152 -0
- package/scripts/install-lib.mjs +1029 -0
- package/scripts/lint-agent-prompts.sh +74 -0
- package/scripts/log-event.sh +44 -0
- package/scripts/model-policy.mjs +183 -0
- package/scripts/publish-if-changed.sh +207 -0
- package/scripts/release.sh +276 -0
- package/scripts/rollback-commits.sh +35 -0
- package/scripts/smoke-test.mjs +194 -0
- package/scripts/state.sh +192 -0
- package/scripts/test-postinstall.mjs +141 -0
- package/scripts/with-fallback.sh +56 -0
- package/scripts/wt-sync-ignored.sh +193 -0
- package/skills/_lib/load-secret.sh +149 -0
- package/skills/bamboo-ci/SKILL.md +81 -0
- package/skills/bamboo-ci/run-bamboo.sh +23 -0
- package/skills/bitbucket-research/SKILL.md +87 -0
- package/skills/bitbucket-research/run-repos.sh +23 -0
- package/skills/confluence-research/SKILL.md +75 -0
- package/skills/confluence-research/run-docs.sh +23 -0
- package/skills/github-oss-research/SKILL.md +59 -0
- package/skills/jira-research/SKILL.md +75 -0
- package/skills/jira-research/run-works.sh +23 -0
- package/src/hooks/guard-bash.sh +67 -0
- package/src/hooks/session-start.sh +96 -0
- package/src/hooks/sync-state.sh +47 -0
- package/stages/01-jira.md +43 -0
- package/stages/01-planning.md +229 -0
- package/stages/02-requirements.md +298 -0
- package/stages/03-scope.md +81 -0
- package/stages/04-analysis.md +281 -0
- package/stages/05-worktree-dev.md +124 -0
- package/stages/06-test.md +161 -0
- package/stages/07-commit.md +229 -0
- package/stages/08-pr.md +177 -0
- package/stages/09-review-comments.md +277 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/cli.js — makdoong2-team installer & doctor.
|
|
3
|
+
//
|
|
4
|
+
// npx makdoong2-team install [--config <dir>] [--force]
|
|
5
|
+
// npx makdoong2-team doctor [--config <dir>]
|
|
6
|
+
// npx makdoong2-team --version | --help
|
|
7
|
+
//
|
|
8
|
+
// Replaces the old bash install.sh. Zero runtime dependencies (plain Node ESM)
|
|
9
|
+
// so `npx` runs instantly with nothing to resolve first. Settings are NOT
|
|
10
|
+
// passed as flags — everything is controlled by ~/.config/opencode/makdoong2-team.json.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
existsSync, readFileSync, readdirSync, statSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import {
|
|
19
|
+
buildPoliciesFromConfig, DEFAULT_ALLOWED_PRIMARIES,
|
|
20
|
+
} from "../scripts/model-policy.mjs";
|
|
21
|
+
import {
|
|
22
|
+
install,
|
|
23
|
+
uninstall,
|
|
24
|
+
resolveConfigDir,
|
|
25
|
+
parseJsonc,
|
|
26
|
+
readCachedVersion,
|
|
27
|
+
opencodeCacheRoot,
|
|
28
|
+
} from "../scripts/install-lib.mjs";
|
|
29
|
+
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const PKG_ROOT = resolve(HERE, "..");
|
|
32
|
+
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
33
|
+
|
|
34
|
+
const TOOLS = ["verify_stage", "dispatch_stage", "dispatch_verifier", "auto_advance_stage", "get_fallback_model"];
|
|
35
|
+
|
|
36
|
+
// Skill directories that used to ship a per-skill secrets.env under
|
|
37
|
+
// ${configDir}/skills/<skill>/. Credentials are now sourced only from
|
|
38
|
+
// makdoong2-team.json .secrets.*; doctor warns when the legacy file lingers.
|
|
39
|
+
const LEGACY_SECRET_SKILL_DIRS = [
|
|
40
|
+
"bitbucket-research",
|
|
41
|
+
"jira-research",
|
|
42
|
+
"confluence-research",
|
|
43
|
+
"bamboo-ci",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Names of the secret keys the schema recognises. Doctor reports which of
|
|
47
|
+
// these are unset so the user knows exactly which research skills are inert.
|
|
48
|
+
const SECRET_KEYS = [
|
|
49
|
+
"BITBUCKET_API_TOKEN",
|
|
50
|
+
"JIRA_API_TOKEN",
|
|
51
|
+
"CONFLUENCE_API_TOKEN",
|
|
52
|
+
"BAMBOO_TOKEN",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// ── tiny output helpers (no deps) ──
|
|
56
|
+
const ok = (m) => console.log(` ✓ ${m}`);
|
|
57
|
+
const info = (m) => console.log(m);
|
|
58
|
+
const warn = (m) => console.warn(` ! ${m}`);
|
|
59
|
+
|
|
60
|
+
function parseFlags(args) {
|
|
61
|
+
const f = {};
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
if (args[i] === "--config") f.config = args[++i];
|
|
64
|
+
else if (args[i] === "--force") f.force = true;
|
|
65
|
+
else if (args[i] === "--help" || args[i] === "-h") f.help = true;
|
|
66
|
+
}
|
|
67
|
+
return f;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function doInstall(flags) {
|
|
71
|
+
const configDir = resolveConfigDir(flags.config);
|
|
72
|
+
const result = install({
|
|
73
|
+
configDir,
|
|
74
|
+
pkgRoot: PKG_ROOT,
|
|
75
|
+
force: flags.force,
|
|
76
|
+
patchOpencode: "always", // preserve current CLI behavior (always patch)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Dependency sanity check
|
|
80
|
+
const missing = ["jq", "git"].filter((b) => !hasBinary(b));
|
|
81
|
+
if (missing.length) warn(`missing runtime dependencies: ${missing.join(", ")}`);
|
|
82
|
+
else ok("runtime dependencies (jq, git) present");
|
|
83
|
+
|
|
84
|
+
const cfgPath = join(configDir, "makdoong2-team.json");
|
|
85
|
+
info("");
|
|
86
|
+
info("Next steps:");
|
|
87
|
+
info(` 1) Edit settings in ${cfgPath}`);
|
|
88
|
+
info(" 2) Restart opencode so the plugin and agents load");
|
|
89
|
+
info(` 3) Sanity check: npx makdoong2-team doctor`);
|
|
90
|
+
info("");
|
|
91
|
+
info("[makdoong2-team] install complete — 막둥이들 출근 준비 끝");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasBinary(bin) {
|
|
95
|
+
try { execFileSync(bin, ["--version"], { stdio: "ignore" }); return true; }
|
|
96
|
+
catch { return false; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function doDoctor(flags) {
|
|
100
|
+
const DEST = resolveConfigDir(flags.config);
|
|
101
|
+
info(`[makdoong2-team] doctor — ${DEST}`);
|
|
102
|
+
let problems = 0;
|
|
103
|
+
const check = (cond, good, bad) => { if (cond) ok(good); else { warn(bad); problems++; } };
|
|
104
|
+
|
|
105
|
+
for (const b of ["jq", "git"]) check(hasBinary(b), `${b} present`, `${b} missing on PATH`);
|
|
106
|
+
|
|
107
|
+
const cfgPath = join(DEST, "makdoong2-team.json");
|
|
108
|
+
let cfgOk = false, cfgParsed = null;
|
|
109
|
+
if (existsSync(cfgPath)) {
|
|
110
|
+
try { cfgParsed = JSON.parse(readFileSync(cfgPath, "utf8")); cfgOk = true; } catch { /* invalid */ }
|
|
111
|
+
}
|
|
112
|
+
check(cfgOk, `config valid: ${cfgPath}`, `config missing or invalid: ${cfgPath} (run: npx makdoong2-team install)`);
|
|
113
|
+
|
|
114
|
+
if (cfgOk) {
|
|
115
|
+
let modelOk = false, modelErr = "";
|
|
116
|
+
try {
|
|
117
|
+
buildPoliciesFromConfig({ agents: cfgParsed?.agents, model_policy: cfgParsed?.model_policy });
|
|
118
|
+
modelOk = true;
|
|
119
|
+
} catch (e) { modelErr = e.message; }
|
|
120
|
+
check(modelOk, "model policy invariants pass",
|
|
121
|
+
`model policy invalid: ${modelErr} (run: npx makdoong2-team validate)`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Config dir checks (agents & skills)
|
|
125
|
+
const configDirChecks = ["agents", "skills/jira-research", "skills/confluence-research", "skills/bitbucket-research", "skills/github-oss-research", "skills/bamboo-ci"];
|
|
126
|
+
for (const d of configDirChecks) {
|
|
127
|
+
check(existsSync(join(DEST, d)), `${d}/ in config dir`, `${d}/ missing in config dir (run: npx makdoong2-team install)`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Package root checks (gates, stages, scripts, src)
|
|
131
|
+
const pkgRootChecks = ["gates", "stages", "scripts", "src"];
|
|
132
|
+
for (const d of pkgRootChecks) {
|
|
133
|
+
check(existsSync(join(PKG_ROOT, d)), `${d}/ in package`, `${d}/ missing in package (reinstall: npm install -g makdoong2-team)`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ocPath = join(DEST, "opencode.json");
|
|
137
|
+
let pluginOk = false, toolsOk = false, ocParseOk = false, ocParseErr = "";
|
|
138
|
+
if (existsSync(ocPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const oc = parseJsonc(readFileSync(ocPath, "utf8"));
|
|
141
|
+
ocParseOk = true;
|
|
142
|
+
const pkgName = PKG.name;
|
|
143
|
+
const matchesEntry = (p) => {
|
|
144
|
+
if (typeof p !== "string") return false;
|
|
145
|
+
return p === pkgName || p.startsWith(`${pkgName}@`);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
pluginOk = Array.isArray(oc.plugin) && oc.plugin.some(p => {
|
|
149
|
+
if (matchesEntry(p)) return true;
|
|
150
|
+
if (Array.isArray(p) && matchesEntry(p[0])) return true;
|
|
151
|
+
return false;
|
|
152
|
+
});
|
|
153
|
+
toolsOk = oc.tools && TOOLS.every((t) => oc.tools[t] === true);
|
|
154
|
+
} catch (e) { ocParseErr = e.message; }
|
|
155
|
+
}
|
|
156
|
+
check(!existsSync(ocPath) || ocParseOk, "opencode.json parses (JSONC tolerated)",
|
|
157
|
+
`opencode.json is unparseable — install silently skips patching it: ${ocParseErr}`);
|
|
158
|
+
check(pluginOk, "opencode.json registers the plugin", "opencode.json missing the plugin entry");
|
|
159
|
+
check(toolsOk, "opencode.json enables custom tools", "opencode.json missing custom tools");
|
|
160
|
+
|
|
161
|
+
// The plugin opencode actually loads lives in the `<name>@latest` cache dir,
|
|
162
|
+
// not in the globally installed npm module. When those drift the user runs
|
|
163
|
+
// old code while `npm ls -g` reports the new version — invisible without
|
|
164
|
+
// this check.
|
|
165
|
+
const cachedDir = join(opencodeCacheRoot(), `${PKG.name}@latest`, "node_modules", PKG.name);
|
|
166
|
+
const cachedVersion = existsSync(cachedDir) ? readCachedVersion(cachedDir) : null;
|
|
167
|
+
check(cachedVersion === null || cachedVersion === PKG.version,
|
|
168
|
+
`opencode plugin cache matches installed version (v${PKG.version})`,
|
|
169
|
+
`opencode plugin cache is stale: ${cachedDir} has v${cachedVersion} but this package is v${PKG.version} ` +
|
|
170
|
+
`— opencode loads the CACHED copy (run: npx makdoong2-team install)`);
|
|
171
|
+
|
|
172
|
+
if (cfgOk && Array.isArray(cfgParsed?.model_policy?.allowed_primaries)
|
|
173
|
+
&& cfgParsed.model_policy.allowed_primaries.length === 0) {
|
|
174
|
+
warn(`model_policy.allowed_primaries is [] — this means "add nothing", not "allow everything"`);
|
|
175
|
+
warn(` the runtime allow-list stays at the built-in defaults; remove the key or list the extra model ids`);
|
|
176
|
+
problems++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Detect legacy secrets.env residue. These files predate the makdoong2-team.json
|
|
180
|
+
// .secrets.* migration and can silently override the new source of truth
|
|
181
|
+
// if any run-*.sh regressed to sourcing them.
|
|
182
|
+
const legacySecrets = LEGACY_SECRET_SKILL_DIRS
|
|
183
|
+
.map(skill => join(DEST, "skills", skill, "secrets.env"))
|
|
184
|
+
.filter(p => existsSync(p));
|
|
185
|
+
if (legacySecrets.length === 0) {
|
|
186
|
+
ok("no legacy skills/*/secrets.env files (credentials sourced from makdoong2-team.json)");
|
|
187
|
+
} else {
|
|
188
|
+
warn(`${legacySecrets.length} legacy secrets.env file(s) still present — migrate their values to makdoong2-team.json .secrets.* then delete:`);
|
|
189
|
+
for (const p of legacySecrets.slice(0, 8)) warn(` ${p}`);
|
|
190
|
+
warn(` fix: npx makdoong2-team install (backs up + removes them automatically)`);
|
|
191
|
+
problems++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Report which .secrets.* keys are unset so the user sees at a glance which
|
|
195
|
+
// research skills will refuse to spawn. Missing config file already reported above.
|
|
196
|
+
if (cfgOk) {
|
|
197
|
+
const secretsObj = cfgParsed?.secrets;
|
|
198
|
+
if (!secretsObj || typeof secretsObj !== "object") {
|
|
199
|
+
warn(`makdoong2-team.json has no "secrets" block — research skills will fail to spawn`);
|
|
200
|
+
warn(` fix: npx makdoong2-team install (adds the scaffolding)`);
|
|
201
|
+
problems++;
|
|
202
|
+
} else {
|
|
203
|
+
const missing = SECRET_KEYS.filter(k => !secretsObj[k] || typeof secretsObj[k] !== "string" || secretsObj[k].trim() === "");
|
|
204
|
+
if (missing.length === 0) {
|
|
205
|
+
ok(`all ${SECRET_KEYS.length} research-skill secret(s) configured`);
|
|
206
|
+
} else {
|
|
207
|
+
warn(`${missing.length}/${SECRET_KEYS.length} research-skill secret(s) unset in makdoong2-team.json: ${missing.join(", ")}`);
|
|
208
|
+
warn(` edit ${cfgPath} and fill in the tokens, then restart opencode`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (cfgOk) {
|
|
214
|
+
const loggingCfg = cfgParsed?.logging;
|
|
215
|
+
if (loggingCfg && typeof loggingCfg === "object") {
|
|
216
|
+
const mode = loggingCfg.mode;
|
|
217
|
+
if (mode !== undefined && mode !== "stdin" && mode !== "file") {
|
|
218
|
+
warn(`logging.mode invalid: "${mode}" (must be "stdin" or "file")`);
|
|
219
|
+
problems++;
|
|
220
|
+
}
|
|
221
|
+
if (mode === "file") {
|
|
222
|
+
const p = loggingCfg.path;
|
|
223
|
+
if (typeof p !== "string" || p.trim() === "") {
|
|
224
|
+
warn(`logging.mode="file" but logging.path is missing/empty — plugin will refuse to start`);
|
|
225
|
+
problems++;
|
|
226
|
+
} else {
|
|
227
|
+
ok(`logging.mode="file" → ${p}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const contaminated = scanContaminatedStates();
|
|
234
|
+
if (contaminated.length === 0) {
|
|
235
|
+
ok("no phantom-key contamination in .makdoong2-team/*/state.json");
|
|
236
|
+
} else {
|
|
237
|
+
warn(`${contaminated.length} state.json file(s) contain flat-notation phantom keys`);
|
|
238
|
+
for (const { path, keys } of contaminated.slice(0, 5)) {
|
|
239
|
+
warn(` ${path}`);
|
|
240
|
+
warn(` phantom keys: ${keys.join(", ")}`);
|
|
241
|
+
}
|
|
242
|
+
if (contaminated.length > 5) {
|
|
243
|
+
warn(` ... and ${contaminated.length - 5} more`);
|
|
244
|
+
}
|
|
245
|
+
warn(` fix: bash <SCRIPTS_DIR>/state.sh migrate <ISSUE_KEY>`);
|
|
246
|
+
warn(` (or trigger via 'bash <SCRIPTS_DIR>/state.sh init <ISSUE_KEY>' — init auto-migrates)`);
|
|
247
|
+
problems++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
info("");
|
|
251
|
+
info(problems === 0 ? "[makdoong2-team] doctor: all good ✓" : `[makdoong2-team] doctor: ${problems} problem(s) found`);
|
|
252
|
+
process.exit(problems === 0 ? 0 : 1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const FLAT_STAGE_KEY_RE = /^[0-9]+_[a-z_]+\.[a-z]+$/;
|
|
256
|
+
|
|
257
|
+
function scanContaminatedStates() {
|
|
258
|
+
const results = [];
|
|
259
|
+
const searchRoots = [process.cwd(), PKG_ROOT];
|
|
260
|
+
const seenPaths = new Set();
|
|
261
|
+
|
|
262
|
+
for (const root of searchRoots) {
|
|
263
|
+
walkForStateFiles(root, seenPaths, results, 0);
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function walkForStateFiles(dir, seen, results, depth) {
|
|
269
|
+
if (depth > 6) return;
|
|
270
|
+
let entries;
|
|
271
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
272
|
+
|
|
273
|
+
for (const name of entries) {
|
|
274
|
+
if (name === "node_modules" || name === ".git" || name.startsWith(".cache")) continue;
|
|
275
|
+
const full = join(dir, name);
|
|
276
|
+
let st;
|
|
277
|
+
try { st = statSync(full); } catch { continue; }
|
|
278
|
+
if (!st.isDirectory()) continue;
|
|
279
|
+
|
|
280
|
+
if (name === ".makdoong2-team") {
|
|
281
|
+
inspectMakdoongDir(full, seen, results);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
walkForStateFiles(full, seen, results, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function inspectMakdoongDir(mkDir, seen, results) {
|
|
289
|
+
let issueDirs;
|
|
290
|
+
try { issueDirs = readdirSync(mkDir); } catch { return; }
|
|
291
|
+
for (const issue of issueDirs) {
|
|
292
|
+
const p = join(mkDir, issue, "state.json");
|
|
293
|
+
if (seen.has(p) || !existsSync(p)) continue;
|
|
294
|
+
seen.add(p);
|
|
295
|
+
try {
|
|
296
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
297
|
+
const stages = obj && typeof obj === "object" ? obj.stages : null;
|
|
298
|
+
if (!stages || typeof stages !== "object") continue;
|
|
299
|
+
const phantomKeys = Object.keys(stages).filter(k => FLAT_STAGE_KEY_RE.test(k));
|
|
300
|
+
if (phantomKeys.length > 0) {
|
|
301
|
+
results.push({ path: p, keys: phantomKeys });
|
|
302
|
+
}
|
|
303
|
+
} catch { /* unreadable/invalid JSON — skip silently */ }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function doUninstall(flags) {
|
|
308
|
+
const configDir = resolveConfigDir(flags.config);
|
|
309
|
+
uninstall({ configDir, pkgRoot: PKG_ROOT });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function doValidate(flags) {
|
|
313
|
+
const DEST = resolveConfigDir(flags.config);
|
|
314
|
+
const cfgPath = join(DEST, "makdoong2-team.json");
|
|
315
|
+
info(`[makdoong2-team] validate — ${cfgPath}`);
|
|
316
|
+
|
|
317
|
+
if (!existsSync(cfgPath)) {
|
|
318
|
+
warn(`config not found at ${cfgPath} — defaults are in effect (nothing to validate)`);
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let cfg;
|
|
323
|
+
try {
|
|
324
|
+
cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error(` \u2717 invalid JSON: ${e.message}`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
ok("JSON parses");
|
|
330
|
+
|
|
331
|
+
let result;
|
|
332
|
+
try {
|
|
333
|
+
result = buildPoliciesFromConfig({ agents: cfg.agents, model_policy: cfg.model_policy });
|
|
334
|
+
} catch (e) {
|
|
335
|
+
console.error(` \u2717 model policy invariants violated:`);
|
|
336
|
+
console.error(` ${e.message}`);
|
|
337
|
+
console.error("");
|
|
338
|
+
console.error(" Hints:");
|
|
339
|
+
console.error(" - Primary must be in allowed_primaries (defaults: " +
|
|
340
|
+
[...DEFAULT_ALLOWED_PRIMARIES].join(", ") + ").");
|
|
341
|
+
console.error(" - To add a new primary, set model_policy.allowed_primaries: [\"<provider/model>\"].");
|
|
342
|
+
console.error(" - Each fallback tier must be strictly lower than its primary tier (low < medium < high < max).");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
ok("policy invariants pass (primary allow-list + fallback tier ordering)");
|
|
346
|
+
|
|
347
|
+
const overriddenAgents = cfg.agents
|
|
348
|
+
? Object.keys(cfg.agents).filter(a => cfg.agents[a] && cfg.agents[a].model)
|
|
349
|
+
: [];
|
|
350
|
+
if (overriddenAgents.length) {
|
|
351
|
+
ok(`overrides applied for: ${overriddenAgents.join(", ")}`);
|
|
352
|
+
} else {
|
|
353
|
+
info(" - no per-agent overrides set (built-in POLICIES in effect)");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const extraPrimaries = Array.isArray(cfg?.model_policy?.allowed_primaries)
|
|
357
|
+
? cfg.model_policy.allowed_primaries.filter(s => typeof s === "string" && !DEFAULT_ALLOWED_PRIMARIES.has(s))
|
|
358
|
+
: [];
|
|
359
|
+
if (extraPrimaries.length) {
|
|
360
|
+
ok(`extra allowed primaries: ${extraPrimaries.join(", ")}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
info("");
|
|
364
|
+
info("Resolved chain (primary \u2192 fallbacks):");
|
|
365
|
+
for (const [agent, pol] of Object.entries(result.policies)) {
|
|
366
|
+
const fbStr = pol.fallbacks.length
|
|
367
|
+
? pol.fallbacks.map(f => `${f.id} (${f.tier})`).join(" \u2192 ")
|
|
368
|
+
: "(none, primary-only)";
|
|
369
|
+
info(` ${agent.padEnd(26)} ${pol.primary.id} (${pol.primary.tier}) \u2192 ${fbStr}`);
|
|
370
|
+
}
|
|
371
|
+
info("");
|
|
372
|
+
info("[makdoong2-team] validate: OK \u2713");
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printHelp() {
|
|
377
|
+
info(`makdoong2-team ${PKG.version}\n`);
|
|
378
|
+
info("Usage:");
|
|
379
|
+
info(" npx makdoong2-team install [--config <dir>] [--force] Install plugin, agents, skills + patch opencode.json");
|
|
380
|
+
info(" npx makdoong2-team uninstall [--config <dir>] Remove agents, skills + revert opencode.json patches");
|
|
381
|
+
info(" npx makdoong2-team doctor [--config <dir>] Diagnose an existing install");
|
|
382
|
+
info(" npx makdoong2-team validate [--config <dir>] Validate makdoong2-team.json model policy");
|
|
383
|
+
info("");
|
|
384
|
+
info(" npx makdoong2-team --version");
|
|
385
|
+
info("");
|
|
386
|
+
info("All settings are controlled by <configDir>/makdoong2-team.json");
|
|
387
|
+
info("(default configDir: ${XDG_CONFIG_HOME:-$HOME/.config}/opencode).");
|
|
388
|
+
info("");
|
|
389
|
+
info("Note: uninstall preserves makdoong2-team.json (credentials).");
|
|
390
|
+
info(" Remove it manually if no longer needed.");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const argv = process.argv.slice(2);
|
|
394
|
+
const cmd = argv[0];
|
|
395
|
+
const flags = parseFlags(argv.slice(1));
|
|
396
|
+
switch (cmd) {
|
|
397
|
+
case "install": doInstall(flags); break;
|
|
398
|
+
case "uninstall": doUninstall(flags); break;
|
|
399
|
+
case "doctor": doDoctor(flags); break;
|
|
400
|
+
case "validate": doValidate(flags); break;
|
|
401
|
+
case "-v": case "--version": case "version": info(PKG.version); break;
|
|
402
|
+
case undefined: case "help": case "-h": case "--help": printHelp(); break;
|
|
403
|
+
default: console.error(`unknown command: ${cmd}\n`); printHelp(); process.exit(1);
|
|
404
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Stage = "1_planning.jira" | "1_planning.requirements" | "1_planning.scope" | "2_implementation.analysis" | "2_implementation.dev" | "2_implementation.test" | "3_delivery.commit" | "3_delivery.pr" | "3_delivery.review";
|
|
2
|
+
export interface PermissionRule {
|
|
3
|
+
bash: Record<string, "allow" | "deny" | "ask">;
|
|
4
|
+
}
|
|
5
|
+
export interface AgentSpec {
|
|
6
|
+
id: string;
|
|
7
|
+
stage: Stage | "all";
|
|
8
|
+
primary_only: boolean;
|
|
9
|
+
permissions: PermissionRule;
|
|
10
|
+
tools: string[];
|
|
11
|
+
skills: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const AGENTS: Record<string, AgentSpec>;
|
|
14
|
+
export declare const STAGE_SPEC_FILES: Record<Stage, string>;
|
|
15
|
+
export declare function agentForStage(stage: Stage): AgentSpec;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// agent-stage-config.ts — stage → agent metadata + permission deltas.
|
|
2
|
+
// Replaces oh-my-openagent's per-agent permission/tool config by encoding
|
|
3
|
+
// it here so we can both (a) emit agent markdown files and (b) validate
|
|
4
|
+
// runtime permissions inside the plugin hook.
|
|
5
|
+
// Locked-down default: deny everything risky, allow only stage-specific tools.
|
|
6
|
+
const RO_PERM = {
|
|
7
|
+
bash: {
|
|
8
|
+
"*": "allow",
|
|
9
|
+
"git commit*": "deny",
|
|
10
|
+
"git push*": "deny",
|
|
11
|
+
"git reset --hard*": "deny",
|
|
12
|
+
"git branch -D*": "deny",
|
|
13
|
+
"git worktree add*": "deny",
|
|
14
|
+
"git worktree remove*": "deny",
|
|
15
|
+
"rm -rf*": "deny",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
// Engineer delta: worktree creation allowed, but commit/push still denied.
|
|
19
|
+
// Allows engineer to create worktrees for isolated development.
|
|
20
|
+
const ENG_PERM = {
|
|
21
|
+
bash: {
|
|
22
|
+
"*": "allow",
|
|
23
|
+
"git commit*": "deny",
|
|
24
|
+
"git push*": "deny",
|
|
25
|
+
"git reset --hard*": "deny",
|
|
26
|
+
"git branch -D*": "deny",
|
|
27
|
+
"git worktree add*": "allow",
|
|
28
|
+
"git worktree remove*": "allow",
|
|
29
|
+
"rm -rf*": "deny",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
// Orchestrator delta: commit/push allowed (PRIMARY-only stages),
|
|
33
|
+
// destructive still requires APPROVED_DESTRUCTIVE marker (enforced by guard-bash.sh).
|
|
34
|
+
const ORCH_PERM = {
|
|
35
|
+
bash: {
|
|
36
|
+
"*": "allow",
|
|
37
|
+
"git commit*": "allow",
|
|
38
|
+
"git push*": "allow",
|
|
39
|
+
"git push --force*": "ask",
|
|
40
|
+
"git push --force-with-lease*": "ask",
|
|
41
|
+
"git reset --hard*": "ask",
|
|
42
|
+
"git branch -D*": "ask",
|
|
43
|
+
"git worktree add*": "ask",
|
|
44
|
+
"git worktree remove*": "ask",
|
|
45
|
+
"rm -rf*": "ask",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export const AGENTS = {
|
|
49
|
+
"makdoong2-team-leader": {
|
|
50
|
+
id: "makdoong2-team-leader",
|
|
51
|
+
stage: "all",
|
|
52
|
+
primary_only: true,
|
|
53
|
+
permissions: ORCH_PERM,
|
|
54
|
+
tools: ["task", "bash", "read", "edit", "write", "skill"],
|
|
55
|
+
skills: ["makdoong2-team"],
|
|
56
|
+
},
|
|
57
|
+
"makdoong2-planner": {
|
|
58
|
+
id: "makdoong2-planner",
|
|
59
|
+
stage: "all",
|
|
60
|
+
primary_only: false,
|
|
61
|
+
permissions: RO_PERM,
|
|
62
|
+
tools: ["bash", "read", "grep", "glob", "task", "skill"],
|
|
63
|
+
skills: ["jira-research", "confluence-research", "bitbucket-research", "github-oss-research"],
|
|
64
|
+
},
|
|
65
|
+
"makdoong2-analyzer": {
|
|
66
|
+
id: "makdoong2-analyzer",
|
|
67
|
+
stage: "all",
|
|
68
|
+
primary_only: false,
|
|
69
|
+
permissions: RO_PERM,
|
|
70
|
+
tools: ["bash", "read", "grep", "glob", "write"],
|
|
71
|
+
skills: [],
|
|
72
|
+
},
|
|
73
|
+
"makdoong2-engineer": {
|
|
74
|
+
id: "makdoong2-engineer",
|
|
75
|
+
stage: "all",
|
|
76
|
+
primary_only: false,
|
|
77
|
+
permissions: ENG_PERM,
|
|
78
|
+
tools: ["bash", "read", "edit", "write", "grep", "glob"],
|
|
79
|
+
skills: [],
|
|
80
|
+
},
|
|
81
|
+
"makdoong2-publisher": {
|
|
82
|
+
id: "makdoong2-publisher",
|
|
83
|
+
stage: "all",
|
|
84
|
+
primary_only: false,
|
|
85
|
+
permissions: RO_PERM,
|
|
86
|
+
tools: ["bash", "read", "skill"],
|
|
87
|
+
skills: ["bitbucket-research"],
|
|
88
|
+
},
|
|
89
|
+
"makdoong2-verifier": {
|
|
90
|
+
id: "makdoong2-verifier",
|
|
91
|
+
stage: "all",
|
|
92
|
+
primary_only: false,
|
|
93
|
+
permissions: RO_PERM,
|
|
94
|
+
tools: ["bash", "read"],
|
|
95
|
+
skills: [],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
export const STAGE_SPEC_FILES = {
|
|
99
|
+
"1_planning.jira": "01-planning.md",
|
|
100
|
+
"1_planning.requirements": "02-requirements.md",
|
|
101
|
+
"1_planning.scope": "03-scope.md",
|
|
102
|
+
"2_implementation.analysis": "04-analysis.md",
|
|
103
|
+
"2_implementation.dev": "05-worktree-dev.md",
|
|
104
|
+
"2_implementation.test": "06-test.md",
|
|
105
|
+
"3_delivery.commit": "07-commit.md",
|
|
106
|
+
"3_delivery.pr": "08-pr.md",
|
|
107
|
+
"3_delivery.review": "09-review-comments.md",
|
|
108
|
+
};
|
|
109
|
+
export function agentForStage(stage) {
|
|
110
|
+
if (stage.startsWith("1_planning."))
|
|
111
|
+
return AGENTS["makdoong2-planner"];
|
|
112
|
+
if (stage === "2_implementation.analysis")
|
|
113
|
+
return AGENTS["makdoong2-analyzer"];
|
|
114
|
+
if (stage.startsWith("2_implementation."))
|
|
115
|
+
return AGENTS["makdoong2-engineer"];
|
|
116
|
+
if (stage.startsWith("3_delivery."))
|
|
117
|
+
return AGENTS["makdoong2-publisher"];
|
|
118
|
+
throw new Error(`no agent registered for stage ${stage}`);
|
|
119
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type FallbackOverrideEntry = string | {
|
|
2
|
+
id: string;
|
|
3
|
+
tier?: "low" | "medium" | "high" | "max";
|
|
4
|
+
};
|
|
5
|
+
export interface AgentOverride {
|
|
6
|
+
model?: string;
|
|
7
|
+
variant?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
|
+
fallback_models?: FallbackOverrideEntry | FallbackOverrideEntry[];
|
|
9
|
+
}
|
|
10
|
+
export interface ModelPolicyConfig {
|
|
11
|
+
allowed_primaries?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface TmuxConfigJson {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
placement?: string;
|
|
16
|
+
layout?: string;
|
|
17
|
+
main_pane_size?: number;
|
|
18
|
+
agent_pane_min_width?: number;
|
|
19
|
+
split_direction?: string;
|
|
20
|
+
attach_command?: string;
|
|
21
|
+
server_url?: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface PathsConfig {
|
|
24
|
+
hooks?: string;
|
|
25
|
+
gates?: string;
|
|
26
|
+
scripts?: string;
|
|
27
|
+
stages?: string;
|
|
28
|
+
skills?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TimeoutConfig {
|
|
31
|
+
substage_minutes?: number;
|
|
32
|
+
per_agent?: Record<string, number>;
|
|
33
|
+
stall_escalate_threshold?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_STALL_ESCALATE_THRESHOLD = 5;
|
|
36
|
+
export type LogLevel = "silent" | "error" | "warn" | "info" | "debug" | "trace";
|
|
37
|
+
export type LogMode = "stdin" | "file";
|
|
38
|
+
export interface LoggingConfig {
|
|
39
|
+
level?: LogLevel;
|
|
40
|
+
mode?: LogMode;
|
|
41
|
+
path?: string | null;
|
|
42
|
+
event_max_chars?: number;
|
|
43
|
+
max_bytes?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface Makdoong2Config {
|
|
46
|
+
agents?: Record<string, AgentOverride>;
|
|
47
|
+
model_policy?: ModelPolicyConfig;
|
|
48
|
+
coverage?: {
|
|
49
|
+
threshold?: number;
|
|
50
|
+
};
|
|
51
|
+
timeout?: TimeoutConfig;
|
|
52
|
+
tmux?: TmuxConfigJson;
|
|
53
|
+
worktree?: {
|
|
54
|
+
extra_exclude?: string;
|
|
55
|
+
};
|
|
56
|
+
paths?: PathsConfig;
|
|
57
|
+
secrets?: Record<string, string>;
|
|
58
|
+
logging?: LoggingConfig;
|
|
59
|
+
}
|
|
60
|
+
export declare const LOG_LEVELS: readonly LogLevel[];
|
|
61
|
+
export declare const LOG_MODES: readonly LogMode[];
|
|
62
|
+
export interface ResolvedLoggingConfig {
|
|
63
|
+
level: LogLevel;
|
|
64
|
+
mode: LogMode;
|
|
65
|
+
path: string | null;
|
|
66
|
+
eventMaxChars: number;
|
|
67
|
+
maxBytes: number;
|
|
68
|
+
}
|
|
69
|
+
export declare const DEFAULT_LOG_MAX_BYTES: number;
|
|
70
|
+
export declare function readLoggingConfig(block?: LoggingConfig): ResolvedLoggingConfig;
|
|
71
|
+
/** Load + cache makdoong2-team.json. Returns {} when absent or invalid (defaults apply). */
|
|
72
|
+
export declare function loadConfig(): Makdoong2Config;
|
|
73
|
+
/** Resolve the five runtime path roots (JSON paths.* override → default).
|
|
74
|
+
*
|
|
75
|
+
* `skills` default is the opencode config dir (`${XDG_CONFIG_HOME:-$HOME/.config}/opencode/skills`)
|
|
76
|
+
* because scripts/install-lib.mjs deploys skill directories there, not into the
|
|
77
|
+
* package root. All other paths default to package-root subdirs.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolvePaths(): Required<PathsConfig>;
|