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/scripts/state.sh
ADDED
|
@@ -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"
|