diff --git a/apps/mcp-server/README.md b/apps/mcp-server/README.md index 4316b66..b40526a 100644 --- a/apps/mcp-server/README.md +++ b/apps/mcp-server/README.md @@ -97,11 +97,11 @@ Claude Code hooks ──stdin──▶ ~/.claude/haribo/hook.mjs ──POST /hoo T1(결정 사항과 다르면 먼저 묻기)을 이 저장소 밖에서도 띄우려고 터미널 알림과 같은 길(`hook.mjs` → `/hooks/<이벤트>`)로 옮기는 중이에요. 화면 글은 `@sil/t1`이 만들어요. -| Claude Code 이벤트 | 서버 주소 | 하는 일 | -| ----------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PostToolUse` (`mcp__haribo__show_standard_check`) | `POST /hooks/after-t1` | `screen`에 따라 ② 다른 결정 사항 / ③ 등록될 모습(받는 이슈는 서버가 DB에서 읽어요) / 예외 안내를 띄우고, Claude에게 다음에 물을 질문을 글자 그대로 넘겨요 | -| `PreToolUse` (`mcp__haribo__save_standard`) | `POST /hooks/before-save` | ③에서 보여 준 값과 다르면 막고(`deny`), 같으면 사람에게 허락을 물어요(`ask`) | -| `PostToolUse`·`PostToolUseFailure` (`mcp__haribo__save_standard`) | `POST /hooks/after-save` | ④ 등록 완료. 그사이 다른 사람이 먼저 바꿨으면(`VERSION_CONFLICT`) 예외 7 | +| Claude Code 이벤트 | 서버 주소 | 하는 일 | +| ----------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PostToolUse` (`mcp__haribo__show_standard_check`) | `POST /hooks/after-t1` | `screen`에 따라 ② 다른 결정 사항 / ③ 등록될 모습(받는 이슈는 서버가 DB에서 읽어요) / 예외 안내를 띄우고, Claude에게 다음에 물을 질문을 글자 그대로 넘겨요. 작업 중에 알아챈 T1에서 아니요면 요청 값이 코드에 이미 있을 때 기존 값으로 되돌리라고 넘겨요 (BDT-95) | +| `PreToolUse` (`mcp__haribo__save_standard`) | `POST /hooks/before-save` | ③에서 보여 준 값과 다르면 막고(`deny`), 같으면 사람에게 허락을 물어요(`ask`) | +| `PostToolUse`·`PostToolUseFailure` (`mcp__haribo__save_standard`) | `POST /hooks/after-save` | ④ 등록 완료. 그사이 다른 사람이 먼저 바꿨으면(`VERSION_CONFLICT`) 예외 7 | - **③ 값은 팀원 PC에 둬요:** 서버는 쉬었다 깨어나면 메모리가 비어서 ③ 값을 기억하지 않아요. `hook.mjs`가 ③ 직후 `~/.claude/haribo/t1-<세션>-<결정 사항 ID>.json`을 남기고, 저장 직전·직후에 그 값을 `t1_preview`로 붙여 보내요. 저장이 끝나면 지워요. ③에서 취소·Esc로 끝나 남은 파일은 새 세션을 켤 때 하루 넘은 것부터 지워요. - **③을 거치지 않은 저장은 막아요:** ③ 파일이 없으면 저장 직전 hook이 막고 `show_standard_check`로 ③부터 하라고 알려요. 안내문 2·3·10번도 저장 전에 ③을 보여 주게 했어요. ③과 다른 ID로 저장할 때도 파일이 없어 막혀요. 서버에 못 붙으면 막지 않아요. diff --git a/apps/mcp-server/src/hooks/t1-hooks.test.ts b/apps/mcp-server/src/hooks/t1-hooks.test.ts index c7dfe6a..de26787 100644 --- a/apps/mcp-server/src/hooks/t1-hooks.test.ts +++ b/apps/mcp-server/src/hooks/t1-hooks.test.ts @@ -123,6 +123,23 @@ describe('after-t1: ② 다른 결정 사항', () => { expect(result.context).toContain('설명하는 글을 쓰지 마세요'); }); + it('작업 중에 알아챘으면 아니요 뒤 요청 값이 코드에 있을 때 되돌리라고 넘겨요 (detect.ask.rollback-if-applied)', async () => { + const result = await afterT1({ + screen: 'conflicts', + conflicts: { + trigger: 'agent', + request: '앱 알림 제목을 32자로 자르기', + conflicts: [CHANGE], + }, + }); + expect(result.context).toContain(T1_COPY.rolledBack); + expect(result.context).toContain('코드의 지금 값을 직접 읽어'); + // 아직 요청 값이 아니면 고치지 않아요 + expect(result.context).toContain( + `아무것도 고치지 않고 "${T1_COPY.notChanged}"`, + ); + }); + it('직접 요청이면 묻지 않고 이유로 가요. 되돌림이면 직접 요청이어도 먼저 물어요', async () => { const conflicts = { trigger: 'direct', @@ -151,6 +168,9 @@ describe('after-t1: ② 다른 결정 사항', () => { expect(revert.context).toContain( `"question":"${T1_COPY.direct.revertAsk}"`, ); + // 직접 요청의 아니요는 별도 이슈라 되돌리라고 넘기지 않아요 + expect(revert.context).not.toContain(T1_COPY.rolledBack); + expect(revert.context).toContain(T1_COPY.notChanged); }); it('입력이 도구 모양과 다르면 아무것도 하지 않아요 (도구가 이미 거절해요)', async () => { diff --git a/apps/mcp-server/src/hooks/t1-hooks.ts b/apps/mcp-server/src/hooks/t1-hooks.ts index 88509cb..42fd51f 100644 --- a/apps/mcp-server/src/hooks/t1-hooks.ts +++ b/apps/mcp-server/src/hooks/t1-hooks.ts @@ -97,6 +97,12 @@ function askJson( const reasonStep = `예면 이유를 정해요. 사람이 대화에서 한 말로 알 수 있으면 한 문장으로 정리하고, 알 수 없으면 "${T1_COPY.reasonPrompt}"를 글로 물어요(지어내지 않아요). 그다음 ${toolName('show_standard_check')}(screen: "preview")로 ③을 보여 줘요.`; +/** + * 작업 중에 알아챘을 때 아니요 뒤 할 일. 요청 값이 이미 코드에 있으면 기존 값으로 되돌려요 + * (detect.ask.rollback-if-applied). 비교는 도구가 아니라 에이전트가 코드를 읽어 직접 해요 (SIL-86) + */ +const agentDeclinedStep = `아니요면 저장하지 않아요. 요청 값이 코드에 이미 들어갔는지 코드의 지금 값을 직접 읽어 확인해요. 요청 값과 같으면 그곳을 기존 결정 사항 값으로 되돌리고 "${T1_COPY.rolledBack}"를 {requested}·{current}만 채워 글자 그대로 보여 준 뒤 멈추고 기다려요. 아직 요청 값이 아니면 아무것도 고치지 않고 "${T1_COPY.notChanged}"를 보여 준 뒤 멈추고 기다려요. 되돌리는 것 말고는 지금 값대로도 코드를 쓰지 않아요.`; + /** ② 다른 결정 사항 뒤 할 일 (예전 CLAUDE.md T1 2~4, 8) */ function conflictsContext(input: T1ConflictsInput): string { const { ask, direct, notChanged } = T1_COPY; @@ -110,7 +116,10 @@ function conflictsContext(input: T1ConflictsInput): string { SHOWN, `AskUserQuestion으로 이 질문을 글자 그대로 물어요(질문에 화면 내용을 다시 적지 않아요): ${askJson(question, ask.header, [ask.yes, ask.no])}`, `선택지 대신 글로 답하면 "${ask.typedReply}"를 쓰고 같은 질문을 다시 물어요.`, - `아니요면 저장하지 않고 "${notChanged}"를 보여 준 뒤 멈추고 기다려요. 지금 값대로도 코드를 쓰지 않아요.`, + // 직접 요청(되돌림)의 아니요는 별도 이슈라 그대로 둬요 (BDT-95 하지 않는 것) + input.trigger === 'agent' + ? agentDeclinedStep + : `아니요면 저장하지 않고 "${notChanged}"를 보여 준 뒤 멈추고 기다려요. 지금 값대로도 코드를 쓰지 않아요.`, reasonStep, ].join('\n'); } diff --git a/packages/t1/README.md b/packages/t1/README.md index 0eb1d3f..b5058e4 100644 --- a/packages/t1/README.md +++ b/packages/t1/README.md @@ -4,12 +4,12 @@ T1(결정 사항과 다르면 먼저 묻기)의 화면 모양, 문구, 저장 MCP 서버(`show_standard_check` 도구와 T1 hooks)가 가져다 써서 `apps/detect/src/t1/`에서 옮겼어요 (BDT-108, D49). 화면은 저장하지 않고 글자만 만들어요. ③ 받는 이슈만 팀 DB를 읽어요. -| 파일 | 내용 | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/copy.ts` | T1 문구 (착수 안내 3절 → FigJam "T1 결정 사항 등록" 10/13). 다른 결정 사항 화면(`conflicts`, 경우 1·2), 묻기, 아니요, 이유 묻기, ③ 등록될 모습, 취소, ④ 등록 완료, 직접 요청(`direct`), 되돌림 화면(`conflicts.revert`, 부건님 시안 10/16). 문구는 여기에만 둬요 | -| `src/save-input.ts` | 등록하기를 골랐을 때 저장 값. `source: ai_chat`, `aiDetected`는 에이전트가 알아챘으면 `true`·직접 요청이면 `false`(D61), `baseVersion`은 `get_standard`의 `currentVersion`, `alwaysOn`·`scope`는 지금 값. 새 결정 사항은 `buildT1NewSaveInput`(`baseVersion: 0`) | -| `src/screens.ts` | hooks가 띄우는 화면 모양(③ 등록될 모습, 다른 결정 사항 화면). 부건님 프로토타입 "T1 결정 사항 등록"(10/14)을 옮겼어요. 칸 계산은 `text.ts` | -| `src/issues.ts` | ③ 이슈 표에 넣을 받는 이슈를 core `listIssues()`로 팀 DB에서 읽어요(팀원 `.env`의 Supabase 주소·공용 키). 알리기와 같은 규칙(담당자 있음, 바꾼 본인 뺌). 못 읽으면 표 칸 이름만 보여요 | -| `src/text.ts` | 화면 칸 계산(한글·이모지는 두 칸), 줄 나누기와 자르기 | +| 파일 | 내용 | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/copy.ts` | T1 문구 (착수 안내 3절 → FigJam "T1 결정 사항 등록" 10/13). 다른 결정 사항 화면(`conflicts`, 경우 1·2), 묻기, 아니요, 아니요 뒤 되돌림(`rolledBack`, BDT-95), 이유 묻기, ③ 등록될 모습, 취소, ④ 등록 완료, 직접 요청(`direct`), 되돌림 화면(`conflicts.revert`, 부건님 시안 10/16). 문구는 여기에만 둬요 | +| `src/save-input.ts` | 등록하기를 골랐을 때 저장 값. `source: ai_chat`, `aiDetected`는 에이전트가 알아챘으면 `true`·직접 요청이면 `false`(D61), `baseVersion`은 `get_standard`의 `currentVersion`, `alwaysOn`·`scope`는 지금 값. 새 결정 사항은 `buildT1NewSaveInput`(`baseVersion: 0`) | +| `src/screens.ts` | hooks가 띄우는 화면 모양(③ 등록될 모습, 다른 결정 사항 화면). 부건님 프로토타입 "T1 결정 사항 등록"(10/14)을 옮겼어요. 칸 계산은 `text.ts` | +| `src/issues.ts` | ③ 이슈 표에 넣을 받는 이슈를 core `listIssues()`로 팀 DB에서 읽어요(팀원 `.env`의 Supabase 주소·공용 키). 알리기와 같은 규칙(담당자 있음, 바꾼 본인 뺌). 못 읽으면 표 칸 이름만 보여요 | +| `src/text.ts` | 화면 칸 계산(한글·이모지는 두 칸), 줄 나누기와 자르기 | 테스트는 `src/t1.test.ts`예요 (`pnpm test`). hooks 테스트는 `apps/mcp-server/src/hooks/t1-hooks.test.ts`예요. diff --git a/packages/t1/src/copy.ts b/packages/t1/src/copy.ts index 77f9686..4da0240 100644 --- a/packages/t1/src/copy.ts +++ b/packages/t1/src/copy.ts @@ -66,6 +66,13 @@ export const T1_COPY = { * 사람이 "40자로 작업해 줘"처럼 어떻게 할지 다시 말할 때만 진행해요 (10/13 경하님) */ notChanged: '기존 결정 사항을 그대로 유지할게요. 다른 작업을 시작해 주세요.', + /** + * 아니요를 골랐는데 요청 값이 이미 코드에 들어가 있을 때. 기존 값으로 되돌린 뒤 notChanged 대신 보여 주고 멈춰요 + * (detect.ask.rollback-if-applied, BDT-95). {requested}는 요청 값, {current}는 기존 결정 사항 값이에요. + * 값마다 받침이 달라 조사는 괄호 밖 "값"에 붙여요 (데모 10/26: "6500를") + */ + rolledBack: + '코드에 이미 들어간 값({requested})을 기존 결정 사항 값({current})으로 되돌렸어요. 다른 작업을 시작해 주세요.', /** 대화에서 이유를 유추할 수 없을 때 글로 묻기. 답을 한 문장으로 정리해 ③에 넣어요 */ reasonPrompt: '이 결정 사항을 바꾸는 이유를 작성해 주세요. 작성해 주신 내용을 기반으로 정리해 드릴게요.', diff --git a/packages/t1/src/t1.test.ts b/packages/t1/src/t1.test.ts index 54db2ba..8b0679b 100644 --- a/packages/t1/src/t1.test.ts +++ b/packages/t1/src/t1.test.ts @@ -94,6 +94,7 @@ describe('T1 문구', () => { } const messages = [ T1_COPY.notChanged, + T1_COPY.rolledBack, T1_COPY.reasonPrompt, T1_COPY.cancelled.withTask, T1_COPY.cancelled.noTask,