makdoong2-team 2.3.0 → 2.3.1
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-planner.md +7 -3
- package/agents/makdoong2-team-leader.md +4 -0
- package/bin/cli.js +17 -4
- package/bin/cli.ts +22 -3
- package/dist/opencode-plugin.js +29 -8
- package/dist/poll-sub-session.d.ts +1 -0
- package/dist/poll-sub-session.js +24 -3
- package/gates/stage3-scope-verify.sh +7 -1
- package/package.json +1 -1
- package/stages/01-planning.md +2 -0
- package/stages/02-requirements.md +2 -0
|
@@ -245,7 +245,9 @@ bash <SCRIPTS_DIR>/state.sh set {ISSUE_KEY} '.stages."1_planning".substages."sco
|
|
|
245
245
|
- 범주 하향(major→minor) — escalation만 허용.
|
|
246
246
|
- 다음 phase 작업(dev/test) 선행.
|
|
247
247
|
- **outer-world 에이전트(Sisyphus / Explore / Librarian / oh-my-openagent 계열 카테고리 등) 위임 금지.** 본 에이전트에는 `Task` 툴이 프론트매터에서 제거되어 있어 물리적으로 스폰 불가. 조사가 필요하면 반드시 `skill_mcp` 로 `jira-research` / `confluence-research` / `bitbucket-research` / `github-oss-research` 스킬만 사용. 이는 planning phase가 makdoong2 서브에이전트 체계 내에서 봉인되어야 한다는 아키텍처 원칙이다.
|
|
248
|
-
- **bash를 통한 파일 쓰기 리디렉션 일체 금지 (READ-ONLY 원칙).**
|
|
248
|
+
- **bash를 통한 파일 쓰기 리디렉션 일체 금지 (READ-ONLY 원칙).** 예외 2가지:
|
|
249
|
+
- `<SCRIPTS_DIR>/state.sh set ...` 을 통한 state.json 마커 기록.
|
|
250
|
+
- **planning 산출물(`.makdoong2-team/<이슈키>/` 아래 `*.md`·`*.json` — `requirements-draft.md` 등)은 `write` 툴로 직접 생성·갱신한다.** 이것은 READ-ONLY 위반이 아니라 stage spec(02-requirements.md §2-0b)이 부과한 **의무**이며, 훅도 이 경로의 `write` 를 명시적으로 허용한다. 단 같은 경로라도 bash 리디렉션·`apply_patch` 는 차단되므로 반드시 `write` 툴을 쓴다.
|
|
249
251
|
- **금지 패턴**: `echo >`, `cat > file`, `cat <<EOF > file`, `tee file`, `sed -i`, `awk ... > file`, `printf > file`, `> file`, `>> file`, `python -c "... open(..., 'w') ..."`, `node -e "... fs.writeFileSync(...) ..."`
|
|
250
252
|
- **위반 예시**:
|
|
251
253
|
```bash
|
|
@@ -264,5 +266,7 @@ bash <SCRIPTS_DIR>/state.sh set {ISSUE_KEY} '.stages."1_planning".substages."sco
|
|
|
264
266
|
# ✅ 허용: state.json 마커 기록 (state.sh 경유)
|
|
265
267
|
bash <SCRIPTS_DIR>/state.sh set PROJ-123 '.stages."1_planning".substages."scope".done' 'true'
|
|
266
268
|
```
|
|
267
|
-
- **위반 결과**: verifier가 `git status`로 untracked 파일 감지 → REJECTED 판정 → 워크플로우 중단
|
|
268
|
-
- **올바른 절차**:
|
|
269
|
+
- **위반 결과**: 훅이 bash 쓰기·`apply_patch` 를 차단하고, 우회에 성공하더라도 verifier가 `git status`로 untracked 파일 감지 → REJECTED 판정 → 워크플로우 중단
|
|
270
|
+
- **올바른 절차**:
|
|
271
|
+
- **planning 산출물(요구사항 초안 등 `.makdoong2-team/<이슈키>/*.md|json`)은 본인이 `write` 툴로 직접 생성한다.** team-leader 반환·dev 위임 대상이 아니다 — 그 시점 파이프라인에는 초안을 대신 만들 역할이 없어 워크플로가 정지한다 (issue #8).
|
|
272
|
+
- **소스 코드** 파일 생성·변경이 필요하면 spec을 team-leader에게 반환하여 dev 단계로 위임한다. Planning 단계는 "무엇을 만들지"만 결정하고, "실제로 만드는 것"은 implementation 단계의 책임이다.
|
|
@@ -98,6 +98,10 @@ permission:
|
|
|
98
98
|
|
|
99
99
|
`MAX_ATTEMPTS` 는 dispatch_stage **호출 1회 내부**의 예산이므로 재호출하면 리셋된다. `hang_history` 누적 상한은 그 리셋을 무력화하기 위한 **호출 간(cross-call) 차단막**이며, 부장님이 우회하면 무한 루프가 된다.
|
|
100
100
|
|
|
101
|
+
## 서브에이전트 능력을 frontmatter `tools:` 목록으로 판정하지 말 것 (hardrule)
|
|
102
|
+
|
|
103
|
+
에이전트 정의 파일의 frontmatter `tools:` 는 **whitelist 가 아니다** — 목록에 없는 툴도 permission 설정이 허용하면 실행된다. 실제로 planner frontmatter 에 `Write` 가 없다는 이유로 "planner 는 구조적으로 파일 생성이 불가능하다" 고 단정하고 워크플로를 중단시킨 오진단이 있었다 — 로그상 planner 는 `write` 를 정상 실행해 왔고 차단 이력이 0건이었다 (GitHub issue #8). 서브에이전트가 파일을 못 만들었다면 원인은 **훅 차단 메시지·permission 프롬프트 대기(PERMISSION_STALL)·프롬프트 지시** 중에 있다. frontmatter 를 근거로 사용자에게 에이전트 정의 수정을 요구하지 말고, 실패한 세션의 실제 차단 로그를 근거로 판단하라.
|
|
104
|
+
|
|
101
105
|
## verdict 는 셋이다 — REJECTED 와 ERROR 를 절대 섞지 말 것 (hardrule)
|
|
102
106
|
|
|
103
107
|
`dispatch_verifier` 의 `verdict` 는 `VERIFIED` / `REJECTED` / `ERROR` 세 값이다.
|
package/bin/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { fileURLToPath } from "node:url";
|
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
22
|
import { buildPoliciesFromConfig, DEFAULT_ALLOWED_PRIMARIES, } from "../scripts/model-policy.mjs";
|
|
23
|
-
import { install, uninstall, resolveConfigDir, parseJsonc, readCachedVersion, opencodeCacheRoot, } from "../scripts/install-lib.mjs";
|
|
23
|
+
import { install, uninstall, resolveConfigDir, parseJsonc, readCachedVersion, opencodeCacheRoot, computeExternalDirPaths, } from "../scripts/install-lib.mjs";
|
|
24
24
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const PKG_ROOT = resolve(HERE, "..");
|
|
26
26
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
@@ -135,7 +135,14 @@ function doDoctor(flags) {
|
|
|
135
135
|
check(existsSync(join(PKG_ROOT, d)), `${d}/ in package`, `${d}/ missing in package (reinstall: npm install -g makdoong2-team)`);
|
|
136
136
|
}
|
|
137
137
|
const ocPath = join(DEST, "opencode.json");
|
|
138
|
-
|
|
138
|
+
// 설치가 심도록 설계된 external_directory 시드. 에이전트 프롬프트가 서브에이전트
|
|
139
|
+
// 세션에 워크스페이스 외부 경로(<pkgRoot>/stages/*.md 등) 읽기를 지시하므로,
|
|
140
|
+
// 이 allow 가 없으면 서브에이전트가 응답 불가한 권한 프롬프트에 걸려
|
|
141
|
+
// PERMISSION_STALL 로 중단된다 (issue #8). 파일 배포만 성공하고 opencode.json
|
|
142
|
+
// 패치가 남지 않은 부분 설치를 doctor 가 잡아야 한다.
|
|
143
|
+
const requiredExtDirPaths = computeExternalDirPaths(PKG_ROOT, DEST);
|
|
144
|
+
let pluginOk = false, toolsOk = false, extDirOk = false, ocParseOk = false, ocParseErr = "";
|
|
145
|
+
let extDirMissing = requiredExtDirPaths;
|
|
139
146
|
if (existsSync(ocPath)) {
|
|
140
147
|
try {
|
|
141
148
|
const oc = parseJsonc(readFileSync(ocPath, "utf8"));
|
|
@@ -155,14 +162,20 @@ function doDoctor(flags) {
|
|
|
155
162
|
});
|
|
156
163
|
const ocTools = oc.tools;
|
|
157
164
|
toolsOk = !!ocTools && TOOLS.every((t) => ocTools[t] === true);
|
|
165
|
+
const extDir = oc.permission?.external_directory;
|
|
166
|
+
extDirMissing = requiredExtDirPaths.filter((p) => !extDir || typeof extDir !== "object" || extDir[p] !== "allow");
|
|
167
|
+
extDirOk = extDirMissing.length === 0;
|
|
158
168
|
}
|
|
159
169
|
catch (e) {
|
|
160
170
|
ocParseErr = e instanceof Error ? e.message : String(e);
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
173
|
check(!existsSync(ocPath) || ocParseOk, "opencode.json parses (JSONC tolerated)", `opencode.json is unparseable — install silently skips patching it: ${ocParseErr}`);
|
|
164
|
-
check(pluginOk, "opencode.json registers the plugin", "opencode.json missing the plugin entry");
|
|
165
|
-
check(toolsOk, "opencode.json enables custom tools", "opencode.json missing custom tools");
|
|
174
|
+
check(pluginOk, "opencode.json registers the plugin", "opencode.json missing the plugin entry (run: npx makdoong2-team install)");
|
|
175
|
+
check(toolsOk, "opencode.json enables custom tools", "opencode.json missing custom tools (run: npx makdoong2-team install)");
|
|
176
|
+
check(extDirOk, "opencode.json allows external_directory (pkgRoot + configDir)", `opencode.json missing permission.external_directory allow(s): ${extDirMissing.join(", ")} ` +
|
|
177
|
+
`— sub-agents will hit unanswerable permission prompts and abort with PERMISSION_STALL ` +
|
|
178
|
+
`(run: npx makdoong2-team install)`);
|
|
166
179
|
// The plugin opencode actually loads lives in the `<name>@latest` cache dir,
|
|
167
180
|
// not in the globally installed npm module. When those drift the user runs
|
|
168
181
|
// old code while `npm ls -g` reports the new version — invisible without
|
package/bin/cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
parseJsonc,
|
|
34
34
|
readCachedVersion,
|
|
35
35
|
opencodeCacheRoot,
|
|
36
|
+
computeExternalDirPaths,
|
|
36
37
|
} from "../scripts/install-lib.mts";
|
|
37
38
|
|
|
38
39
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -150,7 +151,14 @@ function doDoctor(flags: Flags): never {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const ocPath = join(DEST, "opencode.json");
|
|
153
|
-
|
|
154
|
+
// 설치가 심도록 설계된 external_directory 시드. 에이전트 프롬프트가 서브에이전트
|
|
155
|
+
// 세션에 워크스페이스 외부 경로(<pkgRoot>/stages/*.md 등) 읽기를 지시하므로,
|
|
156
|
+
// 이 allow 가 없으면 서브에이전트가 응답 불가한 권한 프롬프트에 걸려
|
|
157
|
+
// PERMISSION_STALL 로 중단된다 (issue #8). 파일 배포만 성공하고 opencode.json
|
|
158
|
+
// 패치가 남지 않은 부분 설치를 doctor 가 잡아야 한다.
|
|
159
|
+
const requiredExtDirPaths = computeExternalDirPaths(PKG_ROOT, DEST);
|
|
160
|
+
let pluginOk = false, toolsOk = false, extDirOk = false, ocParseOk = false, ocParseErr = "";
|
|
161
|
+
let extDirMissing: string[] = requiredExtDirPaths;
|
|
154
162
|
if (existsSync(ocPath)) {
|
|
155
163
|
try {
|
|
156
164
|
const oc = parseJsonc(readFileSync(ocPath, "utf8"));
|
|
@@ -168,12 +176,23 @@ function doDoctor(flags: Flags): never {
|
|
|
168
176
|
});
|
|
169
177
|
const ocTools = oc.tools;
|
|
170
178
|
toolsOk = !!ocTools && TOOLS.every((t) => ocTools[t] === true);
|
|
179
|
+
const extDir = oc.permission?.external_directory;
|
|
180
|
+
extDirMissing = requiredExtDirPaths.filter(
|
|
181
|
+
(p: string) => !extDir || typeof extDir !== "object" || extDir[p] !== "allow",
|
|
182
|
+
);
|
|
183
|
+
extDirOk = extDirMissing.length === 0;
|
|
171
184
|
} catch (e) { ocParseErr = e instanceof Error ? e.message : String(e); }
|
|
172
185
|
}
|
|
173
186
|
check(!existsSync(ocPath) || ocParseOk, "opencode.json parses (JSONC tolerated)",
|
|
174
187
|
`opencode.json is unparseable — install silently skips patching it: ${ocParseErr}`);
|
|
175
|
-
check(pluginOk, "opencode.json registers the plugin",
|
|
176
|
-
|
|
188
|
+
check(pluginOk, "opencode.json registers the plugin",
|
|
189
|
+
"opencode.json missing the plugin entry (run: npx makdoong2-team install)");
|
|
190
|
+
check(toolsOk, "opencode.json enables custom tools",
|
|
191
|
+
"opencode.json missing custom tools (run: npx makdoong2-team install)");
|
|
192
|
+
check(extDirOk, "opencode.json allows external_directory (pkgRoot + configDir)",
|
|
193
|
+
`opencode.json missing permission.external_directory allow(s): ${extDirMissing.join(", ")} ` +
|
|
194
|
+
`— sub-agents will hit unanswerable permission prompts and abort with PERMISSION_STALL ` +
|
|
195
|
+
`(run: npx makdoong2-team install)`);
|
|
177
196
|
|
|
178
197
|
// The plugin opencode actually loads lives in the `<name>@latest` cache dir,
|
|
179
198
|
// not in the globally installed npm module. When those drift the user runs
|
package/dist/opencode-plugin.js
CHANGED
|
@@ -1163,9 +1163,12 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1163
1163
|
`${filePath ? `"${filePath}" 에 ` : ""}쓸 수 없다.\n` +
|
|
1164
1164
|
(allowed === null
|
|
1165
1165
|
? `이 에이전트는 파일을 쓰지 않는다 — 조사 결과는 최종 응답 텍스트로 반환하라.\n`
|
|
1166
|
-
: `허용된 산출물은 .makdoong2-team/<이슈키>/ 아래의 지정된
|
|
1167
|
-
`(패턴: ${allowed.source}).\n`
|
|
1168
|
-
|
|
1166
|
+
: `허용된 산출물은 .makdoong2-team/<이슈키>/ 아래의 지정된 파일뿐이다 ` +
|
|
1167
|
+
`(패턴: ${allowed.source}).\n` +
|
|
1168
|
+
`그 경로에는 지금 즉시 'write' 툴(filePath 인자)로 쓸 수 있다 — ` +
|
|
1169
|
+
`bash 리디렉션·apply_patch 우회가 차단된 것이지 산출물 쓰기 자체가 금지된 것이 아니다. ` +
|
|
1170
|
+
`산출물이 필요하면 포기하지 말고 write 툴로 재시도하라.\n`) +
|
|
1171
|
+
`소스 코드 변경이 필요하면 그 사실을 산출물/응답에 적고 engineer 단계로 넘겨라.`);
|
|
1169
1172
|
}
|
|
1170
1173
|
}
|
|
1171
1174
|
// ── Leader hardrule 1: 직접 파일 편집·생성 금지 (write/edit/patch/multiedit) ──
|
|
@@ -2026,11 +2029,29 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
2026
2029
|
}
|
|
2027
2030
|
promptPromise.catch(() => { });
|
|
2028
2031
|
if (success) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2032
|
+
// hang_history 리셋 조건은 "dispatch 정상 반환" 이 아니라 "substage
|
|
2033
|
+
// 실제 완료(done=true)" 다. 종전에는 세션이 텍스트만 뱉고 done=false
|
|
2034
|
+
// 로 끝나도 리셋됐고, 재-dispatch 를 반복하는 동안 이력이 매번
|
|
2035
|
+
// 비워져 stall_escalate_threshold 가 사실상 도달 불가였다 (issue #8).
|
|
2036
|
+
// cwd 는 effectiveWorktree — 이 substage 의 다른 state.json 접근과
|
|
2037
|
+
// 같은 파일을 봐야 한다 (args.worktree 를 쓰면 교정 발동 시 갈린다).
|
|
2038
|
+
const resetDonePath = `${stageJqPath(args.target_stage)}.done`;
|
|
2039
|
+
const resetDoneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${resetDonePath}`
|
|
2040
|
+
.cwd(effectiveWorktree).quiet().nothrow();
|
|
2041
|
+
const resetDoneValue = resetDoneR.exitCode === 0
|
|
2042
|
+
? (resetDoneR.stdout?.toString().trim() ?? null)
|
|
2043
|
+
: null;
|
|
2044
|
+
if (resetDoneValue === "true") {
|
|
2045
|
+
const resetPath = `${stageJqPath(args.target_stage)}.hang_history`;
|
|
2046
|
+
const resetR = await $ `bash ${SCRIPTS_DIR}/state.sh set ${args.issue} ${resetPath} ${"[]"}`
|
|
2047
|
+
.cwd(effectiveWorktree).quiet().nothrow();
|
|
2048
|
+
logger.debug(`[hang_history] reset issue=${args.issue} stage=${args.target_stage} ` +
|
|
2049
|
+
`exit=${resetR.exitCode} — substage done=true`);
|
|
2050
|
+
}
|
|
2051
|
+
else {
|
|
2052
|
+
logger.debug(`[hang_history] reset skipped issue=${args.issue} stage=${args.target_stage} ` +
|
|
2053
|
+
`done=${resetDoneValue} — dispatch 는 정상 반환했지만 substage 미완료`);
|
|
2054
|
+
}
|
|
2034
2055
|
}
|
|
2035
2056
|
const retryDisallowed = finalOutcome.kind === "timeout" &&
|
|
2036
2057
|
finalOutcome.transientFailures === 0;
|
package/dist/poll-sub-session.js
CHANGED
|
@@ -328,6 +328,7 @@ export async function pollSubSession(client, sessionId, options = {}) {
|
|
|
328
328
|
stalledMs,
|
|
329
329
|
permissionID: p.id,
|
|
330
330
|
permissionType: p.permission,
|
|
331
|
+
permissionPatterns: p.patterns,
|
|
331
332
|
};
|
|
332
333
|
}
|
|
333
334
|
}
|
|
@@ -355,13 +356,29 @@ export async function pollSubSession(client, sessionId, options = {}) {
|
|
|
355
356
|
`— 툴이 실행 중이므로 permission_stall 로 판정하지 않는다`);
|
|
356
357
|
}
|
|
357
358
|
if (hasPendingToolCall && !toolExecuting && stalledMs >= toolCallStallThresholdMs) {
|
|
358
|
-
|
|
359
|
+
// abort 전에 대기 중인 permission 요청을 1회 조회해 어떤 카테고리·경로가
|
|
360
|
+
// 대기 중인지 abort 메시지에 남긴다 (issue #8 제안 2). 종전에는 폴러
|
|
361
|
+
// 로그만으로 대기 대상을 특정할 수 없어 원인 규명에 코드 독해가 필요했다.
|
|
362
|
+
// best-effort — client.permission 이 없거나(구버전 SDK) 조회가 실패해도
|
|
363
|
+
// 종전과 동일한 무정보 outcome 으로 진행한다.
|
|
364
|
+
let stalledPerm;
|
|
365
|
+
if (client.permission) {
|
|
366
|
+
const permResult = await client.permission.list().catch(() => null);
|
|
367
|
+
stalledPerm = (permResult?.data ?? []).find(p => p.sessionID === sessionId);
|
|
368
|
+
}
|
|
369
|
+
err(`[pollSubSession] PERMISSION_STALL session=${sessionId} polls=${pollCount} stalledMs=${stalledMs}` +
|
|
370
|
+
(stalledPerm
|
|
371
|
+
? ` pending permissionID=${stalledPerm.id} type=${stalledPerm.permission} patterns=${JSON.stringify(stalledPerm.patterns)}`
|
|
372
|
+
: ` pending permission unknown (permission.list unavailable or empty)`));
|
|
359
373
|
await client.session.abort({ path: { id: sessionId } }).catch(() => undefined);
|
|
360
374
|
return {
|
|
361
375
|
kind: "permission_stall",
|
|
362
376
|
polls: pollCount,
|
|
363
377
|
elapsedMs: now() - startTime,
|
|
364
378
|
stalledMs,
|
|
379
|
+
permissionID: stalledPerm?.id,
|
|
380
|
+
permissionType: stalledPerm?.permission,
|
|
381
|
+
permissionPatterns: stalledPerm?.patterns,
|
|
365
382
|
};
|
|
366
383
|
}
|
|
367
384
|
// Message stall: sub-agent registered (sessionEverAppeared) and appears
|
|
@@ -587,8 +604,12 @@ export function pollOutcomeToLegacy(outcome) {
|
|
|
587
604
|
case "permission_stall":
|
|
588
605
|
return {
|
|
589
606
|
text: outcome.permissionType
|
|
590
|
-
? `(permission_stall: sub-agent blocked on ${outcome.permissionType} permission
|
|
591
|
-
|
|
607
|
+
? `(permission_stall: sub-agent blocked on ${outcome.permissionType} permission ` +
|
|
608
|
+
`(id=${outcome.permissionID}${outcome.permissionPatterns ? `, patterns=${JSON.stringify(outcome.permissionPatterns)}` : ""}) ` +
|
|
609
|
+
`— cannot be answered in subagent context; aborted after ${outcome.stalledMs}ms)`
|
|
610
|
+
: `(permission_stall: sub-agent tool call stalled for ${outcome.stalledMs}ms — likely waiting for a permission approval ` +
|
|
611
|
+
`that cannot be answered in subagent context; pending permission could not be identified (permission.list unavailable or empty). ` +
|
|
612
|
+
`점검: opencode.json 의 permission.external_directory 시드 존재 여부 (npx makdoong2-team doctor))`,
|
|
592
613
|
success: false,
|
|
593
614
|
};
|
|
594
615
|
case "session_gone":
|
|
@@ -54,8 +54,14 @@ if [ "$SPEC_HASH" != "__MISSING__" ] && [ "$SPEC_HASH" != "null" ] && [ -n "$SPE
|
|
|
54
54
|
fi
|
|
55
55
|
fail "spec_hash 는 기록됐는데 draft_path 마커도 확정 명세 파일도 없다 — requirements substage 를 재작업하라 (stages/02-requirements.md §2-0, §2-5 9번)"
|
|
56
56
|
fi
|
|
57
|
+
# "생성된 적 없음" 을 안내에서 빼면 복구 방향을 잘못 잡는다 — 실제로 가장 흔한
|
|
58
|
+
# 경우인데 종전 메시지는 동기화 누락·삭제만 언급해 리더가 동기화 문제부터
|
|
59
|
+
# 의심했다 (issue #8).
|
|
57
60
|
[ -f "$ROOT/$DRAFT" ] \
|
|
58
|
-
|| fail "draft_path=${DRAFT} 마커는 있으나 파일이 없다 (기준 경로 ${ROOT})
|
|
61
|
+
|| fail "draft_path=${DRAFT} 마커는 있으나 파일이 없다 (기준 경로 ${ROOT}). 가능한 원인 순서대로:
|
|
62
|
+
(1) 애초에 생성된 적 없음 — planner 가 마커만 기록하고 파일 생성에 실패한 경우. requirements substage 를 재작업해 write 툴로 초안부터 생성하라 (stages/02-requirements.md §2-0b),
|
|
63
|
+
(2) worktree 동기화 누락 — 다른 cwd(main repo/worktree)의 같은 상대경로에 파일이 있는지 확인,
|
|
64
|
+
(3) 파일이 삭제됨 — 삭제 경위 확인 후 requirements 재작업"
|
|
59
65
|
ACTUAL="$(sha256sum "$ROOT/$DRAFT" | cut -d' ' -f1)"
|
|
60
66
|
[ "$ACTUAL" = "$SPEC_HASH" ] \
|
|
61
67
|
|| fail "확정 명세 무단 변경 감지 (spec drift) — 동결 후 변경은 사용자 재승인 + spec_hash 재기록 절차만 허용 (stages/02-requirements.md §2-4a)"
|
package/package.json
CHANGED
package/stages/01-planning.md
CHANGED
|
@@ -89,6 +89,8 @@ mkdir -p .makdoong2-team/<이슈키>
|
|
|
89
89
|
|
|
90
90
|
파일 (repo/worktree root 기준 상대경로): `.makdoong2-team/<이슈키>/requirements-draft.md`
|
|
91
91
|
|
|
92
|
+
**초안 파일은 반드시 `write` 툴(filePath 인자)로 생성·갱신한다.** bash 리디렉션(`cat > …`, `printf > …` 등)과 `apply_patch` 는 같은 경로라도 훅이 차단한다 — planner 의 유일한 파일 쓰기 수단은 `write` 다. 이 경로의 `write` 는 planner READ-ONLY 원칙의 명시적 예외이며 훅이 허용한다 (issue #8).
|
|
93
|
+
|
|
92
94
|
초안 구조:
|
|
93
95
|
```markdown
|
|
94
96
|
# 요구사항 초안 — <이슈키>
|
|
@@ -62,6 +62,8 @@ mkdir -p .makdoong2-team/<ISSUE_KEY>
|
|
|
62
62
|
|
|
63
63
|
파일 경로 (repo/worktree root 기준 상대경로): `.makdoong2-team/<ISSUE_KEY>/requirements-draft.md`
|
|
64
64
|
|
|
65
|
+
**초안 파일은 반드시 `write` 툴(filePath 인자)로 생성·갱신한다.** bash 리디렉션(`cat > …`, `printf > …` 등)과 `apply_patch` 는 같은 경로라도 훅이 차단한다 — planner 의 유일한 파일 쓰기 수단은 `write` 다. 이 경로의 `write` 는 planner READ-ONLY 원칙의 명시적 예외이며 훅이 허용한다 (issue #8).
|
|
66
|
+
|
|
65
67
|
초안 초기 구조:
|
|
66
68
|
```markdown
|
|
67
69
|
# 요구사항 초안 — <ISSUE_KEY>
|