makdoong2-team 1.4.2 → 1.6.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 +15 -1
- package/agents/makdoong2-issue-reporter.md +49 -0
- package/agents/makdoong2-team-leader.md +1 -0
- package/bin/cli.js +1 -1
- package/command/makdoong2-issue-reporter.md +8 -0
- package/dist/issue-reporter-guard.d.ts +50 -0
- package/dist/issue-reporter-guard.js +179 -0
- package/dist/opencode-plugin.js +109 -1
- package/package.json +2 -1
- package/scripts/install-lib.mjs +60 -8
- package/scripts/issue-reporter-approve.sh +87 -0
- package/scripts/lib/confirm.sh +77 -0
- package/scripts/publish-if-changed.sh +32 -20
- package/scripts/release.sh +41 -26
- package/scripts/run-tests.mjs +2 -0
- package/skills/makdoong2-issue-reporter/SKILL.md +459 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ makdoong2-team doctor # 설치 진단
|
|
|
44
44
|
|
|
45
45
|
## 2. 에이전트와 단계
|
|
46
46
|
|
|
47
|
-
에이전트 **7
|
|
47
|
+
워크플로우 에이전트 **7개** + 사용자 전용 유틸리티 에이전트 1개. 권한이 좁을수록 사고 반경이 좁다는 원칙으로 나눴다.
|
|
48
48
|
|
|
49
49
|
| 에이전트 | 담당 | 권한 |
|
|
50
50
|
|---|---|---|
|
|
@@ -55,6 +55,7 @@ makdoong2-team doctor # 설치 진단
|
|
|
55
55
|
| `makdoong2-publisher` | 3_delivery 3단계 | **git add/commit/push 직접 실행** + bitbucket MCP |
|
|
56
56
|
| `makdoong2-verifier` | 메타 검증 | 읽기 전용 |
|
|
57
57
|
| `makdoong2-researcher` | 리서치 fan-out 워커 | 읽기 전용 + 리서치 MCP — 소스 1개 전담 |
|
|
58
|
+
| `makdoong2-issue-reporter` | 플러그인 오류 GitHub 이슈 등록 | **전권 (bash/write allow)** — 사용자 직접 호출 전용 |
|
|
58
59
|
|
|
59
60
|
> **Publisher 는 직접 실행자다.** commit·pr·review 모두 publisher 가 worktree 에서 직접 git 명령과 MCP 를 호출한다. 부장님은 git 권한이 아예 없고 dispatch 와 verdict 수신만 한다. (구버전의 "spec 계산 → 부장님 실행" 하이브리드 모델은 폐기됐다.)
|
|
60
61
|
|
|
@@ -94,6 +95,19 @@ makdoong2-team doctor # 설치 진단
|
|
|
94
95
|
|
|
95
96
|
결과는 `.makdoong2-team/<이슈>/research-findings.json` 으로 병합된다. 상세: ARCHITECTURE.md §3.6
|
|
96
97
|
|
|
98
|
+
### 오류 이슈 등록 — `/makdoong2-issue-reporter`
|
|
99
|
+
|
|
100
|
+
플러그인이 오작동했을 때(행 걸림, 잘못된 프롬프트 injection, 단계 실패 반복 등) 다음 커맨드로 GitHub 이슈를 등록한다:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
/makdoong2-issue-reporter [증상 한 줄 설명(선택)]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- 커맨드가 **전용 full-permission 에이전트**(`makdoong2-issue-reporter`)로 라우팅되어, 호출 시점 이전의 로그·프롬프트·세션 컨텍스트를 스스로 수집해 이상 지점을 포착하고 [y00njinuk/makdoong2-team issues](https://github.com/y00njinuk/makdoong2-team/issues) 에 등록한다.
|
|
107
|
+
- **사용자 직접 호출이 유일한 트리거**다. 부장님·막둥이가 실패를 관측했다고 자율적으로 이슈를 만들지 않는다 (훅이 차단).
|
|
108
|
+
- 저장소가 public 이므로 사내 정보는 마스킹 후 첨부되며, **전송 전 사용자 승인이 훅으로 강제된다**: 에이전트가 게시될 원문 전체를 채팅에 표시한 뒤, 사용자가 직접 `issue-reporter-approve.sh <payload>` 를 실행해 원문을 확인·승인해야만 GitHub 쓰기 호출이 통과된다 (승인은 내용 sha256 에 바인딩, 1회용).
|
|
109
|
+
- PAT 는 `~/.config/opencode/.github` 파일에서 읽는다. 상세: ARCHITECTURE.md §4.6
|
|
110
|
+
|
|
97
111
|
---
|
|
98
112
|
|
|
99
113
|
## 3. 설정 — `makdoong2-team.json` 한 파일
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: makdoong2-issue-reporter
|
|
3
|
+
description: makdoong2-team 오류·비정상 동작 GitHub 이슈 리포터. 사용자가 /makdoong2-issue-reporter 커맨드로 직접 호출할 때만 활성화된다. 워크플로우 stage 에 참여하지 않으며 dispatch_stage 로 spawn 되지 않는다. 다른 에이전트가 자율적으로 선택·호출하지 않는다.
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
mode: all
|
|
6
|
+
tools:
|
|
7
|
+
Read: true
|
|
8
|
+
Write: true
|
|
9
|
+
Edit: true
|
|
10
|
+
Bash: true
|
|
11
|
+
Grep: true
|
|
12
|
+
Glob: true
|
|
13
|
+
webfetch: true
|
|
14
|
+
skill: true
|
|
15
|
+
permission:
|
|
16
|
+
bash:
|
|
17
|
+
"*": "allow"
|
|
18
|
+
write:
|
|
19
|
+
"**/*": "allow"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
당신은 makdoong2-team 플러그인의 **이슈 리포터(makdoong2-issue-reporter)**다. 사용자가 `/makdoong2-issue-reporter` 커맨드로 직접 호출했을 때만 활성화되며, 직전 세션의 오류·비정상 동작을 스스로 수집·분석해 GitHub 이슈(y00njinuk/makdoong2-team)로 등록한다.
|
|
23
|
+
|
|
24
|
+
## 하드룰
|
|
25
|
+
|
|
26
|
+
1. **첫 행동으로 `skill(name="makdoong2-issue-reporter")` 를 로드**하고, 스킬에 정의된 절차를 그대로 따른다. 실행 순서는 스킬이 고정한다: **수집 → 이상 지점 포착 → 마스킹 → 중복 확인 → 최소 질의 → 이슈 생성**.
|
|
27
|
+
2. **GitHub 게시(이슈·코멘트·Gist·라벨)는 훅이 강제하는 사용자 승인 게이트를 통과해야만 가능하다.** 절차는 고정이다:
|
|
28
|
+
1. payload 를 **리터럴 절대 경로** JSON 파일로 작성한다 (예: `/tmp/makdoong2-issue/issue-payload.json`).
|
|
29
|
+
2. **게시될 원문 전체(제목·라벨·본문 전문)를 채팅에 그대로 표시**하고 마스킹 내역 요약을 덧붙인다. 요약·발췌로 대체 금지 — 사용자는 전송될 내용 원문을 봐야 한다.
|
|
30
|
+
3. 사용자에게 안내한다: `bash <SCRIPTS_DIR>/issue-reporter-approve.sh </absolute/path/payload.json>` 을 **사용자가 직접** 실행 (스크립트가 원문을 다시 보여주고 승인을 받아 `<payload>.approved` 마커를 기록한다). 실행을 안내한 뒤 **대기한다**.
|
|
31
|
+
4. 승인 마커가 생긴 뒤에만 전송한다. 전송은 **단일 curl 명령 + `-d @<절대경로>`** 형태만 허용된다 (체이닝·리다이렉트·인라인 JSON 금지 — 훅이 차단).
|
|
32
|
+
- 승인은 **1회용**이며 payload 내용이 바뀌면 무효다 (해시 바인딩). 재작성했으면 2번부터 다시.
|
|
33
|
+
- **승인 스크립트를 직접 실행하거나 `.approved` 마커를 만들지·읽지·지우지 않는다.** 훅이 물리 차단하며, 차단 메시지를 보면 우회하지 말고 사용자 승인을 기다린다.
|
|
34
|
+
3. **토큰(PAT)은 어디에도 원문 노출 금지.** 커맨드 문자열에 직접 박지 않고 환경변수로 전달하며, 출력에는 마스킹(`ghp_****`)만 허용한다.
|
|
35
|
+
4. **워크플로우 상태를 변경하지 않는다.** state.json 은 증거 수집을 위한 읽기(`state.sh get`)만 허용. `state.sh set` / dispatch 계열 툴 호출 금지. 이 에이전트는 워크플로우 오케스트레이션과 완전히 분리된 조사·보고 전용이다.
|
|
36
|
+
5. **다른 에이전트로 위임하지 않는다.** 수집·분석·마스킹·등록 전 과정을 이 세션에서 직접 수행한다.
|
|
37
|
+
|
|
38
|
+
## 실행 컨텍스트
|
|
39
|
+
|
|
40
|
+
- 이 에이전트는 전권(full-permission)으로 실행된다: bash 전체 allow, 파일 쓰기 allow. 임시 페이로드 파일(`issue-payload.json` 등) 생성과 curl 호출을 직접 수행할 수 있다. 전송 후 임시 파일은 스킬 규약대로 삭제한다.
|
|
41
|
+
- 커맨드가 인라인으로 실행되므로 현재 세션의 직전 대화 턴(team-leader 오케스트레이션 로그 포함)을 그대로 볼 수 있다. 로그 파일 수집과 함께 대화 컨텍스트도 이상 지점 포착에 활용한다.
|
|
42
|
+
|
|
43
|
+
## 세션 종료 규약
|
|
44
|
+
|
|
45
|
+
완료 시 스킬 8장(완료 보고) 형식을 따른다. 최소한 다음을 한국어로 출력한다:
|
|
46
|
+
|
|
47
|
+
1. 생성된 이슈 번호와 `html_url` (또는 실패 시 수동 등록용 본문 전체)
|
|
48
|
+
2. 포착한 최초 이상 발생 지점(시각·단계·에이전트)과 수집 구간
|
|
49
|
+
3. 마스킹 내역 요약과 `미확인`으로 남긴 항목 목록
|
|
@@ -43,6 +43,7 @@ permission:
|
|
|
43
43
|
4. **모델 실패 시 `get_fallback_model` 툴로 폴백 모델 ID를 받아** `dispatch_stage`의 `model_override`에 넘긴다. 폴백이 exhausted면 사용자에게 보고.
|
|
44
44
|
5. **승인 게이트는 `.policy.auto_approve.<substage>` 마커가 결정한다.** 기본값은 minor·major 공통으로 전 substage `true` 이므로 전 흐름을 무인 진행한다. `.policy.category=="major"` 라도 auto_approve 가 true 인 한 사람 승인 없이 진행된다 — `category` 는 위험도 라벨/향후 opt-in 훅용이다. HITL 이 명시적으로 opt-in 된 경우(예: `.policy.auto_approve."3_delivery.commit"==false`) 에만 해당 substage 직전에 변경 보고서(`change-report.md`) 작성 → 사용자 승인 → 마커 기록 흐름을 밟는다.
|
|
45
45
|
6. **`retry_disallowed=true` 재-dispatch 금지 (hardrule).** `dispatch_stage` 반환 JSON 에 `retry_disallowed: true` 가 포함되면 **동일 stage 를 재호출하지 않는다**. 이 플래그는 `outcome_kind=="timeout"` 이면서 `transient_failures==0` 인 경우에만 세워지며, 네트워크·API 오류 없이 sub-agent 가 model/prompt 이슈로 hang 한 경우다. 재시도해도 동일 실패를 반복해 무한 루프에 빠진다. 대응: (a) `retry_disallowed_reason` 을 사용자에게 그대로 보고, (b) `get_fallback_model` 로 다른 모델 ID 를 받아 `model_override` 로 1회 한정 재시도, (c) fallback exhausted 면 세션 종료 후 사용자 개입 대기. `transient_failures>0` (네트워크 오류 등) 이거나 `retry_disallowed` 필드 자체가 없으면 기존 재시도 정책 유지.
|
|
46
|
+
7. **플러그인 오류 이슈 등록은 사용자 안내만 한다.** makdoong2-team 자체의 결함(행 걸림, 잘못된 프롬프트 injection, 단계 실패 반복 등)을 관측하거나 사용자가 "이슈 등록해줘"라고 요청해도 `skill(name="makdoong2-issue-reporter")` 를 직접 로드하지 않는다 (훅이 차단). 대신 사용자에게 `/makdoong2-issue-reporter [증상 한 줄(선택)]` 커맨드 실행을 안내한다 — 커맨드가 전용 전권 에이전트로 라우팅되어 수집·마스킹·등록을 수행한다.
|
|
46
47
|
|
|
47
48
|
## Hang 이력 조회 규약 (신규 — LLM API 안정성 관측)
|
|
48
49
|
|
package/bin/cli.js
CHANGED
|
@@ -122,7 +122,7 @@ function doDoctor(flags) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// Config dir checks (agents & skills)
|
|
125
|
-
const configDirChecks = ["agents", "skills/jira-research", "skills/confluence-research", "skills/bitbucket-research", "skills/github-oss-research", "skills/bamboo-ci"];
|
|
125
|
+
const configDirChecks = ["agents", "skills/jira-research", "skills/confluence-research", "skills/bitbucket-research", "skills/github-oss-research", "skills/bamboo-ci", "skills/makdoong2-issue-reporter", "command/makdoong2-issue-reporter.md"];
|
|
126
126
|
for (const d of configDirChecks) {
|
|
127
127
|
check(existsSync(join(DEST, d)), `${d}/ in config dir`, `${d}/ missing in config dir (run: npx makdoong2-team install)`);
|
|
128
128
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: makdoong2-team 오류를 조사해 GitHub 이슈로 등록 (전용 에이전트 전권 실행)
|
|
3
|
+
agent: makdoong2-issue-reporter
|
|
4
|
+
subtask: false
|
|
5
|
+
---
|
|
6
|
+
`skill` 툴로 `makdoong2-issue-reporter` 스킬을 로드하고, 스킬 절차대로 수행하라: 호출 시점 이전의 로그·프롬프트·세션 컨텍스트를 수집해 이상 지점을 포착하고, 보안 마스킹과 사용자 최종 확인을 거쳐 GitHub 이슈(y00njinuk/makdoong2-team)로 등록한다.
|
|
7
|
+
|
|
8
|
+
사용자 보충 설명 (없으면 무시): $ARGUMENTS
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const ISSUE_REPORTER_SKILL_NAME = "makdoong2-issue-reporter";
|
|
2
|
+
export declare const ISSUE_REPORTER_AGENT = "makdoong2-issue-reporter";
|
|
3
|
+
export declare const APPROVAL_MARKER_SUFFIX = ".approved";
|
|
4
|
+
export declare const APPROVE_SCRIPT_BASENAME = "issue-reporter-approve.sh";
|
|
5
|
+
/** GitHub API 호출 분류 결과 */
|
|
6
|
+
export type GithubApiCall = {
|
|
7
|
+
kind: "none";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "read";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "mutation";
|
|
12
|
+
payloadPaths: string[];
|
|
13
|
+
problems: string[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: "forbidden-client";
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* issue-reporter 의 bash 명령을 GitHub API 관점에서 분류한다.
|
|
20
|
+
*
|
|
21
|
+
* - api.github.com 미참조 → none (이 게이트와 무관)
|
|
22
|
+
* - curl 이 아닌 클라이언트(node/python/wget/gh …)로 api.github.com 접근
|
|
23
|
+
* → forbidden-client. 훅이 payload 를 검사할 수 있는 형태가 curl -d @file
|
|
24
|
+
* 뿐이므로 다른 클라이언트는 쓰기·읽기 불문 전부 막는다 (읽기는 curl 로).
|
|
25
|
+
* - curl 이지만 mutation 징후(-X POST/PATCH/PUT/DELETE, 데이터 플래그) 없음 → read
|
|
26
|
+
* - mutation → payload 파일 경로 추출. 문제(인라인 데이터, 상대 경로, 변수 포함)는
|
|
27
|
+
* problems 로 수집한다 — 호출부는 problems 가 하나라도 있으면 차단한다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyGithubApiCall(cmd: string): GithubApiCall;
|
|
30
|
+
/** 승인 스크립트 호출 여부 — 에이전트에게는 실행이 금지된다 (사용자 전용). */
|
|
31
|
+
export declare function isApproveScriptInvocation(cmd: string): boolean;
|
|
32
|
+
/** 승인 마커 경로 참조 여부 — 에이전트의 bash/write 에서 일절 금지된다. */
|
|
33
|
+
export declare function referencesApprovalMarker(text: string): boolean;
|
|
34
|
+
/** payload 파일 경로 → 승인 마커 경로 */
|
|
35
|
+
export declare function approvalMarkerPath(payloadPath: string): string;
|
|
36
|
+
export declare function sha256Hex(content: string | Buffer): string;
|
|
37
|
+
/** 마커 파일 내용에서 해시를 파싱한다. 첫 줄이 64자리 hex 가 아니면 null. */
|
|
38
|
+
export declare function parseApprovalMarker(markerContent: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* 승인 검증: 마커의 해시가 현재 payload 내용과 일치하는가.
|
|
41
|
+
* 불일치 사유를 문자열로 반환하고, 유효하면 null.
|
|
42
|
+
*/
|
|
43
|
+
export declare function approvalMismatch(payloadContent: Buffer, markerContent: string): string | null;
|
|
44
|
+
/** skill 툴 args 에서 스킬 이름을 추출한다. { name } 및 { arguments: { name } } 형태 수용. */
|
|
45
|
+
export declare function extractSkillNameFromArgs(args: unknown): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* skill 툴 호출이 issue-reporter 트리거 정책을 위반하면 사용자에게 보여줄
|
|
48
|
+
* 에러 메시지를 반환하고, 정상이면 null 을 반환한다.
|
|
49
|
+
*/
|
|
50
|
+
export declare function issueReporterSkillLoadViolation(agent: string | undefined, args: unknown): string | null;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// makdoong2-issue-reporter 스킬의 사용자-전용 트리거 강제.
|
|
2
|
+
//
|
|
3
|
+
// Lives in its own module rather than opencode-plugin.ts because opencode's
|
|
4
|
+
// plugin loader invokes EVERY named export of the plugin entry file as a
|
|
5
|
+
// plugin factory (see test/plugin-exports-shape.test.mjs). Helpers must be
|
|
6
|
+
// imported, never re-exported from the entry file.
|
|
7
|
+
//
|
|
8
|
+
// 정책: makdoong2-issue-reporter 스킬의 유일한 트리거는 사용자의 직접 호출
|
|
9
|
+
// (/makdoong2-issue-reporter 커맨드)이다. 커맨드는 전용 full-permission
|
|
10
|
+
// 에이전트 makdoong2-issue-reporter 로 라우팅되므로, 그 외 에이전트
|
|
11
|
+
// (team-leader / sealed 서브에이전트)가 skill() 로 자율 로드하는 것은
|
|
12
|
+
// 트리거 정책 위반이다. 1차 방어는 SKILL.md description 의 명시, 이 모듈이
|
|
13
|
+
// tool.execute.before 훅에서 호출되는 2차(런타임) 방어다.
|
|
14
|
+
//
|
|
15
|
+
// agent 미상(undefined)은 차단하지 않는다 — outer/primary 세션 passthrough 로,
|
|
16
|
+
// OUTER_WORLD_TOOLS 가드와 동일한 설계 철학이다. 우리가 식별한 에이전트에만
|
|
17
|
+
// 정책을 적용하고, 식별 밖의 세션은 opencode 자체 permission 에 맡긴다.
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
export const ISSUE_REPORTER_SKILL_NAME = "makdoong2-issue-reporter";
|
|
20
|
+
export const ISSUE_REPORTER_AGENT = "makdoong2-issue-reporter";
|
|
21
|
+
// ── GitHub 게시 승인 게이트 ──────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// 정책: issue-reporter 가 GitHub 에 무엇이든 게시(이슈·코멘트·Gist·라벨)하려면
|
|
24
|
+
// 사용자가 게시될 "원문 전체"를 보고 명시적으로 승인해야 한다. 채팅 승인은
|
|
25
|
+
// 프롬프트 수준 규약일 뿐이라 강제가 아니다 — 여기의 계약이 물리적 강제다:
|
|
26
|
+
//
|
|
27
|
+
// 1. 에이전트는 payload 를 "리터럴 절대 경로" 파일로 만들어 -d @<path> 로만
|
|
28
|
+
// 전달할 수 있다 (인라인 -d '{...}', 변수 경로, curl 외 HTTP 클라이언트 금지).
|
|
29
|
+
// 2. 사용자가 scripts/issue-reporter-approve.sh <payload> 를 "직접" 실행한다.
|
|
30
|
+
// 스크립트는 payload 원문 전체를 화면에 출력한 뒤 stdin 으로 승인을 받고,
|
|
31
|
+
// payload 의 sha256 을 <payload>.approved 마커에 기록한다.
|
|
32
|
+
// 3. tool.execute.before 훅이 GitHub 쓰기 호출 시 마커의 해시와 현재 payload
|
|
33
|
+
// 내용을 대조한다. 승인 후 1바이트라도 바뀌면 차단 — 승인은 특정 원문에
|
|
34
|
+
// 바인딩되고, 다른 내용으로 바꿔치기할 수 없다.
|
|
35
|
+
// 4. 전송이 실행되면 훅(after)이 마커를 삭제한다. 승인은 1회용이다.
|
|
36
|
+
// 5. 에이전트 자신의 승인 스크립트 실행·마커 생성/조작은 훅이 차단한다.
|
|
37
|
+
// 승인 스크립트는 stdin confirm 이라 에이전트 셸에서는 EOF 로도 못 넘어가지만,
|
|
38
|
+
// printf 'y' 파이프 우회가 가능하므로 실행 자체를 막는 것이 1차다.
|
|
39
|
+
export const APPROVAL_MARKER_SUFFIX = ".approved";
|
|
40
|
+
export const APPROVE_SCRIPT_BASENAME = "issue-reporter-approve.sh";
|
|
41
|
+
const MUTATION_METHOD_RE = /(?:-X|--request)[= ]*['"]?(POST|PATCH|PUT|DELETE)\b/i;
|
|
42
|
+
const DATA_FLAG_RE = /(^|[\s'"])(-d|--data|--data-binary|--data-raw|--data-urlencode|--json|-F|--form)([= ]|$)/;
|
|
43
|
+
const PAYLOAD_AT_RE = /(?:-d|--data|--data-binary|--data-raw|--json)[= ]+@(["']?)([^"'\s]+)\1/g;
|
|
44
|
+
/**
|
|
45
|
+
* issue-reporter 의 bash 명령을 GitHub API 관점에서 분류한다.
|
|
46
|
+
*
|
|
47
|
+
* - api.github.com 미참조 → none (이 게이트와 무관)
|
|
48
|
+
* - curl 이 아닌 클라이언트(node/python/wget/gh …)로 api.github.com 접근
|
|
49
|
+
* → forbidden-client. 훅이 payload 를 검사할 수 있는 형태가 curl -d @file
|
|
50
|
+
* 뿐이므로 다른 클라이언트는 쓰기·읽기 불문 전부 막는다 (읽기는 curl 로).
|
|
51
|
+
* - curl 이지만 mutation 징후(-X POST/PATCH/PUT/DELETE, 데이터 플래그) 없음 → read
|
|
52
|
+
* - mutation → payload 파일 경로 추출. 문제(인라인 데이터, 상대 경로, 변수 포함)는
|
|
53
|
+
* problems 로 수집한다 — 호출부는 problems 가 하나라도 있으면 차단한다.
|
|
54
|
+
*/
|
|
55
|
+
export function classifyGithubApiCall(cmd) {
|
|
56
|
+
// gh CLI 는 URL 문자열 없이도 이슈·코멘트·Gist 를 만들 수 있으므로
|
|
57
|
+
// (gh issue create / gh api ...) api.github.com 참조 여부와 무관하게 막는다.
|
|
58
|
+
if (/(^|[;&|(]\s*|\s)gh\s+(api|issue|pr|gist|label|repo|release)\b/.test(cmd)) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "forbidden-client",
|
|
61
|
+
reason: "gh CLI 는 사용할 수 없다. GitHub 접근은 curl 만 허용된다 (쓰기는 curl -d @<절대경로> 형태만 승인 검증 가능).",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!/api\.github\.com/i.test(cmd))
|
|
65
|
+
return { kind: "none" };
|
|
66
|
+
const usesCurl = /(^|[;&|(]\s*|\s)curl(\s|$)/.test(cmd);
|
|
67
|
+
if (!usesCurl) {
|
|
68
|
+
return {
|
|
69
|
+
kind: "forbidden-client",
|
|
70
|
+
reason: "api.github.com 접근은 curl 만 허용된다 (쓰기는 curl -d @<절대경로> 형태만 검증 가능).",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const hasMutationMethod = MUTATION_METHOD_RE.test(cmd);
|
|
74
|
+
const hasDataFlag = DATA_FLAG_RE.test(cmd);
|
|
75
|
+
// curl -G / --get 은 데이터 플래그를 쿼리 스트링으로 변환하는 GET 이다
|
|
76
|
+
// (중복 검색이 --data-urlencode 와 함께 쓴다). mutation method 가 명시되지
|
|
77
|
+
// 않은 -G 호출은 읽기로 분류한다.
|
|
78
|
+
const isGetConverted = /(^|\s)(-G|--get)(\s|$)/.test(cmd) && !hasMutationMethod;
|
|
79
|
+
if (isGetConverted)
|
|
80
|
+
return { kind: "read" };
|
|
81
|
+
if (!hasMutationMethod && !hasDataFlag)
|
|
82
|
+
return { kind: "read" };
|
|
83
|
+
const payloadPaths = [];
|
|
84
|
+
const problems = [];
|
|
85
|
+
for (const m of cmd.matchAll(PAYLOAD_AT_RE)) {
|
|
86
|
+
payloadPaths.push(m[2]);
|
|
87
|
+
}
|
|
88
|
+
if (payloadPaths.length === 0) {
|
|
89
|
+
problems.push("payload 는 반드시 파일로 전달한다: -d @</absolute/path/payload.json>. " +
|
|
90
|
+
"인라인 JSON(-d '{...}')과 stdin(-d @-)은 승인 검증이 불가능해 금지된다.");
|
|
91
|
+
}
|
|
92
|
+
for (const p of payloadPaths) {
|
|
93
|
+
if (!p.startsWith("/")) {
|
|
94
|
+
problems.push(`payload 경로는 리터럴 절대 경로여야 한다: "${p}"`);
|
|
95
|
+
}
|
|
96
|
+
if (/[$\`]/.test(p)) {
|
|
97
|
+
problems.push(`payload 경로에 변수·명령 치환을 쓸 수 없다: "${p}"`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// TOCTOU 방어: 훅은 "명령 실행 전" 파일 해시를 검증하므로, 같은 명령 안에서
|
|
101
|
+
// payload 를 다시 쓰고(curl 앞에 echo > file 등) 전송하면 검증을 우회할 수 있다.
|
|
102
|
+
// 그래서 쓰기 호출은 셸 제어 연산자·치환이 없는 "단일 curl 호출"만 허용한다.
|
|
103
|
+
// (mutation POST URL 에는 &, ; 등이 필요한 경우가 없다 — 읽기 호출은 이 검사 밖.)
|
|
104
|
+
if (/[;|<>\n`]|\$\(|&&|\s&(\s|$)/.test(cmd)) {
|
|
105
|
+
problems.push("GitHub 쓰기 호출은 단일 curl 명령이어야 한다 — 체이닝(;, &&, |, &), " +
|
|
106
|
+
"리다이렉트(<, >), 명령 치환($(), `)을 함께 쓸 수 없다.");
|
|
107
|
+
}
|
|
108
|
+
return { kind: "mutation", payloadPaths, problems };
|
|
109
|
+
}
|
|
110
|
+
/** 승인 스크립트 호출 여부 — 에이전트에게는 실행이 금지된다 (사용자 전용). */
|
|
111
|
+
export function isApproveScriptInvocation(cmd) {
|
|
112
|
+
return cmd.includes("issue-reporter-approve");
|
|
113
|
+
}
|
|
114
|
+
/** 승인 마커 경로 참조 여부 — 에이전트의 bash/write 에서 일절 금지된다. */
|
|
115
|
+
export function referencesApprovalMarker(text) {
|
|
116
|
+
return text.includes(APPROVAL_MARKER_SUFFIX);
|
|
117
|
+
}
|
|
118
|
+
/** payload 파일 경로 → 승인 마커 경로 */
|
|
119
|
+
export function approvalMarkerPath(payloadPath) {
|
|
120
|
+
return `${payloadPath}${APPROVAL_MARKER_SUFFIX}`;
|
|
121
|
+
}
|
|
122
|
+
export function sha256Hex(content) {
|
|
123
|
+
return createHash("sha256").update(content).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
/** 마커 파일 내용에서 해시를 파싱한다. 첫 줄이 64자리 hex 가 아니면 null. */
|
|
126
|
+
export function parseApprovalMarker(markerContent) {
|
|
127
|
+
const first = markerContent.split("\n")[0]?.trim().toLowerCase() ?? "";
|
|
128
|
+
return /^[0-9a-f]{64}$/.test(first) ? first : null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 승인 검증: 마커의 해시가 현재 payload 내용과 일치하는가.
|
|
132
|
+
* 불일치 사유를 문자열로 반환하고, 유효하면 null.
|
|
133
|
+
*/
|
|
134
|
+
export function approvalMismatch(payloadContent, markerContent) {
|
|
135
|
+
const recorded = parseApprovalMarker(markerContent);
|
|
136
|
+
if (recorded === null)
|
|
137
|
+
return "승인 마커 형식이 잘못됐다 (첫 줄이 sha256 hex 가 아님)";
|
|
138
|
+
const actual = sha256Hex(payloadContent);
|
|
139
|
+
if (actual !== recorded) {
|
|
140
|
+
return "payload 내용이 승인 이후 변경됐다 — 승인은 특정 원문에 바인딩되며, 변경된 내용은 재승인이 필요하다";
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** skill 툴 args 에서 스킬 이름을 추출한다. { name } 및 { arguments: { name } } 형태 수용. */
|
|
145
|
+
export function extractSkillNameFromArgs(args) {
|
|
146
|
+
if (!args || typeof args !== "object")
|
|
147
|
+
return undefined;
|
|
148
|
+
const direct = args.name;
|
|
149
|
+
if (typeof direct === "string" && direct.length > 0)
|
|
150
|
+
return direct;
|
|
151
|
+
const nested = args.arguments;
|
|
152
|
+
if (nested && typeof nested === "object") {
|
|
153
|
+
const inner = nested.name;
|
|
154
|
+
if (typeof inner === "string" && inner.length > 0)
|
|
155
|
+
return inner;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* skill 툴 호출이 issue-reporter 트리거 정책을 위반하면 사용자에게 보여줄
|
|
161
|
+
* 에러 메시지를 반환하고, 정상이면 null 을 반환한다.
|
|
162
|
+
*/
|
|
163
|
+
export function issueReporterSkillLoadViolation(agent, args) {
|
|
164
|
+
const skillName = extractSkillNameFromArgs(args);
|
|
165
|
+
if (skillName !== ISSUE_REPORTER_SKILL_NAME)
|
|
166
|
+
return null;
|
|
167
|
+
if (agent === undefined)
|
|
168
|
+
return null;
|
|
169
|
+
if (agent === ISSUE_REPORTER_AGENT)
|
|
170
|
+
return null;
|
|
171
|
+
return (`[makdoong2-team issue-reporter trigger violation]\n` +
|
|
172
|
+
`Agent "${agent}" cannot load skill "${ISSUE_REPORTER_SKILL_NAME}".\n\n` +
|
|
173
|
+
`이 스킬의 유일한 트리거는 사용자의 직접 호출이다. 에이전트가 실패·예외를 ` +
|
|
174
|
+
`관측했더라도 자율적으로 이슈를 등록하지 않는다.\n\n` +
|
|
175
|
+
`**올바른 절차:** 사용자에게 다음 커맨드 실행을 안내하라:\n` +
|
|
176
|
+
` /makdoong2-issue-reporter [증상 한 줄 설명(선택)]\n\n` +
|
|
177
|
+
`커맨드는 전용 full-permission 에이전트(${ISSUE_REPORTER_AGENT})로 라우팅되어 ` +
|
|
178
|
+
`수집·마스킹·등록을 수행한다.`);
|
|
179
|
+
}
|
package/dist/opencode-plugin.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Install path (default): ~/.config/opencode/plugins/makdoong2-team/
|
|
17
17
|
// Place this file at ~/.config/opencode/plugins/makdoong2-team/src/opencode-plugin.ts
|
|
18
18
|
// (or load the npm package via opencode.json "plugin": ["./plugins/makdoong2-team/src/opencode-plugin.ts"]).
|
|
19
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join, resolve as pathResolve, sep as pathSep } from "node:path";
|
|
22
22
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -33,6 +33,7 @@ import { injectAllSecrets } from "./mcp-secret-injector.js";
|
|
|
33
33
|
import { pollSubSession as pollSubSessionCore, pollOutcomeToLegacy } from "./poll-sub-session.js";
|
|
34
34
|
import { logger } from "./logger.js";
|
|
35
35
|
import { redactAndTruncate } from "./redact-secrets.js";
|
|
36
|
+
import { issueReporterSkillLoadViolation, ISSUE_REPORTER_AGENT, APPROVAL_MARKER_SUFFIX, APPROVE_SCRIPT_BASENAME, classifyGithubApiCall, isApproveScriptInvocation, referencesApprovalMarker, approvalMarkerPath, approvalMismatch, } from "./issue-reporter-guard.js";
|
|
36
37
|
// All runtime paths come from makdoong2-team.json (paths.* overrides) or are
|
|
37
38
|
// derived from the opencode config dir. No MAKDOONG2 environment variables.
|
|
38
39
|
const { hooks: HOOKS_DIR, gates: GATES_DIR, scripts: SCRIPTS_DIR, stages: STAGES_DIR, skills: SKILLS_DIR, } = resolvePaths();
|
|
@@ -828,6 +829,10 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
828
829
|
// already omits Task (L1), but sealed workflow is defence in depth — a worker
|
|
829
830
|
// missing here would be caught by nothing at runtime (L2).
|
|
830
831
|
"makdoong2-researcher",
|
|
832
|
+
// User-only issue reporter. 워크플로우에 참여하지 않지만 outer-world 위임은
|
|
833
|
+
// 동일하게 금지 — 수집·마스킹·등록 전 과정을 자기 세션에서 완결해야 하며,
|
|
834
|
+
// 위임하면 마스킹·승인 게이트가 위임처에서 우회될 수 있다.
|
|
835
|
+
ISSUE_REPORTER_AGENT,
|
|
831
836
|
]);
|
|
832
837
|
// 미래의 oh-my-openagent 위임 툴을 조기 발견하기 위한 이름 패턴.
|
|
833
838
|
// 알려진 툴이 아니면서 위임/스폰을 시사하는 이름이 감지되면 경고한다.
|
|
@@ -987,6 +992,18 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
987
992
|
logger.debug(`[makdoong2-team hook] sealed sub-agent "${agent}" called delegation-like tool "${input.tool}" not in blocklist. ` +
|
|
988
993
|
`Consider adding to OUTER_WORLD_TOOLS if this is a new oh-my-openagent delegation tool.`);
|
|
989
994
|
}
|
|
995
|
+
// ── issue-reporter 스킬 사용자-전용 트리거 강제 ──
|
|
996
|
+
// makdoong2-issue-reporter 스킬은 사용자의 /makdoong2-issue-reporter
|
|
997
|
+
// 커맨드(전용 full-permission 에이전트로 라우팅)로만 실행된다. 다른
|
|
998
|
+
// 에이전트의 skill() 자율 로드는 트리거 정책 위반 — 1차 방어는 SKILL.md
|
|
999
|
+
// description, 이 블록이 2차(런타임) 방어다.
|
|
1000
|
+
if (toolLower === "skill") {
|
|
1001
|
+
const violation = issueReporterSkillLoadViolation(agent, output.args);
|
|
1002
|
+
if (violation) {
|
|
1003
|
+
logger.error(`[makdoong2-team hook] BLOCKED: agent "${agent}" attempted autonomous load of user-only skill makdoong2-issue-reporter (sessionID="${sessionID}")`);
|
|
1004
|
+
throw new Error(violation);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
990
1007
|
// ── skill_mcp lazy-load 사전 힌트 ──
|
|
991
1008
|
// opencode 는 skill 이 로드되기 전에 skill_mcp 를 호출하면
|
|
992
1009
|
// "MCP server not found" 로 튕기지만 정작 어떤 skill 을 로드해야 하는지는
|
|
@@ -1003,6 +1020,24 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1003
1020
|
`first invoke skill(name="${skillName}") in the current session, then retry.`);
|
|
1004
1021
|
}
|
|
1005
1022
|
}
|
|
1023
|
+
// ── Issue-reporter: 승인 마커는 어떤 툴로도 만들 수 없다 ──
|
|
1024
|
+
// 마커(<payload>.approved)는 오직 사용자가 issue-reporter-approve.sh 를
|
|
1025
|
+
// 직접 실행해서만 생긴다. write/edit 툴로 마커를 위조하면 게시 게이트가
|
|
1026
|
+
// 무력화되므로 경로에 마커 접미사가 보이면 즉시 차단한다.
|
|
1027
|
+
if (agent === ISSUE_REPORTER_AGENT && (LEADER_FORBIDDEN_TOOLS.has(toolLower) || WRITE_TOOLS.has(toolLower))) {
|
|
1028
|
+
// filePath 인자뿐 아니라 인자 전체를 검사한다 — apply_patch 는 파일 경로가
|
|
1029
|
+
// 패치 본문 안에 들어 있어 filePath 추출로는 마커 위조를 잡을 수 없다.
|
|
1030
|
+
let argsSerialized = "";
|
|
1031
|
+
try {
|
|
1032
|
+
argsSerialized = JSON.stringify(output.args ?? "");
|
|
1033
|
+
}
|
|
1034
|
+
catch { /* ignore */ }
|
|
1035
|
+
if (referencesApprovalMarker(argsSerialized)) {
|
|
1036
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter가 ${input.tool} 인자에서 승인 마커 경로 참조.`);
|
|
1037
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] 승인 마커(*${APPROVAL_MARKER_SUFFIX})는 에이전트가 생성·수정할 수 없다. ` +
|
|
1038
|
+
`사용자가 'bash ${SCRIPTS_DIR}/${APPROVE_SCRIPT_BASENAME} <payload>' 를 직접 실행해야만 승인이 성립한다.`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1006
1041
|
// ── Leader hardrule 1: 직접 파일 편집·생성 금지 (write/edit/patch/multiedit) ──
|
|
1007
1042
|
if (agent === "makdoong2-team-leader" && LEADER_FORBIDDEN_TOOLS.has(toolLower)) {
|
|
1008
1043
|
logger.error(`[makdoong2-team hook] BLOCKED: team-leader가 ${input.tool} 툴 호출을 시도했다.`);
|
|
@@ -1042,6 +1077,56 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1042
1077
|
`• 초안 파일 필요 시: spec을 team-leader에게 반환 → dev 단계에서 engineer가 구현.\n\n` +
|
|
1043
1078
|
`**참조**: agents/${agent}.md "금지" 섹션, CLAUDE.md "워크플로우 상태 & 위임 규약"`);
|
|
1044
1079
|
}
|
|
1080
|
+
// ── Issue-reporter 게시 게이트: GitHub 쓰기는 사용자 승인 원문만 ──
|
|
1081
|
+
// 정책·마커 계약 상세: src/issue-reporter-guard.ts 상단 주석.
|
|
1082
|
+
if (agent === ISSUE_REPORTER_AGENT) {
|
|
1083
|
+
const approveHint = `승인 절차: 에이전트가 payload 원문 전체를 채팅에 표시한 뒤, 사용자가 직접\n` +
|
|
1084
|
+
` bash ${SCRIPTS_DIR}/${APPROVE_SCRIPT_BASENAME} </absolute/path/payload.json>\n` +
|
|
1085
|
+
`를 실행한다 (스크립트가 원문을 다시 보여주고 stdin 승인 후 <payload>${APPROVAL_MARKER_SUFFIX} 마커 기록). ` +
|
|
1086
|
+
`이 승인은 1회용이고 payload 내용이 바뀌면 무효다.`;
|
|
1087
|
+
if (isApproveScriptInvocation(cmd)) {
|
|
1088
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter가 승인 스크립트 실행 시도. cmd="${redactAndTruncate(cmd, 200)}"`);
|
|
1089
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] ${APPROVE_SCRIPT_BASENAME} 는 사용자 전용이다. ` +
|
|
1090
|
+
`에이전트가 실행하면 승인의 의미가 사라진다. 사용자에게 실행을 안내하고 대기하라.\n${approveHint}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (referencesApprovalMarker(cmd)) {
|
|
1093
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter bash가 승인 마커를 참조. cmd="${redactAndTruncate(cmd, 200)}"`);
|
|
1094
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] 승인 마커(*${APPROVAL_MARKER_SUFFIX})는 에이전트가 ` +
|
|
1095
|
+
`생성·수정·삭제·열람할 수 없다. 마커 검증과 소멸은 훅이 수행한다.\n${approveHint}`);
|
|
1096
|
+
}
|
|
1097
|
+
const call = classifyGithubApiCall(cmd);
|
|
1098
|
+
if (call.kind === "forbidden-client") {
|
|
1099
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter가 비-curl 클라이언트로 GitHub API 접근. cmd="${redactAndTruncate(cmd, 200)}"`);
|
|
1100
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] ${call.reason}\n${approveHint}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (call.kind === "mutation") {
|
|
1103
|
+
if (call.problems.length > 0) {
|
|
1104
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter GitHub 쓰기 형식 위반: ${call.problems.join(" / ")}`);
|
|
1105
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] GitHub 쓰기 호출 형식 위반:\n` +
|
|
1106
|
+
call.problems.map((p) => ` - ${p}`).join("\n") + `\n${approveHint}`);
|
|
1107
|
+
}
|
|
1108
|
+
for (const payloadPath of call.payloadPaths) {
|
|
1109
|
+
if (!existsSync(payloadPath)) {
|
|
1110
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] payload 파일이 없다: ${payloadPath}\n${approveHint}`);
|
|
1111
|
+
}
|
|
1112
|
+
const markerPath = approvalMarkerPath(payloadPath);
|
|
1113
|
+
if (!existsSync(markerPath)) {
|
|
1114
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter GitHub 쓰기 — 승인 마커 없음: ${markerPath}`);
|
|
1115
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] 사용자 승인이 없다.\n` +
|
|
1116
|
+
`게시하려는 원문(${payloadPath})을 채팅에 전문 표시한 뒤, 사용자에게 아래 실행을 안내하고 대기하라:\n` +
|
|
1117
|
+
` bash ${SCRIPTS_DIR}/${APPROVE_SCRIPT_BASENAME} ${payloadPath}`);
|
|
1118
|
+
}
|
|
1119
|
+
const mismatch = approvalMismatch(readFileSync(payloadPath), readFileSync(markerPath, "utf8"));
|
|
1120
|
+
if (mismatch !== null) {
|
|
1121
|
+
logger.error(`[makdoong2-team hook] BLOCKED: issue-reporter GitHub 쓰기 — ${mismatch} (${payloadPath})`);
|
|
1122
|
+
throw new Error(`[makdoong2-team issue-reporter 게시 게이트] ${mismatch}.\n` +
|
|
1123
|
+
`현재 원문을 다시 채팅에 전문 표시하고 재승인을 받아라:\n` +
|
|
1124
|
+
` bash ${SCRIPTS_DIR}/${APPROVE_SCRIPT_BASENAME} ${payloadPath}`);
|
|
1125
|
+
}
|
|
1126
|
+
logger.debug(`[makdoong2-team hook] issue-reporter 게시 승인 확인: ${payloadPath} (marker hash 일치)`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1045
1130
|
const hookIssue = sessionIssue.get(sessionID ?? "") ?? "";
|
|
1046
1131
|
const r = await runScript(HOOKS_DIR, "guard-bash.sh", cmd, hookIssue);
|
|
1047
1132
|
if (!r.ok) {
|
|
@@ -1064,6 +1149,29 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
|
|
|
1064
1149
|
else
|
|
1065
1150
|
sessionActiveToolCount.set(afterSessionID, cur - 1);
|
|
1066
1151
|
}
|
|
1152
|
+
// ── Issue-reporter 승인 마커 1회용 소멸 ──
|
|
1153
|
+
// GitHub 쓰기 명령이 "실행"되고 나면 결과와 무관하게 마커를 삭제한다.
|
|
1154
|
+
// 성공: 재게시에는 새 승인이 필요하다. 실패(네트워크 등): 같은 원문이라도
|
|
1155
|
+
// 재시도 전에 재승인을 받는다 — 마커를 남겨두면 실패를 빌미로 승인 한 번에
|
|
1156
|
+
// 여러 번의 전송 시도가 가능해지므로 엄격한 쪽을 택했다.
|
|
1157
|
+
if (toolLowerAfter === "bash") {
|
|
1158
|
+
const afterAgent = afterSessionID
|
|
1159
|
+
? (sessionAgent.get(afterSessionID) ?? pendingDispatch.get(afterSessionID)?.agent)
|
|
1160
|
+
: undefined;
|
|
1161
|
+
if (afterAgent === ISSUE_REPORTER_AGENT) {
|
|
1162
|
+
const afterCmd = input.args?.command ?? "";
|
|
1163
|
+
const afterCall = classifyGithubApiCall(afterCmd);
|
|
1164
|
+
if (afterCall.kind === "mutation") {
|
|
1165
|
+
for (const payloadPath of afterCall.payloadPaths) {
|
|
1166
|
+
const markerPath = approvalMarkerPath(payloadPath);
|
|
1167
|
+
if (existsSync(markerPath)) {
|
|
1168
|
+
rmSync(markerPath, { force: true });
|
|
1169
|
+
logger.debug(`[makdoong2-team hook] issue-reporter 승인 마커 소멸(1회용): ${markerPath}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1067
1175
|
if (afterSessionID && WRITE_TOOLS.has(toolLowerAfter)) {
|
|
1068
1176
|
const agentAfter = sessionAgent.get(afterSessionID)
|
|
1069
1177
|
?? pendingDispatch.get(afterSessionID)?.agent;
|
package/package.json
CHANGED
package/scripts/install-lib.mjs
CHANGED
|
@@ -93,6 +93,11 @@ const TOOL_SEARCH_PLUGIN_PREFIX = "opencode-tool-search";
|
|
|
93
93
|
// and we need the list to remove them without touching unrelated user scripts.
|
|
94
94
|
const UTIL_SCRIPTS = ["state.sh", "rollback-commits.sh", "wt-sync-ignored.sh", "log-event.sh", "config.sh", "model-policy.mjs"];
|
|
95
95
|
const RESEARCH_SKILLS = ["jira-research", "confluence-research", "bitbucket-research", "github-oss-research", "bamboo-ci"];
|
|
96
|
+
// User-invoked utility skills — workflow 밖의 사용자 전용 도구. issue reporter 는
|
|
97
|
+
// /makdoong2-issue-reporter 커맨드(command/ 배포분)로만 트리거되고, 같은 이름의
|
|
98
|
+
// config command 가 opencode 의 skill-derived command 를 덮어써 전용
|
|
99
|
+
// full-permission 에이전트로 라우팅된다.
|
|
100
|
+
const UTILITY_SKILLS = ["makdoong2-issue-reporter"];
|
|
96
101
|
|
|
97
102
|
// Stale config-dir artifacts left behind by pre-refactor installs. Removed
|
|
98
103
|
// (with backup) at the start of install() so a re-run cleanly migrates users
|
|
@@ -315,8 +320,8 @@ export function install(opts) {
|
|
|
315
320
|
// Create directory structure — only agents/ and research skills/ are deployed.
|
|
316
321
|
// All other runtime assets (gates, scripts, stages, references) live in the
|
|
317
322
|
// npm module and are referenced via resolvePaths() in src/config.ts.
|
|
318
|
-
const skillDirs = RESEARCH_SKILLS.map((s) => `skills/${s}`);
|
|
319
|
-
for (const d of ["agents", ...skillDirs]) {
|
|
323
|
+
const skillDirs = [...RESEARCH_SKILLS, ...UTILITY_SKILLS].map((s) => `skills/${s}`);
|
|
324
|
+
for (const d of ["agents", "command", ...skillDirs]) {
|
|
320
325
|
mkdirSync(join(configDir, d), { recursive: true });
|
|
321
326
|
}
|
|
322
327
|
|
|
@@ -353,6 +358,35 @@ export function install(opts) {
|
|
|
353
358
|
}
|
|
354
359
|
ok("research skills (jira/confluence/bitbucket/github-oss/bamboo)");
|
|
355
360
|
|
|
361
|
+
// 2b) Utility skills — user-invoked helpers outside the workflow.
|
|
362
|
+
for (const skill of UTILITY_SKILLS) {
|
|
363
|
+
const src = join(pkgRoot, "skills", skill);
|
|
364
|
+
if (!existsSync(src)) continue;
|
|
365
|
+
const dst = join(configDir, "skills", skill);
|
|
366
|
+
mkdirSync(dst, { recursive: true });
|
|
367
|
+
for (const name of readdirSync(src)) {
|
|
368
|
+
cpSync(join(src, name), join(dst, name));
|
|
369
|
+
if (name.endsWith(".sh")) chmodSync(join(dst, name), 0o755);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
ok("utility skills (makdoong2-issue-reporter)");
|
|
373
|
+
|
|
374
|
+
// 2c) User commands — /makdoong2-issue-reporter 진입점. opencode 는 스킬을
|
|
375
|
+
// 자동으로 같은 이름의 커맨드로 노출하지만 그 커맨드에는 agent 필드가 없어
|
|
376
|
+
// 현재 에이전트(권한 제한된 team-leader 등)로 실행된다. cfg.command 가
|
|
377
|
+
// skill-derived command 보다 우선하므로, 같은 이름의 command 파일을 배포해
|
|
378
|
+
// 전용 full-permission 에이전트로 라우팅한다.
|
|
379
|
+
const commandSrc = join(pkgRoot, "command");
|
|
380
|
+
if (existsSync(commandSrc)) {
|
|
381
|
+
const commandDst = join(configDir, "command");
|
|
382
|
+
mkdirSync(commandDst, { recursive: true });
|
|
383
|
+
for (const name of readdirSync(commandSrc)) {
|
|
384
|
+
if (!name.endsWith(".md")) continue;
|
|
385
|
+
cpSync(join(commandSrc, name), join(commandDst, name));
|
|
386
|
+
}
|
|
387
|
+
ok("user commands (/makdoong2-issue-reporter)");
|
|
388
|
+
}
|
|
389
|
+
|
|
356
390
|
// 3) Config file — seed from default ONLY if absent (never clobber user edits unless --force)
|
|
357
391
|
const cfgPath = join(configDir, "makdoong2-team.json");
|
|
358
392
|
if (!existsSync(cfgPath) || force) {
|
|
@@ -839,8 +873,10 @@ function removeOpencodeCache(pkgRoot, { ok, skip, warn }) {
|
|
|
839
873
|
*
|
|
840
874
|
* What is removed (idempotent — absent targets are silently skipped):
|
|
841
875
|
* 1. Agent definitions: agents/makdoong2-*.md
|
|
842
|
-
* 2.
|
|
843
|
-
*
|
|
876
|
+
* 2. Skill directories: research skills (skills/jira-research, confluence-research,
|
|
877
|
+
* bitbucket-research, ...), utility skills (skills/makdoong2-issue-reporter),
|
|
878
|
+
* skills/_lib/ (only when it contains solely our files), and deployed
|
|
879
|
+
* command files (command/makdoong2-*.md)
|
|
844
880
|
* 3. opencode.json: plugin ref, tools keys, alwaysLoad entries (when
|
|
845
881
|
* opencode-tool-search is configured), pkgRoot/** external_directory entry
|
|
846
882
|
* 4. opencode plugin cache symlinks seeded by seedOpencodeCache()
|
|
@@ -888,10 +924,10 @@ export function uninstall(opts) {
|
|
|
888
924
|
skip("agents/: directory not found");
|
|
889
925
|
}
|
|
890
926
|
|
|
891
|
-
// 2)
|
|
892
|
-
info("Removing
|
|
927
|
+
// 2) Skill directories (research + utility)
|
|
928
|
+
info("Removing skills...");
|
|
893
929
|
let skillsRemoved = 0;
|
|
894
|
-
for (const skill of RESEARCH_SKILLS) {
|
|
930
|
+
for (const skill of [...RESEARCH_SKILLS, ...UTILITY_SKILLS]) {
|
|
895
931
|
const skillDir = join(configDir, "skills", skill);
|
|
896
932
|
if (!existsSync(skillDir)) continue;
|
|
897
933
|
rmSync(skillDir, { recursive: true, force: true });
|
|
@@ -909,9 +945,25 @@ export function uninstall(opts) {
|
|
|
909
945
|
warn("skills/_lib/ contains unrecognised files — leaving in place");
|
|
910
946
|
}
|
|
911
947
|
}
|
|
912
|
-
if (skillsRemoved > 0) ok(`${skillsRemoved}
|
|
948
|
+
if (skillsRemoved > 0) ok(`${skillsRemoved} skill director(y/ies) removed`);
|
|
913
949
|
else skip("skills/: no makdoong2-team skill directories found");
|
|
914
950
|
|
|
951
|
+
// 2b) Deployed command files (only ours — makdoong2-*.md)
|
|
952
|
+
info("Removing command files...");
|
|
953
|
+
const cmdDir = join(configDir, "command");
|
|
954
|
+
if (existsSync(cmdDir)) {
|
|
955
|
+
let cmdRemoved = 0;
|
|
956
|
+
for (const name of readdirSync(cmdDir)) {
|
|
957
|
+
if (!name.startsWith("makdoong2-") || !name.endsWith(".md")) continue;
|
|
958
|
+
rmSync(join(cmdDir, name), { force: true });
|
|
959
|
+
cmdRemoved++;
|
|
960
|
+
}
|
|
961
|
+
if (cmdRemoved > 0) ok(`${cmdRemoved} command file(s) removed from command/`);
|
|
962
|
+
else skip("command/: no makdoong2-*.md files found");
|
|
963
|
+
} else {
|
|
964
|
+
skip("command/: directory not found");
|
|
965
|
+
}
|
|
966
|
+
|
|
915
967
|
// 3) opencode.json
|
|
916
968
|
info("Patching opencode.json...");
|
|
917
969
|
unpatchOpencodeJson(configDir, pkgRoot, { ok, skip, warn });
|