@tienne/gestalt 0.120.2 → 0.121.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.ko.md +14 -0
- package/README.md +21 -0
- package/dist/package.json +4 -3
- package/dist/plugin/.codex-plugin/plugin.json +40 -0
- package/dist/plugin/review-agents/writing-reviewer/AGENT.md +4 -2
- package/dist/plugin/role-agents/_shared/references/ai-tell-quick-rules.md +2 -1
- package/dist/plugin/role-agents/humanize-monolith/AGENT.md +5 -2
- package/dist/plugin/scripts/acquisition.mjs +96 -0
- package/dist/plugin/scripts/gestalt-cli.mjs +144 -0
- package/dist/plugin/scripts/npm-environment.mjs +64 -0
- package/dist/plugin/scripts/package-owner.mjs +20 -0
- package/dist/plugin/scripts/resolve-cli.mjs +22 -0
- package/dist/plugin/skills/_shared/agent-delegation.md +2 -0
- package/dist/plugin/skills/_shared/agent-model.md +2 -0
- package/dist/plugin/skills/_shared/cli-launcher.md +47 -36
- package/dist/plugin/skills/_shared/host-runtime.md +47 -0
- package/dist/plugin/skills/agent/SKILL.md +2 -0
- package/dist/plugin/skills/architecture/SKILL.md +2 -0
- package/dist/plugin/skills/blast-radius/SKILL.md +2 -0
- package/dist/plugin/skills/brief/SKILL.md +2 -0
- package/dist/plugin/skills/build-graph/SKILL.md +2 -0
- package/dist/plugin/skills/diff-radius/SKILL.md +2 -0
- package/dist/plugin/skills/dispatch/SKILL.md +2 -0
- package/dist/plugin/skills/doubt/SKILL.md +2 -0
- package/dist/plugin/skills/execute/SKILL.md +3 -1
- package/dist/plugin/skills/explain/SKILL.md +2 -0
- package/dist/plugin/skills/handoff/SKILL.md +2 -0
- package/dist/plugin/skills/interview/SKILL.md +2 -0
- package/dist/plugin/skills/jira-create/SKILL.md +2 -0
- package/dist/plugin/skills/lens/SKILL.md +2 -0
- package/dist/plugin/skills/local-pr/SKILL.md +4 -2
- package/dist/plugin/skills/market-scan/SKILL.md +2 -0
- package/dist/plugin/skills/pr/SKILL.md +6 -14
- package/dist/plugin/skills/presentation/SKILL.md +2 -0
- package/dist/plugin/skills/research/SKILL.md +2 -0
- package/dist/plugin/skills/reverse-plan/SKILL.md +2 -0
- package/dist/plugin/skills/review/SKILL.md +15 -18
- package/dist/plugin/skills/review-loop/SKILL.md +13 -10
- package/dist/plugin/skills/review-reply/SKILL.md +7 -15
- package/dist/plugin/skills/screen-ref/SKILL.md +2 -0
- package/dist/plugin/skills/setup/SKILL.md +9 -6
- package/dist/plugin/skills/share/SKILL.md +4 -2
- package/dist/plugin/skills/ship/SKILL.md +3 -1
- package/dist/plugin/skills/slack-send/SKILL.md +2 -0
- package/dist/plugin/skills/solve/SKILL.md +2 -0
- package/dist/plugin/skills/spec/SKILL.md +3 -1
- package/dist/plugin/skills/ux-research/SKILL.md +2 -0
- package/dist/plugin/skills/video-digest/SKILL.md +2 -0
- package/dist/schemas/gestalt.schema.json +25 -4
- package/dist/src/cli/commands/init.d.ts +2 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +6 -2
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/setup.d.ts +4 -1
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +2 -1
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +15 -3
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/core/config.d.ts +169 -150
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +41 -11
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/model-diagnostics.d.ts +9 -0
- package/dist/src/core/model-diagnostics.d.ts.map +1 -0
- package/dist/src/core/model-diagnostics.js +47 -0
- package/dist/src/core/model-diagnostics.js.map +1 -0
- package/dist/src/core/types.d.ts +3 -3
- package/dist/src/execute/prompts.d.ts.map +1 -1
- package/dist/src/execute/prompts.js +4 -1
- package/dist/src/execute/prompts.js.map +1 -1
- package/dist/src/mcp/client-host.d.ts +12 -0
- package/dist/src/mcp/client-host.d.ts.map +1 -0
- package/dist/src/mcp/client-host.js +41 -0
- package/dist/src/mcp/client-host.js.map +1 -0
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +20 -6
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/status.d.ts +22 -7
- package/dist/src/mcp/tools/status.d.ts.map +1 -1
- package/dist/src/mcp/tools/status.js +8 -3
- package/dist/src/mcp/tools/status.js.map +1 -1
- package/package.json +4 -3
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/review-agents/writing-reviewer/AGENT.md +4 -2
- package/plugin/role-agents/_shared/references/ai-tell-quick-rules.md +2 -1
- package/plugin/role-agents/humanize-monolith/AGENT.md +5 -2
- package/plugin/scripts/acquisition.mjs +96 -0
- package/plugin/scripts/gestalt-cli.mjs +144 -0
- package/plugin/scripts/npm-environment.mjs +64 -0
- package/plugin/scripts/package-owner.mjs +20 -0
- package/plugin/scripts/resolve-cli.mjs +22 -0
- package/plugin/skills/_shared/agent-delegation.md +2 -0
- package/plugin/skills/_shared/agent-model.md +2 -0
- package/plugin/skills/_shared/cli-launcher.md +47 -36
- package/plugin/skills/_shared/host-runtime.md +47 -0
- package/plugin/skills/agent/SKILL.md +2 -0
- package/plugin/skills/architecture/SKILL.md +2 -0
- package/plugin/skills/blast-radius/SKILL.md +2 -0
- package/plugin/skills/brief/SKILL.md +2 -0
- package/plugin/skills/build-graph/SKILL.md +2 -0
- package/plugin/skills/diff-radius/SKILL.md +2 -0
- package/plugin/skills/dispatch/SKILL.md +2 -0
- package/plugin/skills/doubt/SKILL.md +2 -0
- package/plugin/skills/execute/SKILL.md +3 -1
- package/plugin/skills/explain/SKILL.md +2 -0
- package/plugin/skills/handoff/SKILL.md +2 -0
- package/plugin/skills/interview/SKILL.md +2 -0
- package/plugin/skills/jira-create/SKILL.md +2 -0
- package/plugin/skills/lens/SKILL.md +2 -0
- package/plugin/skills/local-pr/SKILL.md +4 -2
- package/plugin/skills/market-scan/SKILL.md +2 -0
- package/plugin/skills/pr/SKILL.md +6 -14
- package/plugin/skills/presentation/SKILL.md +2 -0
- package/plugin/skills/research/SKILL.md +2 -0
- package/plugin/skills/reverse-plan/SKILL.md +2 -0
- package/plugin/skills/review/SKILL.md +15 -18
- package/plugin/skills/review-loop/SKILL.md +13 -10
- package/plugin/skills/review-reply/SKILL.md +7 -15
- package/plugin/skills/screen-ref/SKILL.md +2 -0
- package/plugin/skills/setup/SKILL.md +9 -6
- package/plugin/skills/share/SKILL.md +4 -2
- package/plugin/skills/ship/SKILL.md +3 -1
- package/plugin/skills/slack-send/SKILL.md +2 -0
- package/plugin/skills/solve/SKILL.md +2 -0
- package/plugin/skills/spec/SKILL.md +3 -1
- package/plugin/skills/ux-research/SKILL.md +2 -0
- package/plugin/skills/video-digest/SKILL.md +2 -0
- package/schemas/gestalt.schema.json +25 -4
package/README.ko.md
CHANGED
|
@@ -246,6 +246,20 @@ codex plugin add gestalt@gestalt
|
|
|
246
246
|
|
|
247
247
|
스킬은 다음 Codex 세션부터 잡혀요. 슬래시 커맨드랑 Claude Code Task 패널은 Claude Code 전용이라, Codex에서는 하려는 일을 말로 설명하면 Codex가 해당 `SKILL.md`를 읽어 진행해요.
|
|
248
248
|
|
|
249
|
+
설치 후 새 세션에서 “Gestalt 상태 확인해줘” 또는 “이 프로젝트를 Codex용 Gestalt로 초기화해줘”라고 요청하면 돼요.
|
|
250
|
+
MCP 초기화 때 Codex를 인식해서 실행 컨텍스트는 기본으로 `AGENTS.md`에 기록하고 에이전트 모델은 현재
|
|
251
|
+
Codex 세션을 상속해요. 명시한 `client`와 모델 설정은 유지하며 `client: "both"`면 두 호스트 파일에 계속 기록해요.
|
|
252
|
+
프로젝트에 Codex 설정을 남기려면 초기화 스킬에 요청하면 돼요. 스킬이 아래의 버전 고정 런처로
|
|
253
|
+
`setup --client codex`를 실행해요. 코드 그래프와 Git 훅까지 설정하려면 `init --client codex`를
|
|
254
|
+
실행하고 고정된 CLI가 아직 이 옵션을 지원하지 않으면 호환 절차를 적용해요.
|
|
255
|
+
|
|
256
|
+
스킬은 [호스트 어댑터](./plugin/skills/_shared/host-runtime.md)로 Claude 도구 예시를 현재 Codex 도구에 맞춰 실행해요.
|
|
257
|
+
CLI 검사는 [버전 고정 런처](./plugin/skills/_shared/cli-launcher.md)를 거쳐서 전역 `gestalt` 설치가 필요 없어요.
|
|
258
|
+
첫 CLI 실행은 플러그인과 같은 npm 버전을 내려받을 수 있으니 Node.js >=22와 npm이 PATH에 있어야 해요.
|
|
259
|
+
|
|
260
|
+
연결이 실패하면 MCP 상태와 Node/npm부터 확인해요. 호스트나 모델이 예상과 다르면 `ges_status`의 `hostClient`,
|
|
261
|
+
`client`, `tierModels`를 먼저 확인해요. Claude Task 패널과 Claude 전용 훅은 Codex 실행을 막지 않아요.
|
|
262
|
+
|
|
249
263
|
---
|
|
250
264
|
|
|
251
265
|
### 옵션 5: OpenAI Codex CLI (MCP만)
|
package/README.md
CHANGED
|
@@ -221,6 +221,27 @@ Skills load on the next Codex session. Slash commands and the Claude Code Task
|
|
|
221
221
|
panel are still Claude Code only — in Codex you invoke a skill by describing the
|
|
222
222
|
task, and Codex reads the matching `SKILL.md`.
|
|
223
223
|
|
|
224
|
+
Start a new session after installing, then ask “Check Gestalt status” or
|
|
225
|
+
“Set up Gestalt for this Codex project.” The server recognizes Codex during MCP
|
|
226
|
+
initialization: new projects record execution context in `AGENTS.md`, and agent
|
|
227
|
+
models inherit the active Codex session by default. Explicit `client` and model
|
|
228
|
+
settings still take precedence; `client: "both"` continues writing both host files.
|
|
229
|
+
For a persistent Codex-only project, ask the setup skill to save `client: "codex"`.
|
|
230
|
+
The skill runs `setup --client codex` through the versioned launcher below
|
|
231
|
+
(or `init --client codex` for the graph and Git hook), with compatibility handling
|
|
232
|
+
when the pinned CLI predates that option.
|
|
233
|
+
|
|
234
|
+
Skills use the bundled [host adapter](./plugin/skills/_shared/host-runtime.md) to
|
|
235
|
+
translate Claude tool examples into the available Codex tools. They run CLI
|
|
236
|
+
checks through the [versioned launcher](./plugin/skills/_shared/cli-launcher.md),
|
|
237
|
+
so no global `gestalt` install is needed. The first CLI call may download the
|
|
238
|
+
plugin's exact npm version and needs Node.js >=22 and npm on PATH.
|
|
239
|
+
|
|
240
|
+
If startup fails, check MCP status and Node/npm availability. If the recorded
|
|
241
|
+
host or model is unexpected, inspect `ges_status` fields `hostClient`, `client`,
|
|
242
|
+
and `tierModels` before changing project settings. Claude Task panels and hooks
|
|
243
|
+
are optional host-specific features; they do not gate Codex execution.
|
|
244
|
+
|
|
224
245
|
---
|
|
225
246
|
|
|
226
247
|
### Option 5: OpenAI Codex CLI (MCP only)
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tienne/gestalt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.121.0",
|
|
4
4
|
"description": "TypeScript AI Development Harness - Gestalt psychology-driven requirement clarification",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "tsx bin/gestalt.ts",
|
|
31
31
|
"build": "tsc -p tsconfig.build.json",
|
|
32
|
-
"postbuild": "rm -rf dist/plugin && mkdir -p dist/plugin && cp -r plugin/agents plugin/role-agents plugin/review-agents plugin/personas plugin/research-sources plugin/skills dist/plugin/ && cp -r schemas dist/ && cp package.json dist/ && chmod +x dist/bin/gestalt.js dist/bin/gestalt-hook.js && pnpm run verify:plugin && pnpm run verify:rules && pnpm run verify:rule-overlap && pnpm run verify:output-style && pnpm run verify:routing && pnpm run verify:agents",
|
|
32
|
+
"postbuild": "rm -rf dist/plugin && mkdir -p dist/plugin && cp -r plugin/agents plugin/role-agents plugin/review-agents plugin/personas plugin/research-sources plugin/skills plugin/scripts plugin/.codex-plugin dist/plugin/ && cp -r schemas dist/ && cp package.json dist/ && chmod +x dist/bin/gestalt.js dist/bin/gestalt-hook.js && pnpm run verify:plugin && pnpm run verify:rules && pnpm run verify:rule-overlap && pnpm run verify:output-style && pnpm run verify:routing && pnpm run verify:agents",
|
|
33
33
|
"verify:plugin": "tsx scripts/verify-plugin-assets.ts",
|
|
34
34
|
"prepublishOnly": "pnpm build",
|
|
35
35
|
"test": "vitest run",
|
|
36
36
|
"test:watch": "vitest",
|
|
37
|
+
"test:plugin-install": "vitest run --config vitest.plugin-install.config.ts",
|
|
37
38
|
"serve": "tsx bin/gestalt.ts serve",
|
|
38
39
|
"bench": "tsx benchmarks/run.ts",
|
|
39
40
|
"resolution-bench": "tsx benchmarks/run-resolution-benchmark.ts",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"postversion": "pnpm run version:sync && pnpm run build:output-style",
|
|
48
49
|
"verify:rules": "tsx scripts/verify-rule-refs.ts",
|
|
49
50
|
"verify:rule-overlap": "tsx scripts/verify-rule-overlap.ts",
|
|
50
|
-
"gate": "pnpm typecheck && pnpm verify:rules && pnpm verify:rule-overlap && pnpm verify:routing && pnpm verify:agents && pnpm verify:skill-confusion && pnpm lint && pnpm format:check && pnpm build && pnpm test",
|
|
51
|
+
"gate": "pnpm typecheck && pnpm verify:rules && pnpm verify:rule-overlap && pnpm verify:routing && pnpm verify:agents && pnpm verify:skill-confusion && pnpm lint && pnpm format:check && pnpm build && pnpm test && pnpm test:plugin-install",
|
|
51
52
|
"humanize:baseline": "tsx scripts/humanize-baseline.ts",
|
|
52
53
|
"build:output-style": "tsx scripts/build-output-style.ts",
|
|
53
54
|
"verify:output-style": "tsx scripts/build-output-style.ts --dry-run",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gestalt",
|
|
3
|
+
"version": "0.121.0",
|
|
4
|
+
"description": "Gestalt psychology-driven AI development harness. Transforms scattered requirements into structured, validated specifications through interactive interviews.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "tienne"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/tienne/gestalt",
|
|
9
|
+
"repository": "https://github.com/tienne/gestalt",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"gestalt",
|
|
13
|
+
"requirements",
|
|
14
|
+
"specification",
|
|
15
|
+
"interview",
|
|
16
|
+
"mcp",
|
|
17
|
+
"ai-development",
|
|
18
|
+
"code-review"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"mcpServers": "./mcp.json",
|
|
22
|
+
"interface": {
|
|
23
|
+
"displayName": "Gestalt",
|
|
24
|
+
"shortDescription": "Requirements-to-spec pipeline with review and PR workflows",
|
|
25
|
+
"longDescription": "Gestalt turns vague ideas into validated specifications through Gestalt-principle interviews, then drives execution, code review, and PR authoring. Bundles workflow skills, specialist agents, and MCP tools.",
|
|
26
|
+
"developerName": "tienne",
|
|
27
|
+
"category": "Developer Tools",
|
|
28
|
+
"capabilities": [
|
|
29
|
+
"Interactive",
|
|
30
|
+
"Read",
|
|
31
|
+
"Write"
|
|
32
|
+
],
|
|
33
|
+
"websiteURL": "https://github.com/tienne/gestalt",
|
|
34
|
+
"defaultPrompt": [
|
|
35
|
+
"Review this PR with the Gestalt review pipeline.",
|
|
36
|
+
"Clarify these requirements into a structured spec.",
|
|
37
|
+
"Write a PR description for the current branch."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -53,17 +53,19 @@ git diff --word-diff=porcelain -- <파일>
|
|
|
53
53
|
|
|
54
54
|
한 조각이 룰에 걸리는지 보려면 그 조각이 들어간 **문장 전체**를 읽어야 합니다. 조각만 떼어 보면 문장 구조로 걸리는 룰(A-18 좌향 수식, C-11 연결어미 뒤 쉼표, E-2 종결어미 반복)을 판정할 수 없습니다. 판정은 문장으로 하되 이슈로 올릴지는 그 룰에 걸린 부분이 바뀐 조각 안에 있는지로 정합니다.
|
|
55
55
|
|
|
56
|
+
아래 런처 경로는 이 문서 디렉토리 기준입니다. 실행 전에 절대 경로로 풉니다. 작업 레포의 동명 파일은 사용하지 않습니다.
|
|
57
|
+
|
|
56
58
|
**정적 탐지를 먼저 돌립니다.** 정규식으로 잡히는 룰은 사람이 눈으로 세는 것보다 정확합니다.
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
|
-
gestalt humanize-check --before <파일> --after <파일> --register doc --json
|
|
61
|
+
node "../../scripts/gestalt-cli.mjs" humanize-check --before <파일> --after <파일> --register doc --json
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
같은 파일을 `--before`와 `--after`에 둘 다 넘기면 윤문 전후 비교가 아니라 그 파일에 남아 있는 S1 패턴을 세어 줍니다. 잡히는 룰은 21개뿐이니 나머지는 읽고 판정합니다.
|
|
63
65
|
|
|
64
66
|
`--register`는 대상 문서의 성격에 맞춰 고릅니다. 문서는 `doc`, 리뷰 코멘트나 대화체는 `chat`, 보고서는 `report`입니다. `chat`에서는 몇몇 S2가 S1으로 올라갑니다. 사람은 대화에서 개념을 명사 덩어리로 뭉치지 않기 때문입니다.
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
런처를 못 찾거나 버전 해석이 실패하면 그 단계를 건너뛰고 직접 읽어 판정합니다. 건너뛴 사실을 보고합니다.
|
|
67
69
|
|
|
68
70
|
## 어휘 선택을 볼 때
|
|
69
71
|
|
|
@@ -214,9 +214,10 @@
|
|
|
214
214
|
## 코드 검사 (자가 채점보다 위)
|
|
215
215
|
|
|
216
216
|
아래 등급은 모델이 스스로 매기는 참고값이다. 실제 판단은 코드가 한다.
|
|
217
|
+
런처 경로는 이 문서 디렉토리 기준이다. 실행 전에 절대 경로로 풀고 작업 레포의 동명 파일을 사용하지 않는다.
|
|
217
218
|
|
|
218
219
|
```bash
|
|
219
|
-
gestalt humanize-check --before before.md --after after.md --register chat
|
|
220
|
+
node "../../../scripts/gestalt-cli.mjs" humanize-check --before before.md --after after.md --register chat
|
|
220
221
|
```
|
|
221
222
|
|
|
222
223
|
변경률, S1 잔존, 보호 토큰 생존, 구조 보존 4가지 측면에서 각각 측정하고 exit code로 답한다
|
|
@@ -74,11 +74,14 @@ S2
|
|
|
74
74
|
|
|
75
75
|
### 0단계 — 스캔 (룰을 좁힌다)
|
|
76
76
|
|
|
77
|
+
아래 런처 경로는 이 문서 디렉토리 기준이다. 실행 전에 절대 경로로 풀고 작업 레포의 동명 파일을 사용하지 않는다.
|
|
78
|
+
런처를 못 찾거나 버전 해석이 실패하면 스캔과 코드 검사를 건너뛰고 직접 읽어 판정한다. 건너뛴 사실을 보고한다.
|
|
79
|
+
|
|
77
80
|
원문이 파일로 있으면 먼저 스캔을 돌린다. 말투는 산문이 `doc`, 리뷰 코멘트와 슬랙이 `chat`,
|
|
78
81
|
보고서가 `report`다.
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
|
-
gestalt humanize-scan --file <원문> --register <doc|chat|report>
|
|
84
|
+
node "../../scripts/gestalt-cli.mjs" humanize-scan --file <원문> --register <doc|chat|report>
|
|
82
85
|
```
|
|
83
86
|
|
|
84
87
|
종료 코드로도 답한다. 걸린 게 있으면 0, 어투는 깨끗한데 맞춤법만 걸리면 11, 탐지기가 가리는 범위에서 0건이면 10이다. 11이면 어투는 그대로 두고 맞춤법 절만 고쳐서 낸다.
|
|
@@ -202,7 +205,7 @@ summary에 적는다. 댈 게 없으면 그건 안 고친 것이라 원문을
|
|
|
202
205
|
위 3단계 자가검증은 스스로 매기는 참고값이다. 원문과 윤문본이 파일로 있으면 판단은 코드에 맡긴다.
|
|
203
206
|
|
|
204
207
|
```bash
|
|
205
|
-
gestalt humanize-check --before <원문> --after <윤문본> --register chat --attempt 1
|
|
208
|
+
node "../../scripts/gestalt-cli.mjs" humanize-check --before <원문> --after <윤문본> --register chat --attempt 1
|
|
206
209
|
```
|
|
207
210
|
|
|
208
211
|
변경률, S1 제거율, 보호 토큰 생존, 구조 보존, 신규 유입을 각각 측정하고 exit code로 답한다
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { setTimeout, clearTimeout } from 'node:timers';
|
|
5
|
+
|
|
6
|
+
// One cancellation scope spans npm resolution, version verification and cwd cleanup.
|
|
7
|
+
// A child's own signal/timeout is a failed attempt, never a user's cancellation.
|
|
8
|
+
export function createAcquisition({ cwd, env }) {
|
|
9
|
+
let current;
|
|
10
|
+
let signal;
|
|
11
|
+
const cancel = (received) => {
|
|
12
|
+
signal = received;
|
|
13
|
+
current?.cancel(received);
|
|
14
|
+
};
|
|
15
|
+
const terminate = () => cancel('SIGTERM');
|
|
16
|
+
const hangup = () => cancel('SIGHUP');
|
|
17
|
+
const interrupt = () => cancel('SIGINT');
|
|
18
|
+
process.on('SIGTERM', terminate);
|
|
19
|
+
process.on('SIGHUP', hangup);
|
|
20
|
+
process.on('SIGINT', interrupt);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
get signal() {
|
|
24
|
+
return signal;
|
|
25
|
+
},
|
|
26
|
+
run(command, args, timeoutMs) {
|
|
27
|
+
return new Promise((resolveResult) => {
|
|
28
|
+
const child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
29
|
+
let stdout = '';
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
let failed = false;
|
|
32
|
+
let interruptTimer;
|
|
33
|
+
let killTimer;
|
|
34
|
+
let drainTimer;
|
|
35
|
+
const stop = (received) => {
|
|
36
|
+
clearTimeout(interruptTimer);
|
|
37
|
+
child.kill(received);
|
|
38
|
+
// Give npm a chance to forward cancellation first. KILL targets only this
|
|
39
|
+
// direct child; descendants can survive if npm cannot relay the signal.
|
|
40
|
+
killTimer ??= setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
41
|
+
};
|
|
42
|
+
const active = {
|
|
43
|
+
cancel(received) {
|
|
44
|
+
if (received === 'SIGINT') {
|
|
45
|
+
// Do not duplicate group INT. Any surviving child gets TERM after grace.
|
|
46
|
+
interruptTimer ??= setTimeout(() => stop('SIGTERM'), 2000);
|
|
47
|
+
} else stop(received);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
current = active;
|
|
51
|
+
const deadline = setTimeout(() => {
|
|
52
|
+
failed = true;
|
|
53
|
+
stop('SIGTERM');
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
child.stdout.setEncoding('utf8');
|
|
56
|
+
child.stdout.on('data', (chunk) => {
|
|
57
|
+
bytes += Buffer.byteLength(chunk);
|
|
58
|
+
if (bytes > 64 * 1024) {
|
|
59
|
+
failed = true;
|
|
60
|
+
stop('SIGTERM');
|
|
61
|
+
} else stdout += chunk;
|
|
62
|
+
});
|
|
63
|
+
child.stderr.resume();
|
|
64
|
+
child.once('error', () => {
|
|
65
|
+
failed = true;
|
|
66
|
+
});
|
|
67
|
+
child.once('exit', () => {
|
|
68
|
+
clearTimeout(deadline);
|
|
69
|
+
clearTimeout(interruptTimer);
|
|
70
|
+
clearTimeout(killTimer);
|
|
71
|
+
if (current === active) current = undefined;
|
|
72
|
+
// Descendants can inherit both pipes after npm exits. Drain briefly, then
|
|
73
|
+
// release both handles so they cannot keep resolution open indefinitely.
|
|
74
|
+
drainTimer = setTimeout(() => {
|
|
75
|
+
child.stdout.destroy();
|
|
76
|
+
child.stderr.destroy();
|
|
77
|
+
}, 1000);
|
|
78
|
+
});
|
|
79
|
+
child.once('close', (status) => {
|
|
80
|
+
clearTimeout(deadline);
|
|
81
|
+
clearTimeout(interruptTimer);
|
|
82
|
+
clearTimeout(killTimer);
|
|
83
|
+
clearTimeout(drainTimer);
|
|
84
|
+
if (current === active) current = undefined;
|
|
85
|
+
resolveResult({ status: failed ? null : status, stdout });
|
|
86
|
+
});
|
|
87
|
+
if (signal) active.cancel(signal);
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
dispose() {
|
|
91
|
+
process.off('SIGTERM', terminate);
|
|
92
|
+
process.off('SIGHUP', hangup);
|
|
93
|
+
process.off('SIGINT', interrupt);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { ownsPackage } from './package-owner.mjs';
|
|
15
|
+
import { createAcquisition } from './acquisition.mjs';
|
|
16
|
+
import { npmEnvironment } from './npm-environment.mjs';
|
|
17
|
+
|
|
18
|
+
const scripts = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pluginRoot = resolve(scripts, '..');
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const fail = (message) => {
|
|
22
|
+
process.stderr.write(`gestalt CLI: ${message}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let version;
|
|
27
|
+
try {
|
|
28
|
+
version = JSON.parse(readFileSync(join(pluginRoot, '.codex-plugin/plugin.json'), 'utf8')).version;
|
|
29
|
+
if (
|
|
30
|
+
typeof version !== 'string' ||
|
|
31
|
+
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
|
|
32
|
+
) {
|
|
33
|
+
throw new Error('invalid version');
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
fail('cannot read the bundled plugin version; reinstall the plugin.');
|
|
37
|
+
}
|
|
38
|
+
if (Number(process.versions.node.split('.')[0]) < 22) fail('Node.js >=22 is required.');
|
|
39
|
+
|
|
40
|
+
// A development checkout is identified by the wrapper's own location, never cwd.
|
|
41
|
+
// Without tsx, use a matching compiled entry if available, then the exact npm pin.
|
|
42
|
+
const checkout = resolve(pluginRoot, '..');
|
|
43
|
+
let localArgs;
|
|
44
|
+
let sourceTsconfig;
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(readFileSync(join(checkout, 'package.json'), 'utf8'));
|
|
47
|
+
const entry = join(checkout, 'bin/gestalt.ts');
|
|
48
|
+
if (pkg.name === '@tienne/gestalt' && pkg.version === version && existsSync(entry)) {
|
|
49
|
+
const tsx = join(checkout, 'node_modules/tsx/dist/loader.mjs');
|
|
50
|
+
// Keep the target project's tsconfig paths out of the source loader.
|
|
51
|
+
if (existsSync(tsx)) {
|
|
52
|
+
localArgs = ['--import', tsx, entry];
|
|
53
|
+
sourceTsconfig = join(checkout, 'tsconfig.json');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!localArgs && pkg.name === '@tienne/gestalt' && pkg.version === version) {
|
|
57
|
+
const compiled = [join(checkout, 'bin/gestalt.js'), join(checkout, 'dist/bin/gestalt.js')].find(
|
|
58
|
+
existsSync,
|
|
59
|
+
);
|
|
60
|
+
if (compiled) localArgs = [compiled];
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// A copied plugin is deliberately independent of its containing project.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let bin;
|
|
67
|
+
if (!localArgs) {
|
|
68
|
+
const neutral = mkdtempSync(join(tmpdir(), 'gestalt-cli-'));
|
|
69
|
+
const acquisition = createAcquisition({ cwd: neutral, env: npmEnvironment(process.env) });
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(join(neutral, 'package.json'), '{"private":true}\n');
|
|
72
|
+
for (const offline of [true, false]) {
|
|
73
|
+
if (!offline)
|
|
74
|
+
process.stderr.write(`gestalt CLI: resolving @tienne/gestalt@${version} from npm.\n`);
|
|
75
|
+
const result = await acquisition.run(
|
|
76
|
+
'npm',
|
|
77
|
+
[
|
|
78
|
+
'exec',
|
|
79
|
+
'--yes',
|
|
80
|
+
`--prefix=${neutral}`,
|
|
81
|
+
'--no-workspaces',
|
|
82
|
+
...(offline ? ['--offline'] : []),
|
|
83
|
+
`--package=@tienne/gestalt@${version}`,
|
|
84
|
+
'--',
|
|
85
|
+
process.execPath,
|
|
86
|
+
join(scripts, 'resolve-cli.mjs'),
|
|
87
|
+
version,
|
|
88
|
+
],
|
|
89
|
+
180_000,
|
|
90
|
+
);
|
|
91
|
+
if (acquisition.signal) break;
|
|
92
|
+
if (result.status !== 0) continue;
|
|
93
|
+
try {
|
|
94
|
+
const candidate = JSON.parse(result.stdout.trim());
|
|
95
|
+
if (typeof candidate !== 'string') continue;
|
|
96
|
+
bin = realpathSync(candidate);
|
|
97
|
+
if (!ownsPackage(bin, version)) {
|
|
98
|
+
bin = undefined;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Do not trust the resolver process or a stale global executable.
|
|
102
|
+
const check = await acquisition.run(process.execPath, [bin, '--version'], 10_000);
|
|
103
|
+
if (acquisition.signal) break;
|
|
104
|
+
if (check.status === 0 && check.stdout.trim() === version) break;
|
|
105
|
+
bin = undefined;
|
|
106
|
+
} catch {
|
|
107
|
+
bin = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
try {
|
|
112
|
+
rmSync(neutral, { recursive: true, force: true });
|
|
113
|
+
} finally {
|
|
114
|
+
acquisition.dispose();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (acquisition.signal) process.kill(process.pid, acquisition.signal);
|
|
118
|
+
if (!bin)
|
|
119
|
+
fail(
|
|
120
|
+
`could not resolve @tienne/gestalt@${version}; check npm, network access, and its native dependencies. No other version was executed.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const child = spawn(process.execPath, [...(localArgs ?? [bin]), ...args], {
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
stdio: 'inherit',
|
|
127
|
+
...(sourceTsconfig ? { env: { ...process.env, TSX_TSCONFIG_PATH: sourceTsconfig } } : {}),
|
|
128
|
+
});
|
|
129
|
+
const terminate = () => child.kill('SIGTERM');
|
|
130
|
+
const hangup = () => child.kill('SIGHUP');
|
|
131
|
+
// Group Ctrl-C reaches the child: do not forward it twice. Programmatic callers
|
|
132
|
+
// must use SIGTERM to cancel only this pid. Signal tests cover both delivery modes.
|
|
133
|
+
const interrupt = () => {};
|
|
134
|
+
process.on('SIGTERM', terminate);
|
|
135
|
+
process.on('SIGHUP', hangup);
|
|
136
|
+
process.on('SIGINT', interrupt);
|
|
137
|
+
child.once('error', (error) => fail(error.message));
|
|
138
|
+
child.once('exit', (code, signal) => {
|
|
139
|
+
process.off('SIGTERM', terminate);
|
|
140
|
+
process.off('SIGHUP', hangup);
|
|
141
|
+
process.off('SIGINT', interrupt);
|
|
142
|
+
if (signal) process.kill(process.pid, signal);
|
|
143
|
+
else process.exit(code ?? 1);
|
|
144
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Registry settings may deliberately use a provider token. Preserve referenced
|
|
7
|
+
// names before filtering known credentials; native-build and network settings pass through.
|
|
8
|
+
export function npmEnvironment(source) {
|
|
9
|
+
const env = { ...source };
|
|
10
|
+
const find = (name) => Object.keys(env).find((key) => key.toLowerCase() === name);
|
|
11
|
+
const get = (name) => {
|
|
12
|
+
const key = find(name);
|
|
13
|
+
return key === undefined ? undefined : env[key];
|
|
14
|
+
};
|
|
15
|
+
const referenced = new Set();
|
|
16
|
+
// Match npm's optional references and escaped dollar signs in configuration.
|
|
17
|
+
const envExpr = /(?<!\\)(\\*)\$\{([^${}?]+)(\?)?\}/g;
|
|
18
|
+
const collect = (text) => {
|
|
19
|
+
for (const match of text.matchAll(envExpr)) {
|
|
20
|
+
if (match[1].length % 2 === 0) referenced.add(match[2].toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const expand = (path) =>
|
|
24
|
+
path.replace(envExpr, (whole, escapes, key, optional) => {
|
|
25
|
+
if (escapes.length % 2) return whole.slice((escapes.length + 1) / 2);
|
|
26
|
+
const value = env[key] ?? (optional ? '' : '${' + key + '}');
|
|
27
|
+
return escapes.slice(escapes.length / 2) + value;
|
|
28
|
+
});
|
|
29
|
+
const home = get('home') ?? get('userprofile') ?? homedir();
|
|
30
|
+
const files = [
|
|
31
|
+
get('npm_config_userconfig') ?? join(home, '.npmrc'),
|
|
32
|
+
get('npm_config_globalconfig') ??
|
|
33
|
+
join(get('npm_config_prefix') ?? dirname(dirname(process.execPath)), 'etc/npmrc'),
|
|
34
|
+
];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const text = readFileSync(expand(file).replace(/^~(?=[/\\])/, home), 'utf8');
|
|
38
|
+
collect(
|
|
39
|
+
text
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter((line) => !/^\s*[#;]/.test(line))
|
|
42
|
+
.join('\n'),
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
// Missing/unreadable npm settings are handled by npm itself.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const [key, value] of Object.entries(env)) {
|
|
49
|
+
if (/^npm_config_/i.test(key)) collect(value);
|
|
50
|
+
}
|
|
51
|
+
for (const key of Object.keys(env)) {
|
|
52
|
+
if (/^npm_|^node_auth_token$/i.test(key) || referenced.has(key.toLowerCase())) continue;
|
|
53
|
+
if (
|
|
54
|
+
/^(ANTHROPIC_|OPENAI_|CLAUDE_CODE_OAUTH_|GEMINI_|CODEX_|XAI_|GROK_|GOOGLE_API_KEY$|AWS_ACCESS_KEY_ID$|AWS_SECRET_ACCESS_KEY$|AWS_SESSION_TOKEN$|GH_TOKEN$|GITHUB_TOKEN$)/i.test(
|
|
55
|
+
key,
|
|
56
|
+
) ||
|
|
57
|
+
/(_API_KEY|_SECRET|_SECRET_KEY|_CLIENT_SECRET|_PASSWORD|_ACCESS_TOKEN|_AUTH_TOKEN|_BOT_TOKEN)$|^HF_TOKEN$/i.test(
|
|
58
|
+
key,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
delete env[key];
|
|
62
|
+
}
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The nearest manifest owns the bin; an unrelated parent must not vouch for it.
|
|
5
|
+
export function ownsPackage(bin, version) {
|
|
6
|
+
try {
|
|
7
|
+
let directory = dirname(bin);
|
|
8
|
+
while (directory !== dirname(directory)) {
|
|
9
|
+
const manifest = join(directory, 'package.json');
|
|
10
|
+
if (existsSync(manifest)) {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
12
|
+
return pkg.name === '@tienne/gestalt' && pkg.version === version;
|
|
13
|
+
}
|
|
14
|
+
directory = dirname(directory);
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { ownsPackage } from './package-owner.mjs';
|
|
5
|
+
|
|
6
|
+
// npm exec prepends its package bin directory. Verify the owning package instead
|
|
7
|
+
// of accepting a same-named executable inherited from the user's PATH.
|
|
8
|
+
const expected = process.argv[2];
|
|
9
|
+
for (const entry of (process.env.PATH ?? '').split(delimiter)) {
|
|
10
|
+
if (!entry) continue;
|
|
11
|
+
const candidate = join(entry, 'gestalt');
|
|
12
|
+
try {
|
|
13
|
+
const bin = realpathSync(candidate);
|
|
14
|
+
if (ownsPackage(bin, expected)) {
|
|
15
|
+
process.stdout.write(JSON.stringify(bin));
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Broken PATH entries and unrelated packages do not resolve this pin.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
process.exitCode = 1;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 에이전트를 서브에이전트로 위임하기 (공유 규칙)
|
|
2
2
|
|
|
3
|
+
> **Codex 등 다른 호스트에서는** [`host-runtime.md`](./host-runtime.md)를 먼저 적용한다. `inherit`는 모델 인자 생략이며 Claude 별칭 폴백을 강제하지 않는다.
|
|
4
|
+
|
|
3
5
|
> **현재 적용: `review`, `pr`, `brief`, `presentation`, `review-reply`, `slack-send`, `jira-create`, `ship`, `explain`, `research`, `ux-research`, `reverse-plan`.** 에이전트를 불러 쓰는 스킬은 전부 위임한다. 새 스킬이 `ges_agent get`을 메인에서 하면 그건 예외가 아니라 빠뜨린 것이다.
|
|
4
6
|
|
|
5
7
|
## 왜 필요한가
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 에이전트 tier로 모델 고르기 (공유 규칙)
|
|
2
2
|
|
|
3
|
+
> **Codex 등 다른 호스트에서는** [`host-runtime.md`](./host-runtime.md)를 먼저 적용한다. `inherit`는 모델 인자 생략이며 Claude 별칭 폴백을 강제하지 않는다.
|
|
4
|
+
|
|
3
5
|
`ges_agent`의 `get`과 `list`는 둘 다 `tier`와 **해석된 `model`** 을 돌려준다. 에이전트 frontmatter의
|
|
4
6
|
tier가 "이 역할이 어느 정도 모델을 필요로 하나"를 선언하고 서버가 그걸 호스트 Agent 도구가 받는
|
|
5
7
|
별칭으로 옮겨준 값이다. 둘의 차이는 `systemPrompt`가 딸려오느냐뿐이다. `get`은 한 에이전트의
|