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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/agents/makdoong2-analyzer.md +135 -0
  4. package/agents/makdoong2-engineer.md +165 -0
  5. package/agents/makdoong2-planner.md +267 -0
  6. package/agents/makdoong2-publisher.md +481 -0
  7. package/agents/makdoong2-team-leader.md +249 -0
  8. package/agents/makdoong2-verifier.md +353 -0
  9. package/assets/makdoong2-team.default.json +46 -0
  10. package/assets/makdoong2-team.schema.json +357 -0
  11. package/bin/cli.js +404 -0
  12. package/dist/agent-stage-config.d.ts +15 -0
  13. package/dist/agent-stage-config.js +119 -0
  14. package/dist/config.d.ts +79 -0
  15. package/dist/config.js +96 -0
  16. package/dist/logger.d.ts +14 -0
  17. package/dist/logger.js +131 -0
  18. package/dist/mcp-secret-injector.d.ts +56 -0
  19. package/dist/mcp-secret-injector.js +89 -0
  20. package/dist/model-chain-cli.d.ts +1 -0
  21. package/dist/model-chain-cli.js +21 -0
  22. package/dist/model-fallback-policy.d.ts +69 -0
  23. package/dist/model-fallback-policy.js +211 -0
  24. package/dist/opencode-plugin.d.ts +8 -0
  25. package/dist/opencode-plugin.js +2457 -0
  26. package/dist/poll-sub-session.d.ts +139 -0
  27. package/dist/poll-sub-session.js +494 -0
  28. package/dist/redact-secrets.d.ts +3 -0
  29. package/dist/redact-secrets.js +68 -0
  30. package/dist/session-index.d.ts +11 -0
  31. package/dist/session-index.js +71 -0
  32. package/dist/skill-mcp-registry.d.ts +59 -0
  33. package/dist/skill-mcp-registry.js +178 -0
  34. package/dist/stall-escalation.d.ts +1 -0
  35. package/dist/stall-escalation.js +22 -0
  36. package/dist/tmux-monitor.d.ts +193 -0
  37. package/dist/tmux-monitor.js +694 -0
  38. package/dist/verdict-hash.d.ts +1 -0
  39. package/dist/verdict-hash.js +62 -0
  40. package/gates/stage-analysis-verify.sh +84 -0
  41. package/gates/stage2-requirements-verify.sh +13 -0
  42. package/gates/stage3-scope-verify.sh +45 -0
  43. package/gates/stage4-dev-post-verify.sh +64 -0
  44. package/gates/stage4-dev-verify.sh +36 -0
  45. package/gates/stage5-coverage-verify.sh +36 -0
  46. package/gates/stage5-test-verify.sh +24 -0
  47. package/gates/stage6-commit-verify.sh +41 -0
  48. package/gates/stage6-post-commit-verify.sh +131 -0
  49. package/gates/stage7-post-pr-verify.sh +53 -0
  50. package/gates/stage7-pr-verify.sh +48 -0
  51. package/gates/stage8-post-review-verify.sh +84 -0
  52. package/gates/stage8-review-verify.sh +45 -0
  53. package/gates/verify.sh +44 -0
  54. package/opencode.json.example +40 -0
  55. package/package.json +84 -0
  56. package/postinstall.mjs +56 -0
  57. package/references/commit-convention.md +130 -0
  58. package/references/jira-issue-templates.md +203 -0
  59. package/references/pr-template.md +381 -0
  60. package/scripts/config.sh +46 -0
  61. package/scripts/coverage-record.sh +67 -0
  62. package/scripts/gate-policy-test.sh +152 -0
  63. package/scripts/install-lib.mjs +1029 -0
  64. package/scripts/lint-agent-prompts.sh +74 -0
  65. package/scripts/log-event.sh +44 -0
  66. package/scripts/model-policy.mjs +183 -0
  67. package/scripts/publish-if-changed.sh +207 -0
  68. package/scripts/release.sh +276 -0
  69. package/scripts/rollback-commits.sh +35 -0
  70. package/scripts/smoke-test.mjs +194 -0
  71. package/scripts/state.sh +192 -0
  72. package/scripts/test-postinstall.mjs +141 -0
  73. package/scripts/with-fallback.sh +56 -0
  74. package/scripts/wt-sync-ignored.sh +193 -0
  75. package/skills/_lib/load-secret.sh +149 -0
  76. package/skills/bamboo-ci/SKILL.md +81 -0
  77. package/skills/bamboo-ci/run-bamboo.sh +23 -0
  78. package/skills/bitbucket-research/SKILL.md +87 -0
  79. package/skills/bitbucket-research/run-repos.sh +23 -0
  80. package/skills/confluence-research/SKILL.md +75 -0
  81. package/skills/confluence-research/run-docs.sh +23 -0
  82. package/skills/github-oss-research/SKILL.md +59 -0
  83. package/skills/jira-research/SKILL.md +75 -0
  84. package/skills/jira-research/run-works.sh +23 -0
  85. package/src/hooks/guard-bash.sh +67 -0
  86. package/src/hooks/session-start.sh +96 -0
  87. package/src/hooks/sync-state.sh +47 -0
  88. package/stages/01-jira.md +43 -0
  89. package/stages/01-planning.md +229 -0
  90. package/stages/02-requirements.md +298 -0
  91. package/stages/03-scope.md +81 -0
  92. package/stages/04-analysis.md +281 -0
  93. package/stages/05-worktree-dev.md +124 -0
  94. package/stages/06-test.md +161 -0
  95. package/stages/07-commit.md +229 -0
  96. package/stages/08-pr.md +177 -0
  97. package/stages/09-review-comments.md +277 -0
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # state.sh — makdoong2-team workflow 상태 파일(state.json) read/write 헬퍼.
3
+ # 상태 파일 경로: <현재 컨텍스트 git toplevel>/.makdoong2-team/<issue>/state.json
4
+ # (main repo에서 호출: main repo, worktree에서 호출: worktree — 실행 컨텍스트 local)
5
+ #
6
+ # 사용:
7
+ # state.sh root # 현재 컨텍스트의 git toplevel 출력 (worktree-local)
8
+ # state.sh issue # 현재 브랜치에서 이슈키 추출 (없으면 빈 값)
9
+ # state.sh init <issue> [worktree] # state.json 최초 생성 (이미 있으면 auto-migrate)
10
+ # state.sh get <issue> <jq-path> # 값 조회. 예: '.stages."3_delivery".substages."commit".done'
11
+ # state.sh set <issue> <jq-path> <json-value># 값 설정. 문자열은 '"pass"' 형태로 전달
12
+ # state.sh migrate <issue> # legacy stage key → 계층형 substages 이관 (idempotent)
13
+ #
14
+ # ── 스키마 규약 (hardrule) ──
15
+ # state.json 은 hierarchical 스키마다:
16
+ # .stages."<PHASE>".substages."<SUBSTAGE>".<field>
17
+ # 예: .stages."1_planning".substages."requirements".done
18
+ #
19
+ # flat 표기 `.stages."<PHASE>.<SUBSTAGE>"` (예: `.stages."1_planning.requirements"`) 는
20
+ # phantom 키를 생성하여 verifier / auto_advance_stage 를 무력화한다. `set` 및 `get`
21
+ # 명령이 flat 표기를 감지하면 정확한 hierarchical 대체 경로를 안내하며 exit 65 로
22
+ # 즉시 중단한다. 예외: `.policy.*` 하위 딕셔너리 키는 flat 형태 (auto_approve.
23
+ # "1_planning.requirements": true) 를 유지한다 — opencode-plugin.ts 의 STAGE_ORDER
24
+ # 상수가 flat 스타일 stage-id 로 정의되어 있기 때문.
25
+ #
26
+ # 의존: jq
27
+ set -euo pipefail
28
+
29
+ root() {
30
+ # 호출 컨텍스트의 git toplevel 을 반환한다.
31
+ # - main repo에서 호출: main repo 경로
32
+ # - worktree에서 호출: 해당 worktree 경로
33
+ # 각 실행 컨텍스트가 자신의 state.json 사본을 사용하므로
34
+ # external-directory guard 가 발동하지 않는다.
35
+ # sub-agent 세션 시작 전 wt-sync-ignored.sh 가 이미 .makdoong2-team/<issue>/
36
+ # 를 worktree로 복사하고, 완료 후 --reverse 로 main repo에 병합한다.
37
+ git rev-parse --show-toplevel 2>/dev/null \
38
+ || pwd
39
+ }
40
+ issue() {
41
+ # 현재 브랜치명에서 이슈키 추출. worktree(feature/PROJ-123) 컨텍스트에서만 신뢰성 있음.
42
+ # main repo(branch=main)에서 호출하면 빈 문자열 반환 — hooks/guards 는 이 함수 대신
43
+ # `root()` + `git worktree list --porcelain` 조합으로 ISSUE를 추출해야 한다.
44
+ git rev-parse --abbrev-ref HEAD 2>/dev/null | grep -oE '[A-Z]+-[0-9]+' | head -n1 || true
45
+ }
46
+ sp() { echo "$(root)/.makdoong2-team/$1/state.json"; }
47
+
48
+ # ── phantom-key guard ──
49
+ # jq path 안에 `.stages."<PHASE>.<SUBSTAGE>"` 형태 (dot 포함 stage-id 를 단일 키로
50
+ # 지정) 가 나타나면 flat 표기로 판정한다. hierarchical 표기는 dot 이 phase 밖으로
51
+ # 벗어나 있으므로 (`.stages."1_planning".substages."jira"`) 이 패턴에 매칭되지
52
+ # 않는다.
53
+ #
54
+ # 매칭되면 정확한 대체 경로를 안내하고 exit 65 로 즉시 실패한다.
55
+ check_flat_stage_notation() {
56
+ local q="$1"
57
+ if [[ "$q" =~ \.stages\.\"([0-9]+_[a-z_]+)\.([a-z]+)\" ]]; then
58
+ local phase="${BASH_REMATCH[1]}"
59
+ local sub="${BASH_REMATCH[2]}"
60
+ local suggested=".stages.\"${phase}\".substages.\"${sub}\""
61
+ cat >&2 <<ERR
62
+ [state.sh] flat stage notation detected — refusing to touch state.json.
63
+ jq path : $q
64
+ problem : ".stages.\"${phase}.${sub}\"" creates a phantom key that
65
+ verifier / auto_advance_stage never read (they use hierarchical).
66
+ fix : replace with hierarchical form:
67
+ $suggested
68
+ legacy : if you have existing flat/phantom nodes, run 'state.sh migrate <issue>'
69
+ to normalize them into the hierarchical tree.
70
+ ERR
71
+ exit 65
72
+ fi
73
+ }
74
+
75
+ cmd="${1:-}"; shift || true
76
+ case "$cmd" in
77
+ root) root ;;
78
+ issue) issue ;;
79
+ init)
80
+ ISSUE="${1:?issue required}"; WT="${2:-$(root)}"
81
+ P="$(sp "$ISSUE")"; mkdir -p "$(dirname "$P")"
82
+ if [ ! -f "$P" ]; then
83
+ cat > "$P" <<JSON
84
+ {
85
+ "issue": "$ISSUE",
86
+ "worktree": "$WT",
87
+ "stages": {
88
+ "1_planning": {
89
+ "done": false,
90
+ "substages": {
91
+ "jira": {"done": false},
92
+ "requirements": {"done": false, "approved_by_user": false},
93
+ "scope": {"done": false, "approved_by_user": false}
94
+ }
95
+ },
96
+ "2_implementation": {
97
+ "done": false,
98
+ "substages": {
99
+ "analysis": {
100
+ "done": false,
101
+ "skipped": false,
102
+ "skip_reason": null,
103
+ "artifact_path": null,
104
+ "self_check": {}
105
+ },
106
+ "dev": {"done": false},
107
+ "test": {"done": false, "unit": "none", "integration": "none", "coverage": "none"}
108
+ }
109
+ },
110
+ "3_delivery": {
111
+ "done": false,
112
+ "substages": {
113
+ "commit": {"done": false},
114
+ "pr": {"draft_url": null},
115
+ "review": {"comments": 0, "comments_per_commit": {}, "plan_path": null, "all_comments_inline": false}
116
+ }
117
+ }
118
+ },
119
+ "policy": null
120
+ }
121
+ JSON
122
+ else
123
+ "$0" migrate "$ISSUE" >/dev/null 2>&1 || true
124
+ fi
125
+ echo "$P" ;;
126
+ get)
127
+ ISSUE="${1:?}"; Q="${2:?}"; P="$(sp "$ISSUE")"
128
+ check_flat_stage_notation "$Q"
129
+ # Semantics contract (rely on this in all gate scripts):
130
+ # * always print exactly one line = the value (jq -r style: null/false/0/{}/"str")
131
+ # * exit 0 for any value that jq successfully evaluated, including null and false
132
+ # * exit 1 only when jq errored (bad expression, corrupt JSON) or file missing
133
+ #
134
+ # Rationale: the historical implementation used `jq -er` which conflates
135
+ # "value is null/false" with "path missing" and emits both jq's own "null"
136
+ # AND a fallback "null" (producing "null\nnull" for null values). Downstream
137
+ # gates then wrap with `|| echo "__MISSING__"` and check `[ "$U" = "null" ]`
138
+ # or `[ "$U" = "__MISSING__" ]`. Multi-line output breaks every such check.
139
+ # Using `jq -r` (no -e) gives clean one-line output and success exit; the
140
+ # `if` form suppresses `set -e` on parse errors so we can emit "null" as
141
+ # missing-marker without doubling.
142
+ if OUT="$(jq -r "$Q" "$P" 2>/dev/null)"; then
143
+ printf '%s\n' "$OUT"
144
+ else
145
+ echo "null"; exit 1
146
+ fi
147
+ ;;
148
+ set)
149
+ ISSUE="${1:?}"; Q="${2:?}"; V="${3:?}"; P="$(sp "$ISSUE")"
150
+ check_flat_stage_notation "$Q"
151
+ tmp="$(mktemp)"; jq "$Q = $V" "$P" > "$tmp" && mv "$tmp" "$P"
152
+ echo "state[$ISSUE] $Q = $V" ;;
153
+ append)
154
+ ISSUE="${1:?}"; Q="${2:?}"; V="${3:?}"; P="$(sp "$ISSUE")"
155
+ check_flat_stage_notation "$Q"
156
+ tmp="$(mktemp)"
157
+ jq --argjson entry "$V" "$Q = ((($Q) // []) + [\$entry])" "$P" > "$tmp" && mv "$tmp" "$P"
158
+ echo "state[$ISSUE] $Q += $V" ;;
159
+ migrate)
160
+ ISSUE="${1:?issue required}"; P="$(sp "$ISSUE")"
161
+ [ -f "$P" ] || { echo "state.json not found: $P" >&2; exit 1; }
162
+ tmp="$(mktemp)"
163
+ jq '
164
+ def move($legacy; $phase; $sub):
165
+ if has("stages") and (.stages | has($legacy)) then
166
+ .stages[$phase] = (.stages[$phase] // {"done": false, "substages": {}})
167
+ | .stages[$phase].substages = (.stages[$phase].substages // {})
168
+ | .stages[$phase].substages[$sub] =
169
+ ((.stages[$phase].substages[$sub] // {}) * .stages[$legacy])
170
+ | del(.stages[$legacy])
171
+ else . end;
172
+
173
+ move("1_jira"; "1_planning"; "jira")
174
+ | move("2_requirements"; "1_planning"; "requirements")
175
+ | move("3_scope"; "1_planning"; "scope")
176
+ | move("4_dev"; "2_implementation"; "dev")
177
+ | move("5_test"; "2_implementation"; "test")
178
+ | move("6_commit"; "3_delivery"; "commit")
179
+ | move("7_pr"; "3_delivery"; "pr")
180
+ | move("8_review"; "3_delivery"; "review")
181
+ | move("2_implementation.dev"; "2_implementation"; "dev")
182
+ | move("2_implementation.test"; "2_implementation"; "test")
183
+ | move("1_planning.jira"; "1_planning"; "jira")
184
+ | move("1_planning.requirements"; "1_planning"; "requirements")
185
+ | move("1_planning.scope"; "1_planning"; "scope")
186
+ | move("3_delivery.commit"; "3_delivery"; "commit")
187
+ | move("3_delivery.pr"; "3_delivery"; "pr")
188
+ | move("3_delivery.review"; "3_delivery"; "review")
189
+ ' "$P" > "$tmp" && mv "$tmp" "$P"
190
+ echo "migrated[$ISSUE] $P" ;;
191
+ *) echo "usage: state.sh {root|issue|init|get|set|append|migrate} ..." >&2; exit 64 ;;
192
+ esac
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ // scripts/test-postinstall.mjs — integration test for npm install -g postinstall hook
3
+ //
4
+ // Simulates global install in isolated HOME directory:
5
+ // 1) npm pack → produces tarball
6
+ // 2) npm install -g <tarball> with isolated HOME
7
+ // 3) Verify deployment artifacts
8
+ // 4) Verify idempotency (re-install doesn't create duplicate backups)
9
+ // 5) Verify secrets.env preservation
10
+
11
+ import { mkdtempSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync, statSync } from "node:fs";
12
+ import { tmpdir, platform } from "node:os";
13
+ import { join, dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { spawnSync } from "node:child_process";
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url));
18
+ const PKG_ROOT = join(HERE, "..");
19
+
20
+ function run(cmd, args, opts = {}) {
21
+ const result = spawnSync(cmd, args, { stdio: "pipe", encoding: "utf8", ...opts });
22
+ if (result.error) throw result.error;
23
+ return result;
24
+ }
25
+
26
+ function assert(cond, msg) {
27
+ if (!cond) {
28
+ console.error(`✗ FAIL: ${msg}`);
29
+ process.exit(1);
30
+ }
31
+ console.log(`✓ ${msg}`);
32
+ }
33
+
34
+ console.log("[test-postinstall] Starting npm install -g smoke test");
35
+
36
+ // 1) npm pack
37
+ console.log("\n[1/6] Running npm pack...");
38
+ const packResult = run("npm", ["pack"], { cwd: PKG_ROOT });
39
+ if (packResult.status !== 0) {
40
+ console.error("npm pack failed:", packResult.stderr);
41
+ process.exit(1);
42
+ }
43
+ const tarballLine = packResult.stdout.trim().split("\n").pop();
44
+ const tarball = join(PKG_ROOT, tarballLine);
45
+ assert(existsSync(tarball), `tarball created: ${tarball}`);
46
+
47
+ try {
48
+ // 2) Create isolated HOME
49
+ console.log("\n[2/6] Setting up isolated HOME...");
50
+ const tmpHome = mkdtempSync(join(tmpdir(), "mkd2-postinstall-"));
51
+ const npmPrefix = join(tmpHome, "npm");
52
+ const configDir = join(tmpHome, ".config", "opencode");
53
+
54
+ console.log(` tmpHome: ${tmpHome}`);
55
+ console.log(` npmPrefix: ${npmPrefix}`);
56
+ console.log(` configDir: ${configDir}`);
57
+
58
+ // 3) npm install -g with isolated env
59
+ console.log("\n[3/6] Running npm install -g (first install)...");
60
+ const installEnv = {
61
+ ...process.env,
62
+ HOME: tmpHome,
63
+ npm_config_prefix: npmPrefix,
64
+ npm_config_global: "true",
65
+ CI: undefined,
66
+ CONTINUOUS_INTEGRATION: undefined,
67
+ };
68
+ const installResult = run("npm", ["install", "-g", tarball], { env: installEnv });
69
+ if (installResult.status !== 0) {
70
+ console.error("npm install -g failed:", installResult.stderr);
71
+ process.exit(1);
72
+ }
73
+ console.log(" npm install -g completed");
74
+
75
+ // 4) Verify deployment artifacts
76
+ console.log("\n[4/6] Verifying deployment artifacts...");
77
+
78
+ // Binary shim
79
+ const isWin = platform() === "win32";
80
+ const binName = isWin ? "makdoong2-team.cmd" : "makdoong2-team";
81
+ const binPath = join(npmPrefix, "bin", binName);
82
+ assert(existsSync(binPath), `binary shim exists: ${binPath}`);
83
+
84
+ // Agents in configDir
85
+ const agentPath = join(configDir, "agents", "makdoong2-team-leader.md");
86
+ assert(existsSync(agentPath), `agent deployed to configDir: ${agentPath}`);
87
+
88
+ // Skills in configDir
89
+ const skillPath = join(configDir, "skills", "jira-research", "SKILL.md");
90
+ assert(existsSync(skillPath), `skill deployed to configDir: ${skillPath}`);
91
+
92
+ // opencode.json plugin ref (relative path to npm module)
93
+ const ocPath = join(configDir, "opencode.json");
94
+ assert(existsSync(ocPath), `opencode.json created: ${ocPath}`);
95
+ const oc = JSON.parse(readFileSync(ocPath, "utf8"));
96
+ assert(
97
+ Array.isArray(oc.plugin) && oc.plugin.some(p =>
98
+ (typeof p === "string" && p.includes("opencode-plugin.ts")) ||
99
+ (Array.isArray(p) && p[0].includes("opencode-plugin.ts"))
100
+ ),
101
+ "opencode.json has plugin ref"
102
+ );
103
+ assert(oc.tools?.verify_stage === true, "opencode.json has tools");
104
+
105
+ // makdoong2-team doctor
106
+ console.log("\n[5/6] Running makdoong2-team doctor...");
107
+ const doctorResult = run(binPath, ["doctor"], { env: installEnv });
108
+ assert(doctorResult.status === 0, "makdoong2-team doctor exits 0");
109
+
110
+ // 5) Re-install (idempotency test)
111
+ console.log("\n[6/6] Re-installing (idempotency check)...");
112
+ const beforeOcBackups = readdirSync(configDir).filter(f => f.startsWith("opencode.json.bak."));
113
+ const reinstallResult = run("npm", ["install", "-g", tarball], { env: installEnv });
114
+ assert(reinstallResult.status === 0, "re-install succeeds");
115
+
116
+ const afterOcBackups = readdirSync(configDir).filter(f => f.startsWith("opencode.json.bak."));
117
+ assert(
118
+ afterOcBackups.length === beforeOcBackups.length,
119
+ `no new opencode.json backups on re-install (idempotent): ${beforeOcBackups.length} → ${afterOcBackups.length}`
120
+ );
121
+
122
+ // 6) secrets.env preservation
123
+ console.log("\n[Bonus] Testing secrets.env preservation...");
124
+ const secretsPath = join(configDir, "skills", "jira-research", "secrets.env");
125
+ const secretContent = "JIRA_TOKEN=test123";
126
+ writeFileSync(secretsPath, secretContent);
127
+
128
+ const reinstall2Result = run("npm", ["install", "-g", tarball], { env: installEnv });
129
+ assert(reinstall2Result.status === 0, "third install succeeds");
130
+
131
+ const afterSecrets = readFileSync(secretsPath, "utf8");
132
+ assert(afterSecrets === secretContent, "secrets.env preserved across re-install");
133
+
134
+ console.log("\n✅ [test-postinstall] All checks passed!");
135
+
136
+ // Cleanup
137
+ rmSync(tmpHome, { recursive: true, force: true });
138
+ } finally {
139
+ // Cleanup tarball
140
+ if (existsSync(tarball)) rmSync(tarball);
141
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # with-fallback.sh — production-grade model fallback wrapper (Track B).
3
+ #
4
+ # Wraps `opencode run` and retries with the next model in an agent's chain
5
+ # when the LLM call hits a rate limit / 5xx. Complements the in-plugin
6
+ # `next_model` tool by handling cases where the failure aborts the run
7
+ # before the agent can call the tool itself.
8
+ #
9
+ # Usage:
10
+ # with-fallback.sh <agent-id> -- <opencode args>
11
+ #
12
+ # Example:
13
+ # with-fallback.sh stage4-dev -- run --agent stage4-dev "Implement PROJ-12345"
14
+ #
15
+ # Reads model chain from the same model-router.ts via a tiny shim:
16
+ # node $SCRIPT_DIR/../dist/model-chain-cli.js <agent-id>
17
+ # (the shim emits JSON: [{"id": "...", "variant": "..."}, ...])
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+
22
+ AGENT="${1:?usage: with-fallback.sh <agent-id> -- <opencode args>}"
23
+ shift
24
+ [ "${1:-}" = "--" ] && shift || true
25
+
26
+ CHAIN_JSON="$(node "$SCRIPT_DIR/../dist/model-chain-cli.js" "$AGENT" 2>/dev/null || echo '[]')"
27
+ LEN=$(printf '%s' "$CHAIN_JSON" | jq 'length')
28
+
29
+ if [ "$LEN" -eq 0 ]; then
30
+ echo "[with-fallback] no chain for $AGENT — running primary only" >&2
31
+ exec opencode "$@"
32
+ fi
33
+
34
+ for i in $(seq 0 $((LEN - 1))); do
35
+ MODEL=$(printf '%s' "$CHAIN_JSON" | jq -r ".[$i].id")
36
+ echo "[with-fallback] attempt $((i+1))/$LEN with model=$MODEL" >&2
37
+
38
+ # Inject --model. If user already passed --model, that wins (skip injection).
39
+ if printf '%s\n' "$@" | grep -qE '^--model$|^--model='; then
40
+ opencode "$@" && exit 0
41
+ else
42
+ opencode --model "$MODEL" "$@" && exit 0
43
+ fi
44
+ CODE=$?
45
+
46
+ # Decide whether to retry. Treat 1 / 124 (timeout) / 137 (oom) as
47
+ # retryable; SIGINT (130) and everything else as terminal so user cancel
48
+ # or real errors are not masked.
49
+ case "$CODE" in
50
+ 1|124|137) echo "[with-fallback] exit $CODE — trying next model" >&2 ;;
51
+ *) echo "[with-fallback] exit $CODE — terminal, not retrying" >&2; exit "$CODE" ;;
52
+ esac
53
+ done
54
+
55
+ echo "[with-fallback] chain exhausted for $AGENT" >&2
56
+ exit 1
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ # wt-sync-ignored.sh <worktree-path> [issue]
3
+ # 메인 repo의 gitignore된(추적 안 되는) 파일을 새 worktree로 복사한다.
4
+ # [issue]: 특정 이슈 스코프의 .makdoong2-team/<issue>/ 디렉토리만 복사 (다른 이슈 디렉토리는 제외).
5
+ # .env, .env.local, .idea/, .metals/, .bsp/ 등 로컬 셋업 파일이
6
+ # worktree마다 수동 복사되지 않도록 자동화한다.
7
+ #
8
+ # 기본 제외: 빌드 산출물·캐시 디렉토리(target/, build/, .gradle/, node_modules/, .venv/, .bloop/ 등).
9
+ # 워크트리마다 독립 빌드를 권장하므로 빌드 산출물은 복사하지 않는다.
10
+ # 추가 제외가 필요하면 makdoong2-team.json 의 worktree.extra_exclude 에 ':' 구분으로 지정.
11
+ # 예: "worktree": { "extra_exclude": ".idea/:tmp/" }
12
+ set -euo pipefail
13
+
14
+ # --reverse: worktree → main repo 역방향 동기화 (issue-scoped .makdoong2-team/<issue>/ 만)
15
+ REVERSE=false
16
+ while [[ "${1:-}" == --* ]]; do
17
+ case "$1" in
18
+ --reverse) REVERSE=true; shift ;;
19
+ *) echo "[wt-sync-ignored] 알 수 없는 옵션: $1" >&2; exit 1 ;;
20
+ esac
21
+ done
22
+
23
+ WT="${1:?usage: $0 [--reverse] <worktree-path> [issue]}"
24
+ ISSUE="${2:-}"
25
+ [ -d "$WT" ] || { echo "[wt-sync-ignored] worktree 경로 없음: $WT" >&2; exit 1; }
26
+
27
+ # 메인 repo 식별 (worktree 안/밖 어디서 실행해도 동일)
28
+ MAIN="$(git -C "$WT" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
29
+ { [ -n "$MAIN" ] && [ -d "$MAIN" ]; } || { echo "[wt-sync-ignored] 메인 repo 식별 실패" >&2; exit 1; }
30
+ if [ "$WT" = "$MAIN" ]; then
31
+ echo "[wt-sync-ignored] worktree와 메인이 동일 경로 — 종료"
32
+ exit 0
33
+ fi
34
+
35
+ # ── reverse 모드: worktree → main repo (issue-scoped) ─────────────────────────
36
+ if $REVERSE; then
37
+ if [ -z "$ISSUE" ]; then
38
+ echo "[wt-sync-ignored] --reverse 는 issue 인자 필수" >&2; exit 1
39
+ fi
40
+ SRC_DIR="$WT/.makdoong2-team/$ISSUE"
41
+ DST_DIR="$MAIN/.makdoong2-team/$ISSUE"
42
+ if [ -d "$SRC_DIR" ]; then
43
+ mkdir -p "$DST_DIR"
44
+ cp -a "$SRC_DIR/." "$DST_DIR/"
45
+ r_count=$(find "$SRC_DIR" -type f | wc -l | tr -d ' ')
46
+ echo "[wt-sync-ignored] reverse sync +${r_count} files: $WT → $MAIN (issue=$ISSUE)"
47
+ else
48
+ echo "[wt-sync-ignored] reverse sync: source 없음: $SRC_DIR" >&2
49
+ fi
50
+ exit 0
51
+ fi
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ # 기본 제외 (디렉토리는 '/' 끝)
55
+ DEFAULT_EXCLUDES=(
56
+ "target/" "build/" "out/" "dist/"
57
+ "node_modules/" ".gradle/" ".venv/"
58
+ "__pycache__/" ".pytest_cache/" ".mypy_cache/"
59
+ ".bloop/" "project/target/" "project/project/"
60
+ )
61
+ EXCLUDES=("${DEFAULT_EXCLUDES[@]}")
62
+ EXTRA_EXCLUDE="$("$(cd "$(dirname "$0")" && pwd)/config.sh" get worktree.extra_exclude "")"
63
+ if [ -n "$EXTRA_EXCLUDE" ]; then
64
+ IFS=':' read -ra extra <<< "$EXTRA_EXCLUDE"
65
+ EXCLUDES+=("${extra[@]}")
66
+ fi
67
+
68
+ # 제외 패턴 매치 함수: 경로 $1 이 EXCLUDES 어느 하나에 걸리면 0(true)
69
+ is_excluded() {
70
+ local f="$1" ex
71
+ if [ -n "$ISSUE" ] && [[ "$f" == ".makdoong2-team/"* ]]; then
72
+ return 0
73
+ fi
74
+ for ex in "${EXCLUDES[@]}"; do
75
+ if [[ "${ex: -1}" == "/" ]]; then
76
+ # 디렉토리 패턴: prefix 매치
77
+ [[ "$f" == "$ex"* || "$f" == "${ex%/}" ]] && return 0
78
+ else
79
+ # 파일 패턴: 정확 일치 또는 경로 하위
80
+ [[ "$f" == "$ex" || "$f" == "$ex/"* ]] && return 0
81
+ fi
82
+ done
83
+ return 1
84
+ }
85
+
86
+ # 메인 repo의 gitignore된 파일/디렉토리 목록 (null-delimited)
87
+ copied=0; skipped=0
88
+ while IFS= read -r -d '' f; do
89
+ if is_excluded "$f"; then
90
+ skipped=$((skipped+1))
91
+ continue
92
+ fi
93
+ src="$MAIN/$f"
94
+ dst="$WT/$f"
95
+ # f가 디렉토리로 끝나면(/) 통째로, 아니면 파일로 복사
96
+ if [[ "${f: -1}" == "/" ]]; then
97
+ mkdir -p "$dst"
98
+ cp -a "$src." "$dst" 2>/dev/null || cp -a "$src" "$(dirname "$dst")/"
99
+ else
100
+ mkdir -p "$(dirname "$dst")"
101
+ cp -a "$src" "$dst"
102
+ fi
103
+ copied=$((copied+1))
104
+ done < <(cd "$MAIN" && git ls-files --others --ignored --exclude-standard -z)
105
+
106
+ echo "[wt-sync-ignored] 복사 $copied / 제외 $skipped (제외 패턴: ${EXCLUDES[*]})"
107
+ echo "[wt-sync-ignored] 소스: $MAIN"
108
+ echo "[wt-sync-ignored] 대상: $WT"
109
+
110
+ # issue-scoped whitelist
111
+ if [ -n "$ISSUE" ]; then
112
+ wl_copied=0
113
+ SRC_DIR="$MAIN/.makdoong2-team/$ISSUE"
114
+ DST_DIR="$WT/.makdoong2-team/$ISSUE"
115
+ if [ -d "$SRC_DIR" ]; then
116
+ mkdir -p "$DST_DIR"
117
+ cp -a "$SRC_DIR/." "$DST_DIR/"
118
+ wl_copied=$(find "$SRC_DIR" -type f | wc -l | tr -d ' ')
119
+ fi
120
+ echo "[wt-sync-ignored] whitelist +$wl_copied (issue=$ISSUE)"
121
+ else
122
+ echo "[wt-sync-ignored] whitelist skipped (issue 인자 없음)"
123
+ fi
124
+
125
+ # Baseline .git/info/exclude template — 프로젝트 종류를 감지해 부산물 패턴을 추가한다.
126
+ # .gitignore 파일 대신 git 공통 디렉토리의 info/exclude 에 기록하므로
127
+ # 모든 linked worktree 에 자동 적용되며 저장소에 커밋되지 않는다.
128
+ # publisher 의 stage7 entry gate 가 pytest 결과물 (.coverage, .pytest_cache/) 이나
129
+ # node_modules 부산물 때문에 false-positive 로 차단되는 문제를 예방.
130
+ # 이미 존재하는 라인은 skip 하여 사용자 편집을 덮어쓰지 않는다.
131
+ ensure_baseline_gitexclude() {
132
+ local wt=$1
133
+ local git_common_dir
134
+ git_common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null)"
135
+ if [ -z "$git_common_dir" ]; then
136
+ echo "[wt-sync-ignored] git-common-dir 식별 실패, exclude 스킵" >&2
137
+ return 0
138
+ fi
139
+ if [[ "$git_common_dir" != /* ]]; then
140
+ git_common_dir="$(cd "$wt/$git_common_dir" 2>/dev/null && pwd -P)" || {
141
+ echo "[wt-sync-ignored] git-common-dir 절대경로 변환 실패" >&2
142
+ return 0
143
+ }
144
+ fi
145
+ local exclude_file="$git_common_dir/info/exclude"
146
+ mkdir -p "$(dirname "$exclude_file")"
147
+ local added=0
148
+ local -a lines=()
149
+
150
+ # 공통 (모든 프로젝트)
151
+ lines+=(".DS_Store" "Thumbs.db" "*.log" "*.swp")
152
+
153
+ # Python
154
+ if [ -f "$wt/pyproject.toml" ] || [ -f "$wt/requirements.txt" ] || [ -f "$wt/setup.py" ] || [ -f "$wt/setup.cfg" ]; then
155
+ lines+=("__pycache__/" "*.pyc" "*.pyo" ".coverage" ".coverage.*" "htmlcov/" ".pytest_cache/" ".mypy_cache/" ".ruff_cache/" ".tox/" "*.egg-info/" "dist/" "build/" ".venv/" "venv/")
156
+ fi
157
+
158
+ # Node
159
+ if [ -f "$wt/package.json" ]; then
160
+ lines+=("node_modules/" ".npm/" ".pnp.*" ".yarn/*" "!.yarn/patches" "!.yarn/releases" "!.yarn/plugins" "!.yarn/sdks" "!.yarn/versions" ".next/" ".nuxt/" ".turbo/" "coverage/" ".nyc_output/")
161
+ fi
162
+
163
+ # JVM (Gradle/Maven/sbt)
164
+ if [ -f "$wt/build.gradle" ] || [ -f "$wt/build.gradle.kts" ] || [ -f "$wt/pom.xml" ] || [ -f "$wt/build.sbt" ]; then
165
+ lines+=("target/" "build/" ".gradle/" ".idea/" "*.iml" ".bloop/" ".metals/" ".bsp/" "project/target/" "project/project/")
166
+ fi
167
+
168
+ # Rust
169
+ if [ -f "$wt/Cargo.toml" ]; then
170
+ lines+=("target/" "Cargo.lock.bak")
171
+ fi
172
+
173
+ # Go
174
+ if [ -f "$wt/go.mod" ]; then
175
+ lines+=("vendor/" "*.test" "*.out")
176
+ fi
177
+
178
+ # 존재하지 않는 라인만 append
179
+ touch "$exclude_file"
180
+ local ln
181
+ for ln in "${lines[@]}"; do
182
+ if ! grep -qxF -- "$ln" "$exclude_file" 2>/dev/null; then
183
+ printf '%s\n' "$ln" >> "$exclude_file"
184
+ added=$((added+1))
185
+ fi
186
+ done
187
+
188
+ if [ "$added" -gt 0 ]; then
189
+ echo "[wt-sync-ignored] baseline .git/info/exclude +$added lines (project-type detected)"
190
+ fi
191
+ }
192
+
193
+ ensure_baseline_gitexclude "$WT"