Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/apps/detect/src/t1/hook-conflicts.ts\""
},
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/apps/detect/src/t1/hook-notice.ts\""
}
]
},
Expand All @@ -27,6 +31,17 @@
]
}
],
"PostToolUseFailure": [
{
"matcher": "mcp__haribo__save_standard",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/apps/detect/src/t1/hook-saved.ts\""
}
]
}
],
"PreToolUse": [
{
"matcher": "mcp__haribo__save_standard",
Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
문구는 `apps/detect/src/t1/copy.ts`의 `T1_COPY`를 글자 그대로 쓴다(바꿔 말하지 않는다). 선택지는 거기 있는 것만 쓰고 새로 만들지 않는다.
1. haribo `get_standard`로 지금 영역·제목·값·`alwaysOn`·`currentVersion`과 적용 범위(`versions[0].scope`)를 읽는다.
2. 다른 결정 사항 화면: `trigger`(작업 중에 알아챘으면 `"agent"`, 8의 직접 요청이면 `"direct"`), 부탁받은 작업이나 요청을 한 줄로 정리한 `request`, 바꾸려는 결정 사항(`kind: "change"`, 맨 앞)·바꾸면 함께 어긋나는 다른 결정 사항(`kind: "mismatch"`)을 JSON으로 `node apps/detect/src/t1/conflicts.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다). 항목마다 `id`·`title`·`kind`·`why`(왜 그렇게 판단했는지 한 문장)를 넣고, 낱말만 겹치는 결정 사항은 넣지 않는다. 화면을 요약하거나 "화면을 띄웠어요"처럼 설명하는 글을 쓰지 않고 바로 다음으로 간다(③도 같다). 그다음 `ask`를 AskUserQuestion으로 묻는다(header는 `ask.header`, 선택지는 `yes`·`no`의 label·description 그대로, 질문에 화면 내용을 다시 적지 않는다). 선택지 대신 글로 답하면 `ask.typedReply`를 쓰고 같은 질문을 다시 묻는다.
3. 아니요면 저장하지 않고 `notChanged`를 보여 준 뒤 작업을 멈추고 기다린다. 지금 값대로도 코드를 쓰지 않는다. 사람이 "40자로 작업해 줘"처럼 어떻게 할지 다시 말할 때만 그대로 진행한다.
3. 아니요면 저장하지 않고 `notChanged`를 보여 준 뒤 작업을 멈추고 기다린다. 지금 값대로도 코드를 쓰지 않는다. 사람이 "40자로 작업해 줘"처럼 어떻게 할지 다시 말할 때만 그대로 진행한다. 아니요는 그 프롬프트로 시작한 작업 하나에만 적용된다. 새 프롬프트로 다시 결정 사항과 다른 값을 쓰게 되면 새 작업이라 T1을 1부터 다시 한다(예외 11).
4. 예면 이유를 정한다. 사람이 대화에서 한 말로 이유를 알 수 있으면 그 말을 한 문장으로 정리한다. 알 수 없으면 `reasonPrompt`를 글로 묻고(AskUserQuestion이 아님) 답을 한 문장으로 정리한다. 근거 없이 지어내지 않는다.
5. ③ 등록될 모습: 등록할 값(`id`·`title`·`value`·`reason`·`alwaysOn`, 그리고 바꾸는 사람 이름 `changedBy` = 이 세션 `list_pending`의 `recipient`, 모르면 뺀다)을 JSON으로 `node apps/detect/src/t1/preview.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다). 그다음 `preview.confirm`을 AskUserQuestion으로 묻는다(header는 `preview.header`, 선택지는 `register`·`edit`·`cancel`의 label·description 그대로, 질문과 설명에 ③ 내용을 다시 적지 않는다. 명령 출력의 "알림 받는 이슈"가 1개 이상이면 `register` 대신 `registerWithIssues`를 쓴다). 수정하기면 `editPrompt`를 글로 묻고 고친 뒤 5를 다시 한다. 취소하기면 저장하지 않고 `cancelled`를 보여 준 뒤 하던 작업을 멈추고 사람의 말을 기다린다. `cancelled`는 다른 작업 중에 T1이 떴으면 `withTask`, 결정 사항을 바꿔 달라는 것 자체가 요청이었으면 `noTask`를 쓴다.
6. 등록하기면 haribo `save_standard`로 저장한다. 저장되면 hook이 ④ 등록 완료를 띄우니 ④를 다시 쓰거나 설명하지 않는다. 하던 작업이 있으면 `saved.next.withTask`를 쓰고 바꾼 값으로 그 작업을 이어가고, 결정 사항을 바꿔 달라는 것 자체가 요청이었으면 `saved.next.noTask`를 쓴다. `baseVersion`은 1에서 읽은 `currentVersion`, `alwaysOn`과 `scope`는 지금 값 그대로(scope를 비우면 적용 범위가 지워진다. 지금 값이 null이면 칸을 뺀다), `source`는 `ai_chat`, `aiDetected`는 `true`(직접 요청이면 `false`) (`apps/detect/src/t1/save-input.ts`와 같은 값, D61). 저장 결과의 `changelogLine`을 team-wiki/CHANGELOG.md 맨 아래에 그대로 붙인다(work.wiki.changelog). 위키 파일의 값·버전은 고치지 않는다 (위키 문서 버전과 DB 버전은 별개).
Expand All @@ -24,6 +24,11 @@
- 기존 결정 사항이 있으면: 1로 읽고, 2의 다른 결정 사항 화면은 `trigger: "direct"`로 띄우되 `ask`는 묻지 않고 4로 간다(3도 건너뛴다). 5의 선택 창 질문은 `preview.confirm` 대신 `direct.confirm`.
- 기존 결정 사항이 없으면(`search_standards`로 찾아도 없음): 2의 다른 결정 사항 화면을 `trigger: "direct"`, `conflicts: []`로 띄우고(초록 막대 한 줄) `ask`는 묻지 않고 4로 간다. 5의 JSON에 `"isNew": true`를 더한다(③ 제목 줄에 "새 결정 사항" 표시, 받는 이슈는 0개). 5의 선택 창 질문은 `preview.confirm`("이대로 등록할까요?")이고 `direct.confirm`은 쓰지 않는다(바뀌는 기존 결정 사항이 없어서). ID는 `data.standard.id-format`대로 정하고, `alwaysOn`은 사람이 말하지 않았으면 `false`로 ③에 보여 준다. 6에서 `baseVersion`은 `0`, `scope` 칸은 뺀다.
- 6에서 `aiDetected`는 `false`, 취소하기·Esc면 `cancelled.noTask`를 쓴다.
9. 예외 안내(10/14 확정): 아래 값을 JSON으로 `node apps/detect/src/t1/notice.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다. 안내를 다시 쓰거나 설명하지 않는다).
- 조사 실패(예외 3): T1 중에 `search_standards`·`get_standard`가 실패하면 "다른 결정 사항 없음"으로 넘어가지 않는다. `{"kind":"searchFailed","trigger":…}`(2와 같은 trigger)를 띄우고 하던 작업이나 등록을 멈춘 뒤 사람의 말을 기다린다.
- 완전히 같음(충돌 3): 직접 요청한 값이 그 결정 사항의 지금 값과 같으면(같은 ID·같은 값) `{"kind":"same","existing":{"id","title","value"}}`를 띄우고 선택지 없이 끝낸다. 저장하지 않는다.
- 뜻이 같음(충돌 3): 새 결정 사항을 만들어 달라는데 다른 ID의 결정 사항이 같은 뜻이면 `{"kind":"similar","existing":{…}}`를 띄우고 `similarAsk`를 AskUserQuestion으로 묻는다. 예면 새 결정 사항으로 4부터 하고(`isNew`), 아니요면 `{"kind":"similarDeclined"}`를 띄우고 끝낸다.
- 저장하는 사이 바뀜(예외 7): `save_standard`가 `VERSION_CONFLICT`로 거절되면 저장 도구 hook이 노란 막대 안내를 바로 띄운다(`notice.ts`를 실행하지 않는다). 1부터 다시 한다. `get_standard`로 새 `currentVersion`을 읽어 2의 다른 결정 사항 화면을 다시 띄우고, 사람이 이미 고른 예와 정한 이유는 그대로 두고 5(③)부터 다시 묻는다.
- 어느 경우든 6에서 저장하지 않았으면 결정 사항과 다른 값으로 코드를 쓰지 않는다.
- (T1로 저장한 경우는 위 6을 따르고 이 줄은 하지 않는다) 기준을 바꾸기로 하면: 해당 기준의 값·이유·근거를 고치고 버전을 1 올린 뒤, CHANGELOG.md 맨 아래에 한 줄을 남긴다.
형식: 날짜 시각 | 누가 | 기준 ID 이전→새 버전 | 이전 값 → 새 값 | 이유 | 출처(코드/AI 대화/Figma/회의/Slack)
Expand Down
14 changes: 8 additions & 6 deletions apps/detect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
| `src/t1/screens.ts` | hooks가 띄우는 화면 모양(③ 등록될 모습, 다른 결정 사항 화면). 부건님 프로토타입 "T1 결정 사항 등록"(10/14)을 옮겼어요. 칸 계산은 `text.ts` |
| `src/t1/conflicts.ts` | 다른 결정 사항 화면 값을 임시 파일(`haribo-t1-conflicts-<UUID>.json`)에 들고 있는 명령. hook이 띄운 뒤 바로 지워요 (경우 1·2) |
| `src/t1/issues.ts` | ③ 이슈 표에 넣을 받는 이슈를 core `listIssues()`로 팀 DB에서 읽어요(팀원 `.env`의 Supabase 주소·공용 키). 알리기와 같은 규칙(담당자 있음, 바꾼 본인 뺌). 못 읽으면 표 칸 이름만 보여요 |
| `src/t1/notice.ts` | 예외 안내(조사 실패, 완전히 같음, 뜻이 같음) 값을 임시 파일(`haribo-t1-notice-<UUID>.json`)에 들고 있는 명령. hook이 띄운 뒤 바로 지워요 (10/14 확정 예외) |
| `src/t1/preview.ts` | ③ 등록될 모습 값을 임시 파일(운영체제 임시 폴더 `haribo-t1-pending-<결정 사항 ID>.json`, 결정 사항마다 따로)에 들고 있는 명령. 에이전트가 터미널로 불러요 |
| `src/t1/pending.ts` | 임시 값 쓰기·읽기·지우기·비교. 시간으로 지우지 않고, 다음 ③이 덮어쓰거나 저장하면 지워요 |
| `src/t1/preview-command.ts` | Bash 명령이 `preview.ts`·`conflicts.ts` 실행인지 알아보는 규칙. 앞에 `cd "…" &&`를 붙이거나 전체 경로로 불러도 알아봐요. 패키지를 불러오지 않아서 설치 전에도 다른 명령에서 hook 오류가 나지 않아요 |
Expand All @@ -32,12 +33,13 @@
- 기존 결정 사항이 있으면: `conflicts.ts`를 `trigger: "direct"`로 실행(머리표 "요청하신 변경이 기존 결정 사항과 달라요") → 묻기(예/아니요)는 건너뛰고 이유 → ③ → "달라지게 되는 기존 결정 사항 내역을 확인했나요? 이대로 등록할까요?"
- 기존 결정 사항이 없으면: `conflicts.ts`를 `conflicts: []`로 실행(hook이 초록 막대 "요청하신 변경과 다른 결정 사항이 없어요"를 띄움) → 이유 → `preview.ts`에 `"isNew": true`(③ 제목 줄에 "새 결정 사항", 받는 이슈 없음) → "이대로 등록할까요?" → 새 결정 사항으로 저장

| hook | 언제 | 하는 일 |
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `hook-preview.ts` | `preview.ts` 명령 직후 | ③을 띄워요 (위아래 선, 청록 굵은 제목, 항상 적용은 예일 때만 굵게). hook 글은 도구 **직후**에만 화면에 보여요 |
| `hook-conflicts.ts` | `conflicts.ts` 명령 직후 | 다른 결정 사항 화면을 띄우고 임시 값을 지워요 (노란 막대 머리표, 결정 사항 제목은 굵게) |
| `hook-guard.ts` | `save_standard` 직전 | ③을 보여 준 같은 결정 사항인데 값이 다르면 저장을 막아요. 다른 저장(새로 만들기 등)은 막지 않아요 |
| `hook-saved.ts` | `save_standard` 직후 | ③을 거친 저장이 성공하면 ④ 등록 완료(초록 첫 줄, 제목·항상 적용, 보낸 이슈 수)를 띄우고 저장한 결정 사항의 임시 값만 지워요 |
| hook | 언제 | 하는 일 |
| ------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hook-preview.ts` | `preview.ts` 명령 직후 | ③을 띄워요 (위아래 선, 청록 굵은 제목, 항상 적용은 예일 때만 굵게). hook 글은 도구 **직후**에만 화면에 보여요 |
| `hook-conflicts.ts` | `conflicts.ts` 명령 직후 | 다른 결정 사항 화면을 띄우고 임시 값을 지워요 (노란 막대 머리표, 결정 사항 제목은 굵게) |
| `hook-guard.ts` | `save_standard` 직전 | ③을 보여 준 같은 결정 사항인데 값이 다르면 저장을 막아요. 다른 저장(새로 만들기 등)은 막지 않아요 |
| `hook-notice.ts` | `notice.ts` 명령 직후 | 예외 안내를 띄우고 임시 값을 지워요 (빨강·노랑·기본 글자색 막대) |
| `hook-saved.ts` | `save_standard` 직후(실패 포함) | ③을 거친 저장이 성공하면 ④ 등록 완료(초록 첫 줄, 제목·항상 적용, 보낸 이슈 수)를, 그사이 바뀌어 거절되면(`VERSION_CONFLICT`) 예외 7 안내(노란 막대)를 띄우고 저장한 결정 사항의 임시 값만 지워요 |

안전장치로 `.claude/settings.json`의 `permissions.ask`에 `save_standard`와 `revert`를 넣어, 저장하기 전에 Claude Code가 한 번 더 허락을 받아요. auto mode에서도 뜨고, 허락 창에서 "Yes, and don't ask again"을 골라도 다음에 다시 물어요 (10/13 시험).

Expand Down
31 changes: 31 additions & 0 deletions apps/detect/src/t1/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,37 @@ export const T1_COPY = {
confirm:
'달라지게 되는 기존 결정 사항 내역을 확인했나요? 이대로 등록할까요?',
},
/**
* 예외 안내(10/14 확정, 결정기록 4-2). hook(`hook-notice.ts`)이 `screens.ts`의 모양으로 띄워요.
* 막대 색: 조사 실패·완전히 같음은 빨강, 뜻이 같음·저장하는 사이 바뀜은 노랑, 뜻이 같음 → 아니요는 기본 글자색
*/
notices: {
/** 예외 3: 결정 사항을 불러오지 못하면 "다른 결정 사항 없음"으로 넘어가지 않고 멈춰요 */
searchFailed: {
head: {
agent: '다른 결정 사항을 조회하지 못해 하던 작업을 멈췄어요.',
direct: '다른 결정 사항을 조회하지 못해 등록을 멈췄어요.',
},
body: '잠시 뒤 다시 시도해 보시고 반복된다면 담당자에게 알려 주세요.',
},
/** 충돌 3: 같은 ID·같은 값이면 선택지 없이 끝내요 */
same: '같은 결정 사항이 있어서 등록하지 않았어요.',
/** 충돌 3: AI가 같은 뜻이라고 보면 새로 등록할지 물어요 (제안 문구, 10/14) */
similar: '같은 뜻의 결정 사항이 이미 있어요.',
similarDeclined: '등록하지 않았어요. 기존 결정 사항을 그대로 둘게요.',
/** 예외 7: 저장이 거절되면(VERSION_CONFLICT) 다른 결정 사항 확인부터 다시 해요 */
saveRace: {
head: '저장하는 사이 다른 사람이 먼저 이 결정 사항을 바꿨어요.',
body: '바뀐 내용을 다시 확인할게요.',
},
},
/** 충돌 3 뜻이 같음 뒤 묻기 (AskUserQuestion). 보기 이름은 짧게, 설명은 아래 줄 (제안 문구, 10/14) */
similarAsk: {
header: '결정 사항 등록',
question: '그래도 새 결정 사항으로 등록할까요?',
yes: { label: '예', description: '등록해요' },
no: { label: '아니요', description: '등록하지 않아요' },
},
/**
* ④ 등록 완료. 저장 직후 hook(`hook-saved.ts`)이 `screens.ts`의 모양으로 띄워요 (프로토타입 10/14).
* 받는 이슈 수는 ③에서 `preview.ts`가 읽어 둔 값이에요. 못 읽었으면 그 줄을 빼요
Expand Down
28 changes: 28 additions & 0 deletions apps/detect/src/t1/hook-notice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* hook (PostToolUse, Bash): `notice.ts`를 실행한 직후 T1 예외 안내를 띄워요 (10/14 확정 예외, SIL-66).
* 다른 명령이면 아무것도 하지 않아요. hook 글은 도구 직후에만 화면에 보여서 이 자리에 둬요 (10/13 시험).
*/
import { readFileSync } from 'node:fs';
import {
findNoticeToken,
isNoticeCommand,
screenHookOutput,
} from './preview-command.ts';

const event = JSON.parse(readFileSync(0, 'utf8')) as {
tool_input?: { command?: string };
tool_response?: { stdout?: string };
};
if (!isNoticeCommand(String(event.tool_input?.command ?? ''))) {
process.exit(0);
}

// 이 명령일 때만 패키지(@sil/core)를 불러와요 (preview-command.ts 설명)
const { takeNotice } = await import('./pending.ts');
const { noticeScreen } = await import('./screens.ts');
const input = takeNotice(
findNoticeToken(String(event.tool_response?.stdout ?? '')),
);
if (!input) process.exit(0);

process.stdout.write(screenHookOutput(noticeScreen(input)));
24 changes: 21 additions & 3 deletions apps/detect/src/t1/hook-saved.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
/**
* hook (PostToolUse, save_standard): 저장 직후 ④ 등록 완료를 띄우고 저장한 결정 사항의 ③ 임시 값을 지워요 (SIL-66 T1).
* hook (PostToolUse·PostToolUseFailure, save_standard): 저장 직후 ④ 등록 완료를 띄우고 저장한 결정 사항의 ③ 임시 값을 지워요 (SIL-66 T1).
* - ③을 거친 저장(임시 값이 있음)이 성공했을 때만 ④를 띄워요. T1이 아닌 저장에는 아무것도 띄우지 않아요.
* - 그사이 다른 사람이 먼저 바꿔 거절됐으면(VERSION_CONFLICT) 예외 7 안내를 바로 이 자리에서 띄워요 (프로토타입 race, 10/14).
* - 다른 결정 사항의 임시 값은 건드리지 않아요 (다른 Claude Code 창이 쓰는 중일 수 있어요).
*/
import { readFileSync } from 'node:fs';
import { clearPending, readPending } from './pending.ts';
import { isSavedResponse, screenHookOutput } from './preview-command.ts';
import { savedScreen } from './screens.ts';
import {
isSavedResponse,
isVersionConflict,
SAVE_RACE_CONTEXT,
screenHookOutput,
} from './preview-command.ts';
import { noticeScreen, savedScreen } from './screens.ts';

const event = JSON.parse(readFileSync(0, 'utf8')) as {
hook_event_name?: string;
tool_input?: { id?: string };
tool_response?: unknown;
/** PostToolUseFailure일 때 실패 내용 */
error?: unknown;
};
const eventName = event.hook_event_name ?? 'PostToolUse';
const id = event.tool_input?.id;
const pending = readPending(id);
clearPending(id);
Expand All @@ -31,4 +41,12 @@ if (pending && isSavedResponse(event.tool_response)) {
),
),
);
} else if (pending && isVersionConflict(event.tool_response, event.error)) {
process.stdout.write(
screenHookOutput(
noticeScreen({ kind: 'saveRace' }),
SAVE_RACE_CONTEXT,
eventName,
),
);
}
Loading
Loading