@ps-neko/nekowork 0.1.0-alpha.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/AGENTS.md +112 -0
- package/CLAUDE.md +81 -0
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/REVIEW.md +96 -0
- package/RULES.md +51 -0
- package/SOUL.md +21 -0
- package/WORKING-CONTEXT.md +52 -0
- package/agent.yaml +219 -0
- package/agents/architect.md +57 -0
- package/agents/code-reviewer.md +60 -0
- package/agents/codex-challenger.md +53 -0
- package/agents/codex-reviewer.md +56 -0
- package/agents/debugger.md +33 -0
- package/agents/doc-writer.md +51 -0
- package/agents/executor.md +41 -0
- package/agents/planner.md +49 -0
- package/agents/research.md +50 -0
- package/agents/security-reviewer.md +47 -0
- package/agents/test-engineer.md +41 -0
- package/bridge/mcp-server.js +301 -0
- package/commands/claude-led-codex-review.md +29 -0
- package/docs/ADVANCED.md +321 -0
- package/docs/AI-DEVELOPMENT-LIFECYCLE.md +105 -0
- package/docs/ARCHITECTURE.md +205 -0
- package/docs/AUDIT.md +114 -0
- package/docs/AUTH-MIGRATION.md +282 -0
- package/docs/CHANGELOG.md +97 -0
- package/docs/CLI-STAGES.md +89 -0
- package/docs/CODEMAPS/README.md +15 -0
- package/docs/CODEMAPS/agents.md +22 -0
- package/docs/CODEMAPS/bridge.md +18 -0
- package/docs/CODEMAPS/hooks.md +28 -0
- package/docs/CODEMAPS/manifests.md +14 -0
- package/docs/CODEMAPS/rules.md +22 -0
- package/docs/CODEMAPS/schemas.md +21 -0
- package/docs/CODEMAPS/scripts.md +158 -0
- package/docs/CODEMAPS/skills.md +29 -0
- package/docs/CODEMAPS/tests.md +98 -0
- package/docs/CORE-INVARIANTS.md +38 -0
- package/docs/DEMO.md +110 -0
- package/docs/EXAMPLE-PROJECT.md +92 -0
- package/docs/PORTING.md +154 -0
- package/docs/PRODUCT-PRINCIPLES.md +303 -0
- package/docs/PUBLISH-ALPHA.md +106 -0
- package/docs/QUICKSTART.md +344 -0
- package/docs/RELEASE-READINESS.md +140 -0
- package/docs/RISK-CLASSIFIER.md +50 -0
- package/docs/RUNBOOK.md +146 -0
- package/docs/SECURITY.md +79 -0
- package/docs/SETUP.md +142 -0
- package/docs/WHY-NEKOWORK.md +64 -0
- package/docs/case-studies/README.md +16 -0
- package/docs/case-studies/SINDRESORHUS-IS-PLAIN-OBJ.md +141 -0
- package/docs/dev-log/2026-04-29-p1-recovery.md +142 -0
- package/docs/dev-log/2026-04-29-week1-4.md +81 -0
- package/docs/examples/GITHUB-ACTIONS-HARDENING.md +86 -0
- package/docs/examples/QUALITY-LIFECYCLE-SMOKE.md +32 -0
- package/docs/examples/TRADING-DASHBOARD-MOCK.md +65 -0
- package/docs/workflows-stash/README.md +32 -0
- package/docs/workflows-stash/harness-review.yml +166 -0
- package/docs/workflows-stash/harness-validate.yml +48 -0
- package/examples/github-actions-hardening/.github/workflows/hardened-validate.yml +38 -0
- package/examples/github-actions-hardening/README.md +31 -0
- package/examples/github-actions-hardening/case-study/ASK.md +26 -0
- package/examples/github-actions-hardening/case-study/GATE_STATUS.md +28 -0
- package/examples/github-actions-hardening/case-study/PLAN.md +25 -0
- package/examples/github-actions-hardening/case-study/SHIP_READY.md +21 -0
- package/examples/github-actions-hardening/case-study/TASK.md +30 -0
- package/examples/github-actions-hardening/case-study/TEAM_HANDOFFS.md +37 -0
- package/examples/github-actions-hardening/case-study/VERIFY_SUMMARY.md +35 -0
- package/examples/github-actions-hardening/case-study/WORK_SUMMARY.md +24 -0
- package/examples/github-actions-hardening/package.json +12 -0
- package/examples/github-actions-hardening/scripts/check.mjs +43 -0
- package/examples/quality-lifecycle-smoke/README.md +30 -0
- package/examples/quality-lifecycle-smoke/case-study/ASK.md +24 -0
- package/examples/quality-lifecycle-smoke/case-study/GATE_STATUS.md +10 -0
- package/examples/quality-lifecycle-smoke/case-study/PLAN.md +19 -0
- package/examples/quality-lifecycle-smoke/case-study/SHIP_READY.md +11 -0
- package/examples/quality-lifecycle-smoke/case-study/TASK.md +19 -0
- package/examples/quality-lifecycle-smoke/case-study/TEAM_HANDOFFS.md +21 -0
- package/examples/quality-lifecycle-smoke/case-study/VERIFY_SUMMARY.md +44 -0
- package/examples/quality-lifecycle-smoke/case-study/WORK_SUMMARY.md +19 -0
- package/examples/quality-lifecycle-smoke/package.json +8 -0
- package/examples/quality-lifecycle-smoke/scripts/check.mjs +44 -0
- package/examples/trading-dashboard-mock/README.md +33 -0
- package/examples/trading-dashboard-mock/case-study/ASK.md +24 -0
- package/examples/trading-dashboard-mock/case-study/GATE_STATUS.md +28 -0
- package/examples/trading-dashboard-mock/case-study/PLAN.md +23 -0
- package/examples/trading-dashboard-mock/case-study/SHIP_READY.md +21 -0
- package/examples/trading-dashboard-mock/case-study/TASK.md +29 -0
- package/examples/trading-dashboard-mock/case-study/TEAM_HANDOFFS.md +49 -0
- package/examples/trading-dashboard-mock/case-study/VERIFY_SUMMARY.md +35 -0
- package/examples/trading-dashboard-mock/case-study/WORK_SUMMARY.md +27 -0
- package/examples/trading-dashboard-mock/fixtures/market.json +9 -0
- package/examples/trading-dashboard-mock/index.html +76 -0
- package/examples/trading-dashboard-mock/package.json +9 -0
- package/examples/trading-dashboard-mock/scripts/check.mjs +54 -0
- package/examples/trading-dashboard-mock/src/app.js +83 -0
- package/examples/trading-dashboard-mock/src/styles.css +227 -0
- package/hooks/hooks.json +44 -0
- package/hooks/scripts/config-protection.js +34 -0
- package/hooks/scripts/gateguard-fact-force.js +146 -0
- package/hooks/scripts/persistent-mode.mjs +27 -0
- package/hooks/scripts/pre-bash-dispatcher.js +63 -0
- package/hooks/scripts/quality-gate.js +106 -0
- package/manifests/install-components.json +195 -0
- package/manifests/install-modules.json +101 -0
- package/manifests/install-profiles.json +134 -0
- package/package.json +96 -0
- package/rules/common/coding-style.md +71 -0
- package/rules/common/security.md +69 -0
- package/rules/common/testing.md +58 -0
- package/rules/python/coding-style.md +80 -0
- package/rules/python/testing.md +86 -0
- package/rules/typescript/coding-style.md +97 -0
- package/rules/typescript/security.md +67 -0
- package/rules/typescript/testing.md +78 -0
- package/schemas/agent-yaml.schema.json +168 -0
- package/schemas/agent.schema.json +32 -0
- package/schemas/handoff.schema.json +105 -0
- package/schemas/hooks.schema.json +35 -0
- package/schemas/install-components.schema.json +46 -0
- package/schemas/install-modules.schema.json +39 -0
- package/schemas/install-profiles.schema.json +32 -0
- package/schemas/install-state.schema.json +42 -0
- package/schemas/routing.schema.json +42 -0
- package/schemas/skill.schema.json +19 -0
- package/scripts/agents/dispatch.js +144 -0
- package/scripts/agents/runners/claude.js +214 -0
- package/scripts/agents/runners/codex.js +233 -0
- package/scripts/agents/runners/gemini.js +92 -0
- package/scripts/agents/runners/mock.js +107 -0
- package/scripts/auth/github-import-gh.js +52 -0
- package/scripts/auth/github-login.js +79 -0
- package/scripts/auth/github-logout.js +21 -0
- package/scripts/auth/github-status.js +46 -0
- package/scripts/build-claude.js +101 -0
- package/scripts/build-codemaps.js +286 -0
- package/scripts/build-codex.js +93 -0
- package/scripts/build-cursor.js +132 -0
- package/scripts/build-gemini.js +117 -0
- package/scripts/build-opencode.js +117 -0
- package/scripts/ci/catalog.js +120 -0
- package/scripts/ci/check-markers.js +48 -0
- package/scripts/ci/security-hardening.js +270 -0
- package/scripts/ci/validate-agents.js +88 -0
- package/scripts/ci/validate-hooks.js +99 -0
- package/scripts/ci/validate-manifests.js +128 -0
- package/scripts/ci/validate-skills.js +93 -0
- package/scripts/cli.js +1134 -0
- package/scripts/core/auth-guard.js +22 -0
- package/scripts/core/build-roots.js +11 -0
- package/scripts/core/cli-resolver.js +64 -0
- package/scripts/core/execution-workspace.js +84 -0
- package/scripts/core/git-mutation-guard.js +79 -0
- package/scripts/core/install-state.js +125 -0
- package/scripts/core/json-extractor.js +32 -0
- package/scripts/core/subprocess.js +74 -0
- package/scripts/daemon/wait.js +278 -0
- package/scripts/demo-external-project.js +222 -0
- package/scripts/demo-quick-run.js +193 -0
- package/scripts/demo-review.js +204 -0
- package/scripts/doctor.js +296 -0
- package/scripts/install-apply.js +185 -0
- package/scripts/install-plan.js +411 -0
- package/scripts/lib/acceptance-criteria.js +105 -0
- package/scripts/lib/costs.js +82 -0
- package/scripts/lib/instincts.js +194 -0
- package/scripts/lib/keychain.js +85 -0
- package/scripts/lib/profile-policy.js +134 -0
- package/scripts/lib/profile-safety.js +81 -0
- package/scripts/lib/risk-classifier.js +145 -0
- package/scripts/lib/router.js +138 -0
- package/scripts/lib/severity.js +99 -0
- package/scripts/lib/token-vault.js +136 -0
- package/scripts/orchestrators/apply.js +225 -0
- package/scripts/orchestrators/ask.js +143 -0
- package/scripts/orchestrators/gate.js +179 -0
- package/scripts/orchestrators/ralph.js +179 -0
- package/scripts/orchestrators/review.js +452 -0
- package/scripts/orchestrators/run.js +151 -0
- package/scripts/orchestrators/ship.js +339 -0
- package/scripts/orchestrators/team-lite.js +270 -0
- package/scripts/orchestrators/team.js +244 -0
- package/scripts/orchestrators/verify.js +306 -0
- package/scripts/orchestrators/work.js +207 -0
- package/scripts/portability/simulate-port.js +220 -0
- package/scripts/repair.js +184 -0
- package/scripts/sync-claude-md.js +220 -0
- package/scripts/verify/claude-live.js +30 -0
- package/scripts/verify/codex-live.js +60 -0
- package/scripts/verify/gemini-live.js +48 -0
- package/scripts/verify/runtime.js +105 -0
- package/skills/claude-led-codex-review/SKILL.md +133 -0
- package/skills/plan-eng-review/SKILL.md +51 -0
- package/skills/porting/SKILL.md +69 -0
- package/skills/ralph/SKILL.md +48 -0
- package/skills/release-readiness/SKILL.md +62 -0
- package/skills/review/SKILL.md +42 -0
- package/skills/security-hardening/SKILL.md +59 -0
- package/skills/ship/SKILL.md +44 -0
- package/skills/tdd-workflow/SKILL.md +42 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// claude-led-codex-review 풀사이클 시뮬레이션 (Week 1 데모).
|
|
3
|
+
// 실제 LLM 호출은 안 함 — 7단계의 핸드오프 파일 / 상태 / round 카운터가 잘 흐르는지만 검증.
|
|
4
|
+
// 사용자 룰("git push 사용자 확인") 우선이라 실 ship 은 안 함.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
const TASK = process.argv[2] || 'JWT 검증 미들웨어 추가';
|
|
15
|
+
const SESSION_ID = process.argv[3] || `demo-${Date.now()}`;
|
|
16
|
+
const SECURE = process.argv.includes('--secure');
|
|
17
|
+
const NO_SHIP = process.argv.includes('--no-ship');
|
|
18
|
+
|
|
19
|
+
const SESSION_DIR = path.join(ROOT, '.harness', 'state', 'sessions', SESSION_ID);
|
|
20
|
+
fs.mkdirSync(path.join(SESSION_DIR, 'handoffs'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
console.log(`\n=== claude-led-codex-review demo ===`);
|
|
23
|
+
console.log(`session : ${SESSION_ID}`);
|
|
24
|
+
console.log(`task : ${TASK}`);
|
|
25
|
+
console.log(`flags : ${SECURE ? '--secure ' : ''}${NO_SHIP ? '--no-ship' : ''}`);
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
function callMcp(tool, args) {
|
|
29
|
+
// 단순 시뮬레이션: 직접 디스크에 쓰기.
|
|
30
|
+
if (tool === 'state_write') {
|
|
31
|
+
const file = path.join(SESSION_DIR, args.key === 'prd' ? 'prd.json' : args.key);
|
|
32
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
|
+
fs.writeFileSync(file, typeof args.value === 'string' ? args.value : JSON.stringify(args.value, null, 2));
|
|
34
|
+
} else if (tool === 'handoff_write') {
|
|
35
|
+
const stageOrder = ['ideate', 'plan', 'implement', 'self-review', 'codex-review', 'codex-challenge', 'ship'];
|
|
36
|
+
const nn = String(stageOrder.indexOf(args.stage) + 1).padStart(2, '0');
|
|
37
|
+
const base = handoffBase(nn, args);
|
|
38
|
+
const md = renderHandoff(args);
|
|
39
|
+
fs.writeFileSync(path.join(SESSION_DIR, 'handoffs', `${base}.md`), md);
|
|
40
|
+
fs.writeFileSync(path.join(SESSION_DIR, 'handoffs', `${base}.json`),
|
|
41
|
+
JSON.stringify({ ...args, timestamp: new Date().toISOString() }, null, 2));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handoffBase(nn, args) {
|
|
46
|
+
const round = Number(args.round || 1);
|
|
47
|
+
return `${nn}-${args.stage}${round > 1 ? `-r${round}` : ''}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderHandoff(a) {
|
|
51
|
+
const L = [];
|
|
52
|
+
L.push(`# Handoff: ${a.stage} (round ${a.round || 1}, agent: ${a.agent})`);
|
|
53
|
+
L.push('');
|
|
54
|
+
L.push(`**Decided**: ${a.decided}`);
|
|
55
|
+
if (a.rejected) L.push(`**Rejected**: ${a.rejected}`);
|
|
56
|
+
if (a.risks) L.push(`**Risks**: ${a.risks}`);
|
|
57
|
+
L.push(`**Files**: ${(a.files || []).join(', ')}`);
|
|
58
|
+
if (a.remaining) L.push(`**Remaining**: ${a.remaining}`);
|
|
59
|
+
if (a.verdict) L.push(`**Verdict**: ${a.verdict}`);
|
|
60
|
+
if (a.issues?.length) {
|
|
61
|
+
L.push(''); L.push('## Issues');
|
|
62
|
+
for (const i of a.issues) L.push(`- [${i.severity}/${i.category}] ${i.file || ''} — ${i.summary}`);
|
|
63
|
+
}
|
|
64
|
+
return L.join('\n') + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function step(n, name, fn) {
|
|
68
|
+
console.log(`[${n}] ${name} …`);
|
|
69
|
+
fn();
|
|
70
|
+
console.log(` ✓ handoff ${path.relative(ROOT, path.join(SESSION_DIR, 'handoffs')).replace(/\\/g, '/')}/...`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- 1. ideate ----
|
|
74
|
+
step(1, 'ideate (research, planner)', () => {
|
|
75
|
+
callMcp('handoff_write', {
|
|
76
|
+
stage: 'ideate', agent: 'planner', round: 1,
|
|
77
|
+
decided: `${TASK} 의 후보 접근 3개 비교 후 표준 lib jose 채택`,
|
|
78
|
+
rejected: 'jsonwebtoken (CVE 이력), 자체 구현 (유지보수 부담)',
|
|
79
|
+
risks: '키 회전 정책 미정',
|
|
80
|
+
files: ['src/auth/jwt.ts (예정)'],
|
|
81
|
+
remaining: 'planner 에 PRD 시드',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- 2. plan ----
|
|
86
|
+
step(2, 'plan (planner)', () => {
|
|
87
|
+
callMcp('state_write', { key: 'prd', value: {
|
|
88
|
+
task: TASK,
|
|
89
|
+
acceptance: [
|
|
90
|
+
{ id: 'AC-001', desc: 'verifyJwt 가 유효한 토큰을 통과시킨다', passes: false },
|
|
91
|
+
{ id: 'AC-002', desc: 'verifyJwt 가 만료 토큰을 거절한다', passes: false },
|
|
92
|
+
{ id: 'AC-003', desc: 'verifyJwt 가 잘못된 서명을 거절한다', passes: false },
|
|
93
|
+
],
|
|
94
|
+
non_goals: ['키 회전', '리프레시 토큰'],
|
|
95
|
+
}});
|
|
96
|
+
callMcp('handoff_write', {
|
|
97
|
+
stage: 'plan', agent: 'planner', round: 1,
|
|
98
|
+
decided: 'AC 3개로 분해. jose 라이브러리. 단계 4 에서 security-reviewer 추가 강제 (auth 영역)',
|
|
99
|
+
files: ['prd.json'], remaining: 'executor 에 핸드오프',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ---- 3. implement ----
|
|
104
|
+
step(3, 'implement (executor + test-engineer, TDD)', () => {
|
|
105
|
+
callMcp('handoff_write', {
|
|
106
|
+
stage: 'implement', agent: 'executor', round: 1,
|
|
107
|
+
decided: 'TDD 3 사이클. AC-001/002/003 모두 GREEN. quality-gate 통과.',
|
|
108
|
+
files: ['src/auth/jwt.ts', 'tests/unit/jwt.test.ts'],
|
|
109
|
+
remaining: 'self-review',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---- 4. self-review ----
|
|
114
|
+
let round = 1;
|
|
115
|
+
let block = false;
|
|
116
|
+
step(4, `self-review (code-reviewer, opus, ro) round ${round}`, () => {
|
|
117
|
+
const issues = [
|
|
118
|
+
{ severity: 'high', category: 'security', file: 'src/auth/jwt.ts', line: 23,
|
|
119
|
+
summary: 'iat / nbf 검증 누락', why: '토큰의 발급 시각 검증 없음. 미래 토큰 허용 가능' },
|
|
120
|
+
{ severity: 'medium', category: 'test', file: 'tests/unit/jwt.test.ts',
|
|
121
|
+
summary: 'audience claim 케이스 부족', why: 'aud 미스매치 테스트 없음' },
|
|
122
|
+
];
|
|
123
|
+
callMcp('handoff_write', {
|
|
124
|
+
stage: 'self-review', agent: 'code-reviewer', round,
|
|
125
|
+
decided: 'high 1, medium 1 발견. fix loop 진입.',
|
|
126
|
+
files: ['src/auth/jwt.ts'], remaining: '단계 3a fix-loop',
|
|
127
|
+
issues, verdict: 'approve_with_fixes', confidence: 0.85,
|
|
128
|
+
});
|
|
129
|
+
block = issues.some(i => i.severity === 'critical');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---- 3a. fix loop (high 발견 → executor 재호출) ----
|
|
133
|
+
console.log('[3a] fix-loop: executor 재호출 (high 수정) …');
|
|
134
|
+
round = 2;
|
|
135
|
+
callMcp('handoff_write', {
|
|
136
|
+
stage: 'implement', agent: 'executor', round,
|
|
137
|
+
decided: 'iat/nbf 검증 추가. audience claim 테스트 추가.',
|
|
138
|
+
files: ['src/auth/jwt.ts', 'tests/unit/jwt.test.ts'],
|
|
139
|
+
remaining: 'self-review 재실행',
|
|
140
|
+
});
|
|
141
|
+
console.log(` ✓ round ${round} 진입`);
|
|
142
|
+
|
|
143
|
+
step(4, `self-review round ${round}`, () => {
|
|
144
|
+
callMcp('handoff_write', {
|
|
145
|
+
stage: 'self-review', agent: 'code-reviewer', round,
|
|
146
|
+
decided: '이전 high 해결. 신규 발견 0건.',
|
|
147
|
+
files: ['src/auth/jwt.ts'], remaining: 'codex-review',
|
|
148
|
+
verdict: 'approve', confidence: 0.92,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---- 5. codex-review ----
|
|
153
|
+
round = 1;
|
|
154
|
+
step(5, 'codex-review (Codex CLI 별도 세션)', () => {
|
|
155
|
+
callMcp('handoff_write', {
|
|
156
|
+
stage: 'codex-review', agent: 'codex-reviewer', round,
|
|
157
|
+
decided: 'Claude self-review 와 일치. 추가 발견 medium 1 (네트워크 timeout 미설정).',
|
|
158
|
+
rejected: '없음',
|
|
159
|
+
files: ['src/auth/jwt.ts'],
|
|
160
|
+
remaining: '--secure 가 활성이면 단계 6, 아니면 단계 7',
|
|
161
|
+
issues: [{ severity: 'medium', category: 'correctness', file: 'src/auth/jwt.ts',
|
|
162
|
+
summary: 'JWKS fetch timeout 미설정', why: 'fetch 가 무한 대기 가능' }],
|
|
163
|
+
verdict: 'approve_with_fixes', confidence: 0.88,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---- 6. codex-challenge (--secure 자동 활성: auth/ 디렉터리 변경) ----
|
|
168
|
+
const isAuthChange = true;
|
|
169
|
+
const wantChallenge = SECURE || isAuthChange;
|
|
170
|
+
if (wantChallenge) {
|
|
171
|
+
step(6, 'codex-challenge (auth 영역 자동 활성)', () => {
|
|
172
|
+
callMcp('handoff_write', {
|
|
173
|
+
stage: 'codex-challenge', agent: 'codex-challenger', round: 1,
|
|
174
|
+
decided: '적대적 시나리오 5건 검토. 신규 critical 0, high 0, info 1 (replay 방어 권장).',
|
|
175
|
+
files: ['src/auth/jwt.ts'],
|
|
176
|
+
remaining: 'ship',
|
|
177
|
+
issues: [{ severity: 'info', category: 'security', summary: 'jti / replay cache 권장 (현 PRD 비목표)' }],
|
|
178
|
+
verdict: 'approve', confidence: 0.95,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
console.log('[6] codex-challenge skipped (--secure 미활성, auth 영역 아님)');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- 7. ship ----
|
|
186
|
+
if (NO_SHIP) {
|
|
187
|
+
console.log('[7] ship skipped (--no-ship)');
|
|
188
|
+
} else {
|
|
189
|
+
step(7, 'ship (doc-writer + git-master)', () => {
|
|
190
|
+
callMcp('handoff_write', {
|
|
191
|
+
stage: 'ship', agent: 'doc-writer', round: 1,
|
|
192
|
+
decided: 'CHANGELOG 갱신. PR 본문 한국어 초안. 자동 push 안 함 (사용자 룰).',
|
|
193
|
+
files: ['docs/CHANGELOG.md', 'WORKING-CONTEXT.md'],
|
|
194
|
+
remaining: '사용자 검토 후 gh pr create 또는 git push 수동 실행',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('\n=== 결과 ===');
|
|
200
|
+
const handoffs = fs.readdirSync(path.join(SESSION_DIR, 'handoffs')).filter(f => f.endsWith('.md')).sort();
|
|
201
|
+
for (const f of handoffs) console.log(' - handoffs/' + f);
|
|
202
|
+
console.log('\n=== prd.json ===');
|
|
203
|
+
console.log(fs.readFileSync(path.join(SESSION_DIR, 'prd.json'), 'utf8'));
|
|
204
|
+
console.log('=== 데모 종료 ===\n');
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { resolveProviderCli } from './core/cli-resolver.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const BLOCKED_ENV = {
|
|
12
|
+
claude: ['ANTHROPIC_API_KEY'],
|
|
13
|
+
codex: ['OPENAI_API_KEY'],
|
|
14
|
+
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const STATUS_RANK = { PASS: 0, WARN: 1, FAIL: 2 };
|
|
18
|
+
|
|
19
|
+
export function parseDoctorArgs(argv = []) {
|
|
20
|
+
const opts = {
|
|
21
|
+
json: false,
|
|
22
|
+
geminiSmoke: false,
|
|
23
|
+
projectRoot: null,
|
|
24
|
+
quick: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg === '--json') opts.json = true;
|
|
30
|
+
else if (arg === '--gemini-smoke') opts.geminiSmoke = true;
|
|
31
|
+
else if (arg === '--quick') opts.quick = true;
|
|
32
|
+
else if (arg === '--project-root') {
|
|
33
|
+
const value = argv[++i];
|
|
34
|
+
if (!value || value.startsWith('--')) throw new Error('--project-root requires a value');
|
|
35
|
+
opts.projectRoot = value;
|
|
36
|
+
} else if (arg.startsWith('--project-root=')) {
|
|
37
|
+
opts.projectRoot = arg.slice('--project-root='.length);
|
|
38
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
39
|
+
opts.help = true;
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(`unknown doctor option: ${arg}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return opts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildDoctorReport(options = {}) {
|
|
49
|
+
const harnessRoot = path.resolve(options.harnessRoot || DEFAULT_ROOT);
|
|
50
|
+
const projectRoot = path.resolve(options.projectRoot || process.env.HARNESS_PROJECT_ROOT || process.cwd());
|
|
51
|
+
const env = options.env || process.env;
|
|
52
|
+
const runCommand = options.runCommand || defaultRunCommand;
|
|
53
|
+
const nodeVersion = options.nodeVersion || process.versions.node;
|
|
54
|
+
const quick = Boolean(options.quick);
|
|
55
|
+
const geminiSmoke = Boolean(options.geminiSmoke);
|
|
56
|
+
|
|
57
|
+
const checks = [];
|
|
58
|
+
|
|
59
|
+
checks.push(checkNodeVersion(nodeVersion));
|
|
60
|
+
checks.push(checkPackageMetadata(harnessRoot));
|
|
61
|
+
checks.push(checkGitWorktree(projectRoot, runCommand));
|
|
62
|
+
checks.push(checkApiKeyEnvironment(env));
|
|
63
|
+
|
|
64
|
+
checks.push(...checkProviderClis({ harnessRoot, projectRoot, env, runCommand, geminiSmoke }));
|
|
65
|
+
|
|
66
|
+
if (geminiSmoke) {
|
|
67
|
+
checks.push(checkCommand('gemini live smoke', 'node scripts/verify/gemini-live.js', runCommand, harnessRoot, [
|
|
68
|
+
process.execPath,
|
|
69
|
+
['scripts/verify/gemini-live.js'],
|
|
70
|
+
]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!quick) {
|
|
74
|
+
checks.push(checkCommand('repair freshness', 'node scripts/repair.js --check', runCommand, harnessRoot, [
|
|
75
|
+
process.execPath,
|
|
76
|
+
['scripts/repair.js', '--check'],
|
|
77
|
+
]));
|
|
78
|
+
checks.push(checkCommand('CLAUDE.md sync', 'node scripts/sync-claude-md.js --check', runCommand, harnessRoot, [
|
|
79
|
+
process.execPath,
|
|
80
|
+
['scripts/sync-claude-md.js', '--check'],
|
|
81
|
+
]));
|
|
82
|
+
checks.push(checkCommand('codemaps freshness', 'node scripts/build-codemaps.js --check', runCommand, harnessRoot, [
|
|
83
|
+
process.execPath,
|
|
84
|
+
['scripts/build-codemaps.js', '--check'],
|
|
85
|
+
]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const summary = summarize(checks);
|
|
89
|
+
return {
|
|
90
|
+
name: 'NEKOWORK doctor',
|
|
91
|
+
harnessRoot,
|
|
92
|
+
projectRoot,
|
|
93
|
+
quick,
|
|
94
|
+
geminiSmoke,
|
|
95
|
+
summary,
|
|
96
|
+
checks,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderDoctorReport(report) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push('NEKOWORK doctor');
|
|
103
|
+
lines.push(`harness root : ${report.harnessRoot}`);
|
|
104
|
+
lines.push(`project root : ${report.projectRoot}`);
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(`${pad('STATUS', 6)} ${pad('CHECK', 22)} MESSAGE`);
|
|
107
|
+
lines.push(`${'-'.repeat(6)} ${'-'.repeat(22)} ${'-'.repeat(50)}`);
|
|
108
|
+
for (const check of report.checks) {
|
|
109
|
+
lines.push(`${pad(check.status, 6)} ${pad(check.name, 22)} ${check.message}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(`summary: ${report.summary.status} (${report.summary.pass} pass, ${report.summary.warn} warn, ${report.summary.fail} fail)`);
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkNodeVersion(version) {
|
|
117
|
+
const major = Number(String(version).split('.')[0]);
|
|
118
|
+
if (major >= 22) return pass('node', `Node ${version}`);
|
|
119
|
+
return fail('node', `Node ${version}; required >= 22`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function checkPackageMetadata(root) {
|
|
123
|
+
try {
|
|
124
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
125
|
+
if (pkg.name !== '@ps-neko/nekowork') {
|
|
126
|
+
return fail('package metadata', `unexpected package name: ${pkg.name}`);
|
|
127
|
+
}
|
|
128
|
+
if (!pkg.version) return fail('package metadata', 'missing package version');
|
|
129
|
+
if (pkg.private === true) {
|
|
130
|
+
return pass('package metadata', `${pkg.name}@${pkg.version}; private publish disabled`);
|
|
131
|
+
}
|
|
132
|
+
if (pkg.private === false && isPublicAlphaVersion(pkg.version)) {
|
|
133
|
+
return pass('package metadata', `${pkg.name}@${pkg.version}; public alpha publish candidate`);
|
|
134
|
+
}
|
|
135
|
+
return warn('package metadata', `${pkg.name}@${pkg.version}; publish guard is not explicit`);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return fail('package metadata', `cannot read package.json: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isPublicAlphaVersion(version) {
|
|
142
|
+
return /^\d+\.\d+\.\d+-alpha\.\d+$/.test(String(version));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkGitWorktree(projectRoot, runCommand) {
|
|
146
|
+
const result = runCommand('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectRoot, timeoutMs: 5000 });
|
|
147
|
+
if (result.status === 0 && result.stdout.trim() === 'true') {
|
|
148
|
+
return pass('git worktree', 'project root is inside a git worktree');
|
|
149
|
+
}
|
|
150
|
+
return warn('git worktree', 'project root is not a git worktree; reviews still run, but git-aware guards are limited');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkApiKeyEnvironment(env) {
|
|
154
|
+
const found = [];
|
|
155
|
+
for (const keys of Object.values(BLOCKED_ENV)) {
|
|
156
|
+
for (const key of keys) if (env[key]) found.push(key);
|
|
157
|
+
}
|
|
158
|
+
if (!found.length) return pass('api key env', 'no delegated-provider API key overrides detected');
|
|
159
|
+
if (env.HARNESS_AUTH_ALLOW_ENV_OVERRIDE === '1') {
|
|
160
|
+
return warn('api key env', `explicit metered opt-in is enabled for: ${found.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
return warn('api key env', `will be blocked before delegated CLI calls: ${found.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkProviderClis({ harnessRoot, projectRoot, env, runCommand, geminiSmoke }) {
|
|
166
|
+
return [
|
|
167
|
+
checkProviderCli('claude', ['auth', 'status'], { harnessRoot, projectRoot, env, runCommand }),
|
|
168
|
+
checkProviderCli('codex', ['login', 'status'], { harnessRoot, projectRoot, env, runCommand }),
|
|
169
|
+
checkProviderCli('gemini', null, { harnessRoot, projectRoot, env, runCommand, geminiSmoke }),
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function checkProviderCli(provider, authArgs, { harnessRoot, projectRoot, env, runCommand, geminiSmoke }) {
|
|
174
|
+
let resolved;
|
|
175
|
+
try {
|
|
176
|
+
resolved = resolveProviderCli(provider, {
|
|
177
|
+
root: projectRoot,
|
|
178
|
+
roots: [projectRoot, harnessRoot],
|
|
179
|
+
env,
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return fail(`${provider} cli`, error.message.split('\n')[0]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!resolved) return warn(`${provider} cli`, `${provider} CLI not found on PATH; mock mode still works`);
|
|
186
|
+
|
|
187
|
+
if (!authArgs) {
|
|
188
|
+
if (provider === 'gemini' && geminiSmoke) {
|
|
189
|
+
return pass(`${provider} cli`, `${resolved}; installed; live smoke requested`);
|
|
190
|
+
}
|
|
191
|
+
return warn(`${provider} cli`, `${resolved}; installed; auth status is not checked non-interactively; run --gemini-smoke or npm run verify:gemini`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const auth = runCommand(resolved, authArgs, { cwd: projectRoot, timeoutMs: 10000 });
|
|
195
|
+
if (auth.status === 0) return pass(`${provider} cli`, `${resolved}; auth status OK`);
|
|
196
|
+
|
|
197
|
+
const detail = firstNonEmptyLine(auth.stderr) || firstNonEmptyLine(auth.stdout) || `exit ${auth.status}`;
|
|
198
|
+
return warn(`${provider} cli`, `${resolved}; auth status not ready (${detail})`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkCommand(name, label, runCommand, cwd, command) {
|
|
202
|
+
const [bin, args] = command;
|
|
203
|
+
const result = runCommand(bin, args, { cwd, timeoutMs: 30000 });
|
|
204
|
+
if (result.status === 0) return pass(name, `${label} passed`);
|
|
205
|
+
const detail = firstNonEmptyLine(result.stderr) || firstNonEmptyLine(result.stdout) || `exit ${result.status}`;
|
|
206
|
+
return fail(name, `${label} failed: ${detail}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function summarize(checks) {
|
|
210
|
+
const counts = { pass: 0, warn: 0, fail: 0 };
|
|
211
|
+
for (const check of checks) counts[check.status.toLowerCase()]++;
|
|
212
|
+
const status = counts.fail ? 'FAIL' : (counts.warn ? 'WARN' : 'PASS');
|
|
213
|
+
return { status, ...counts };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function pass(name, message) {
|
|
217
|
+
return { status: 'PASS', name, message };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function warn(name, message) {
|
|
221
|
+
return { status: 'WARN', name, message };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function fail(name, message) {
|
|
225
|
+
return { status: 'FAIL', name, message };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function pad(value, width) {
|
|
229
|
+
return String(value).padEnd(width);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function firstNonEmptyLine(value = '') {
|
|
233
|
+
return String(value).split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function defaultRunCommand(command, args, options = {}) {
|
|
237
|
+
const invocation = normalizeSpawnInvocation(command, args);
|
|
238
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
239
|
+
cwd: options.cwd,
|
|
240
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
timeout: options.timeoutMs || 10000,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
status: result.status ?? (result.error ? 1 : 0),
|
|
246
|
+
stdout: result.stdout || '',
|
|
247
|
+
stderr: result.stderr || (result.error ? result.error.message : ''),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeSpawnInvocation(command, args) {
|
|
252
|
+
const ext = path.extname(command).toLowerCase();
|
|
253
|
+
if (process.platform === 'win32' && (ext === '.cmd' || ext === '.bat')) {
|
|
254
|
+
return { command: 'cmd.exe', args: ['/d', '/c', command, ...args] };
|
|
255
|
+
}
|
|
256
|
+
return { command, args };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function printHelp() {
|
|
260
|
+
console.log(`NEKOWORK doctor
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
harness doctor [--project-root <dir>] [--quick] [--gemini-smoke] [--json]
|
|
264
|
+
|
|
265
|
+
Checks:
|
|
266
|
+
- Node.js version
|
|
267
|
+
- package metadata and publish guard
|
|
268
|
+
- git worktree
|
|
269
|
+
- delegated-provider API key environment overrides
|
|
270
|
+
- Claude/Codex/Gemini CLI presence and auth where non-interactive status exists
|
|
271
|
+
- Gemini live smoke when --gemini-smoke is set
|
|
272
|
+
- repair, CLAUDE.md sync, and codemap freshness unless --quick is set
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
277
|
+
try {
|
|
278
|
+
const opts = parseDoctorArgs(process.argv.slice(2));
|
|
279
|
+
if (opts.help) {
|
|
280
|
+
printHelp();
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
const report = buildDoctorReport({
|
|
284
|
+
harnessRoot: DEFAULT_ROOT,
|
|
285
|
+
projectRoot: opts.projectRoot,
|
|
286
|
+
quick: opts.quick,
|
|
287
|
+
geminiSmoke: opts.geminiSmoke,
|
|
288
|
+
});
|
|
289
|
+
if (opts.json) console.log(JSON.stringify(report, null, 2));
|
|
290
|
+
else console.log(renderDoctorReport(report));
|
|
291
|
+
process.exit(STATUS_RANK[report.summary.status] === 2 ? 1 : 0);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(error.message);
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// HARNESS install --apply : plan 단계 검증 → harness 별 빌드 (agent.yaml harnesses 전부) → install-state 기록 → 마커 검증.
|
|
3
|
+
// 멱등(idempotent). 실패 시 롤백은 git checkout 으로.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import YAML from 'yaml';
|
|
10
|
+
import {
|
|
11
|
+
buildInstallState,
|
|
12
|
+
loadInstallState,
|
|
13
|
+
writeInstallState,
|
|
14
|
+
} from './core/install-state.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = {
|
|
21
|
+
profile: null,
|
|
22
|
+
harness: null,
|
|
23
|
+
force: false,
|
|
24
|
+
dryRun: false,
|
|
25
|
+
projectRoot: null,
|
|
26
|
+
modules: [],
|
|
27
|
+
withoutModules: [],
|
|
28
|
+
components: [],
|
|
29
|
+
withoutComponents: [],
|
|
30
|
+
};
|
|
31
|
+
for (let i = 2; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === '--profile') args.profile = takeValue(argv, i++, a);
|
|
34
|
+
else if (a === '--harness' || a === '--target') args.harness = takeValue(argv, i++, a);
|
|
35
|
+
else if (a === '--module' || a === '--with-module') args.modules.push(takeValue(argv, i++, a));
|
|
36
|
+
else if (a === '--without-module') args.withoutModules.push(takeValue(argv, i++, a));
|
|
37
|
+
else if (a === '--component' || a === '--with-component') args.components.push(takeValue(argv, i++, a));
|
|
38
|
+
else if (a === '--without-component') args.withoutComponents.push(takeValue(argv, i++, a));
|
|
39
|
+
else if (a === '--project-root' || a === '--target-root') args.projectRoot = takeValue(argv, i++, a);
|
|
40
|
+
else if (a === '--force') args.force = true;
|
|
41
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
42
|
+
else if (a === '--help' || a === '-h') { printHelp(); process.exit(0); }
|
|
43
|
+
else { console.error(`알 수 없는 인자: ${a}`); process.exit(2); }
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function takeValue(argv, i, flag) {
|
|
49
|
+
const value = argv[i + 1];
|
|
50
|
+
if (!value || value.startsWith('--')) {
|
|
51
|
+
console.error(`${flag} value required`);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printHelp() {
|
|
58
|
+
console.log(`
|
|
59
|
+
HARNESS install --apply
|
|
60
|
+
|
|
61
|
+
사용법:
|
|
62
|
+
install.sh --apply [--profile <name>] [--harness <name>] [--module <id>] [--component <id>] [--project-root <dir>] [--force] [--dry-run]
|
|
63
|
+
|
|
64
|
+
옵션:
|
|
65
|
+
--profile <name> 프로파일 선택 (기본: agent.yaml profiles.default)
|
|
66
|
+
--harness <name> 특정 하네스만 빌드 (claude | codex | cursor | gemini | opencode)
|
|
67
|
+
--target <name> --harness alias
|
|
68
|
+
--module <id> include an additional module, repeatable
|
|
69
|
+
--without-module <id> exclude a module, repeatable
|
|
70
|
+
--component <id> include a direct component, repeatable
|
|
71
|
+
--without-component <id> exclude a component, repeatable
|
|
72
|
+
--project-root <dir> write harness outputs and .harness/install-state.json to this project root
|
|
73
|
+
--target-root <dir> alias for --project-root
|
|
74
|
+
--force 기존 출력 무시하고 재생성
|
|
75
|
+
--dry-run plan 만 다시 출력하고 종료
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runBuilder(name, targetRoot) {
|
|
80
|
+
const script = path.join(__dirname, `build-${name}.js`);
|
|
81
|
+
if (!fs.existsSync(script)) {
|
|
82
|
+
console.error(` [SKIP] build-${name}.js 없음`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const r = spawnSync(process.execPath, [script], {
|
|
86
|
+
stdio: 'inherit',
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
HARNESS_SOURCE_ROOT: ROOT,
|
|
90
|
+
HARNESS_TARGET_ROOT: targetRoot,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return r.status === 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function recordState(profile, harnessDefs, builders, targetRoot) {
|
|
97
|
+
const previousState = loadInstallState(targetRoot);
|
|
98
|
+
const { state, sourceSha } = buildInstallState(ROOT, {
|
|
99
|
+
targetRoot,
|
|
100
|
+
profile,
|
|
101
|
+
harnessDefs,
|
|
102
|
+
harnessNames: builders,
|
|
103
|
+
previousState,
|
|
104
|
+
});
|
|
105
|
+
const stateFile = writeInstallState(targetRoot, state, { schemaRoot: ROOT });
|
|
106
|
+
console.log(` state: ${path.relative(targetRoot, stateFile)}`);
|
|
107
|
+
console.log(` source_sha256: ${sourceSha.slice(0, 12)}…`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = parseArgs(process.argv);
|
|
112
|
+
const targetRoot = path.resolve(args.projectRoot || process.env.HARNESS_PROJECT_ROOT || ROOT);
|
|
113
|
+
|
|
114
|
+
// 1. plan 먼저 (검증 + 결과 표시)
|
|
115
|
+
console.log('=> plan 단계');
|
|
116
|
+
const planResult = spawnSync(
|
|
117
|
+
process.execPath,
|
|
118
|
+
[path.join(__dirname, 'install-plan.js'), ...planArgs(args)],
|
|
119
|
+
{ stdio: 'inherit' },
|
|
120
|
+
);
|
|
121
|
+
if (planResult.status !== 0) {
|
|
122
|
+
console.error('plan 실패. apply 중단.');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (args.dryRun) {
|
|
127
|
+
console.log('--dry-run: apply 안 함.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`=> target root: ${targetRoot}`);
|
|
132
|
+
|
|
133
|
+
// 2. .mcp.json 검증 (있으면 OK, 없으면 경고)
|
|
134
|
+
if (!fs.existsSync(path.join(ROOT, '.mcp.json'))) {
|
|
135
|
+
console.warn('WARN: .mcp.json 없음. bridge/mcp-server.js 등록 필요.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3. 빌더 실행 (agent.yaml harnesses 의 name 그대로)
|
|
139
|
+
const manifest = YAML.parse(fs.readFileSync(path.join(ROOT, 'agent.yaml'), 'utf8'));
|
|
140
|
+
const harnessDefs = manifest.harnesses || [];
|
|
141
|
+
const allBuilders = harnessDefs.map(h => h.name);
|
|
142
|
+
const builders = args.harness ? [args.harness] : allBuilders;
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(`=> apply: ${builders.join(', ')}`);
|
|
145
|
+
for (const b of builders) {
|
|
146
|
+
console.log('');
|
|
147
|
+
if (!runBuilder(b, targetRoot)) {
|
|
148
|
+
console.error(`build-${b} 실패`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 4. state 기록
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('=> state 기록');
|
|
156
|
+
recordState(args.profile || manifest.profiles?.default || 'developer', harnessDefs, builders, targetRoot);
|
|
157
|
+
|
|
158
|
+
// 5. 마커 검증
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log('=> 마커 검증');
|
|
161
|
+
const markerCheck = spawnSync(process.execPath, [path.join(__dirname, 'ci', 'check-markers.js')], { stdio: 'inherit' });
|
|
162
|
+
if (markerCheck.status !== 0) {
|
|
163
|
+
console.warn('WARN: 마커 검증 실패. CLAUDE.md 영역 확인 필요.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log('apply 완료.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function planArgs(args) {
|
|
171
|
+
return [
|
|
172
|
+
...(args.profile ? ['--profile', args.profile] : []),
|
|
173
|
+
...(args.harness ? ['--target', args.harness] : []),
|
|
174
|
+
...(args.projectRoot ? ['--project-root', args.projectRoot] : []),
|
|
175
|
+
...args.modules.flatMap(v => ['--module', v]),
|
|
176
|
+
...args.withoutModules.flatMap(v => ['--without-module', v]),
|
|
177
|
+
...args.components.flatMap(v => ['--component', v]),
|
|
178
|
+
...args.withoutComponents.flatMap(v => ['--without-component', v]),
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
main().catch((e) => {
|
|
183
|
+
console.error('UNEXPECTED:', e?.stack || e);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|