makdoong2-team 1.7.0 → 1.8.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/README.md +1 -0
- package/agents/makdoong2-issue-reporter.md +2 -1
- package/agents/makdoong2-verifier.md +26 -4
- package/dist/opencode-plugin.js +15 -6
- package/dist/state-access-guard.d.ts +38 -1
- package/dist/state-access-guard.js +100 -5
- package/gates/stage3-scope-verify.sh +15 -2
- package/package.json +1 -1
- package/scripts/lib/git-exclude.sh +60 -0
- package/scripts/run-tests.mjs +1 -0
- package/scripts/state.sh +10 -0
- package/scripts/wt-sync-ignored.sh +10 -25
- package/skills/makdoong2-issue-reporter/SKILL.md +186 -44
- package/stages/02-requirements.md +10 -2
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ makdoong2-team doctor # 설치 진단
|
|
|
106
106
|
- 커맨드가 **전용 full-permission 에이전트**(`makdoong2-issue-reporter`)로 라우팅되어, 호출 시점 이전의 로그·프롬프트·세션 컨텍스트를 스스로 수집해 이상 지점을 포착하고 [y00njinuk/makdoong2-team issues](https://github.com/y00njinuk/makdoong2-team/issues) 에 등록한다.
|
|
107
107
|
- **사용자 직접 호출이 유일한 트리거**다. 부장님·막둥이가 실패를 관측했다고 자율적으로 이슈를 만들지 않는다 (훅이 차단). 이 에이전트는 **선택 가능한 에이전트 목록에도, `@` 멘션 목록에도 뜨지 않으며**, `task` 툴로 spawn 하는 것도 차단된다 — 진입점은 위 커맨드 하나뿐이다.
|
|
108
108
|
- 저장소가 public 이므로 사내 정보는 마스킹 후 첨부되며, **전송 전 승인을 세션 안에서 묻는다**: 에이전트가 게시될 원문 전체를 `cat` 으로 표시하면 훅이 그 sha256 을 기록하고, 전송용 curl 호출 시 opencode 가 게시 여부를 묻는다(yes/no). 표시하지 않았거나 표시 후 내용이 바뀌면 전송이 차단된다 (승인은 사용자가 본 원문에 바인딩, 1회용). 승인 프롬프트에서는 **"Allow once"** 를 고른다 — "always" 는 남은 세션의 승인 질문을 없앤다.
|
|
109
|
+
- **이슈 본문은 고정 양식으로 쓴다.** 증상 · 환경 · 재현 절차 · 기대/실제 동작 · 실패 지점 · 타임라인 · 에러 메시지 · 재현성/영향 범위 · 시도한 조치 · 증거(필수)에, 조사에서 근거가 나오면 관련 관찰 · 의심 근본 원인 코드 · 부수 관찰 · 제안(조건부)이 붙는다. 양식은 발명이 아니라 [#5](https://github.com/y00njinuk/makdoong2-team/issues/5) 가 실제로 갖췄던 구성을 규약화한 것이다 — 그 이슈의 증거가 그대로 진단에 쓰여 v1.7.0 수정으로 이어졌다. 상세: `skills/makdoong2-issue-reporter/SKILL.md` §6
|
|
109
110
|
- PAT 는 `~/.config/opencode/.github` 파일에서 읽는다. **파일이 없거나 토큰을 찾지 못하면 중단하지 않고, 발급 URL 과 최소 권한(fine-grained: Issues Read/write, classic: `public_repo`)을 안내해 사용자에게 발급을 요청한 뒤 대기**한다. 401/403 도 같은 재발급 경로를 탄다. 상세: ARCHITECTURE.md §4.6
|
|
110
111
|
|
|
111
112
|
---
|
|
@@ -39,7 +39,7 @@ permission:
|
|
|
39
39
|
|
|
40
40
|
## 하드룰
|
|
41
41
|
|
|
42
|
-
1. **첫 행동으로 `skill(name="makdoong2-issue-reporter")` 를 로드**하고, 스킬에 정의된 절차를 그대로 따른다. 실행 순서는 스킬이 고정한다: **수집 → 이상 지점 포착 → 마스킹 → 중복 확인 → 최소 질의 → 이슈 생성**.
|
|
42
|
+
1. **첫 행동으로 `skill(name="makdoong2-issue-reporter")` 를 로드**하고, 스킬에 정의된 절차를 그대로 따른다. 실행 순서는 스킬이 고정한다: **수집 → 이상 지점 포착 → 마스킹 → 중복 확인 → 최소 질의 → 본문 작성 → 이슈 생성**.
|
|
43
43
|
2. **GitHub 게시(이슈·코멘트·Gist·라벨)는 훅이 강제하는 사용자 승인 게이트를 통과해야만 가능하다.** 절차는 고정이다:
|
|
44
44
|
1. payload 를 **리터럴 절대 경로** JSON 파일로 작성한다 (예: `/tmp/makdoong2-issue/issue-payload.json`).
|
|
45
45
|
2. **게시될 원문 전체를 `cat <payload>` 로 세션에 표시**하고 마스킹 내역 요약을 덧붙인다. 이 `cat` 은 **체이닝 없이 단독 실행**해야 하며(`;`·`&&`·리다이렉트·`$()` 금지), 훅이 이 시점의 sha256 을 표시 증명으로 기록한다. 요약·발췌로 대체 금지 — 사용자는 전송될 원문을 봐야 한다.
|
|
@@ -50,6 +50,7 @@ permission:
|
|
|
50
50
|
3. **토큰(PAT)은 어디에도 원문 노출 금지.** 커맨드 문자열에 직접 박지 않고 환경변수로 전달하며, 출력에는 마스킹(`ghp_****`)만 허용한다.
|
|
51
51
|
4. **워크플로우 상태를 변경하지 않는다.** state.json 은 증거 수집을 위한 읽기(`state.sh get`)만 허용. `state.sh set` / dispatch 계열 툴 호출 금지. 이 에이전트는 워크플로우 오케스트레이션과 완전히 분리된 조사·보고 전용이다.
|
|
52
52
|
5. **다른 에이전트로 위임하지 않는다.** 수집·분석·마스킹·등록 전 과정을 이 세션에서 직접 수행한다.
|
|
53
|
+
6. **이슈 본문은 스킬 6장 양식을 따른다.** 필수 섹션 10개와 `## 증거` 를 순서대로 채우고, 3장 수집에서 근거가 나온 조건부 섹션(`## 관련 관찰` / `## 참고: 의심 근본 원인 코드` / `## 부수 관찰 (minor)` / `## 제안 (참고)`)을 빠뜨리지 않는다. 모든 인용에 출처(파일+라인 또는 세션+턴)를 달고 단정과 추정을 구분한다(추정은 `> 추정:`). **payload 를 `cat` 으로 표시하기 전에 6.6 자기 점검을 통과시킨다** — 표시 후 본문을 고치면 표시 증명이 무효가 되어 하드룰 2 를 2번부터 다시 밟아야 한다. 기준 사례는 이슈 #5 다.
|
|
53
54
|
|
|
54
55
|
## 실행 컨텍스트
|
|
55
56
|
|
|
@@ -94,10 +94,10 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
94
94
|
`<STAGES_DIR>/NN-*.md`을 읽어 단계가 요구한 *명시적 산출물*을 추출한다.
|
|
95
95
|
- 1_planning.jira: **통합 planning spec(01-planning.md)** 사용 — 3개 substage를 한 번에 처리하므로 다음 모두 확인:
|
|
96
96
|
- `jira`: `template_validation` 6항목 모두 기록 + `validation_passed=true` + `done=true`
|
|
97
|
-
- `requirements`: `done=true` + `policy.category`(minor|major) 설정 + `self_check.categorized==true` + `
|
|
97
|
+
- `requirements`: `done=true` + `policy.category`(minor|major) 설정 + `self_check.categorized==true` + **`draft_path` 마커 기록 + 그 경로의 파일 존재** (아래 §2-4 참조)
|
|
98
98
|
- `scope`: `done=true` + `self_check.paths_explicit=true`
|
|
99
99
|
- **interview_required=true가 기록되어 있고 requirements.done=false이면**: 인터뷰 대기 상태 → **REJECTED** (부장님이 인터뷰 후 재dispatch 필요)
|
|
100
|
-
- 1_planning.requirements: (단독 dispatch 폴백 시) `done_at` / `verification_pending`
|
|
100
|
+
- 1_planning.requirements: (단독 dispatch 폴백 시) `done_at` / `verification_pending` + **`draft_path` 마커 기록 + 그 경로의 파일 존재** (아래 §2-4) + `.policy.category` 설정 + `self_check.categorized==true`
|
|
101
101
|
- 1_planning.scope: (단독 dispatch 폴백 시) 4가지 출력 형식 항목 + `done=true`
|
|
102
102
|
- 2_implementation.analysis:
|
|
103
103
|
- `.skipped == true` 이면 즉시 **VERIFIED** (게이트가 SKIP 처리한 경우이므로 산출물 없음)
|
|
@@ -114,7 +114,11 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
|
|
|
114
114
|
- `integration_points` 배열 길이 >= 1
|
|
115
115
|
- `test_conventions.framework` 가 존재하고 비어있지 않음 (`null`/빈 문자열 불가 — `"none"` 은 허용)
|
|
116
116
|
- `.self_check` 의 **7개** boolean (`has_project_structure` / `has_dependencies` / `has_task_relevant_files` / `has_conventions` / `has_integration_points` / `has_test_conventions` / `json_schema_valid`) 모두 true
|
|
117
|
-
- `git status --porcelain` 이 `workspace-analysis.json` 외 다른 파일 변경/신규를 보고하지 않음 (analyzer 위반 신호)
|
|
117
|
+
- `git status --porcelain` 이 **플러그인 자신의 상태 디렉터리(`.makdoong2-team/`)를 뺀 뒤** `workspace-analysis.json` 외 다른 파일 변경/신규를 보고하지 않음 (analyzer 위반 신호)
|
|
118
|
+
```bash
|
|
119
|
+
git status --porcelain | grep -v '\.makdoong2-team/' | grep -v 'workspace-analysis\.json'
|
|
120
|
+
```
|
|
121
|
+
출력이 비어 있어야 한다. `.makdoong2-team/` 은 플러그인이 작업 트리 안에 만드는 **자기 상태**이지 analyzer 의 부산물이 아니다. 이것을 위반으로 세면 해당 패턴이 git exclude 에 없는 저장소에서 **항상 REJECTED** 가 나고, 그 시점에 exclude 를 고칠 권한을 가진 역할이 파이프라인에 없어(analyzer 는 산출물 1개만 쓰기 가능 · team-leader 는 하드룰 2 로 차단 · engineer 는 analysis 통과 후 단계) 동일 사유 무한 루프가 된다 (issue #6-②).
|
|
118
122
|
- 2_implementation.dev: `done=true` + sub-agent output에 "테스트 추가" 명시 / 5체크
|
|
119
123
|
- 2_implementation.test: `.stages."2_implementation".substages."test"` 의 각 필드가 아래 조건을 모두 충족해야 함
|
|
120
124
|
- `.unit` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
|
|
@@ -271,10 +275,28 @@ BB_BASE=$(jq -r '.hosts.BITBUCKET_API_BASE_PATH' ~/.config/opencode/makdoong2-te
|
|
|
271
275
|
|
|
272
276
|
5. 위 4단계 모두 통과 시 § 2-2 (앵커 재검증) 로 계속.
|
|
273
277
|
|
|
278
|
+
### 2-4. 1_planning.requirements 전용: draft_path 마커 재검증
|
|
279
|
+
|
|
280
|
+
게이트(`stage3-scope-verify.sh`)는 `spec_hash` 가 기록돼 있으면 `draft_path` **마커**를 하드 요구한다. 반면 종전 verifier 기준은 `requirements-draft.md` **파일 존재**만 봤다. 두 기준이 어긋나 있어서, `spec_hash` 는 기록하고 `draft_path` 는 빠뜨린 채 `done=true` 로 끝난 substage 를 verifier 가 VERIFIED 로 통과시키고 **바로 다음 게이트가 하드 차단**하는 정지가 발생했다 (issue #6-①). 파일은 멀쩡히 있으므로 "파일 존재" 검사로는 절대 잡히지 않는다.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
DRAFT=$(bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".draft_path' | tr -d '"')
|
|
284
|
+
{ [ -n "$DRAFT" ] && [ "$DRAFT" != "null" ]; } || REJECTED # 마커 누락 — 파일이 있어도 REJECTED
|
|
285
|
+
ROOT=$(bash <SCRIPTS_DIR>/state.sh root)
|
|
286
|
+
if [[ "$DRAFT" == /* ]]; then DRAFT_ABS="$DRAFT"; else DRAFT_ABS="$ROOT/$DRAFT"; fi
|
|
287
|
+
[ -f "$DRAFT_ABS" ] || REJECTED
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
REJECTED 시 `findings[].item` 은 `requirements.draft_path_missing`, `next_action` 은 `REVERT_DONE_MARKER`. 재작업하는 planner 는 `stages/02-requirements.md` §2-5 의 9번 항목(`draft_recorded`)을 채우면 된다.
|
|
291
|
+
|
|
292
|
+
**게이트 · self_check · verifier 세 곳의 필수 마커 정의는 항상 일치해야 한다.** 한 곳만 고치면 같은 정지가 그대로 재현된다.
|
|
293
|
+
|
|
274
294
|
### 3. Sub-agent 출력 정합성 점검
|
|
275
295
|
|
|
276
296
|
다음 *추론형* 의심 신호를 확인한다:
|
|
277
|
-
- **빈 응답** — 산출물 언급 없이 "완료"
|
|
297
|
+
- **빈 응답** — 산출물 언급 없이 "완료" 선언.
|
|
298
|
+
**단, §1·§2 의 결정론적 검사를 전부 통과했다면 이 신호만으로 REJECTED 하지 않는다.** `dispatch_stage` 는 최종 텍스트가 없어도 `.done=true` 면 성공으로 처리한다 (`shouldOverrideEmptyOutcome` — 최종 텍스트를 생략하는 로컬 모델 보상). 그 보상과 이 신호가 어긋나 있으면 그런 모델에서는 **모든 substage 가 REJECTED** 된다 (issue #6 부수 관찰 1). 산출물과 마커가 결정론적으로 검증되는데 산문이 없다는 것은 실패의 증거가 아니다.
|
|
299
|
+
마커나 산출물이 하나라도 어긋나면 그때는 REJECTED 이지만, 그건 이 추론 신호가 아니라 **§1·§2 위반**으로 기록한다 (`findings[].item` 을 결정론적 항목명으로 적는다 — 원인을 "빈 응답" 으로 돌리면 재작업 방향이 어긋난다).
|
|
278
300
|
- **테스트 삭제 정황** — output에 "기존 테스트 제거" / `// @ts-ignore` 추가 / `as any` 도입 언급
|
|
279
301
|
- **인라인 disable** — `eslint-disable` / `# noqa` 등으로 게이트 우회
|
|
280
302
|
- **자기선언 완료** — `done=true`를 임의로 기록했으나 self_check 누락
|
package/dist/opencode-plugin.js
CHANGED
|
@@ -25,7 +25,7 @@ import { computeVerdictHash } from "./verdict-hash.js";
|
|
|
25
25
|
import { nextModel, applyConfigOverrides, POLICIES } from "./model-fallback-policy.js";
|
|
26
26
|
import { agentForStage, STAGE_SPEC_FILES } from "./agent-stage-config.js";
|
|
27
27
|
import { shouldEscalateStall } from "./stall-escalation.js";
|
|
28
|
-
import { buildStateWriteBlockMessage, classifyStateJsonAccess, STATE_SH_CALL_RE } from "./state-access-guard.js";
|
|
28
|
+
import { buildStateWriteBlockMessage, classifyStateJsonAccess, looksLikeRedirection, STATE_SH_CALL_RE, stripQuotedSpans, } from "./state-access-guard.js";
|
|
29
29
|
import { RESEARCH_SOURCES, DEFAULT_RESEARCH_TIMEOUT_MINUTES, buildResearchPrompt, mergeResearchFindings, normalizeQueries, parseResearchOutput, resolveParallelism, summarizeOutcomes, } from "./research-fanout.js";
|
|
30
30
|
import { TmuxMonitor, readTmuxConfig, orphanCleanupGuard } from "./tmux-monitor.js";
|
|
31
31
|
import { resolvePaths, loadConfig, readLoggingConfig, DEFAULT_STALL_ESCALATE_THRESHOLD, } from "./config.js";
|
|
@@ -266,18 +266,27 @@ export function looksLikeFileWrite(cmd) {
|
|
|
266
266
|
return false;
|
|
267
267
|
if (STATE_SH_CALL_RE.test(cmd))
|
|
268
268
|
return false;
|
|
269
|
+
// 리디렉션 계열은 인용 구간을 지운 문자열로 판정한다 — 따옴표 안의 `>` 는
|
|
270
|
+
// 셸 메타문자가 아니라 리터럴이다. `jq -e '… length >= 1'` 같은 읽기 전용
|
|
271
|
+
// 술어가 "파일 쓰기" 로 잡혀 analyzer 가 자기 산출물을 검증하지 못했다 (issue #6-③).
|
|
272
|
+
const bare = stripQuotedSpans(cmd);
|
|
273
|
+
// 셸 인라인 스크립트는 인용 구간이 사라지면 내부 리디렉션이 보이지 않는다 → 통째로 차단.
|
|
274
|
+
// 스크립트 파일 실행(`bash <SCRIPTS_DIR>/state.sh …`)은 `-c` 가 없어 매치되지 않는다.
|
|
275
|
+
if (/\b(?:ba|z|k|da)?sh\b\s+-\w*c\b/.test(cmd))
|
|
276
|
+
return true;
|
|
277
|
+
if (/(^|[|&;\s(`{])eval\s/.test(cmd))
|
|
278
|
+
return true;
|
|
269
279
|
if (/(^|[|&;])\s*(tee|dd)\b/.test(cmd))
|
|
270
280
|
return true;
|
|
271
281
|
if (/\bsed\b[^|;&]*\s-i(?:\b|['"])/.test(cmd))
|
|
272
282
|
return true;
|
|
273
|
-
if (/\bawk\b[^|;&]*?(?<![0-9])>\s*(?![&/])\S/.test(
|
|
274
|
-
return true;
|
|
275
|
-
if (/(^|[|;&\s])(cat|printf|echo)\b[^|;&]*?(<<\S+.*?)?(?<![0-9])>\s*(?![&/])\S/.test(cmd))
|
|
283
|
+
if (/\bawk\b[^|;&]*?(?<![0-9])>\s*(?![&/])\S/.test(bare))
|
|
276
284
|
return true;
|
|
277
|
-
if (/(^|[
|
|
285
|
+
if (/(^|[|;&\s])(cat|printf|echo)\b[^|;&]*?(<<\S+.*?)?(?<![0-9])>\s*(?![&/])\S/.test(bare))
|
|
278
286
|
return true;
|
|
279
|
-
if (
|
|
287
|
+
if (looksLikeRedirection(cmd))
|
|
280
288
|
return true;
|
|
289
|
+
// 인터프리터 인라인 스크립트는 따옴표 *안* 을 봐야 하므로 원문으로 판정한다.
|
|
281
290
|
if (/\bpython3?\s+-c\s+["'][^"']*open\s*\([^)]*["']\s*,\s*["'][wa]/.test(cmd))
|
|
282
291
|
return true;
|
|
283
292
|
if (/\bnode\s+-e\s+["'].*?(?:writeFileSync|writeFile\b|appendFileSync|createWriteStream)/.test(cmd))
|
|
@@ -10,6 +10,42 @@ export declare const STATE_JSON_PATH_RE: RegExp;
|
|
|
10
10
|
export declare const STATE_SH_SUBCOMMANDS: readonly ["root", "issue", "init", "status", "get", "set", "append", "migrate"];
|
|
11
11
|
/** `state.sh <승인 서브커맨드>` 호출 탐지. 목록은 STATE_SH_SUBCOMMANDS 하나에서만 온다. */
|
|
12
12
|
export declare const STATE_SH_CALL_RE: RegExp;
|
|
13
|
+
/**
|
|
14
|
+
* 따옴표 안의 내용을 공백으로 덮는다 (따옴표 자체와 **문자 오프셋**은 보존).
|
|
15
|
+
*
|
|
16
|
+
* ── 배경 (issue #6-③) ──
|
|
17
|
+
* 셸에서 따옴표 안의 `>`·`|`·`;` 는 메타문자가 아니라 리터럴인데, 종전 판정은
|
|
18
|
+
* 명령 문자열 전체를 훑었다. 그래서 analyzer 가 자기 산출물을 검증하려고 실행한
|
|
19
|
+
* jq -e '… and (.task_relevant_files | length >= 1)' …
|
|
20
|
+
* 의 `>=` 가 "출력 리디렉션" 으로, `|` 가 "파이프 세그먼트 경계" 로 잡혀
|
|
21
|
+
* 읽기 전용 술어가 3회 차단됐다 (리디렉션도 파이프도 없었다). `>=` 하나만 예외
|
|
22
|
+
* 처리하면 `awk '$1 > 2'`·`grep '>'` 같은 같은 계열이 계속 남는다 — 셸 문법을
|
|
23
|
+
* 그대로 모델링하는 쪽이 맞다.
|
|
24
|
+
*
|
|
25
|
+
* 길이를 보존하는 이유: 마스킹된 문자열에서 찾은 위치를 **원문에 그대로** 대응시켜
|
|
26
|
+
* 세그먼트를 잘라내기 위해서다 (`splitUnquotedSegments`).
|
|
27
|
+
*
|
|
28
|
+
* 보수적 처리 두 가지 — 오탐(막음)에는 우회로가 있지만 미탐(허용)은 복구 수단이 없다:
|
|
29
|
+
* - 따옴표가 닫히지 않으면 그 지점부터 원문을 그대로 남긴다.
|
|
30
|
+
* - 큰따옴표 안에 명령 치환(`$(` / 백틱)이 있으면 그 span 은 덮지 않는다.
|
|
31
|
+
* 큰따옴표 안에서는 치환이 실제로 실행되므로 `"$(cat a > b)"` 가 숨겨진다.
|
|
32
|
+
*/
|
|
33
|
+
export declare function stripQuotedSpans(cmd: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* 출력 리디렉션(`>` / `>>`) 탐지. `2>/dev/null`, `1>&2`, `>/dev/null` 은 제외한다.
|
|
36
|
+
*
|
|
37
|
+
* 인용 구간을 덮은 문자열로만 판정한다 — 따옴표 안의 `>` 는 리디렉션이 아니다.
|
|
38
|
+
* `looksLikeFileWrite`(leader/planner/analyzer 하드룰)와 아래 WRITE_INDICATORS 가
|
|
39
|
+
* 같은 함수를 쓴다. 한쪽만 고치면 다른 쪽에서 같은 오탐이 남는다.
|
|
40
|
+
*/
|
|
41
|
+
export declare function looksLikeRedirection(cmd: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* 인용 밖의 `;` `|` `&` `\n` 에서만 자른 **원문** 세그먼트.
|
|
44
|
+
*
|
|
45
|
+
* 종전에는 원문을 그대로 split 해서 `jq '.a | length'` 의 파이프가 세그먼트를
|
|
46
|
+
* 갈랐고, 뒷조각의 선두 토큰(`length`)이 읽기 allowlist 에 없어 차단됐다 (#6-③).
|
|
47
|
+
*/
|
|
48
|
+
export declare function splitUnquotedSegments(cmd: string): string[];
|
|
13
49
|
export type StateAccessVerdict =
|
|
14
50
|
/** 명령에 state.json 경로가 없다 — 이 가드의 관심 밖. */
|
|
15
51
|
{
|
|
@@ -34,7 +70,8 @@ export type StateAccessVerdict =
|
|
|
34
70
|
*
|
|
35
71
|
* 순서가 계약이다:
|
|
36
72
|
* 1. 경로 없음 → unrelated
|
|
37
|
-
* 2.
|
|
73
|
+
* 2. 리디렉션(인용 구간 제외) → write
|
|
74
|
+
* 2'. 그 밖의 쓰기 지표 → write (state.sh 호출이 같이 있어도 차단)
|
|
38
75
|
* 3. state.sh 승인 호출 → approved-helper
|
|
39
76
|
* 4. state.json 을 언급하는 모든 세그먼트가 읽기 전용 allowlist → read-only
|
|
40
77
|
* 5. 그 외 → write (모르는 명령은 차단)
|
|
@@ -38,6 +38,96 @@ export const STATE_SH_SUBCOMMANDS = [
|
|
|
38
38
|
];
|
|
39
39
|
/** `state.sh <승인 서브커맨드>` 호출 탐지. 목록은 STATE_SH_SUBCOMMANDS 하나에서만 온다. */
|
|
40
40
|
export const STATE_SH_CALL_RE = new RegExp(String.raw `state\.sh\s+(?:${STATE_SH_SUBCOMMANDS.join("|")})(?![\w-])`);
|
|
41
|
+
/**
|
|
42
|
+
* 따옴표 안의 내용을 공백으로 덮는다 (따옴표 자체와 **문자 오프셋**은 보존).
|
|
43
|
+
*
|
|
44
|
+
* ── 배경 (issue #6-③) ──
|
|
45
|
+
* 셸에서 따옴표 안의 `>`·`|`·`;` 는 메타문자가 아니라 리터럴인데, 종전 판정은
|
|
46
|
+
* 명령 문자열 전체를 훑었다. 그래서 analyzer 가 자기 산출물을 검증하려고 실행한
|
|
47
|
+
* jq -e '… and (.task_relevant_files | length >= 1)' …
|
|
48
|
+
* 의 `>=` 가 "출력 리디렉션" 으로, `|` 가 "파이프 세그먼트 경계" 로 잡혀
|
|
49
|
+
* 읽기 전용 술어가 3회 차단됐다 (리디렉션도 파이프도 없었다). `>=` 하나만 예외
|
|
50
|
+
* 처리하면 `awk '$1 > 2'`·`grep '>'` 같은 같은 계열이 계속 남는다 — 셸 문법을
|
|
51
|
+
* 그대로 모델링하는 쪽이 맞다.
|
|
52
|
+
*
|
|
53
|
+
* 길이를 보존하는 이유: 마스킹된 문자열에서 찾은 위치를 **원문에 그대로** 대응시켜
|
|
54
|
+
* 세그먼트를 잘라내기 위해서다 (`splitUnquotedSegments`).
|
|
55
|
+
*
|
|
56
|
+
* 보수적 처리 두 가지 — 오탐(막음)에는 우회로가 있지만 미탐(허용)은 복구 수단이 없다:
|
|
57
|
+
* - 따옴표가 닫히지 않으면 그 지점부터 원문을 그대로 남긴다.
|
|
58
|
+
* - 큰따옴표 안에 명령 치환(`$(` / 백틱)이 있으면 그 span 은 덮지 않는다.
|
|
59
|
+
* 큰따옴표 안에서는 치환이 실제로 실행되므로 `"$(cat a > b)"` 가 숨겨진다.
|
|
60
|
+
*/
|
|
61
|
+
export function stripQuotedSpans(cmd) {
|
|
62
|
+
const out = [];
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < cmd.length) {
|
|
65
|
+
const ch = cmd[i];
|
|
66
|
+
if (ch !== "'" && ch !== '"') {
|
|
67
|
+
out.push(ch);
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let close = -1;
|
|
72
|
+
for (let j = i + 1; j < cmd.length; j++) {
|
|
73
|
+
if (cmd[j] === "\\" && ch === '"') {
|
|
74
|
+
j++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (cmd[j] === ch) {
|
|
78
|
+
close = j;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (close < 0) {
|
|
83
|
+
// 미종료 따옴표 — 남은 전부를 원문으로 둔다 (메타문자가 계속 보이므로 차단 쪽).
|
|
84
|
+
out.push(cmd.slice(i));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
const body = cmd.slice(i + 1, close);
|
|
88
|
+
if (ch === '"' && /\$\(|`/.test(body)) {
|
|
89
|
+
// 큰따옴표 안의 명령 치환은 실제로 실행된다 → 덮지 않는다.
|
|
90
|
+
out.push(cmd.slice(i, close + 1));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out.push(ch, " ".repeat(body.length), ch);
|
|
94
|
+
}
|
|
95
|
+
i = close + 1;
|
|
96
|
+
}
|
|
97
|
+
return out.join("");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 출력 리디렉션(`>` / `>>`) 탐지. `2>/dev/null`, `1>&2`, `>/dev/null` 은 제외한다.
|
|
101
|
+
*
|
|
102
|
+
* 인용 구간을 덮은 문자열로만 판정한다 — 따옴표 안의 `>` 는 리디렉션이 아니다.
|
|
103
|
+
* `looksLikeFileWrite`(leader/planner/analyzer 하드룰)와 아래 WRITE_INDICATORS 가
|
|
104
|
+
* 같은 함수를 쓴다. 한쪽만 고치면 다른 쪽에서 같은 오탐이 남는다.
|
|
105
|
+
*/
|
|
106
|
+
export function looksLikeRedirection(cmd) {
|
|
107
|
+
return /(?:^|[|&;\s(`{])>>?\s*(?!&|\/dev\/(?:null|stderr|stdout|tty)(?![\w/]))\S/
|
|
108
|
+
.test(stripQuotedSpans(cmd));
|
|
109
|
+
}
|
|
110
|
+
/** 세그먼트 구분자 — 인용 밖에서만 유효하다. */
|
|
111
|
+
const SEGMENT_SPLIT_RE = /\|\||&&|[;|&\n]/g;
|
|
112
|
+
/**
|
|
113
|
+
* 인용 밖의 `;` `|` `&` `\n` 에서만 자른 **원문** 세그먼트.
|
|
114
|
+
*
|
|
115
|
+
* 종전에는 원문을 그대로 split 해서 `jq '.a | length'` 의 파이프가 세그먼트를
|
|
116
|
+
* 갈랐고, 뒷조각의 선두 토큰(`length`)이 읽기 allowlist 에 없어 차단됐다 (#6-③).
|
|
117
|
+
*/
|
|
118
|
+
export function splitUnquotedSegments(cmd) {
|
|
119
|
+
const masked = stripQuotedSpans(cmd);
|
|
120
|
+
const segments = [];
|
|
121
|
+
let start = 0;
|
|
122
|
+
SEGMENT_SPLIT_RE.lastIndex = 0;
|
|
123
|
+
let m;
|
|
124
|
+
while ((m = SEGMENT_SPLIT_RE.exec(masked)) !== null) {
|
|
125
|
+
segments.push(cmd.slice(start, m.index));
|
|
126
|
+
start = m.index + m[0].length;
|
|
127
|
+
}
|
|
128
|
+
segments.push(cmd.slice(start));
|
|
129
|
+
return segments;
|
|
130
|
+
}
|
|
41
131
|
/**
|
|
42
132
|
* 쓰기 의도 지표. 하나라도 매칭되면 명령 전체를 차단한다.
|
|
43
133
|
*
|
|
@@ -45,14 +135,17 @@ export const STATE_SH_CALL_RE = new RegExp(String.raw `state\.sh\s+(?:${STATE_SH
|
|
|
45
135
|
* 승인된 호출에 쓰기를 끼워 넣는 밀수 경로를 막기 위해서다.
|
|
46
136
|
*/
|
|
47
137
|
const WRITE_INDICATORS = [
|
|
48
|
-
// `>` / `>>` 리디렉션. `2>/dev/null`, `1>&2`, `>/dev/null` 은 제외한다.
|
|
49
|
-
[/(?:^|[|&;\s(`{])>>?\s*(?!&|\/dev\/(?:null|stderr|stdout|tty)(?![\w/]))\S/, "출력 리디렉션 (> / >>)"],
|
|
50
138
|
[/(?:^|[|&;(`{])\s*(?:tee|dd|sponge)\b/, "tee / dd / sponge"],
|
|
51
139
|
[/\bsed\b[^|;&]*\s-i(?:\b|['"])/, "sed -i (in-place 편집)"],
|
|
52
140
|
[/\b(?:perl|ruby)\b[^|;&]*\s-\w*i\b/, "perl / ruby -i (in-place 편집)"],
|
|
53
141
|
// 인터프리터 인라인 스크립트는 읽기/쓰기를 정적으로 구분할 수 없다 → 전부 차단.
|
|
54
142
|
// 읽기가 필요하면 cat / jq / head 를 쓴다.
|
|
55
143
|
[/\b(?:python3?|node|bun|deno|ruby|perl|php)\b\s+-\w*[ce]\b/, "인터프리터 인라인 스크립트 (-c / -e)"],
|
|
144
|
+
// 셸 인라인 스크립트도 정적으로 분해할 수 없다. 인용 구간을 지우면서(#6-③)
|
|
145
|
+
// `bash -c 'echo x > f'` 의 리디렉션이 보이지 않게 되므로 여기서 함께 막는다.
|
|
146
|
+
// 스크립트 파일 실행(`bash <SCRIPTS_DIR>/state.sh …`)은 `-c` 가 없어 매치되지 않는다.
|
|
147
|
+
[/\b(?:ba|z|k|da)?sh\b\s+-\w*c\b/, "셸 인라인 스크립트 (sh -c)"],
|
|
148
|
+
[/(?:^|[|&;\s(`{])eval\s/, "eval"],
|
|
56
149
|
[/(?:^|[|&;\s(`{])(?:cp|mv|rm|ln|install|touch|truncate|shred|chmod|chown|unlink|rsync|mkfifo)\s/, "파일 조작 명령"],
|
|
57
150
|
[/\bgit\b(?:\s+-\S+(?:\s+\S+)?)*\s+(?:add|rm|mv|checkout|restore|stash|apply|clean|update-index|reset|commit)\b/, "git 쓰기 서브커맨드"],
|
|
58
151
|
[/(?:^|[|&;\s(`{])(?:vi|vim|nano|emacs|ed|ex|patch)\s/, "편집기 / patch"],
|
|
@@ -75,7 +168,6 @@ const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
|
75
168
|
const GIT_OPTIONS_WITH_VALUE = new Set([
|
|
76
169
|
"-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path",
|
|
77
170
|
]);
|
|
78
|
-
const SEGMENT_SPLIT_RE = /\|\||&&|[;|&\n]/;
|
|
79
171
|
/** 세그먼트 선두의 괄호·역따옴표·환경변수 대입을 벗겨 실제 명령 토큰을 얻는다. */
|
|
80
172
|
function segmentHead(segment) {
|
|
81
173
|
let s = segment;
|
|
@@ -112,7 +204,8 @@ function gitSubcommand(rest) {
|
|
|
112
204
|
*
|
|
113
205
|
* 순서가 계약이다:
|
|
114
206
|
* 1. 경로 없음 → unrelated
|
|
115
|
-
* 2.
|
|
207
|
+
* 2. 리디렉션(인용 구간 제외) → write
|
|
208
|
+
* 2'. 그 밖의 쓰기 지표 → write (state.sh 호출이 같이 있어도 차단)
|
|
116
209
|
* 3. state.sh 승인 호출 → approved-helper
|
|
117
210
|
* 4. state.json 을 언급하는 모든 세그먼트가 읽기 전용 allowlist → read-only
|
|
118
211
|
* 5. 그 외 → write (모르는 명령은 차단)
|
|
@@ -120,6 +213,8 @@ function gitSubcommand(rest) {
|
|
|
120
213
|
export function classifyStateJsonAccess(cmd) {
|
|
121
214
|
if (!STATE_JSON_PATH_RE.test(cmd))
|
|
122
215
|
return { kind: "unrelated" };
|
|
216
|
+
if (looksLikeRedirection(cmd))
|
|
217
|
+
return { kind: "write", reason: "출력 리디렉션 (> / >>)" };
|
|
123
218
|
for (const [re, reason] of WRITE_INDICATORS) {
|
|
124
219
|
if (re.test(cmd))
|
|
125
220
|
return { kind: "write", reason };
|
|
@@ -127,7 +222,7 @@ export function classifyStateJsonAccess(cmd) {
|
|
|
127
222
|
if (STATE_SH_CALL_RE.test(cmd))
|
|
128
223
|
return { kind: "approved-helper" };
|
|
129
224
|
const readers = [];
|
|
130
|
-
for (const segment of cmd
|
|
225
|
+
for (const segment of splitUnquotedSegments(cmd)) {
|
|
131
226
|
if (!STATE_JSON_PATH_RE.test(segment))
|
|
132
227
|
continue;
|
|
133
228
|
const { head, rest } = segmentHead(segment);
|
|
@@ -36,8 +36,21 @@ SPEC_HASH="$(q '.stages."1_planning".substages."requirements".spec_hash')"
|
|
|
36
36
|
if [ "$SPEC_HASH" != "__MISSING__" ] && [ "$SPEC_HASH" != "null" ] && [ -n "$SPEC_HASH" ]; then
|
|
37
37
|
DRAFT="$(q '.stages."1_planning".substages."requirements".draft_path')"
|
|
38
38
|
ROOT="$("$HERE/../scripts/state.sh" root)"
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# 마커 누락과 파일 부재를 구분해서 알린다. 종전에는 둘 다 "확정 명세 파일 없음" 으로
|
|
40
|
+
# 뭉뚱그려서, 파일은 멀쩡히 있고 마커만 빠진 흔한 경우(issue #6-①)에 무엇을 해야
|
|
41
|
+
# 하는지 알 수 없었다. 마커 기록은 team-leader 에게 허용된 state.sh set 경로다.
|
|
42
|
+
DEFAULT_DRAFT=".makdoong2-team/${ISSUE}/requirements-draft.md"
|
|
43
|
+
if [ "$DRAFT" = "__MISSING__" ] || [ "$DRAFT" = "null" ] || [ -z "$DRAFT" ]; then
|
|
44
|
+
if [ -f "$ROOT/$DEFAULT_DRAFT" ]; then
|
|
45
|
+
fail "spec_hash 는 기록됐는데 draft_path 마커가 없다 — 파일은 ${DEFAULT_DRAFT} 에 있다.
|
|
46
|
+
복구: (1) sha256sum \"${ROOT}/${DEFAULT_DRAFT}\" 가 spec_hash(${SPEC_HASH}) 와 같은지 대조하고,
|
|
47
|
+
(2) 같으면 state.sh set 으로 requirements.draft_path 에 \"${DEFAULT_DRAFT}\" 를 기록한다 (stages/02-requirements.md §2-0),
|
|
48
|
+
(3) 다르면 명세가 동결 후 변경된 것이므로 requirements substage 를 재작업한다 (§2-4a)."
|
|
49
|
+
fi
|
|
50
|
+
fail "spec_hash 는 기록됐는데 draft_path 마커도 확정 명세 파일도 없다 — requirements substage 를 재작업하라 (stages/02-requirements.md §2-0, §2-5 9번)"
|
|
51
|
+
fi
|
|
52
|
+
[ -f "$ROOT/$DRAFT" ] \
|
|
53
|
+
|| fail "draft_path=${DRAFT} 마커는 있으나 파일이 없다 (기준 경로 ${ROOT}) — worktree 동기화 누락이거나 파일이 삭제됐다"
|
|
41
54
|
ACTUAL="$(sha256sum "$ROOT/$DRAFT" | cut -d' ' -f1)"
|
|
42
55
|
[ "$ACTUAL" = "$SPEC_HASH" ] \
|
|
43
56
|
|| fail "확정 명세 무단 변경 감지 (spec drift) — 동결 후 변경은 사용자 재승인 + spec_hash 재기록 절차만 허용 (stages/02-requirements.md §2-4a)"
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# git-exclude.sh — `.git/info/exclude` 에 패턴을 등록하는 공용 헬퍼.
|
|
2
|
+
# source 전용 (직접 실행하지 않는다).
|
|
3
|
+
#
|
|
4
|
+
# ── 배경 (issue #6-②) ──
|
|
5
|
+
# 플러그인은 자기 상태를 작업 트리 안(`.makdoong2-team/`)에 만들면서 git exclude 에는
|
|
6
|
+
# 등록하지 않았다. 그래서 그 패턴이 `.gitignore` 에도 `.git/info/exclude` 에도 없는
|
|
7
|
+
# 저장소에서는 `git status --porcelain` 이 항상 `?? .makdoong2-team/` 를 보고했고,
|
|
8
|
+
# "git status 청결" 을 요구하는 `2_implementation.analysis` verifier 가 **항상
|
|
9
|
+
# REJECTED** 를 냈다 (산출물·마커는 전부 정상인데도).
|
|
10
|
+
#
|
|
11
|
+
# 더 나쁜 것은 그 시점에 exclude 를 고칠 권한을 가진 역할이 파이프라인에 없다는 점이다:
|
|
12
|
+
# - analyzer : write 권한이 산출물 1개로 제한
|
|
13
|
+
# - team-leader: 하드룰 2 훅이 bash 파일 쓰기를 차단
|
|
14
|
+
# - engineer : `2_implementation.dev` 단계라 analysis 를 통과해야 도달
|
|
15
|
+
# 결국 동일 사유 REJECTED 가 무한 반복되고 사용자가 직접 한 줄을 넣어야 풀렸다.
|
|
16
|
+
#
|
|
17
|
+
# ── 왜 wt-sync-ignored.sh 의 ensure_baseline_gitexclude() 만으로는 부족한가 ──
|
|
18
|
+
# 그 함수는 **worktree 생성(= dev 진입) 시점에만** 돈다. plugin 의 wt-sync 호출은
|
|
19
|
+
# 전부 `DEV_OR_LATER_STAGES` / `worktree !== cwd` 로 가드되어 있다. analysis 는
|
|
20
|
+
# 그보다 앞선 main repo 단계라 그때는 한 번도 실행되지 않았다. 그래서 상태 디렉터리를
|
|
21
|
+
# **처음 만드는** `state.sh init` 에서도 등록한다.
|
|
22
|
+
#
|
|
23
|
+
# `.git/info/exclude` 는 커밋되지 않는 로컬 파일이라 대상 저장소의 이력을 건드리지 않는다.
|
|
24
|
+
# worktree 의 `--git-common-dir` 은 main repo 의 `.git` 을 가리키므로 한 번 등록하면
|
|
25
|
+
# main repo 와 모든 worktree 가 함께 적용받는다.
|
|
26
|
+
|
|
27
|
+
# 플러그인이 작업 트리 안에 만드는 자기 상태 디렉터리. 이 상수가 유일한 출처다.
|
|
28
|
+
MAKDOONG2_STATE_DIR_PATTERN=".makdoong2-team/"
|
|
29
|
+
|
|
30
|
+
# ensure_git_exclude_lines <repo-dir> <pattern>...
|
|
31
|
+
# 없는 라인만 append 한다. 추가한 개수를 stdout 으로 출력한다.
|
|
32
|
+
# git 저장소가 아니거나 파일을 쓸 수 없으면 0 을 출력하고 **성공으로 끝낸다** —
|
|
33
|
+
# 호출부(state.sh init 등)는 `set -e` 아래에서 돌고, exclude 등록 실패가
|
|
34
|
+
# 워크플로우 시작 자체를 막아서는 안 된다.
|
|
35
|
+
ensure_git_exclude_lines() {
|
|
36
|
+
local dir=$1
|
|
37
|
+
shift || true
|
|
38
|
+
[ "$#" -gt 0 ] || { echo 0; return 0; }
|
|
39
|
+
|
|
40
|
+
local common
|
|
41
|
+
common="$(git -C "${dir}" rev-parse --git-common-dir 2>/dev/null)" || { echo 0; return 0; }
|
|
42
|
+
[ -n "${common}" ] || { echo 0; return 0; }
|
|
43
|
+
if [[ "${common}" != /* ]]; then
|
|
44
|
+
common="$(cd "${dir}/${common}" 2>/dev/null && pwd -P)" || { echo 0; return 0; }
|
|
45
|
+
[ -n "${common}" ] || { echo 0; return 0; }
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
local file="${common}/info/exclude"
|
|
49
|
+
mkdir -p "${common}/info" 2>/dev/null || { echo 0; return 0; }
|
|
50
|
+
touch "${file}" 2>/dev/null || { echo 0; return 0; }
|
|
51
|
+
|
|
52
|
+
local added=0 ln
|
|
53
|
+
for ln in "$@"; do
|
|
54
|
+
[ -n "${ln}" ] || continue
|
|
55
|
+
if ! grep -qxF -- "${ln}" "${file}" 2>/dev/null; then
|
|
56
|
+
printf '%s\n' "${ln}" >> "${file}" 2>/dev/null && added=$((added + 1))
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
echo "${added}"
|
|
60
|
+
}
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -39,6 +39,7 @@ const STEPS = [
|
|
|
39
39
|
"node test/skill-mcp-registry.test.mjs",
|
|
40
40
|
"node --test test/state-write-guard.test.mjs",
|
|
41
41
|
"node --test test/state-access-guard.test.mjs",
|
|
42
|
+
"node --test test/git-exclude-registration.test.mjs",
|
|
42
43
|
"node --test test/issue-reporter-guard.test.mjs",
|
|
43
44
|
"node --test test/release-confirm.test.mjs",
|
|
44
45
|
"node --test test/state-sh-schema.test.mjs",
|
package/scripts/state.sh
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
# 의존: jq
|
|
29
29
|
set -euo pipefail
|
|
30
30
|
|
|
31
|
+
# 플러그인 자기 상태 디렉터리를 git exclude 에 등록하기 위한 공용 헬퍼 (issue #6-②).
|
|
32
|
+
# shellcheck source=lib/git-exclude.sh
|
|
33
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/git-exclude.sh"
|
|
34
|
+
|
|
31
35
|
root() {
|
|
32
36
|
# 호출 컨텍스트의 git toplevel 을 반환한다.
|
|
33
37
|
# - main repo에서 호출: main repo 경로
|
|
@@ -92,6 +96,12 @@ case "$cmd" in
|
|
|
92
96
|
init)
|
|
93
97
|
ISSUE="${1:?issue required}"; WT="${2:-$(root)}"
|
|
94
98
|
P="$(sp "$ISSUE")"; mkdir -p "$(dirname "$P")"
|
|
99
|
+
# 상태 디렉터리를 만드는 바로 그 자리에서 git exclude 에 등록한다. 여기서 하지
|
|
100
|
+
# 않으면 `git status` 가 플러그인 자신의 파일을 보고하고, "git status 청결" 을
|
|
101
|
+
# 요구하는 2_implementation.analysis verifier 가 항상 REJECTED 를 낸다 (issue #6-②).
|
|
102
|
+
# 이 시점(main repo)은 worktree 생성 전이라 wt-sync-ignored.sh 가 아직 돌지 않는다.
|
|
103
|
+
ADDED_EXCLUDE="$(ensure_git_exclude_lines "$(root)" "${MAKDOONG2_STATE_DIR_PATTERN}")"
|
|
104
|
+
[ "${ADDED_EXCLUDE}" = "0" ] || echo "[state.sh] .git/info/exclude += ${MAKDOONG2_STATE_DIR_PATTERN}" >&2
|
|
95
105
|
if [ ! -f "$P" ]; then
|
|
96
106
|
cat > "$P" <<JSON
|
|
97
107
|
{
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
# 예: "worktree": { "extra_exclude": ".idea/:tmp/" }
|
|
12
12
|
set -euo pipefail
|
|
13
13
|
|
|
14
|
+
# shellcheck source=lib/git-exclude.sh
|
|
15
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/git-exclude.sh"
|
|
16
|
+
|
|
14
17
|
# --reverse: worktree → main repo 역방향 동기화 (issue-scoped .makdoong2-team/<issue>/ 만)
|
|
15
18
|
REVERSE=false
|
|
16
19
|
while [[ "${1:-}" == --* ]]; do
|
|
@@ -130,24 +133,13 @@ fi
|
|
|
130
133
|
# 이미 존재하는 라인은 skip 하여 사용자 편집을 덮어쓰지 않는다.
|
|
131
134
|
ensure_baseline_gitexclude() {
|
|
132
135
|
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
136
|
local -a lines=()
|
|
149
137
|
|
|
150
138
|
# 공통 (모든 프로젝트)
|
|
139
|
+
# 플러그인 자신의 상태 디렉터리를 맨 앞에 둔다 — 이게 빠져 있으면 analysis verifier 가
|
|
140
|
+
# 항상 REJECTED 를 낸다 (issue #6-②). state.sh init 에서도 등록하지만, 이미 state.json 이
|
|
141
|
+
# 있어 init 의 등록 경로를 타지 않은 기존 워크플로우는 여기서 뒤늦게 구제된다.
|
|
142
|
+
lines+=("${MAKDOONG2_STATE_DIR_PATTERN}")
|
|
151
143
|
lines+=(".DS_Store" "Thumbs.db" "*.log" "*.swp")
|
|
152
144
|
|
|
153
145
|
# Python
|
|
@@ -175,16 +167,9 @@ ensure_baseline_gitexclude() {
|
|
|
175
167
|
lines+=("vendor/" "*.test" "*.out")
|
|
176
168
|
fi
|
|
177
169
|
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
170
|
+
# append 는 공용 헬퍼가 한다 (git-common-dir 해석·중복 검사 포함).
|
|
171
|
+
local added
|
|
172
|
+
added="$(ensure_git_exclude_lines "$wt" "${lines[@]}")"
|
|
188
173
|
if [ "$added" -gt 0 ]; then
|
|
189
174
|
echo "[wt-sync-ignored] baseline .git/info/exclude +$added lines (project-type detected)"
|
|
190
175
|
fi
|
|
@@ -22,7 +22,7 @@ makdoong2-team 플러그인의 결함을 재현 가능한 형태로 GitHub 이
|
|
|
22
22
|
- 사용자에게 "무슨 문제였나요"를 먼저 묻지 않는다. 호출 자체가 "직전에 뭔가 잘못됐으니 조사해서 남겨라"라는 지시다.
|
|
23
23
|
- 사용자가 증상을 한 줄만 말하거나 아무 설명 없이 호출해도 동작해야 한다. 부족한 정보는 질의가 아니라 **수집과 분석으로 먼저 메운다**.
|
|
24
24
|
- 질의(4장)는 수집·분석으로 확정할 수 없는 항목(기대 동작, 재현성, 시도한 조치)과 **분석 결과 확인**에만 사용한다.
|
|
25
|
-
- 실행 순서는 고정한다: **수집(3장) → 이상 지점 포착(3.3) → 마스킹(2장) → 중복 확인(5장) → 최소 질의(4장) → 이슈 생성(7장)**.
|
|
25
|
+
- 실행 순서는 고정한다: **수집(3장) → 이상 지점 포착(3.3) → 마스킹(2장) → 중복 확인(5장) → 최소 질의(4장) → 본문 작성(6장) → 이슈 생성(7장)**.
|
|
26
26
|
|
|
27
27
|
## 0. 대상 리소스
|
|
28
28
|
|
|
@@ -130,6 +130,8 @@ test -n "$GH_TOKEN" || echo "NO_TOKEN"
|
|
|
130
130
|
| 모델·프로바이더 사내 엔드포인트 | 사내 모델 서버 주소 | `<internal-model-endpoint>` |
|
|
131
131
|
| 토큰 유사 문자열 | `ghp_...`, `eyJ...`(JWT) | `<redacted-token>` |
|
|
132
132
|
|
|
133
|
+
같은 원본은 항상 같은 자리표시자로, 다른 원본은 다른 자리표시자로 쓴다. **같은 종류가 둘 이상이면 접미사로 구분한다** — `<internal-repo-A>` / `<internal-repo-B>`. 둘을 한 이름으로 뭉치면 "두 프로젝트에서 같은 증상" 과 "한 프로젝트에서 두 번" 이 구별되지 않아 재현 조건이 무너진다.
|
|
134
|
+
|
|
133
135
|
### 2.3 절차
|
|
134
136
|
|
|
135
137
|
1. 첨부 후보 텍스트를 모은 뒤, **첨부 직전에** 마스킹 스캔을 1회 수행한다.
|
|
@@ -325,101 +327,241 @@ curl -sS -X POST https://api.github.com/repos/y00njinuk/makdoong2-team/issues/<n
|
|
|
325
327
|
|
|
326
328
|
> 코멘트 추가도 GitHub 쓰기이므로 7-1 의 사용자 승인 게이트를 동일하게 거친다.
|
|
327
329
|
|
|
328
|
-
|
|
330
|
+
**열린 이슈 목록을 이 문서에 하드코딩하지 않는다.** 스냅샷은 곧 낡고, 삭제·이관된 이슈를 가리키면 중복 판정 자체가 어긋난다(과거 이 자리에 있던 #1·#4 표가 그렇게 됐다 — 두 이슈는 현재 `410 Gone` 이다). 목록은 매 실행 시 위 검색으로 얻는다.
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
|
333
|
-
|
|
332
|
+
판정 기준:
|
|
333
|
+
|
|
334
|
+
| 판정 | 조건 | 처리 |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| 동일 | 실패 지점(에이전트 + 단계)과 근본 증상이 모두 같다 | 신규 생성 대신 **코멘트** |
|
|
337
|
+
| 유사 | 증상은 닮았으나 실패 지점 또는 재현 경로가 다르다 | 신규 생성 + 본문에 관련 이슈 링크 |
|
|
338
|
+
| 무관 | 위 둘 다 아니다 | 신규 생성 |
|
|
334
339
|
|
|
335
|
-
|
|
340
|
+
**닫힌 이슈도 검색 대상이다** (`is:open` 을 빼고 한 번 더 조회한다). 이미 고쳐진 결함의 재발이면 그 사실 자체가 가장 중요한 정보이므로, 신규 이슈보다 원 이슈에 코멘트로 재발을 알리고 어떤 버전에서 다시 났는지 명시한다.
|
|
341
|
+
|
|
342
|
+
코멘트로 축적할 때도 6장 양식을 그대로 쓴다. 원 이슈와 동일한 항목(환경·재현 절차)은 `#<번호> 와 동일` 로 적고 **차이점과 새 증거만** 덧붙인다.
|
|
336
343
|
|
|
337
344
|
---
|
|
338
345
|
|
|
339
|
-
## 6. 이슈 본문
|
|
346
|
+
## 6. 이슈 양식 (본문 작성 가이드)
|
|
347
|
+
|
|
348
|
+
**이슈는 유지보수자가 그대로 진단에 쓸 수 있는 형태여야 한다.** 기준 사례는 [#5](https://github.com/y00njinuk/makdoong2-team/issues/5) 다 — 처리 코멘트가 타임라인·로그 발췌·반복 차단 표·의심 코드 지목을 지목하며 "그대로 진단에 쓰였다" 고 밝혔고, 본문 `## 제안` 3건이 항목별로 수정에 반영됐다. 아래 양식은 그 이슈가 실제로 갖췄던 구성을 규약으로 고정한 것이다.
|
|
340
349
|
|
|
341
|
-
|
|
350
|
+
**증상 한 줄 + 로그 덤프는 이 양식이 아니다.** 3장에서 수집·포착한 것을 아래 구조에 배치하는 것이 이 스킬의 산출물이다.
|
|
342
351
|
|
|
343
|
-
|
|
344
|
-
|
|
352
|
+
### 6.0 공통 원칙 (모든 섹션에 적용)
|
|
353
|
+
|
|
354
|
+
| 원칙 | 의미 |
|
|
355
|
+
|---|---|
|
|
356
|
+
| **출처 없는 인용 금지** | 모든 코드블록·표 인용에 출처를 단다 — 파일 경로 + 라인 번호(`opencode.log line 19618`), 또는 세션 ID + 턴 번호. 출처 없는 발췌는 유지보수자가 원본을 되짚을 수 없어 가치가 절반이 된다 |
|
|
357
|
+
| **관측과 추정의 분리** | 로그·응답으로 확인한 것만 단정형으로 쓴다. 추정은 `> 추정:` 블록쿼트에 넣고 근거와 "추정이며 단정은 아님" 을 함께 적는다 (3.3 판정 규칙과 동일) |
|
|
358
|
+
| **재현 가능성 우선** | 제3자가 재현 절차만 읽고 같은 상태를 만들 수 있어야 한다. 비정상으로 보이는 전제가 있으면 **"왜 그 상태였는지"** 를 함께 적는다 — 결함이 아닌 것을 결함으로 오인시키지 않기 위해서다 |
|
|
359
|
+
| **마스킹은 1:1 대응** | 같은 원본은 항상 같은 자리표시자로, 다른 원본은 다른 자리표시자로 쓴다. 같은 종류가 둘 이상이면 접미사로 구분한다 (`<internal-repo-A>` / `<internal-repo-B>`). 본문 끝 고지 2줄은 생략 불가 |
|
|
345
360
|
|
|
346
|
-
|
|
361
|
+
### 6.1 제목
|
|
362
|
+
|
|
363
|
+
접두 태그·이슈 번호·심각도 없이 **한국어 서술형 한 문장**으로 쓴다. 구성은 `<어디서> + <무엇이 어떻게 잘못되어> + <그 결과>` 이고 `현상` 또는 `이슈` 로 끝맺는다.
|
|
364
|
+
|
|
365
|
+
기준 예(#5):
|
|
366
|
+
|
|
367
|
+
> state.json hardrule 훅이 읽기 전용 진단 명령까지 차단하여 state_unreadable 이후 복구 절차가 막히는 현상
|
|
368
|
+
|
|
369
|
+
| 조각 | 해당 부분 |
|
|
370
|
+
|---|---|
|
|
371
|
+
| 어디서 | `state.json hardrule 훅이` |
|
|
372
|
+
| 무엇이 어떻게 | `읽기 전용 진단 명령까지 차단하여` |
|
|
373
|
+
| 그 결과 | `state_unreadable 이후 복구 절차가 막히는 현상` |
|
|
374
|
+
|
|
375
|
+
규칙:
|
|
376
|
+
|
|
377
|
+
- 사내 식별자(고객사명, 사내 시스템·저장소명, 실제 Jira 키)를 넣지 않는다. 제목도 본문과 **동일하게** 2장 규칙을 받는다.
|
|
378
|
+
- 커밋 제목의 50자 제한은 **적용하지 않는다** — 증상이 특정되는 길이를 우선한다. 다만 한 문장을 넘기지 않는다.
|
|
379
|
+
- **결과를 빼지 않는다.** `훅이 명령을 차단하는 현상` 은 무엇이 막혔는지가 없어 유지보수자가 우선순위를 매길 수 없다.
|
|
380
|
+
- **원인을 제목에 단정하지 않는다.** 원인은 `> 추정:` 과 `## 참고: 의심 근본 원인 코드` 의 몫이다. 제목은 관측된 현상만 담는다.
|
|
381
|
+
|
|
382
|
+
### 6.2 섹션 구성
|
|
383
|
+
|
|
384
|
+
**순서 고정.** 필수 섹션은 내용이 없어도 생략하지 않고 `없음` / `미확인` 을 적는다. 조건부 섹션은 해당 사실이 있을 때만 넣고, 없으면 헤딩째 뺀다(빈 섹션을 남기지 않는다).
|
|
385
|
+
|
|
386
|
+
| # | 섹션 | 구분 | 담는 것 |
|
|
387
|
+
|---|---|---|---|
|
|
388
|
+
| 1 | `## 증상` | 필수 | 한두 문장 요약. 핵심 메커니즘만 굵게 |
|
|
389
|
+
| 2 | `## 환경` | 필수 | 표 — 버전·OS·런타임·모델·발생 시각·세션 ID |
|
|
390
|
+
| 3 | `## 재현 절차` | 필수 | 번호 매긴 단계. 각 단계의 입력과 관측된 반환값 |
|
|
391
|
+
| 4 | `## 기대 동작` | 필수 | 정상이라면 어떠해야 하는지 **+ 그 근거** |
|
|
392
|
+
| 5 | `## 실제 동작` | 필수 | 관측된 결과. 에이전트 자신의 오판도 포함 |
|
|
393
|
+
| 6 | `## 실패 지점` | 필수 | 관련 에이전트 / 단계 / 연동 경로 / 프롬프트 이상 4줄 |
|
|
394
|
+
| 7 | `## 타임라인 (수집 구간: …)` | 필수 | 표 + `> 추정:` |
|
|
395
|
+
| 8 | `## 에러 메시지` | 필수 | 번호 매긴 다목록. 항목마다 출처 |
|
|
396
|
+
| 9 | `## 재현성 / 영향 범위` | 필수 | 재현 조건(결정적인가)과 파급 범위 |
|
|
397
|
+
| 10 | `## 시도한 조치` | 필수 | 없으면 `없음` |
|
|
398
|
+
| 11 | `## 관련 관찰: <요약>` | 조건부 | 같은 증상이 과거·다른 프로젝트에서 반복된 기록 |
|
|
399
|
+
| 12 | `## 참고: 의심 근본 원인 코드 (<버전>, <파일>)` | 조건부 | 설치된 코드에서 원인 후보를 지목할 수 있을 때 |
|
|
400
|
+
| 13 | `## 부수 관찰 (minor)` | 조건부 | 본 결함과 별개지만 같은 조사에서 발견한 사소한 문제 |
|
|
401
|
+
| 14 | `## 제안 (참고)` | 조건부 | 수정 방향. 번호 매긴 목록 |
|
|
402
|
+
| 15 | `## 증거` | 필수 | `<details>` 블록 + 마스킹 고지 2줄 |
|
|
403
|
+
|
|
404
|
+
**11–14 는 "여유 있으면 쓰는 것" 이 아니다.** 3장 수집에서 근거가 나왔는데도 빼면 유지보수자가 같은 조사를 처음부터 다시 한다. #5 는 넷을 모두 채웠고 처리 코멘트가 항목별로 그것을 인용했다. 반대로 **근거 없이 채우지도 않는다** — 추측으로 만든 `## 제안` 은 진단을 잘못된 방향으로 끈다.
|
|
405
|
+
|
|
406
|
+
### 6.3 본문 템플릿
|
|
347
407
|
|
|
348
408
|
~~~markdown
|
|
349
409
|
## 증상
|
|
350
|
-
<한두
|
|
410
|
+
<한두 문장. "무엇이 어떤 조건에서 어떻게 되어 무엇이 막혔는가". 핵심 메커니즘만 굵게>
|
|
351
411
|
|
|
352
412
|
## 환경
|
|
353
413
|
| 항목 | 값 |
|
|
354
414
|
|---|---|
|
|
355
415
|
| OpenCode | <version> |
|
|
356
|
-
| omo | <version> |
|
|
357
|
-
| makdoong2-team | <
|
|
358
|
-
| OS | <os> (WSL2
|
|
359
|
-
| Runtime | node <ver>
|
|
416
|
+
| omo (oh-my-openagent) | <version> |
|
|
417
|
+
| makdoong2-team | v<설치 버전> (commit <short-sha>) |
|
|
418
|
+
| OS | <os> (WSL2 여부·커널 포함) |
|
|
419
|
+
| Runtime | node <ver> (bun <ver> 또는 "bun 미설치") |
|
|
360
420
|
| Provider / Model | <provider> / <model> |
|
|
361
|
-
| 발생 시각 | <ISO8601> |
|
|
421
|
+
| 발생 시각 | <ISO8601 KST> (<ISO8601 UTC>) |
|
|
362
422
|
| 세션 ID | <session-id> |
|
|
363
423
|
|
|
364
424
|
## 재현 절차
|
|
365
|
-
1.
|
|
366
|
-
2.
|
|
367
|
-
3.
|
|
425
|
+
1. <초기 상태와 그 상태가 된 경위>
|
|
426
|
+
2. <실행한 조작 — 프롬프트·커맨드 원문>
|
|
427
|
+
3. <관측된 반환값·응답>
|
|
428
|
+
4. <이상이 나타난 지점>
|
|
368
429
|
|
|
369
430
|
## 기대 동작
|
|
370
|
-
|
|
431
|
+
- <정상이라면 어떠해야 하는가>
|
|
432
|
+
- <그렇게 판단하는 근거 — 문서·에러 메시지 문구·기존 계약>
|
|
371
433
|
|
|
372
434
|
## 실제 동작
|
|
373
|
-
|
|
435
|
+
- <관측된 결과>
|
|
436
|
+
- <에이전트의 오판이 있었다면 그것도>
|
|
374
437
|
|
|
375
438
|
## 실패 지점
|
|
376
|
-
- 관련 에이전트:
|
|
377
|
-
- 단계: <1_planning.jira 등
|
|
378
|
-
- 연동 경로: <Teams / MCP /
|
|
379
|
-
- 프롬프트 이상: <injection / 누락 / 중복 /
|
|
439
|
+
- 관련 에이전트: <이름 또는 unknown>
|
|
440
|
+
- 단계: <1_planning.jira 등 단계명. 해당 없으면 "요청 분배 / 오케스트레이션">
|
|
441
|
+
- 연동 경로: <Teams / MCP / Jira / 없음(로컬 플러그인 훅) — 구체 지점까지>
|
|
442
|
+
- 프롬프트 이상: <injection / 누락 / 중복 / 없음 — 아니면 "없음" 과 그 근거>
|
|
380
443
|
|
|
381
|
-
## 타임라인 (수집 구간:
|
|
382
|
-
| 시각 | 지점 | 관측 내용 |
|
|
444
|
+
## 타임라인 (수집 구간: <시작> ~ <끝>)
|
|
445
|
+
| 시각 (KST) | 지점 | 관측 내용 |
|
|
383
446
|
|---|---|---|
|
|
447
|
+
| <ts> | 세션 시작 | <...> |
|
|
384
448
|
| <ts> | 마지막 정상 단계 | <...> |
|
|
385
449
|
| <ts> | **최초 이상 발생** | <...> |
|
|
386
450
|
| <ts> | 파급 | <...> |
|
|
387
451
|
|
|
388
|
-
> 추정: <원인
|
|
452
|
+
> 추정: <원인 추정 + 근거. 근거의 출처(코드 독해/로그 패턴)와 "단정은 아님" 을 명시. 근거가 없으면 "미상">
|
|
389
453
|
|
|
390
454
|
## 에러 메시지
|
|
455
|
+
1. <어디서 나온 무엇인지 — 예: `auto_advance_stage` 반환값 (JSON 필드 전문, 경로 마스킹)>
|
|
456
|
+
```
|
|
457
|
+
<원문>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
2. <두 번째 메시지 — 예: 훅 차단 로그 (`/var/log/opencode/opencode.log` line <N>)>
|
|
391
461
|
```
|
|
392
|
-
|
|
462
|
+
<원문>
|
|
393
463
|
```
|
|
394
464
|
|
|
395
465
|
## 재현성 / 영향 범위
|
|
396
|
-
- 재현성: <항상 / 간헐적(n회 중 m회) / 1회성>
|
|
397
|
-
- 영향 범위:
|
|
466
|
+
- 재현성: <항상(결정적 — 조건 명시) / 간헐적(n회 중 m회) / 1회성>
|
|
467
|
+
- 영향 범위: <어느 에이전트·단계·기능까지 번지는가>
|
|
398
468
|
|
|
399
469
|
## 시도한 조치
|
|
400
|
-
-
|
|
470
|
+
- <이미 해본 우회·수정과 그 결과. 없으면 "없음">
|
|
401
471
|
|
|
402
|
-
##
|
|
403
|
-
|
|
404
|
-
|
|
472
|
+
## 관련 관찰: <요약> (조건부)
|
|
473
|
+
| 시각 | 에이전트 | 관측 |
|
|
474
|
+
|---|---|---|
|
|
475
|
+
| <ts> | <agent> | <...> |
|
|
405
476
|
|
|
477
|
+
## 참고: 의심 근본 원인 코드 (<버전>, <파일>) (조건부)
|
|
478
|
+
```<lang>
|
|
479
|
+
<해당 코드 발췌>
|
|
406
480
|
```
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
</details>
|
|
481
|
+
- <왜 이것이 의심되는가>
|
|
482
|
+
- <설치본 코드 독해 기반 추정임을 명시>
|
|
410
483
|
|
|
484
|
+
## 부수 관찰 (minor) (조건부)
|
|
485
|
+
<본 결함과 별개지만 같은 조사에서 발견한 사소한 문제>
|
|
486
|
+
|
|
487
|
+
## 제안 (참고) (조건부)
|
|
488
|
+
1. <수정 방향 A>
|
|
489
|
+
2. <수정 방향 B>
|
|
490
|
+
|
|
491
|
+
## 증거
|
|
411
492
|
<details>
|
|
412
|
-
<summary
|
|
493
|
+
<summary>opencode.log 발췌 (lines N–M, 경로·이슈키 마스킹됨)</summary>
|
|
413
494
|
|
|
414
495
|
```
|
|
415
|
-
<
|
|
496
|
+
<log>
|
|
416
497
|
```
|
|
417
498
|
</details>
|
|
418
499
|
|
|
419
|
-
> 마스킹 내역: <가린 항목 종류와
|
|
500
|
+
> 마스킹 내역: <가린 항목 종류와 건수. 스캔으로 확인한 "포함되지 않음" 도 함께>
|
|
420
501
|
> 마스킹으로 생략된 정보: <있으면 기재, 없으면 "없음">
|
|
421
502
|
~~~
|
|
422
503
|
|
|
504
|
+
### 6.4 섹션별 작성 규칙
|
|
505
|
+
|
|
506
|
+
**`## 증상`** — 조건·메커니즘·결과가 한 덩어리로 읽혀야 한다. 굵게는 원인 메커니즘 한 곳에만 쓴다(전부 굵으면 강조가 사라진다). "안 됩니다"·"이상합니다" 같은 체감 표현 대신 관측 가능한 서술로 쓴다.
|
|
507
|
+
|
|
508
|
+
**`## 환경`** — 4.2 의 자동 수집 결과를 그대로 옮긴다. makdoong2-team 은 **설치된 버전**을 쓴다(개발 중인 HEAD 가 아니라). 사용자가 구버전을 쓰고 있으면 그 사실이 곧 답인 경우가 있다. 사내 모델 엔드포인트는 마스킹하되 provider/model 이름 자체는 남긴다.
|
|
509
|
+
|
|
510
|
+
**`## 재현 절차`** — 각 단계에 **입력과 관측된 반환값**을 함께 적는다. 비정상으로 보이는 전제(파일 부재, 이상한 설정)가 있으면 괄호로 경위를 밝힌다 — #5 의 `(state 파일 부재는 사용자의 의도적 삭제이며, 데이터 손실·동기화 실패가 아님)` 이 그 예다. 이 한 줄이 없으면 유지보수자는 "동기화 버그" 를 먼저 쫓는다.
|
|
511
|
+
|
|
512
|
+
**`## 기대 동작`** — 기대만 쓰고 근거를 빼지 않는다. 근거는 문서 문구, 에러 메시지가 스스로 열거한 범위, 기존 계약 중 하나를 인용한다. #5 는 "훅의 에러 메시지 자체가 금지 대상으로 쓰기만 열거한다" 를 근거로 들어 기대가 임의 요구가 아님을 보였다.
|
|
513
|
+
|
|
514
|
+
**`## 실제 동작`** — 결과와 함께 **에이전트 자신의 오판**도 적는다. #5 는 leader 가 차단 사유를 "하드룰 2 위반" 으로 잘못 인용한 것을 기록했고, 그것이 별도의 수정 항목(오인 유발 메시지 개선)이 됐다. 자기 오판을 감추면 그 결함은 영영 보고되지 않는다.
|
|
515
|
+
|
|
516
|
+
**`## 실패 지점`** — 4줄 모두 채운다. 해당 없는 항목은 `없음` + 근거를 쓴다(`프롬프트 이상: 없음 (injection 아님. 훅 차단은 설계대로 동작했으나 대상이 읽기 전용 명령)`). 빈칸으로 두면 "조사하지 않음" 과 구별되지 않는다.
|
|
517
|
+
|
|
518
|
+
**`## 타임라인`** — 헤딩에 수집 구간을 명시한다. **시각은 KST 표기하고 UTC 를 병기**한다(로그는 UTC 라 병기가 없으면 대조가 안 된다). `**최초 이상 발생**` 행은 굵게 표시해 3.3 의 포착 결과를 한눈에 드러낸다. 마지막에 `> 추정:` 한 덩어리를 둔다.
|
|
519
|
+
|
|
520
|
+
**`## 에러 메시지`** — 메시지가 둘 이상이면 **번호를 매겨 나눈다**. 항목마다 "무엇의 어디서 나온 메시지인지" 한 줄을 앞에 두고 코드블록을 붙인다. 툴 반환값 JSON 은 요약하지 말고 **필드 전문**을 싣는다 — `next_action` 처럼 에이전트 행동을 지시한 필드가 원인 규명의 핵심인 경우가 있다.
|
|
521
|
+
|
|
522
|
+
**`## 재현성 / 영향 범위`** — `항상` 이면 무엇이 결정적인지 괄호로 밝힌다(`항상(결정적 — 명령 문자열에 state.json 경로가 포함되면 무조건 차단)`). `간헐적` 이면 관측 횟수를 분수로 쓴다.
|
|
523
|
+
|
|
524
|
+
**`## 관련 관찰`** (조건부) — 로그에서 같은 증상의 과거 발생을 찾았을 때 넣는다. 표로 시각·에이전트·명령(요약)을 나열하고, **어느 것이 정당한 동작이고 어느 것이 오탐인지 행마다 표시**한다. #5 의 9건 표는 그중 2건을 `(쓰기 측 — 차단 자체는 적절)` 로 구분해, 수정 범위를 정확히 좁혔다.
|
|
525
|
+
|
|
526
|
+
**`## 참고: 의심 근본 원인 코드`** (조건부) — 설치본(`dist/`)에서 해당 함수·분기를 발췌하고 헤딩에 버전과 파일을 적는다. 왜 의심되는지 불릿으로 덧붙이고, **코드 독해 기반 추정임을 명시**한다. 사내 코드는 여기 넣지 않는다(2.1) — 이 섹션은 makdoong2-team 자체 코드 전용이다.
|
|
527
|
+
|
|
528
|
+
**`## 부수 관찰 (minor)`** (조건부) — 본 결함과 무관하지만 같은 조사에서 눈에 띈 것. 별도 이슈로 올릴 만큼은 아닌 것만 넣는다. 크면 별도 이슈로 분리한다.
|
|
529
|
+
|
|
530
|
+
**`## 제안 (참고)`** (조건부) — 번호 매긴 목록. **관측에서 직접 도출되는 것만** 쓴다. 헤딩의 `(참고)` 를 지우지 않는다 — 결정 권한은 유지보수자에게 있다.
|
|
531
|
+
|
|
532
|
+
**`## 증거`** — 3.4·3.5 를 따른다. `<summary>` 안에는 꺾쇠(`<`, `>`)를 쓰지 않는다(HTML 로 파싱된다). 마지막 고지 2줄은 필수이며, **"토큰·비밀번호·사내 IP 는 포함되지 않음(스캔으로 확인)" 처럼 확인한 사실도 함께 적는다** — 무엇을 안 가렸는지가 아니라 무엇을 확인했는지가 신뢰의 근거다.
|
|
533
|
+
|
|
534
|
+
### 6.5 기준 사례
|
|
535
|
+
|
|
536
|
+
[#5](https://github.com/y00njinuk/makdoong2-team/issues/5) 를 참고 원문으로 삼는다. 필요하면 `webfetch` 또는 아래로 원문을 확인한다.
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
curl -sS -H "Accept: application/vnd.github+json" \
|
|
540
|
+
https://api.github.com/repos/y00njinuk/makdoong2-team/issues/5
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
다만 **#5 를 복사하지 않는다.** 참고 대상은 구조와 서술 밀도이지 내용이 아니다. 섹션 11–14 의 채택 여부는 이번 조사에서 실제로 나온 근거로 결정한다.
|
|
544
|
+
|
|
545
|
+
### 6.6 제출 전 자기 점검 (7-1 표시 직전)
|
|
546
|
+
|
|
547
|
+
payload 를 `cat` 으로 표시하기 **전에** 아래를 전부 통과시킨다. 하나라도 걸리면 본문을 고친 뒤 다시 점검한다(표시 후 수정하면 표시 증명이 무효가 되어 7-1 을 처음부터 다시 해야 한다).
|
|
548
|
+
|
|
549
|
+
| # | 점검 | 불통과 시 |
|
|
550
|
+
|---|---|---|
|
|
551
|
+
| 1 | 필수 섹션 10개(1–10)와 `## 증거` 가 순서대로 모두 있다 | 빠진 섹션 추가 (`없음`/`미확인` 이라도) |
|
|
552
|
+
| 2 | 제목이 `<어디서> + <무엇이 어떻게> + <그 결과>` 한 문장이고 사내 식별자가 없다 | 6.1 로 재작성 |
|
|
553
|
+
| 3 | `## 타임라인` 에 `**최초 이상 발생**` 행이 있고 KST·UTC 가 병기됐다 | 3.3 포착 결과 반영 |
|
|
554
|
+
| 4 | 모든 코드블록·표 인용에 출처(파일+라인 / 세션+턴)가 붙어 있다 | 출처 추가, 못 달면 삭제 |
|
|
555
|
+
| 5 | 단정과 추정이 구분됐다 — 추정은 전부 `> 추정:` 또는 "추정" 명시 안에 있다 | 단정형 서술을 추정으로 되돌린다 |
|
|
556
|
+
| 6 | 3장에서 나온 근거 중 11–14 에 들어갈 것이 남아 있지 않다 | 해당 섹션 추가 |
|
|
557
|
+
| 7 | 마스킹 자리표시자가 1:1 대응하고, 같은 종류 복수는 접미사로 구분된다 | 자리표시자 재정리 |
|
|
558
|
+
| 8 | 본문 끝 `마스킹 내역` / `마스킹으로 생략된 정보` 2줄이 있다 | 추가 |
|
|
559
|
+
| 9 | 사내 코드·고객 데이터·자격 증명이 어디에도 없다 (2장 grep + 내용 판단) | 해당 블록 제외 |
|
|
560
|
+
| 10 | 본문이 65536자 이내다 | 3.5 방식 2(Gist)로 전환 |
|
|
561
|
+
| 11 | 라벨이 저장소에 실재한다 (기본 `bug` 단독) | 7-2 의 라벨 목록으로 교정 |
|
|
562
|
+
|
|
563
|
+
점검 결과는 사용자에게 별도로 보고하지 않는다. 7-1 에서 표시하는 원문이 곧 결과다.
|
|
564
|
+
|
|
423
565
|
---
|
|
424
566
|
|
|
425
567
|
## 7. 이슈 생성
|
|
@@ -428,7 +570,7 @@ curl -sS -X POST https://api.github.com/repos/y00njinuk/makdoong2-team/issues/<n
|
|
|
428
570
|
|
|
429
571
|
GitHub 쓰기 호출(이슈·코멘트·Gist·라벨 생성)은 다음 절차를 거쳐야만 훅을 통과한다:
|
|
430
572
|
|
|
431
|
-
1. payload 를 **리터럴 절대 경로** 파일로 작성한다 (예: `/tmp/makdoong2-issue/issue-payload.json`). 상대 경로·변수 포함 경로는 훅이 거부한다.
|
|
573
|
+
1. **본문을 6장 양식으로 작성하고 6.6 자기 점검을 통과시킨 뒤**, payload 를 **리터럴 절대 경로** 파일로 작성한다 (예: `/tmp/makdoong2-issue/issue-payload.json`). 상대 경로·변수 포함 경로는 훅이 거부한다. 점검을 표시 이후로 미루지 않는다 — 표시 후 본문을 고치면 표시 증명이 무효가 되어 2번부터 다시 해야 한다.
|
|
432
574
|
2. **게시될 원문 전체를 세션에 그대로 표시한다** — 제목, 라벨, 본문 전문. 요약·발췌로 대체하지 않는다. 표시는 **체이닝 없는 단독 `cat`** 이어야 한다:
|
|
433
575
|
```bash
|
|
434
576
|
cat /tmp/makdoong2-issue/issue-payload.json
|
|
@@ -280,7 +280,7 @@ major 로 판정되어도 `auto_approve` 맵은 **모두 true** 로 두고 `"cat
|
|
|
280
280
|
|
|
281
281
|
## 2-5. 최종 자가 검증 (Pre-Completion Checklist)
|
|
282
282
|
|
|
283
|
-
`done=true` 직전, 다음
|
|
283
|
+
`done=true` 직전, 다음 9항목을 자체 확인하고 state.json에 결과를 기록한다.
|
|
284
284
|
하나라도 false면 완료 기록 금지.
|
|
285
285
|
|
|
286
286
|
| 항목 | 확인 |
|
|
@@ -293,10 +293,18 @@ major 로 판정되어도 `auto_approve` 맵은 **모두 true** 로 두고 `"cat
|
|
|
293
293
|
| 6 | 작업 범주화(2-4b)가 끝나 `.policy.category`(minor\|major)와 `auto_approve` 맵이 기록되었다 |
|
|
294
294
|
| 7 | `ambiguity_score`가 산정·기록되었고 최종값 ≤ 0.2 이다 (2-3-2b) |
|
|
295
295
|
| 8 | 확정 명세가 동결되어 `spec_hash`가 기록되었다 (2-4a) |
|
|
296
|
+
| 9 | `draft_path` 마커가 state.json에 기록되었다 (2-0). **`spec_hash`와 한 쌍이다** — `stage3-scope-verify.sh`가 `spec_hash`만 있고 `draft_path`가 없으면 `1_planning.scope` 진입을 하드 차단한다 |
|
|
297
|
+
|
|
298
|
+
**9번은 자기선언이 아니라 실제 값을 읽어 확인한다** — `draft_synced`(파일 동기화)가 true 여도 마커는 빠질 수 있다. 실제로 `spec_hash`만 기록되고 `draft_path`가 누락된 채 8항목 전부 true 로 종료되어, 다음 게이트에서 워크플로우가 정지한 사례가 있다 (issue #6-①).
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".draft_path'
|
|
302
|
+
# → "null" 이면 2-0 의 set 명령으로 먼저 기록한 뒤 self_check 을 기록한다.
|
|
303
|
+
```
|
|
296
304
|
|
|
297
305
|
```bash
|
|
298
306
|
bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".self_check' \
|
|
299
|
-
'{"checklist_complete": true, "conflicts_resolved": true, "user_confirmed": true, "scope_clean": true, "draft_synced": true, "categorized": true, "ambiguity_converged": true, "spec_frozen": true}'
|
|
307
|
+
'{"checklist_complete": true, "conflicts_resolved": true, "user_confirmed": true, "scope_clean": true, "draft_synced": true, "categorized": true, "ambiguity_converged": true, "spec_frozen": true, "draft_recorded": true}'
|
|
300
308
|
```
|
|
301
309
|
|
|
302
310
|
## 완료 기록
|