makdoong2-team 3.0.1 → 3.0.3
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/agents/makdoong2-team-leader.md +25 -0
- package/agents/makdoong2-verifier.md +11 -1
- package/dist/opencode-plugin.js +156 -5
- package/dist/poll-sub-session.d.ts +22 -0
- package/dist/poll-sub-session.js +58 -4
- package/gates/stage4-dev-post-verify.sh +37 -0
- package/package.json +1 -1
- package/scripts/run-tests.mts +1 -0
- package/scripts/state.sh +32 -0
- package/stages/01-planning.md +15 -0
- package/stages/02-requirements.md +22 -0
- package/stages/05-worktree-dev.md +37 -1
|
@@ -121,6 +121,28 @@ permission:
|
|
|
121
121
|
|
|
122
122
|
`ok: true` 만 보고 넘어가지 말 것 — `paused` 와 `unknown` 도 `ok: true` 이며, 둘 다 substage 는 끝나지 않았다.
|
|
123
123
|
|
|
124
|
+
## `permission_stall` 은 "승인 대기" 가 아니다 — 이미 종료된 것이다 (hardrule)
|
|
125
|
+
|
|
126
|
+
`dispatch_stage` 가 `outcome_kind: "permission_stall"` 을 반환하면, 그 시점에 이미 **권한 요청은 자동 거부됐고 서브세션은 abort 됐다.** 헤드리스 서브세션에는 승인을 받을 채널 자체가 없어서 훅이 대신 거부한 것이다 — 사용자가 승인할 대상이 애초에 존재하지 않는다.
|
|
127
|
+
|
|
128
|
+
**금지**: "worktree 접근 권한 승인 대기 — 승인한 뒤 재개해 주세요" 류의 보고. 실제로 이 오판이 워크플로를 세웠다. 도구가 반환한 `output` 에는 `aborted after {N}ms` 로 **종료됐다는 사실**이 적혀 있었고 "승인을 기다리라" 는 지시는 한 글자도 없었다 (GitHub issue #12). 응답의 `awaiting_user_approval: false` / `session_aborted: true` 가 그 점을 못 박는다.
|
|
129
|
+
|
|
130
|
+
읽어야 할 필드는 셋이다:
|
|
131
|
+
|
|
132
|
+
| 필드 | 뜻 |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `permission_reason` | `outside_allowed_roots` / `non_external_permission` / `tool_call_stall` — **처방이 서로 다르다** |
|
|
135
|
+
| `permission_patterns` | 실제로 차단된 경로 패턴 |
|
|
136
|
+
| `permission_scope` | 자동 승인되는 범위 (worktree 의 부모 디렉토리 이하) |
|
|
137
|
+
|
|
138
|
+
`permission_reason` 별 조치:
|
|
139
|
+
|
|
140
|
+
- **`outside_allowed_roots`** — 서브에이전트가 워크스페이스 밖 경로를 요청했다. `dispatch_stage` 가 이미 허용 범위를 프롬프트에 주입해 자동 재디스패치를 시도했고(`attempts` 확인), 그 예산까지 소진한 상태다. **같은 인자로 재호출하지 않는다.** `permission_patterns` 와 `permission_scope` 를 그대로 인용해 보고하고 사용자 지시를 기다린다. 스코프를 넓혀 달라는 요청은 하지 않는다 — 조부모 이상을 여는 것은 형제 프로젝트 전체를 사람 확인 없이 승인 대상으로 만드는 일이라 설계상 거부된다.
|
|
141
|
+
- **`non_external_permission`** — 경로 문제가 아니라 해당 에이전트 frontmatter 의 `permission:` 블록 문제다 (정식 키는 `edit`; `write` 는 존재하지 않아 조용히 무시된다). 사용자에게 보고한다.
|
|
142
|
+
- **`tool_call_stall`** — 부분 설치 의심. `npx makdoong2-team doctor` 실행을 안내한다.
|
|
143
|
+
|
|
144
|
+
세 경우 모두 `next_action` 을 그대로 따른다 (하드룰 4). 이 실패는 `hang_history` 에 `reason: "permission_stall:<사유>"` 로 기록되므로, 반복 호출하면 `stall_escalate_threshold` 에서 차단된다.
|
|
145
|
+
|
|
124
146
|
## verdict 는 셋이다 — REJECTED 와 ERROR 를 절대 섞지 말 것 (hardrule)
|
|
125
147
|
|
|
126
148
|
`dispatch_verifier` 의 `verdict` 는 `VERIFIED` / `REJECTED` / `ERROR` 세 값이다.
|
|
@@ -147,6 +169,8 @@ permission:
|
|
|
147
169
|
2. **REJECTED 사유는 dispatch_verifier 가 자동으로 state.json 에 기록**한다 (`last_verdict_reason` / `last_verdict_reason_hash` / `same_reason_streak` / `rejected_count`). 부장님이 별도 기록할 필요 없다.
|
|
148
170
|
3. **재-dispatch 시 dispatch_stage 가 자동으로 이전 사유를 프롬프트에 재주입**한다. 부장님은 그냥 `dispatch_stage(issue, target_stage, worktree)` 를 다시 호출하면 된다.
|
|
149
171
|
4. **동일 REJECTED 사유 연속 5회 감지 시 dispatch_verifier 응답에 `same_reason_streak_exceeded: true`** 가 포함된다. 이때는 재시도를 중단하고 사용자에게 상황을 보고한다 (해시 기반 자동 무한루프 방지장치).
|
|
172
|
+
5. **`.done=false` 재설정은 당신의 cwd(main repo)에서 그대로 실행한다.** worktree 로 옮겨가지 않는다 — `dispatch_stage` 가 서브세션 생성 전에 main→worktree 정방향 동기화를 하므로 그 값이 전달된다. worktree 사본을 직접 고치면 그 동기화가 덮어쓴다.
|
|
173
|
+
6. 그런데도 `already_done: true` 가 오면 응답의 `state_copy_mismatch` / `main_repo_done` / `worktree_done` 을 먼저 읽는다. `state_copy_mismatch: true` 는 **자동 동기화가 실패했다**는 뜻이다 — 동기화를 손으로 다시 실행하지 말고 `next_action` 이 지시하는 `state.sh status` 로 확인한 뒤, 해소되지 않으면 사용자에게 에스컬레이션한다.
|
|
150
174
|
|
|
151
175
|
### 응답 처리 순서
|
|
152
176
|
|
|
@@ -318,6 +342,7 @@ loop (max_substage_retries=3 per substage):
|
|
|
318
342
|
- 모델 폴백 시: `[fallback] <agent>: <primary> → <fallback> (reason: ...)`
|
|
319
343
|
- `retry_disallowed` 감지 시: `[substage X RETRY DISALLOWED] outcome=timeout, transient_failures=0 — sub-agent hang. <retry_disallowed_reason>`
|
|
320
344
|
- `session_gone` 최종 실패 시: `[substage X SESSION_GONE gone_reason=<message_stall|status_absent>] dispatch_stage 3회 자동 redispatch 후 실패. attempts=<N>, previous_session_ids=[...]`
|
|
345
|
+
- `permission_stall` 시: `[substage X PERMISSION BLOCKED reason=<permission_reason>] 서브세션이 차단되어 **이미 종료됨** (승인 대기 아님). 차단 경로=<permission_patterns>, 허용 범위=<permission_scope>, attempts=<N>`
|
|
321
346
|
- verifier ERROR 시: `[substage X VERIFIER ERROR source=<verdict_source>] 검증 미수행 — verifier 만 재호출 (streak=<N>/3). stage 는 건드리지 않음`
|
|
322
347
|
- verifier ERROR streak 초과 시: `[substage X VERIFIER ERROR STREAK EXCEEDED] 판정 3회 연속 실패. source=<verdict_source>. 사용자 개입 필요.`
|
|
323
348
|
- REJECTED 재시도 시: `[substage X REJECTED retry] streak=<N>, reason_prefix="<40자>" → dispatch_stage 재호출`
|
|
@@ -87,6 +87,8 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
87
87
|
|
|
88
88
|
기대: 모든 항목(boolean)이 `true`인 JSON 객체. 누락·`false`·문법 오류 = **REJECTED**.
|
|
89
89
|
|
|
90
|
+
> **유일한 예외**: `2_implementation.dev` 의 `new_tests_added` 는 `1_planning.requirements` 가 테스트 범위 제외를 선언한 경우(`test_scope.new_tests_required == false`) `false` 가 정상이다. 판정 규칙은 §2 의 `2_implementation.dev` 항목에 있다. 이 예외를 모르는 채 "모든 항목 true" 만 적용하면, 승인된 스코프 아웃이 무한 반려로 되돌아온다 (issue #11).
|
|
91
|
+
|
|
90
92
|
> **⚠️ 스키마 규약:** state.sh 의 모든 jq path 는 `.stages."<PHASE>".substages."<SUBSTAGE>".<field>` 형태를 따른다. 참조: CLAUDE.md "워크플로우 상태 & 위임 규약".
|
|
91
93
|
|
|
92
94
|
### 2. 단계 명세 재대조
|
|
@@ -119,7 +121,15 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
119
121
|
git status --porcelain | grep -v '\.makdoong2-team/' | grep -v 'workspace-analysis\.json'
|
|
120
122
|
```
|
|
121
123
|
출력이 비어 있어야 한다. `.makdoong2-team/` 은 플러그인이 작업 트리 안에 만드는 **자기 상태**이지 analyzer 의 부산물이 아니다. 이것을 위반으로 세면 해당 패턴이 git exclude 에 없는 저장소에서 **항상 REJECTED** 가 나고, 그 시점에 exclude 를 고칠 권한을 가진 역할이 파이프라인에 없어(analyzer 는 산출물 1개만 쓰기 가능 · team-leader 는 하드룰 2 로 차단 · engineer 는 analysis 통과 후 단계) 동일 사유 무한 루프가 된다 (issue #6-②).
|
|
122
|
-
- 2_implementation.dev: `done=true` +
|
|
124
|
+
- 2_implementation.dev: `done=true` + `self_check` 6항목 + **테스트 동반 원칙은 requirements 의 선언을 따른다** (issue #11)
|
|
125
|
+
- 판정 근거는 **state.json 마커와 게이트 재실행**이다. sub-agent output 은 보조 근거이며, **output 이 비어 있거나 특정 문구가 없다는 사실만으로 REJECTED 하지 않는다** — 마커가 충족되면 VERIFIED 다. (`dispatch_stage` 는 텍스트 없이 종료한 세션도 `.done=true` 만으로 성공 처리한다. 그 경로를 verifier 가 문구 검색으로 뒤집으면 재작업 루프만 남는다.)
|
|
126
|
+
- `REQ` = `.stages."1_planning".substages."requirements".test_scope.new_tests_required` 를 읽는다. **마커 부재·`null` 은 `true` 로 간주한다 (fail-closed).**
|
|
127
|
+
```bash
|
|
128
|
+
bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".test_scope'
|
|
129
|
+
```
|
|
130
|
+
- `REQ == true` → `self_check.new_tests_added == true` 여야 한다. 추가로 staged 변경(`git diff --cached --name-only`)에 실제 테스트 파일이 있는지 확인한다 — 판단 기준은 `workspace-analysis.json` 의 `test_conventions` 다. 마커는 true 인데 테스트 파일이 하나도 없으면 **REJECTED**.
|
|
131
|
+
- `REQ == false` → 테스트 범위 제외가 **요구사항 단계에서 이미 승인·동결된 것**이다. `self_check.new_tests_added` 가 `false` 여도 정상이며, 이때 `.stages."2_implementation".substages."dev".new_tests_waived == true` 만 확인한다. **테스트가 없다는 이유로 REJECTED 하지 않는다** — 그 반려가 승인된 스코프 밖의 테스트 코드를 유입시킨 사고의 직접 원인이었다.
|
|
132
|
+
- 어느 경우든 `gates/stage4-dev-post-verify.sh <이슈키>` 를 재실행해 exit 0 을 확인한다.
|
|
123
133
|
- 2_implementation.test: `.stages."2_implementation".substages."test"` 의 각 필드가 아래 조건을 모두 충족해야 함
|
|
124
134
|
- `.unit` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
|
|
125
135
|
- `.integration` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
|
package/dist/opencode-plugin.js
CHANGED
|
@@ -622,7 +622,11 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
622
622
|
// 같이 남겨 어느 사본이 몇 개를 읽었는지 바로 대응시킨다.
|
|
623
623
|
logger.debug(`[permission] plugin-own allows: ${pluginOwnPatterns.length}개 ` +
|
|
624
624
|
`(${pluginOwnPatterns.join(", ")}) directory=${directory}`);
|
|
625
|
+
// 두 줄의 개수가 다른 것은 정상이다 — 아래가 위를 **포함**한다. 내역을
|
|
626
|
+
// (plugin-own N + opencode.json M) 로 분해해 두지 않으면 "5개 / 7개" 두 줄이
|
|
627
|
+
// 같은 목록의 불일치처럼 읽힌다 (GitHub #12 부수 관찰).
|
|
625
628
|
logger.debug(`[permission] configured external_directory allows: ${configuredAllowPatterns.length}개` +
|
|
629
|
+
` (= plugin-own ${pluginOwnPatterns.length} + opencode.json ${configuredAllowPatterns.length - pluginOwnPatterns.length})` +
|
|
626
630
|
(configuredAllowPatterns.length ? ` (${configuredAllowPatterns.slice(0, 3).join(", ")}…)` : "") +
|
|
627
631
|
` directory=${directory}`);
|
|
628
632
|
// 툴 실행 신호는 레지스트리(`registry.activeToolCalls` / `lastToolExecuteAt`)에
|
|
@@ -1711,22 +1715,73 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1711
1715
|
effectiveWorktree = storedWt;
|
|
1712
1716
|
}
|
|
1713
1717
|
}
|
|
1718
|
+
// ── main repo → worktree 정방향 동기화 ──
|
|
1719
|
+
// dispatch_verifier 는 서브세션 생성 전에 이 동기화를 하는데 dispatch_stage
|
|
1720
|
+
// 에는 없었다. 그 비대칭이 REJECTED 재작업 규약을 구조적으로 깨뜨린다:
|
|
1721
|
+
// 규약은 team-leader 에게 `.done=false` 재설정을 시키는데, 리더의 cwd 는
|
|
1722
|
+
// main repo 이고 아래 done 검사는 worktree 사본을 본다. 리더가 규약대로
|
|
1723
|
+
// 했는데도 `already_done: true` 로 차단되고, 리더는 오류 문구만으로는
|
|
1724
|
+
// 원인을 알 수 없어 같은 실수를 반복했다 (issue #11).
|
|
1725
|
+
// 동기화 방향은 파이프라인 불변식과 같다 — main 이 durable 사본, worktree
|
|
1726
|
+
// 는 forward-seed / reverse-merge 되는 작업 사본이다 (finally 의 REVERSE).
|
|
1727
|
+
if (effectiveWorktree !== cwd) {
|
|
1728
|
+
logger.debug(`[wt-sync] FORWARD issue=${args.issue} worktree=${effectiveWorktree} ` +
|
|
1729
|
+
`caller=dispatch_stage stage=${args.target_stage}`);
|
|
1730
|
+
const fwdSync = await $ `bash ${SCRIPTS_DIR}/wt-sync-ignored.sh ${effectiveWorktree} ${args.issue}`
|
|
1731
|
+
.cwd(cwd).quiet().nothrow();
|
|
1732
|
+
if (fwdSync.exitCode !== 0) {
|
|
1733
|
+
logger.warn(`[wt-sync] FORWARD FAIL issue=${args.issue} caller=dispatch_stage exit=${fwdSync.exitCode} ` +
|
|
1734
|
+
`stderr=${redactAndTruncate(fwdSync.stderr?.toString() ?? "", 200)}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1714
1737
|
// done=true stage 재-dispatch 방지 (sub-agent tool-call loop → timeout/empty output).
|
|
1715
1738
|
// 3_delivery.* 는 hybrid stage (publisher = spec provider) 로 재-진입이 정상 흐름이라 제외.
|
|
1716
1739
|
const isHybridDelivery = args.target_stage.startsWith("3_delivery.");
|
|
1717
1740
|
if (!isHybridDelivery) {
|
|
1718
|
-
const
|
|
1741
|
+
const donePath = stageJqPath(args.target_stage) + ".done";
|
|
1742
|
+
const doneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${donePath}`
|
|
1719
1743
|
.cwd(effectiveWorktree).quiet().nothrow();
|
|
1720
1744
|
if (doneR.exitCode === 0 && doneR.stdout?.toString().trim() === "true") {
|
|
1745
|
+
// 위 정방향 동기화가 성공했다면 두 사본은 같아야 한다. 그래도 다르면
|
|
1746
|
+
// 동기화가 실패한 것이고, 그 사실을 추측이 아니라 관측으로 알린다 —
|
|
1747
|
+
// 종전 문구는 "이미 done=true" 만 말해서, 사본 불일치라는 실제 원인을
|
|
1748
|
+
// 리더가 스스로 추론해야 했다 (state_unreadable 은 이미 안내한다).
|
|
1749
|
+
let mainRepoDone = null;
|
|
1750
|
+
if (effectiveWorktree !== cwd) {
|
|
1751
|
+
const mainDoneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${donePath}`
|
|
1752
|
+
.cwd(cwd).quiet().nothrow();
|
|
1753
|
+
if (mainDoneR.exitCode === 0)
|
|
1754
|
+
mainRepoDone = mainDoneR.stdout?.toString().trim() ?? null;
|
|
1755
|
+
}
|
|
1756
|
+
const copyMismatch = mainRepoDone !== null && mainRepoDone !== "true";
|
|
1721
1757
|
return JSON.stringify({
|
|
1722
1758
|
ok: false,
|
|
1723
1759
|
gate: args.target_stage,
|
|
1724
1760
|
stage: args.target_stage,
|
|
1725
1761
|
agent: spec.id,
|
|
1726
1762
|
already_done: true,
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1763
|
+
state_copy_mismatch: copyMismatch,
|
|
1764
|
+
worktree_done: "true",
|
|
1765
|
+
main_repo_done: mainRepoDone,
|
|
1766
|
+
reason: copyMismatch
|
|
1767
|
+
? `Stage '${args.target_stage}' is done=true in the worktree state.json copy but ` +
|
|
1768
|
+
`done=${mainRepoDone} in the main repo copy. state.sh 는 호출 cwd 의 git toplevel 을 ` +
|
|
1769
|
+
`쓰므로 두 사본은 분리돼 있고, 이 검사는 worktree 사본을 본다. ` +
|
|
1770
|
+
`main repo cwd 에서 '.done' 을 되돌렸다면 그 변경은 worktree 사본에 반영되지 않은 것이다 ` +
|
|
1771
|
+
`(정방향 동기화를 이미 1회 자동 시도했고 실패했다).`
|
|
1772
|
+
: `Stage '${args.target_stage}' is already done=true. ` +
|
|
1773
|
+
`Re-dispatching a completed stage causes sub-agent tool-call loops (timeout/empty output). ` +
|
|
1774
|
+
`Call auto_advance_stage to obtain the correct next stage instead. ` +
|
|
1775
|
+
`참고: 방금 '.done=false' 로 되돌렸는데도 이 응답이 왔다면, 다른 cwd(main repo)에서 ` +
|
|
1776
|
+
`state.json 을 조작해 worktree 사본과 불일치한 경우다 — state_unreadable 과 같은 메커니즘이다.`,
|
|
1777
|
+
next_action: copyMismatch
|
|
1778
|
+
? `동기화는 이미 자동 시도했고 실패했습니다 — 직접 재실행하지 마세요. ` +
|
|
1779
|
+
`'bash ${SCRIPTS_DIR}/state.sh status ${args.issue}' 로 사본 상태를 확인하고, ` +
|
|
1780
|
+
`해소되지 않으면 사용자에게 에스컬레이션하세요.`
|
|
1781
|
+
: `auto_advance_stage(issue: "${args.issue}") 로 올바른 다음 단계를 받으세요. ` +
|
|
1782
|
+
`REJECTED 재작업 중이라면 '.done' 재설정이 실제로 반영됐는지 ` +
|
|
1783
|
+
`bash ${SCRIPTS_DIR}/state.sh get ${args.issue} <done jq-path> 로 먼저 확인하세요 ` +
|
|
1784
|
+
`(jq-path: ${donePath}).`,
|
|
1730
1785
|
});
|
|
1731
1786
|
}
|
|
1732
1787
|
}
|
|
@@ -1831,6 +1886,11 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1831
1886
|
const lastVerdictStreak = lastVerdictStreakR.exitCode === 0
|
|
1832
1887
|
? (parseInt(lastVerdictStreakR.stdout?.toString().trim() || "0", 10) || 0)
|
|
1833
1888
|
: 0;
|
|
1889
|
+
// 직전 attempt 가 워크스페이스 밖 경로 요청으로 자동 거부·abort 된 경우,
|
|
1890
|
+
// 새 세션 프롬프트에 "무엇이 막혔고 어디까지가 허용인지" 를 주입한다.
|
|
1891
|
+
// 이것이 없으면 재디스패치는 같은 경로를 다시 요청해 같은 지점에서 죽는다
|
|
1892
|
+
// — 서브세션은 이전 세션의 대화 이력을 이어받지 못하기 때문이다 (GitHub #12).
|
|
1893
|
+
let permissionBlockNote = null;
|
|
1834
1894
|
const buildPromptText = (attemptNum, priorSessionIds) => {
|
|
1835
1895
|
const base = [
|
|
1836
1896
|
`Working directory (ABSOLUTE): ${effectiveWorktree}`,
|
|
@@ -1842,7 +1902,7 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1842
1902
|
`Stage 명세 파일은 위 Stages directory 경로에서 읽으시오. \`<SCRIPTS_DIR>/../stages/\` 상대경로를 사용하지 마시오.`,
|
|
1843
1903
|
];
|
|
1844
1904
|
if (args.target_stage === "2_implementation.dev") {
|
|
1845
|
-
base.push(`\n=== dev substage 요구사항 소스 우선순위 ===`, `구현 착수 전 요구사항은 반드시 다음 순서로 참조한다:`, ` a) FIRST — requirements-draft.md 를 우선 확인 (아래 bash 스니펫 그대로 실행):`, buildDraftPathReadSnippet(args.issue, " "), ` 파일이 존재하고 비어있지 않으면 이 파일이 요구사항의 진실의 원천이다.`, ` b) FALLBACK — draft_path 미기록/파일 부재/빈 파일 인 경우 Jira 이슈 조회:`, ` skill(name="jira-research") 로 works MCP 로드 →`, ` skill_mcp(mcp_name="works", tool_name="getIssue", arguments={"issueKey":"${args.issue}"})`, ` c) 두 소스 모두 접근 불가하면 사용자에게 상황을 보고하고 즉시 종료.`, `주의: requirements-draft.md 없이 Jira 이슈만으로 구현 범위를 재결정하지 말 것 — draft 가 없다는 것은 planning 이 부적절하게 진행된 신호이므로 사용자에게 보고하고
|
|
1905
|
+
base.push(`\n=== dev substage 요구사항 소스 우선순위 ===`, `구현 착수 전 요구사항은 반드시 다음 순서로 참조한다:`, ` a) FIRST — requirements-draft.md 를 우선 확인 (아래 bash 스니펫 그대로 실행):`, buildDraftPathReadSnippet(args.issue, " "), ` 파일이 존재하고 비어있지 않으면 이 파일이 요구사항의 진실의 원천이다.`, ` b) FALLBACK — draft_path 미기록/파일 부재/빈 파일 인 경우 Jira 이슈 조회:`, ` skill(name="jira-research") 로 works MCP 로드 →`, ` skill_mcp(mcp_name="works", tool_name="getIssue", arguments={"issueKey":"${args.issue}"})`, ` c) 두 소스 모두 접근 불가하면 사용자에게 상황을 보고하고 즉시 종료.`, `주의: requirements-draft.md 없이 Jira 이슈만으로 구현 범위를 재결정하지 말 것 — draft 가 없다는 것은 planning 이 부적절하게 진행된 신호이므로 사용자에게 보고하고 종료하시오.`, `\n=== 테스트 동반 원칙은 requirements 의 선언을 따른다 (issue #11) ===`, `테스트 추가 여부는 스스로 정하지 않는다. 아래를 실행해 1_planning.requirements 가 승인·동결한 선언을 먼저 읽는다:`, ` bash ${SCRIPTS_DIR}/state.sh get ${args.issue} '.stages."1_planning".substages."requirements".test_scope'`, ` - new_tests_required=true (마커 부재·null 도 true 로 간주 — fail-closed) → 변경에 대한 테스트를 함께 추가하고 self_check.new_tests_added=true 로 기록한다.`, ` - new_tests_required=false → 테스트 추가는 이번 이슈의 범위 밖이다. 추가하지 않는 것이 정답이며, self_check.new_tests_added=false 와 함께 dev.new_tests_waived=true 마커를 남긴다.`, `이 마커는 읽기 전용이다 — engineer 가 test_scope 를 쓰거나 고치지 않는다. 선언과 실제 작업이 맞지 않으면 임의로 면제·추가하지 말고 최종 출력에 적어 보고한다.`);
|
|
1846
1906
|
}
|
|
1847
1907
|
if (attemptNum > 1) {
|
|
1848
1908
|
base.push(`\n=== 재개(resume) 지시 — 이전 세션 중단됨 ===`, `이전 세션 ID: ${priorSessionIds.join(", ")} (attempt ${attemptNum - 1})`, `중단 원인: 이전 sub-session이 stall/gone 감지되어 새 세션으로 이어서 진행합니다.`, `context 승계 방식: opencode API는 세션 간 대화 이력을 옮기지 못하므로 state.json 을 진실의 원천으로 사용합니다.`, `첫 번째 필수 작업:`, ` 1) bash ${SCRIPTS_DIR}/state.sh get ${args.issue} '.' 로 현재 상태 전량 조회`, ` 2) 이미 done=true 로 기록된 substage / 마커는 재실행하지 말고 skip`, ` 3) 미완료 substage 부터 stage spec 순서대로 이어서 진행`, ` 4) 완료 시 관례대로 요약 출력 후 종료`, `주의: state.json 마커가 이미 target substage 완료를 나타내면 즉시 요약만 출력하고 종료하시오 (재작업 금지).`);
|
|
@@ -1850,6 +1910,9 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1850
1910
|
if (lastVerdictReason) {
|
|
1851
1911
|
base.push(`\n=== 이전 검증 실패 사유 (재작업 시 참고) ===`, `직전 attempt 가 makdoong2-verifier 에 의해 REJECTED 되었다. 같은 사유로 다시 실패하면 무한 루프로 판정된다 (동일 사유 ${lastVerdictStreak}회 연속 감지 중, 5회 도달 시 자동 중단).`, `아래 verifier 판정 원문을 읽고 지적된 규칙 위반·마커 누락·검증 실패 항목을 최우선으로 해결하시오. 동일한 접근을 반복하지 말고 finding 에 지적된 대안 (파일 재분할·마커 재기록·형식 수정 등) 을 시도하시오.`, `--- verifier 판정 원문 (앞 4000자) ---`, lastVerdictReason, `--- 원문 끝 ---`);
|
|
1852
1912
|
}
|
|
1913
|
+
if (permissionBlockNote) {
|
|
1914
|
+
base.push(`\n=== 경로 접근 차단 — 직전 세션이 이 사유로 즉시 중단됐다 (반복 금지) ===`, permissionBlockNote);
|
|
1915
|
+
}
|
|
1853
1916
|
if (args.context)
|
|
1854
1917
|
base.push(args.context);
|
|
1855
1918
|
return base.join("\n");
|
|
@@ -2156,6 +2219,94 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
2156
2219
|
});
|
|
2157
2220
|
continue;
|
|
2158
2221
|
}
|
|
2222
|
+
// ── permission_stall: 하드 실패가 아니라 회복 가능한 차단이다 ──────────
|
|
2223
|
+
//
|
|
2224
|
+
// 종전에는 여기서 곧장 최종 결과로 떨어졌다. `attempts` 가 1에서 늘지
|
|
2225
|
+
// 않았고, `next_action` 도 비어 있었다 — 그 공백을 team-leader 가
|
|
2226
|
+
// "사용자 승인 대기" 로 채워 넣어, 헤드리스 서브세션에는 존재하지도 않는
|
|
2227
|
+
// 승인 행위를 사용자에게 요구하며 워크플로가 멈췄다 (GitHub #12).
|
|
2228
|
+
//
|
|
2229
|
+
// `outside_allowed_roots` 만 재디스패치한다. 이 사유는 "경로를 좁히면
|
|
2230
|
+
// 끝날 일" 이라 새 세션에 허용 범위를 알려주면 진행할 수 있다. 나머지
|
|
2231
|
+
// 둘은 경로 문제가 아니다 — `non_external_permission` 은 에이전트
|
|
2232
|
+
// frontmatter 설정, `tool_call_stall` 은 설치 상태의 문제라 같은 조건의
|
|
2233
|
+
// 재시도가 결과를 바꾸지 못한다.
|
|
2234
|
+
if (outcome.kind === "permission_stall") {
|
|
2235
|
+
const blockedPatterns = outcome.permissionPatterns ?? [];
|
|
2236
|
+
const recoverable = outcome.permissionReason === "outside_allowed_roots";
|
|
2237
|
+
const currentMaxAttempts = maxAttemptsForCurrentModel();
|
|
2238
|
+
promptPromise.catch(() => { });
|
|
2239
|
+
if (recoverable && attempt < currentMaxAttempts) {
|
|
2240
|
+
const scopeLine = outcome.permissionScope
|
|
2241
|
+
? `자동 승인 범위: ${outcome.permissionScope}/ 이하 — worktree(${effectiveWorktree}) 와 그 형제 디렉토리까지다. 그 위(조부모 이상)는 열리지 않는다.`
|
|
2242
|
+
: `자동 승인 범위: worktree(${effectiveWorktree}) 와 그 형제 디렉토리까지다. 그 위는 열리지 않는다.`;
|
|
2243
|
+
permissionBlockNote = [
|
|
2244
|
+
`직전 attempt(${attempt}) 는 워크스페이스 밖 경로에 대한 external_directory 권한 요청 때문에 자동 거부되고 세션이 abort 됐다.`,
|
|
2245
|
+
`차단된 요청 패턴: ${JSON.stringify(blockedPatterns)}`,
|
|
2246
|
+
scopeLine,
|
|
2247
|
+
`서브세션에는 이 승인을 받을 채널이 없다 — 같은 경로를 다시 요청하면 또 즉시 중단되고, 그때까지 한 작업은 전부 버려진다.`,
|
|
2248
|
+
`조치:`,
|
|
2249
|
+
` - glob / grep / read / list 의 경로 인자를 위 허용 범위 안으로 좁혀라. 경로 인자를 아예 주지 않고 cwd 기준 상대 패턴을 쓰는 것이 가장 안전하다.`,
|
|
2250
|
+
` - 저장소 밖을 훑어야만 하는 작업이라면 수행하지 말고, 그 사실과 이유를 최종 출력에 적어 보고하라.`,
|
|
2251
|
+
` - 임시 파일은 /tmp 가 아니라 ${effectiveWorktree}/.makdoong2-team/${args.issue}/tmp/ 에 만든다.`,
|
|
2252
|
+
].join("\n");
|
|
2253
|
+
logger.warn(`[dispatch_stage] PERMISSION_BLOCK session=${subSessionID} stage=${args.target_stage} ` +
|
|
2254
|
+
`attempt=${attempt}/${currentMaxAttempts} reason=${outcome.permissionReason} ` +
|
|
2255
|
+
`patterns=${JSON.stringify(blockedPatterns)} scope=${outcome.permissionScope ?? "unknown"} ` +
|
|
2256
|
+
`— redispatching with allowed-scope guidance injected`);
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
// 예산 소진(또는 회복 불가 사유). hang_history 는 dispatch_stage 호출
|
|
2260
|
+
// 사이를 넘어 살아남는 유일한 카운터다 — 여기에 남기지 않으면 리더가
|
|
2261
|
+
// 같은 조건으로 무한히 재호출해도 cross-call 상한에 영영 닿지 않는다.
|
|
2262
|
+
const stallEntry = JSON.stringify({
|
|
2263
|
+
attempt,
|
|
2264
|
+
at: new Date().toISOString(),
|
|
2265
|
+
reason: `permission_stall:${outcome.permissionReason ?? "unknown"}`,
|
|
2266
|
+
patterns: blockedPatterns,
|
|
2267
|
+
scope: outcome.permissionScope ?? null,
|
|
2268
|
+
elapsed_ms: outcome.elapsedMs,
|
|
2269
|
+
polls: outcome.polls,
|
|
2270
|
+
session_id: subSessionID,
|
|
2271
|
+
model: activeModelFull,
|
|
2272
|
+
fallback_depth: activeFallbackDepth,
|
|
2273
|
+
final: true,
|
|
2274
|
+
});
|
|
2275
|
+
const stallJqPath = stageJqPath(args.target_stage) + ".hang_history";
|
|
2276
|
+
const stallAppend = await $ `bash ${SCRIPTS_DIR}/state.sh append ${args.issue} ${stallJqPath} ${stallEntry}`
|
|
2277
|
+
.cwd(effectiveWorktree).quiet().nothrow();
|
|
2278
|
+
logger.error(`[dispatch_stage] PERMISSION_STALL_FINAL session=${subSessionID} stage=${args.target_stage} ` +
|
|
2279
|
+
`attempts=${attempt} reason=${outcome.permissionReason} recoverable=${recoverable} ` +
|
|
2280
|
+
`patterns=${JSON.stringify(blockedPatterns)} scope=${outcome.permissionScope ?? "unknown"} ` +
|
|
2281
|
+
`hang_history append exit=${stallAppend.exitCode}`);
|
|
2282
|
+
finalResultJson = JSON.stringify({
|
|
2283
|
+
ok: false,
|
|
2284
|
+
stage: args.target_stage,
|
|
2285
|
+
agent: spec.id,
|
|
2286
|
+
model: activeModelFull,
|
|
2287
|
+
session_id: subSessionID,
|
|
2288
|
+
previous_session_ids: attemptSessionIds.slice(0, -1),
|
|
2289
|
+
attempts: attempt,
|
|
2290
|
+
fallback_depth: activeFallbackDepth,
|
|
2291
|
+
output: finalLegacy.text.slice(0, 8000),
|
|
2292
|
+
outcome_kind: "permission_stall",
|
|
2293
|
+
permission_reason: outcome.permissionReason ?? "unknown",
|
|
2294
|
+
permission_patterns: blockedPatterns,
|
|
2295
|
+
permission_scope: outcome.permissionScope ?? null,
|
|
2296
|
+
permission_id: outcome.permissionID,
|
|
2297
|
+
permission_type: outcome.permissionType,
|
|
2298
|
+
// 승인 대기가 아니라 "이미 종료됨" 이다. 이 두 필드를 읽고 보고한다.
|
|
2299
|
+
awaiting_user_approval: false,
|
|
2300
|
+
session_aborted: true,
|
|
2301
|
+
stage_done: null,
|
|
2302
|
+
completion: "incomplete",
|
|
2303
|
+
polls: outcome.polls,
|
|
2304
|
+
elapsed_ms: outcome.elapsedMs,
|
|
2305
|
+
next_action: finalLegacy.nextAction,
|
|
2306
|
+
reason: finalLegacy.text,
|
|
2307
|
+
});
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2159
2310
|
if (outcome.kind === "empty") {
|
|
2160
2311
|
const isPreambleOnly = outcome.reason === "preamble_only";
|
|
2161
2312
|
logger.debug(`[dispatch_stage] empty output detected — sending one-time ${isPreambleOnly ? "action" : "summary"} re-prompt: ` +
|
|
@@ -99,6 +99,7 @@ export type PollOutcome = {
|
|
|
99
99
|
permissionType?: string;
|
|
100
100
|
permissionPatterns?: string[];
|
|
101
101
|
permissionReason?: PermissionStallReason;
|
|
102
|
+
permissionScope?: string;
|
|
102
103
|
} | {
|
|
103
104
|
kind: "session_gone";
|
|
104
105
|
polls: number;
|
|
@@ -128,6 +129,18 @@ export interface PollOptions {
|
|
|
128
129
|
nudgeAtFraction?: number;
|
|
129
130
|
onNudge?: (sessionId: string, elapsedMs: number) => Promise<void>;
|
|
130
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* 자동 승인 범위의 루트 = worktree 의 **직계 부모 한 단계**.
|
|
134
|
+
*
|
|
135
|
+
* 형제(메인 저장소·다른 worktree)까지는 열고 그 위는 닫는다. 조부모를 열면
|
|
136
|
+
* `/root/IdeaProjects/*` 하나가 모든 프로젝트 그룹을, 극단적으로 `/` 가 파일시스템
|
|
137
|
+
* 전체를 자동 승인 대상으로 만든다 — 이 함수의 반환값은 사람의 확인 없이 `allow`
|
|
138
|
+
* 로 응답되므로 그 확장은 승인 게이트 자체를 무력화한다 (GitHub #12 의 제안 1을
|
|
139
|
+
* 채택하지 않은 이유). 조부모 요청은 **차단이 정답**이고, 회복은 범위를 넓히는 것이
|
|
140
|
+
* 아니라 서브에이전트에게 허용 범위를 알려주고 재시도시키는 쪽(dispatch_stage 의
|
|
141
|
+
* permission-block 재디스패치)이 맡는다.
|
|
142
|
+
*/
|
|
143
|
+
export declare function worktreeAllowedScope(worktree: string): string;
|
|
131
144
|
export declare function isWithinWorktreeScope(patterns: string[], worktree: string): boolean;
|
|
132
145
|
export declare function isMatchedByConfiguredRules(patterns: string[], allowedGlobs: string[]): boolean;
|
|
133
146
|
/**
|
|
@@ -148,6 +161,14 @@ export declare function isMatchedByConfiguredRules(patterns: string[], allowedGl
|
|
|
148
161
|
* distinction so `dispatch_stage` can return `ok:false` on empty output.
|
|
149
162
|
*/
|
|
150
163
|
export declare function pollSubSession(client: PollClientLike, sessionId: string, options?: PollOptions): Promise<PollOutcome>;
|
|
164
|
+
/**
|
|
165
|
+
* `permission_stall` 보고 시 team-leader 가 그대로 따라야 하는 지시.
|
|
166
|
+
*
|
|
167
|
+
* dispatch_stage 응답의 `next_action` 으로 실린다. 하드룰 4("next_action 을 그대로
|
|
168
|
+
* 따른다")가 적용되는 자리이며, 문구를 바꿀 때는 agents/makdoong2-team-leader.md 의
|
|
169
|
+
* permission_stall 하드룰과 함께 고친다 (test/poll-permission-scope.test.ts 가 강제).
|
|
170
|
+
*/
|
|
171
|
+
export declare const PERMISSION_STALL_NEXT_ACTION: string;
|
|
151
172
|
/**
|
|
152
173
|
* Convert a {@link PollOutcome} into a display string plus a boolean success
|
|
153
174
|
* flag. Used by callers that need a single string for downstream serialization
|
|
@@ -157,4 +178,5 @@ export declare function pollSubSession(client: PollClientLike, sessionId: string
|
|
|
157
178
|
export declare function pollOutcomeToLegacy(outcome: PollOutcome): {
|
|
158
179
|
text: string;
|
|
159
180
|
success: boolean;
|
|
181
|
+
nextAction?: string;
|
|
160
182
|
};
|
package/dist/poll-sub-session.js
CHANGED
|
@@ -87,6 +87,20 @@ function posixDirname(p) {
|
|
|
87
87
|
const idx = trimmed.lastIndexOf("/");
|
|
88
88
|
return idx <= 0 ? "/" : trimmed.slice(0, idx);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* 자동 승인 범위의 루트 = worktree 의 **직계 부모 한 단계**.
|
|
92
|
+
*
|
|
93
|
+
* 형제(메인 저장소·다른 worktree)까지는 열고 그 위는 닫는다. 조부모를 열면
|
|
94
|
+
* `/root/IdeaProjects/*` 하나가 모든 프로젝트 그룹을, 극단적으로 `/` 가 파일시스템
|
|
95
|
+
* 전체를 자동 승인 대상으로 만든다 — 이 함수의 반환값은 사람의 확인 없이 `allow`
|
|
96
|
+
* 로 응답되므로 그 확장은 승인 게이트 자체를 무력화한다 (GitHub #12 의 제안 1을
|
|
97
|
+
* 채택하지 않은 이유). 조부모 요청은 **차단이 정답**이고, 회복은 범위를 넓히는 것이
|
|
98
|
+
* 아니라 서브에이전트에게 허용 범위를 알려주고 재시도시키는 쪽(dispatch_stage 의
|
|
99
|
+
* permission-block 재디스패치)이 맡는다.
|
|
100
|
+
*/
|
|
101
|
+
export function worktreeAllowedScope(worktree) {
|
|
102
|
+
return posixDirname(worktree);
|
|
103
|
+
}
|
|
90
104
|
// Returns true when every permission pattern resides within the allowed scope,
|
|
91
105
|
// defined as the parent directory of the worktree (siblings are the main repo
|
|
92
106
|
// and other worktrees — all legitimate access targets for engineers).
|
|
@@ -94,7 +108,7 @@ function posixDirname(p) {
|
|
|
94
108
|
export function isWithinWorktreeScope(patterns, worktree) {
|
|
95
109
|
if (!worktree || patterns.length === 0)
|
|
96
110
|
return false;
|
|
97
|
-
const scope =
|
|
111
|
+
const scope = worktreeAllowedScope(worktree);
|
|
98
112
|
return patterns.every(pat => {
|
|
99
113
|
const base = pat.replace(/\/?(\*+\/?)+$/, "");
|
|
100
114
|
return base === scope || base.startsWith(scope + "/");
|
|
@@ -369,6 +383,11 @@ export async function pollSubSession(client, sessionId, options = {}) {
|
|
|
369
383
|
permissionType: p.permission,
|
|
370
384
|
permissionPatterns: p.patterns,
|
|
371
385
|
permissionReason: reason,
|
|
386
|
+
// 무엇이 막혔는지(patterns)만으로는 처방이 안 나온다 — **어디까지가
|
|
387
|
+
// 허용인지**를 같이 실어야 서브에이전트가 경로를 좁혀 재시도할 수 있다.
|
|
388
|
+
permissionScope: options.allowedWorktree
|
|
389
|
+
? worktreeAllowedScope(options.allowedWorktree)
|
|
390
|
+
: undefined,
|
|
372
391
|
};
|
|
373
392
|
}
|
|
374
393
|
}
|
|
@@ -621,6 +640,21 @@ export async function pollSubSession(client, sessionId, options = {}) {
|
|
|
621
640
|
transientFailures,
|
|
622
641
|
};
|
|
623
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* `permission_stall` 보고 시 team-leader 가 그대로 따라야 하는 지시.
|
|
645
|
+
*
|
|
646
|
+
* dispatch_stage 응답의 `next_action` 으로 실린다. 하드룰 4("next_action 을 그대로
|
|
647
|
+
* 따른다")가 적용되는 자리이며, 문구를 바꿀 때는 agents/makdoong2-team-leader.md 의
|
|
648
|
+
* permission_stall 하드룰과 함께 고친다 (test/poll-permission-scope.test.ts 가 강제).
|
|
649
|
+
*/
|
|
650
|
+
export const PERMISSION_STALL_NEXT_ACTION = "이 실패는 사용자 승인으로 해소되지 않는다 — 권한 요청은 이미 자동 거부됐고 서브세션도 abort 된 뒤다. " +
|
|
651
|
+
"헤드리스 서브세션에는 승인을 받을 채널 자체가 없으므로, 사용자에게 '권한을 승인한 뒤 재개해 달라' 고 " +
|
|
652
|
+
"요청하는 것은 실행 불가능한 지시다. 절대 그렇게 보고하지 말 것. " +
|
|
653
|
+
"output 원문과 permission_patterns / permission_scope 를 그대로 인용해 '차단되어 종료됨' 으로 보고하고, " +
|
|
654
|
+
"permission_reason 별 처방을 따르라: outside_allowed_roots → 자동 재디스패치(허용 범위 안내 주입)가 이미 " +
|
|
655
|
+
"소진된 상태다. 같은 인자로 재호출하지 말고 차단된 경로와 허용 범위를 보고한 뒤 사용자 지시를 기다린다. " +
|
|
656
|
+
"non_external_permission → 해당 에이전트 frontmatter 의 permission 블록 문제다 (정식 키는 `edit`). " +
|
|
657
|
+
"tool_call_stall → `npx makdoong2-team doctor` 로 설치 상태를 점검한다.";
|
|
624
658
|
/**
|
|
625
659
|
* Convert a {@link PollOutcome} into a display string plus a boolean success
|
|
626
660
|
* flag. Used by callers that need a single string for downstream serialization
|
|
@@ -654,11 +688,26 @@ export function pollOutcomeToLegacy(outcome) {
|
|
|
654
688
|
: "an unidentified permission request";
|
|
655
689
|
const remedy = (() => {
|
|
656
690
|
switch (outcome.permissionReason) {
|
|
657
|
-
case "outside_allowed_roots":
|
|
658
|
-
|
|
659
|
-
|
|
691
|
+
case "outside_allowed_roots": {
|
|
692
|
+
// 종전에는 처방이 "임시 파일은 worktree 안에 써라" 하나뿐이었다. 정작
|
|
693
|
+
// 실제로 막힌 것은 read-only `glob` 이었고(GitHub #12), 그 안내는 상황과
|
|
694
|
+
// 무관해 아무 조치도 유도하지 못했다. 무엇이 막혔는지(patterns)와
|
|
695
|
+
// **어디까지가 허용인지**(scope)를 먼저 못 박고, 조회/쓰기 두 갈래로 나눈다.
|
|
696
|
+
const blocked = outcome.permissionPatterns?.length
|
|
697
|
+
? JSON.stringify(outcome.permissionPatterns)
|
|
698
|
+
: "(패턴 미상)";
|
|
699
|
+
const scope = outcome.permissionScope
|
|
700
|
+
? `\`${outcome.permissionScope}/\` 이하 (worktree 와 그 형제 디렉토리)`
|
|
701
|
+
: "worktree 와 그 형제 디렉토리";
|
|
702
|
+
return `워크스페이스 밖 경로 접근이라 차단됐다 — 요청 ${blocked}, 자동 승인 범위 ${scope}. ` +
|
|
703
|
+
"그 위(조부모 이상)는 설계상 열지 않는다: 한 번 열면 형제 프로젝트 전체가 사람의 확인 없이 승인 대상이 된다. " +
|
|
704
|
+
"조치는 둘 중 하나다. " +
|
|
705
|
+
"(1) 조회(glob/grep/read/list)라면 경로 인자를 허용 범위 안으로 좁혀라 — 경로 인자 없이 cwd 기준 상대 패턴을 쓰는 것이 가장 안전하다. " +
|
|
706
|
+
"저장소 밖을 훑어야만 하는 작업이면 수행하지 말고 그 사실을 최종 출력에 적어 보고하라. " +
|
|
707
|
+
"(2) 임시 파일이 필요하면 `/tmp` 이 아니라 worktree 안의 " +
|
|
660
708
|
"`.makdoong2-team/<이슈키>/tmp/` 에 만들어라 — 그 경로는 cwd 안이라 승인이 필요 없고, " +
|
|
661
709
|
"git exclude 와 worktree 동기화 대상이다. `/tmp` 에 쓴 것은 동기화도 커밋도 되지 않아 조용히 사라진다.";
|
|
710
|
+
}
|
|
662
711
|
case "non_external_permission":
|
|
663
712
|
return "external_directory 가 아닌 권한 요청이 서브에이전트에 도달했다 — 경로 문제가 아니라 " +
|
|
664
713
|
"에이전트 permission 설정 문제다. 해당 에이전트의 frontmatter `permission:` 블록을 점검하라 " +
|
|
@@ -675,6 +724,11 @@ export function pollOutcomeToLegacy(outcome) {
|
|
|
675
724
|
`aborted after ${outcome.stalledMs}ms. reason=${outcome.permissionReason ?? "unknown"})\n` +
|
|
676
725
|
`→ ${remedy}`,
|
|
677
726
|
success: false,
|
|
727
|
+
// 이 한 줄이 없어서 team-leader 가 "이미 abort 됨" 을 "승인 대기 중" 으로
|
|
728
|
+
// 재해석해, 존재하지 않는 승인 행위를 사용자에게 요구하며 워크플로를
|
|
729
|
+
// 세웠다 (GitHub #12). 헤드리스 서브세션에는 승인 채널이 없다 — 그래서
|
|
730
|
+
// 훅이 대신 거부한 것이고, 사용자가 할 수 있는 일은 애초에 없다.
|
|
731
|
+
nextAction: PERMISSION_STALL_NEXT_ACTION,
|
|
678
732
|
};
|
|
679
733
|
}
|
|
680
734
|
case "session_gone":
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# 1. worktree 가 존재하는가
|
|
6
6
|
# 2. dev-written-files.txt 에 기록된 모든 파일이 staging(index) 혹은 HEAD tree 에 존재하는가
|
|
7
7
|
# 3. .gitignore 를 존중한 untracked 파일이 0인가
|
|
8
|
+
# 4. 테스트 동반 원칙이 1_planning.requirements 의 test_scope 선언과 일치하는가
|
|
8
9
|
#
|
|
9
10
|
# 불변식: 3_delivery.commit 는 untracked 를 자동 제외하므로,
|
|
10
11
|
# 본 게이트를 통과한 파일만이 커밋 대상에 진입한다.
|
|
@@ -66,4 +67,40 @@ $(echo "$UNTRACKED" | sed 's/^/ - /')"
|
|
|
66
67
|
fail "$MSG"
|
|
67
68
|
fi
|
|
68
69
|
|
|
70
|
+
# ── 4. 테스트 동반 원칙 — requirements 의 선언을 따른다 (issue #11) ──────────
|
|
71
|
+
#
|
|
72
|
+
# 종전에는 이 검사가 게이트에 없고 verifier 만 알고 있었으며, 그 verifier 기준은
|
|
73
|
+
# "sub-agent output 에 '테스트 추가' 명시" 라는 무조건 요구였다. requirements 가
|
|
74
|
+
# 테스트 범위 제외를 승인·동결해도 그 결정을 참조하는 경로가 없어서, 순수 설정·
|
|
75
|
+
# 인프라 전환 작업마다 REJECTED 가 반복되고 결국 engineer 가 승인된 스코프 밖의
|
|
76
|
+
# 테스트를 추가했다. 게이트·stage spec·verifier 세 곳이 같은 선언을 보게 한다.
|
|
77
|
+
#
|
|
78
|
+
# 판정 규칙 (fail-closed):
|
|
79
|
+
# REQ = requirements.test_scope.new_tests_required — 부재/null = true 로 간주
|
|
80
|
+
# REQ=true → self_check.new_tests_added 는 true 여야 한다
|
|
81
|
+
# REQ=false → new_tests_added 가 false 여도 되지만, 슬립과 구분하기 위해
|
|
82
|
+
# dev.new_tests_waived=true 마커가 함께 있어야 한다
|
|
83
|
+
#
|
|
84
|
+
# self_check 자체가 없는 구형 state 는 검사하지 않는다 (기존 동작 보존).
|
|
85
|
+
q(){ local __v; if __v="$("$HERE/../scripts/state.sh" get "$ISSUE" "$1" 2>/dev/null)"; then printf "%s" "$__v"; else printf "__MISSING__"; fi; }
|
|
86
|
+
|
|
87
|
+
SELF_CHECK="$(q '.stages."2_implementation".substages."dev".self_check')"
|
|
88
|
+
if [ "${SELF_CHECK}" != "__MISSING__" ] && [ "${SELF_CHECK}" != "null" ] && [ -n "${SELF_CHECK}" ]; then
|
|
89
|
+
NEW_TESTS_ADDED="$(q '.stages."2_implementation".substages."dev".self_check.new_tests_added')"
|
|
90
|
+
if [ "${NEW_TESTS_ADDED}" != "true" ]; then
|
|
91
|
+
REQ="$(q '.stages."1_planning".substages."requirements".test_scope.new_tests_required')"
|
|
92
|
+
if [ "${REQ}" != "false" ]; then
|
|
93
|
+
fail "테스트 동반 원칙 미충족: self_check.new_tests_added=${NEW_TESTS_ADDED} 인데
|
|
94
|
+
requirements 의 테스트 범위 선언이 테스트 추가를 요구한다 (test_scope.new_tests_required=${REQ}; 부재/null 은 true 로 간주).
|
|
95
|
+
→ 조치 A: 변경에 대한 테스트를 추가하고 new_tests_added=true 로 다시 기록한다.
|
|
96
|
+
→ 조치 B: 이 이슈가 테스트를 붙일 수 없는 성질이라면 임의로 면제하지 말고 부장님에게 보고한다 —
|
|
97
|
+
테스트 범위 제외는 1_planning.requirements 에서만 승인·기록할 수 있다 (stages/02-requirements.md §2-6a)."
|
|
98
|
+
fi
|
|
99
|
+
WAIVED="$(q '.stages."2_implementation".substages."dev".new_tests_waived')"
|
|
100
|
+
[ "${WAIVED}" = "true" ] || fail "테스트 면제 마커 누락: requirements 가 테스트 추가를 제외했지만(test_scope.new_tests_required=false)
|
|
101
|
+
dev.new_tests_waived 마커가 없다 — new_tests_added=false 가 의도된 면제인지 기록 누락인지 구분할 수 없다.
|
|
102
|
+
→ 조치: bash <SCRIPTS_DIR>/state.sh set ${ISSUE} '.stages.\"2_implementation\".substages.\"dev\".new_tests_waived' 'true'"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
69
106
|
echo "MAKDOONG2-GATE OK: 2_implementation.dev_post"
|
package/package.json
CHANGED
package/scripts/run-tests.mts
CHANGED
|
@@ -60,6 +60,7 @@ const STEPS = [
|
|
|
60
60
|
"node --test test/gate-post-pr-verify.test.ts",
|
|
61
61
|
"node --test test/gate-post-review-verify.test.ts",
|
|
62
62
|
"node --test test/gate-requirements-quality.test.ts",
|
|
63
|
+
"node --test test/dev-test-scope-declaration.test.ts",
|
|
63
64
|
"node --test test/worktree-sync-gate.test.ts",
|
|
64
65
|
"node --test test/planner-prompt-early-exit.test.ts",
|
|
65
66
|
"node --test test/plugin-bug-fixes.test.ts",
|
package/scripts/state.sh
CHANGED
|
@@ -61,6 +61,37 @@ validate_issue() {
|
|
|
61
61
|
|
|
62
62
|
sp() { validate_issue "$1"; echo "$(root)/.makdoong2-team/$1/state.json"; }
|
|
63
63
|
|
|
64
|
+
# ── 사본 불일치 경고 ────────────────────────────────────────────────────────
|
|
65
|
+
# state.json 사본은 cwd(git toplevel)마다 하나씩이다. 전용 worktree 가 이미 있는데
|
|
66
|
+
# main repo cwd 에서 `set` 을 실행하면, 갱신되는 것은 main 사본이고 dispatch_stage /
|
|
67
|
+
# 게이트가 보는 것은 worktree 사본이다 — 쓴 사람은 반영됐다고 믿는데 파이프라인은
|
|
68
|
+
# 옛 값을 본다. 실제로 REJECTED 재작업 규약(`.done=false` 재설정)이 이 경로에서
|
|
69
|
+
# 두 번 연속 `already_done: true` 오차단으로 되돌아왔고, 오류 문구도 원인을 알려주지
|
|
70
|
+
# 않아 같은 실수가 반복됐다 (issue #11).
|
|
71
|
+
#
|
|
72
|
+
# 쓰기 자체는 막지 않는다 — main 사본을 고치는 것이 옳은 상황도 있다. 어느 사본을
|
|
73
|
+
# 건드렸는지 stderr 로 알리기만 한다 (stdout 계약·종료 코드 불변).
|
|
74
|
+
norm_path() { if [ -d "$1" ]; then (cd "$1" && pwd -P); else printf '%s' "$1"; fi; }
|
|
75
|
+
|
|
76
|
+
warn_if_copy_split() {
|
|
77
|
+
local file="$1" here there
|
|
78
|
+
[ -f "${file}" ] || return 0
|
|
79
|
+
there="$(jq -r '.worktree // ""' "${file}" 2>/dev/null || true)"
|
|
80
|
+
{ [ -n "${there}" ] && [ "${there}" != "null" ] && [ -d "${there}" ]; } || return 0
|
|
81
|
+
here="$(norm_path "$(root)")"
|
|
82
|
+
there="$(norm_path "${there}")"
|
|
83
|
+
[ "${here}" != "${there}" ] || return 0
|
|
84
|
+
cat >&2 <<WARN
|
|
85
|
+
[state.sh] 경고: 지금 갱신한 것은 이 cwd 의 사본이지, 이 이슈의 전용 worktree 사본이 아니다.
|
|
86
|
+
갱신한 사본 : ${here}
|
|
87
|
+
worktree : ${there}
|
|
88
|
+
state.json 사본은 cwd(git toplevel)마다 하나씩이고, dispatch_stage 와 dev 이후 게이트는
|
|
89
|
+
worktree 사본을 본다. 자동 동기화(wt-sync-ignored.sh)는 서브에이전트 세션 앞뒤에서만 돌므로
|
|
90
|
+
이 쓰기는 다음 dispatch 까지 worktree 사본에 반영되지 않을 수 있다.
|
|
91
|
+
의도한 것이 worktree 사본이면 그 경로를 cwd 로 하여 다시 실행한다.
|
|
92
|
+
WARN
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
# usage_die <시그니처> [부연 설명...]
|
|
65
96
|
# `${2:?}` 가 뱉는 raw bash 에러(`line 127: 2: parameter null or not set`)는 복구
|
|
66
97
|
# 작업 중인 에이전트에게 아무것도 알려주지 못한다. 대신 무엇을 어떻게 부를지 적는다.
|
|
@@ -294,6 +325,7 @@ JSON
|
|
|
294
325
|
P="$(sp "${ISSUE}")"
|
|
295
326
|
check_flat_stage_notation "$Q"
|
|
296
327
|
write_json_atomic "$P" "set ${Q}" "$Q = $V"
|
|
328
|
+
warn_if_copy_split "$P"
|
|
297
329
|
echo "state[$ISSUE] $Q = $V" ;;
|
|
298
330
|
append)
|
|
299
331
|
ISSUE="${1:-}"; Q="${2:-}"; V="${3:-}"
|
package/stages/01-planning.md
CHANGED
|
@@ -225,6 +225,21 @@ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.scope_size' '"large"'
|
|
|
225
225
|
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_planning.requirements"'
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
+
**테스트 범위 선언 (`test_scope` — 기계 판독 마커, 필수)**: 위 `**테스트 범위**` 서술은 사람이 읽는 문장이라 dev 게이트·verifier 가 해석할 수 없다. 같은 결정을 마커로 한 번 더 기록한다 — 없으면 `2_implementation.dev` 의 테스트 동반 원칙이 승인된 스코프 아웃을 보지 못하고 무조건 적용된다 (issue #11). 상세: `stages/02-requirements.md` §2-6a.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# 테스트를 동반하는 일반적인 경우
|
|
232
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
|
|
233
|
+
'{"new_tests_required": true, "unit": "<대상 클래스/메서드>", "integration": "<빌드 플랜명/시나리오>", "rationale": "<한 줄 근거>"}'
|
|
234
|
+
|
|
235
|
+
# 테스트 추가를 이번 이슈 범위에서 제외하기로 승인한 경우 (스코프 아웃에도 함께 명시)
|
|
236
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
|
|
237
|
+
'{"new_tests_required": false, "unit": null, "integration": "<기존 통합 테스트로만 검증>", "rationale": "<왜 제외가 타당한지>"}'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- 마커 부재는 `new_tests_required: true` 로 간주된다 (fail-closed) — 선언 누락이 테스트 면제로 둔갑하지 않는다.
|
|
241
|
+
- `test_scope_defined` self_check 항목은 **이 마커를 실제로 기록했다는 뜻**이다. 기록 후 값을 읽어 확인한다.
|
|
242
|
+
|
|
228
243
|
### 2-6. Requirements 완료 기록
|
|
229
244
|
|
|
230
245
|
```bash
|
|
@@ -352,3 +352,25 @@ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_plannin
|
|
|
352
352
|
```
|
|
353
353
|
|
|
354
354
|
self_check 에 `paths_explicit` / `test_scope_defined` / `atomic_units` / `scope_out_listed` 4항목을 함께 기록한다.
|
|
355
|
+
|
|
356
|
+
### 2-6a. 테스트 범위 선언 (`test_scope` — 기계 판독 마커, 필수)
|
|
357
|
+
|
|
358
|
+
위 `**테스트 범위**` 서술은 사람이 읽는 문장이라 dev 단계의 게이트·verifier 가 해석할 수 없다. **같은 결정을 기계가 읽는 마커로 한 번 더 기록한다** — 이 마커가 없으면 `2_implementation.dev` 의 "테스트 동반 원칙" 이 승인된 스코프 아웃을 보지 못한 채 무조건 적용되어, 요구사항 밖의 테스트 코드가 강제로 추가된다 (issue #11).
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# 테스트를 동반하는 일반적인 경우
|
|
362
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
|
|
363
|
+
'{"new_tests_required": true, "unit": "<대상 클래스/메서드>", "integration": "<빌드 플랜명/시나리오>", "rationale": "<한 줄 근거>"}'
|
|
364
|
+
|
|
365
|
+
# 테스트 추가를 이번 이슈의 범위에서 제외하기로 승인한 경우
|
|
366
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
|
|
367
|
+
'{"new_tests_required": false, "unit": null, "integration": "<기존 통합 테스트로만 검증>", "rationale": "<왜 제외가 타당한지 — 예: 애플리케이션 코드가 아닌 배포 설정 전환이라 단위 테스트 대상이 없음>"}'
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
- `new_tests_required` 는 **`true` 가 기본값**이다. 마커를 기록하지 않으면 하류(게이트·verifier)는 `true` 로 간주한다 (fail-closed) — 선언 누락이 "테스트 면제" 로 둔갑하지 않는다.
|
|
371
|
+
- `false` 로 선언하려면 **스코프 아웃 항목에 그 사실이 함께 적혀 있어야 하고**, §2-3-2 의 "스코프 아웃 항목은 항상 명시적으로 확인한다" 절차를 거쳐야 한다. `rationale` 은 빈 문자열·`null` 금지.
|
|
372
|
+
- `test_scope_defined` self_check 항목은 **이 마커를 실제로 기록했다는 뜻**이다. 자기선언이 아니라 값을 읽어 확인한다:
|
|
373
|
+
```bash
|
|
374
|
+
bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".test_scope'
|
|
375
|
+
```
|
|
376
|
+
- 이 선언은 요구사항 명세의 일부다 — 동결(2-4a) 이후 변경은 §2-4a 3번의 재승인 절차만 허용한다. **engineer / verifier 는 이 마커를 쓰지 않는다. 읽기만 한다.**
|
|
@@ -99,6 +99,25 @@ bash <SCRIPTS_DIR>/wt-sync-ignored.sh "$WT" "<이슈키>"
|
|
|
99
99
|
- **outer-world 에이전트 위임 금지** — engineer 프론트매터에 `Task` 툴이 없으므로 물리적으로 스폰 불가. 구현·조사·리팩토링 모두 본 에이전트가 직접 수행한다. 조사가 필요하면 `skill_mcp` 로 makdoong2 스킬(`bitbucket-research` 등)만 사용.
|
|
100
100
|
- 3단계 작업 단위 순서대로 구현한다. 한 단위가 끝나면 커밋 가능 상태로 만들어 둔다(실제 커밋은 6단계).
|
|
101
101
|
|
|
102
|
+
## 4-4-pre. 테스트 범위 선언 조회 (필수 — 자가 검증 전)
|
|
103
|
+
|
|
104
|
+
**테스트 동반 원칙은 무조건 적용되지 않는다.** `1_planning.requirements` 가 승인·동결한 테스트 범위 선언(`test_scope`)을 먼저 읽고, 그 선언이 정한 대로만 적용한다. 이 조회를 건너뛰면 "단위 테스트는 이번 변경 대상에 적용하지 않는다" 고 **이미 승인된** 이슈에서도 테스트 추가가 강제되어, 승인된 스코프 밖의 코드가 유입된다 (issue #11).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
NEW_TESTS_REQUIRED="$(bash <SCRIPTS_DIR>/state.sh get <이슈키> \
|
|
108
|
+
'.stages."1_planning".substages."requirements".test_scope.new_tests_required' 2>/dev/null || echo 'true')"
|
|
109
|
+
# 마커 부재("null")·조회 실패는 true 로 간주한다 (fail-closed).
|
|
110
|
+
[ "$NEW_TESTS_REQUIRED" = "false" ] || NEW_TESTS_REQUIRED=true
|
|
111
|
+
echo "new_tests_required=$NEW_TESTS_REQUIRED"
|
|
112
|
+
|
|
113
|
+
# 근거(왜 제외됐는지)도 함께 읽어 최종 출력에 인용한다.
|
|
114
|
+
bash <SCRIPTS_DIR>/state.sh get <이슈키> \
|
|
115
|
+
'.stages."1_planning".substages."requirements".test_scope.rationale' 2>/dev/null || true
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **이 마커를 engineer 가 쓰는 것은 금지다 (hardrule).** 읽기 전용이다. 테스트가 불필요해 보인다는 자체 판단으로 `test_scope` 를 기록·수정하면 요구사항 동결(§2-4a)을 우회하는 것이다. 선언이 실제 작업과 맞지 않으면 `done` 을 기록하지 말고 부장님에게 보고한다.
|
|
119
|
+
- `new_tests_required=true` 인데 대상이 테스트를 붙일 수 없는 성질(순수 설정 파일 등)이라고 판단되면 — **임의로 면제하지 말고** 그 사실을 최종 출력에 적어 부장님이 requirements 재작업 여부를 결정하게 한다.
|
|
120
|
+
|
|
102
121
|
## 4-4. 최종 자가 검증 (Pre-Completion Checklist)
|
|
103
122
|
|
|
104
123
|
`done=true` 직전, 아래 6체크를 자가 검증한다.
|
|
@@ -108,18 +127,35 @@ bash <SCRIPTS_DIR>/wt-sync-ignored.sh "$WT" "<이슈키>"
|
|
|
108
127
|
|---|---|
|
|
109
128
|
| 1 | 3단계에서 합의한 모든 수정/추가 파일이 구현되었다 (스코프 100% 충족) |
|
|
110
129
|
| 2 | 기존 테스트(`sbt test` / `./gradlew test` / `mvn test` 등)가 모두 통과한다 |
|
|
111
|
-
| 3 | 새 기능·버그 수정에 대한 테스트가 함께 추가되었다 (테스트 동반 원칙) |
|
|
130
|
+
| 3 | **`new_tests_required=true` 인 경우에만** — 새 기능·버그 수정에 대한 테스트가 함께 추가되었다 (테스트 동반 원칙). `false` 면 이 항목은 면제되며 테스트를 추가하지 않는 것이 정답이다 |
|
|
112
131
|
| 4 | 타입/린트/컴파일 에러가 0이다 |
|
|
113
132
|
| 5 | `.env` / secrets / API 키 / 하드코딩된 비밀이 코드·테스트·로그에 노출되지 않았다 |
|
|
114
133
|
| 6 | `write`/`edit`/`patch`/`multiedit` 로 편집한 모든 파일이 staging area 에 반영되었다 (`git ls-files --others --exclude-standard` 결과 0) |
|
|
115
134
|
|
|
116
135
|
> 항목 6은 `tool.execute.after` 훅이 매 write 완료 시 자동으로 `git add`를 수행하므로 기본적으로 자동 충족된다. 훅 실패로 untracked 가 남으면 §4-5 exit gate 가 BLOCK 하여 재작업을 요구한다.
|
|
117
136
|
|
|
137
|
+
**`new_tests_required=true` (기본)** — 테스트를 추가하고 `new_tests_added: true` 로 기록한다:
|
|
138
|
+
|
|
118
139
|
```bash
|
|
119
140
|
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".self_check' \
|
|
120
141
|
'{"scope_met": true, "existing_tests_pass": true, "new_tests_added": true, "type_lint_clean": true, "no_secrets": true, "all_writes_staged": true}'
|
|
121
142
|
```
|
|
122
143
|
|
|
144
|
+
**`new_tests_required=false` (승인된 스코프 아웃)** — `new_tests_added` 를 `false` 로 기록하고, 그것이 슬립이 아니라 선언에 따른 면제임을 나타내는 `new_tests_waived` 마커를 함께 남긴다. **두 기록은 한 쌍이다** — `new_tests_waived` 없이 `new_tests_added: false` 만 있으면 §4-5 exit gate 가 BLOCK 한다:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".self_check' \
|
|
148
|
+
'{"scope_met": true, "existing_tests_pass": true, "new_tests_added": false, "type_lint_clean": true, "no_secrets": true, "all_writes_staged": true}'
|
|
149
|
+
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".new_tests_waived' 'true'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 최종 출력에 반드시 포함할 것
|
|
153
|
+
|
|
154
|
+
verifier 는 state.json 마커로 판정하지만, 사람이 스코프 이탈을 조기에 발견할 수 있도록 출력에 한 줄을 남긴다:
|
|
155
|
+
|
|
156
|
+
- `new_tests_required=true` → 추가한 테스트 파일 목록과 실행 결과
|
|
157
|
+
- `new_tests_required=false` → `테스트 추가 없음 — requirements 의 test_scope.new_tests_required=false (사유: <rationale>)`
|
|
158
|
+
|
|
123
159
|
## 4-5. Exit Gate 실행 (staging 강제)
|
|
124
160
|
|
|
125
161
|
```bash
|