makdoong2-team 3.0.2 → 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.
@@ -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` 세 값이다.
@@ -320,6 +342,7 @@ loop (max_substage_retries=3 per substage):
320
342
  - 모델 폴백 시: `[fallback] <agent>: <primary> → <fallback> (reason: ...)`
321
343
  - `retry_disallowed` 감지 시: `[substage X RETRY DISALLOWED] outcome=timeout, transient_failures=0 — sub-agent hang. <retry_disallowed_reason>`
322
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>`
323
346
  - verifier ERROR 시: `[substage X VERIFIER ERROR source=<verdict_source>] 검증 미수행 — verifier 만 재호출 (streak=<N>/3). stage 는 건드리지 않음`
324
347
  - verifier ERROR streak 초과 시: `[substage X VERIFIER ERROR STREAK EXCEEDED] 판정 3회 연속 실패. source=<verdict_source>. 사용자 개입 필요.`
325
348
  - REJECTED 재시도 시: `[substage X REJECTED retry] streak=<N>, reason_prefix="<40자>" → dispatch_stage 재호출`
@@ -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`)에
@@ -1882,6 +1886,11 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
1882
1886
  const lastVerdictStreak = lastVerdictStreakR.exitCode === 0
1883
1887
  ? (parseInt(lastVerdictStreakR.stdout?.toString().trim() || "0", 10) || 0)
1884
1888
  : 0;
1889
+ // 직전 attempt 가 워크스페이스 밖 경로 요청으로 자동 거부·abort 된 경우,
1890
+ // 새 세션 프롬프트에 "무엇이 막혔고 어디까지가 허용인지" 를 주입한다.
1891
+ // 이것이 없으면 재디스패치는 같은 경로를 다시 요청해 같은 지점에서 죽는다
1892
+ // — 서브세션은 이전 세션의 대화 이력을 이어받지 못하기 때문이다 (GitHub #12).
1893
+ let permissionBlockNote = null;
1885
1894
  const buildPromptText = (attemptNum, priorSessionIds) => {
1886
1895
  const base = [
1887
1896
  `Working directory (ABSOLUTE): ${effectiveWorktree}`,
@@ -1901,6 +1910,9 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
1901
1910
  if (lastVerdictReason) {
1902
1911
  base.push(`\n=== 이전 검증 실패 사유 (재작업 시 참고) ===`, `직전 attempt 가 makdoong2-verifier 에 의해 REJECTED 되었다. 같은 사유로 다시 실패하면 무한 루프로 판정된다 (동일 사유 ${lastVerdictStreak}회 연속 감지 중, 5회 도달 시 자동 중단).`, `아래 verifier 판정 원문을 읽고 지적된 규칙 위반·마커 누락·검증 실패 항목을 최우선으로 해결하시오. 동일한 접근을 반복하지 말고 finding 에 지적된 대안 (파일 재분할·마커 재기록·형식 수정 등) 을 시도하시오.`, `--- verifier 판정 원문 (앞 4000자) ---`, lastVerdictReason, `--- 원문 끝 ---`);
1903
1912
  }
1913
+ if (permissionBlockNote) {
1914
+ base.push(`\n=== 경로 접근 차단 — 직전 세션이 이 사유로 즉시 중단됐다 (반복 금지) ===`, permissionBlockNote);
1915
+ }
1904
1916
  if (args.context)
1905
1917
  base.push(args.context);
1906
1918
  return base.join("\n");
@@ -2207,6 +2219,94 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
2207
2219
  });
2208
2220
  continue;
2209
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
+ }
2210
2310
  if (outcome.kind === "empty") {
2211
2311
  const isPreambleOnly = outcome.reason === "preamble_only";
2212
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
  };
@@ -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 = posixDirname(worktree);
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
- return "워크스페이스 경로 접근이라 차단됐다. " +
659
- "임시 파일이 필요하면 `/tmp` 아니라 worktree 안의 " +
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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makdoong2-team",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },