@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,452 @@
|
|
|
1
|
+
// 7단계 review 오케스트레이터.
|
|
2
|
+
// claude-led-codex-review SKILL 의 Stage Routing 표를 코드로 구현.
|
|
3
|
+
//
|
|
4
|
+
// 핵심 규칙:
|
|
5
|
+
// - 단계 5/6 의 verdict 가 block 또는 critical/high 발견 시 fix loop (executor 재호출, round++)
|
|
6
|
+
// - round 한도 = 3. critical 발견 또는 round ≥ 3 → human gate.
|
|
7
|
+
// - --secure 또는 보안 카테고리(auth/crypto/token/cert/csrf/webhook 등) 변경 자동 감지 → 단계 6 활성.
|
|
8
|
+
// - --fast → 단계 1·6 스킵. --secure 와 동시 사용은 거절.
|
|
9
|
+
// - --no-ship → 단계 7 생략.
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { dispatch } from '../agents/dispatch.js';
|
|
14
|
+
import { applyExecutionDiff, withExecutionWorkspace } from '../core/execution-workspace.js';
|
|
15
|
+
import { record as instinctRecord } from '../lib/instincts.js';
|
|
16
|
+
import { isSensitiveWork } from '../lib/risk-classifier.js';
|
|
17
|
+
|
|
18
|
+
const STAGE_INDEX = {
|
|
19
|
+
ideate: '01', plan: '02', implement: '03', 'self-review': '04',
|
|
20
|
+
'codex-review': '05', 'codex-challenge': '06', ship: '07',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ROUND_LIMIT = Number(process.env.HARNESS_REVIEW_ROUND_LIMIT || 3);
|
|
24
|
+
// 단어 경계(\b)는 [A-Za-z0-9_] 사이에 매칭하지 않으므로 'session_id' 의 'session' 처럼
|
|
25
|
+
// _ 로 이어진 경우는 매칭 안 됨. 변형은 별도 패턴으로 명시한다.
|
|
26
|
+
export { SENSITIVE_PATTERNS } from '../lib/risk-classifier.js';
|
|
27
|
+
const LEGACY_SENSITIVE_PATTERNS = [
|
|
28
|
+
// 인증 / 세션 / 시크릿
|
|
29
|
+
/\bauth\b/i, /\bcrypto\b/i, /\bpayment\b/i, /\bsession\b/i,
|
|
30
|
+
/\bpermission\b/i, /\boauth\b/i, /\bjwt\b/i, /\bpassword\b/i, /\bsecret\b/i,
|
|
31
|
+
// 자격증명 / 토큰
|
|
32
|
+
/\btoken\b/i, /\bapikey\b/i, /\bapi[-_]key\b/i,
|
|
33
|
+
// 인증서 / 전송보안
|
|
34
|
+
/\bcert\b/i, /\btls\b/i, /\bssl\b/i, /\bmtls\b/i,
|
|
35
|
+
// 웹 보안
|
|
36
|
+
/\bcsrf\b/i, /\bcors\b/i, /\bxss\b/i,
|
|
37
|
+
// 외부 검증 누락 다발
|
|
38
|
+
/\bwebhook\b/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export async function reviewCycle(opts) {
|
|
42
|
+
const harnessRoot = opts.harnessRoot || process.cwd();
|
|
43
|
+
const projectRoot = opts.projectRoot || harnessRoot;
|
|
44
|
+
const sessionId = opts.sessionId || `review-${Date.now()}`;
|
|
45
|
+
const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
|
|
46
|
+
fs.mkdirSync(path.join(sessionDir, 'handoffs'), { recursive: true });
|
|
47
|
+
|
|
48
|
+
const live = !!opts.live;
|
|
49
|
+
const fast = !!opts.fast;
|
|
50
|
+
const noShip = !!opts.noShip;
|
|
51
|
+
const noCodex = !!opts.noCodex;
|
|
52
|
+
let secureRequested = !!opts.secure;
|
|
53
|
+
if (fast && secureRequested) {
|
|
54
|
+
throw new Error('--secure 와 --fast 는 함께 쓸 수 없습니다. 보안 검증이 필요하면 --secure 만 사용하세요.');
|
|
55
|
+
}
|
|
56
|
+
if (noCodex && secureRequested) {
|
|
57
|
+
throw new Error('--no-codex 와 --secure 는 함께 쓸 수 없습니다. 보안 검증이 필요하면 Codex 단계를 유지하세요.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const summaryBase = {
|
|
61
|
+
task: opts.task,
|
|
62
|
+
live,
|
|
63
|
+
fast,
|
|
64
|
+
noShip,
|
|
65
|
+
noCodex,
|
|
66
|
+
secureRequested,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const log = (msg) => console.log(`[review:${sessionId}] ${msg}`);
|
|
70
|
+
|
|
71
|
+
log(`task: ${opts.task}`);
|
|
72
|
+
log(`mode: ${live ? 'live' : 'mock'}${fast ? ' --fast' : ''}${noShip ? ' --no-ship' : ''}${noCodex ? ' --no-codex' : ''}${secureRequested ? ' --secure' : ''}`);
|
|
73
|
+
if (path.resolve(harnessRoot) !== path.resolve(projectRoot)) {
|
|
74
|
+
log(`harness root: ${harnessRoot}`);
|
|
75
|
+
log(`project root: ${projectRoot}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handoffs = [];
|
|
79
|
+
const writeHandoff = (h) => {
|
|
80
|
+
const base = handoffBase(h);
|
|
81
|
+
fs.writeFileSync(path.join(sessionDir, 'handoffs', `${base}.md`), renderHandoff(h));
|
|
82
|
+
fs.writeFileSync(path.join(sessionDir, 'handoffs', `${base}.json`), JSON.stringify(h, null, 2));
|
|
83
|
+
handoffs.push(h);
|
|
84
|
+
// 인스팅트 자동 누적
|
|
85
|
+
try {
|
|
86
|
+
// 이슈 패턴: severity + category + 파일 prefix
|
|
87
|
+
for (const i of (h.issues || [])) {
|
|
88
|
+
instinctRecord({
|
|
89
|
+
kind: 'issue-pattern',
|
|
90
|
+
key: `${i.severity || '?'}/${i.category || '?'}/${(i.file || '').split('/')[0] || '_'}`,
|
|
91
|
+
summary: `${i.severity}/${i.category} in ${i.file || '?'}: ${i.summary || ''}`.slice(0, 200),
|
|
92
|
+
evidence: { sessionId, stage: h.stage, file: i.file, summary: i.summary },
|
|
93
|
+
scope: 'global',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// verdict 흐름
|
|
97
|
+
if (h.verdict) {
|
|
98
|
+
instinctRecord({
|
|
99
|
+
kind: 'fix-flow',
|
|
100
|
+
key: `${h.stage}→${h.verdict}@round${h.round || 1}`,
|
|
101
|
+
summary: `${h.stage} round ${h.round || 1} → ${h.verdict}`,
|
|
102
|
+
evidence: { sessionId, stage: h.stage, verdict: h.verdict, round: h.round || 1 },
|
|
103
|
+
scope: 'global',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch { /* instinct 실패는 review 자체를 막지 않음 */ }
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ---- 1. ideate ----
|
|
110
|
+
if (!fast) {
|
|
111
|
+
log('1 ideate');
|
|
112
|
+
const h1 = await runWithFallback({ agent: 'planner', stage: 'ideate', task: opts.task, live, harnessRoot, projectRoot });
|
|
113
|
+
writeHandoff(h1);
|
|
114
|
+
} else {
|
|
115
|
+
log('1 ideate skipped (--fast)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- 2. plan ----
|
|
119
|
+
log('2 plan');
|
|
120
|
+
const h2 = await runWithFallback({ agent: 'planner', stage: 'plan', task: opts.task, live, harnessRoot, projectRoot });
|
|
121
|
+
writeHandoff(h2);
|
|
122
|
+
|
|
123
|
+
// mock 일 경우 prdSeed 가 같이 옴. PRD 저장.
|
|
124
|
+
if (h2.prdSeed) {
|
|
125
|
+
fs.writeFileSync(path.join(sessionDir, 'prd.json'), JSON.stringify(h2.prdSeed, null, 2));
|
|
126
|
+
}
|
|
127
|
+
if (opts.stopAfter === 'plan') {
|
|
128
|
+
const result = {
|
|
129
|
+
sessionId,
|
|
130
|
+
sessionDir,
|
|
131
|
+
mode: 'legacy-full-review-cycle',
|
|
132
|
+
handoffs,
|
|
133
|
+
files: dedupe(h2.files || []),
|
|
134
|
+
secureActive: false,
|
|
135
|
+
verdict: 'planned',
|
|
136
|
+
humanGate: false,
|
|
137
|
+
stoppedAt: 'plan',
|
|
138
|
+
targetProjectMutated: false,
|
|
139
|
+
};
|
|
140
|
+
writeReviewSummary(sessionDir, result, summaryBase);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
const prd = readPrd(sessionDir);
|
|
144
|
+
let currentDiff = opts.diff || '';
|
|
145
|
+
let targetProjectMutated = false;
|
|
146
|
+
|
|
147
|
+
// sensitive path 감지
|
|
148
|
+
const sensitiveHit = isSensitiveWork({ task: opts.task, files: h2.files || [] });
|
|
149
|
+
|
|
150
|
+
// ---- 3. implement ----
|
|
151
|
+
log('3 implement');
|
|
152
|
+
const impl3 = await runImplementStage({
|
|
153
|
+
agent: 'executor', stage: 'implement', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
154
|
+
context: { prd, acCount: prd?.acceptance?.length || 3 },
|
|
155
|
+
});
|
|
156
|
+
const h3 = impl3.handoff;
|
|
157
|
+
if (impl3.diff) currentDiff = impl3.diff;
|
|
158
|
+
writeHandoff(h3);
|
|
159
|
+
|
|
160
|
+
// ---- 4. self-review (round loop) ----
|
|
161
|
+
let reviewRound = 0;
|
|
162
|
+
let lastVerdict = null;
|
|
163
|
+
const allFiles = [...(h3.files || []), ...(impl3.files || [])];
|
|
164
|
+
while (true) {
|
|
165
|
+
reviewRound++;
|
|
166
|
+
log(`4 self-review (round ${reviewRound})`);
|
|
167
|
+
const hSelf = await runWithFallback({
|
|
168
|
+
agent: 'code-reviewer', stage: 'self-review', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
169
|
+
context: { round: reviewRound, prd, priorHandoffs: handoffs.slice(-3), diff: currentDiff },
|
|
170
|
+
});
|
|
171
|
+
hSelf.round = reviewRound;
|
|
172
|
+
writeHandoff(hSelf);
|
|
173
|
+
lastVerdict = hSelf.verdict;
|
|
174
|
+
if (hasCritical(hSelf.issues) || reviewRound >= ROUND_LIMIT) {
|
|
175
|
+
if (hasCritical(hSelf.issues)) return humanGate(sessionDir, 'critical 발견 (단계 4)', sessionId, handoffs, summaryBase);
|
|
176
|
+
if (reviewRound >= ROUND_LIMIT && lastVerdict !== 'approve') {
|
|
177
|
+
return humanGate(sessionDir, `round ≥ ${ROUND_LIMIT}, verdict=${lastVerdict}`, sessionId, handoffs, summaryBase);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (lastVerdict === 'approve') break;
|
|
181
|
+
if (lastVerdict === 'block' || lastVerdict === 'approve_with_fixes') {
|
|
182
|
+
log(`fix-loop: executor round ${reviewRound + 1}`);
|
|
183
|
+
const implFix = await runImplementStage({
|
|
184
|
+
agent: 'executor', stage: 'implement', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
185
|
+
context: { prd, round: reviewRound + 1, issues: hSelf.issues, diff: currentDiff },
|
|
186
|
+
});
|
|
187
|
+
const hFix = implFix.handoff;
|
|
188
|
+
hFix.round = reviewRound + 1;
|
|
189
|
+
if (implFix.diff) currentDiff = implFix.diff;
|
|
190
|
+
allFiles.push(...(hFix.files || []), ...(implFix.files || []));
|
|
191
|
+
writeHandoff(hFix);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (opts.stopAfter === 'self-review') {
|
|
197
|
+
const result = {
|
|
198
|
+
sessionId,
|
|
199
|
+
sessionDir,
|
|
200
|
+
mode: 'legacy-full-review-cycle',
|
|
201
|
+
handoffs,
|
|
202
|
+
files: dedupe(allFiles),
|
|
203
|
+
secureActive: false,
|
|
204
|
+
verdict: lastVerdict || 'approve',
|
|
205
|
+
humanGate: false,
|
|
206
|
+
stoppedAt: 'self-review',
|
|
207
|
+
targetProjectMutated: false,
|
|
208
|
+
};
|
|
209
|
+
writeReviewSummary(sessionDir, result, summaryBase);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- 5 + 6. codex-review + codex-challenge (병렬 실행) ----
|
|
214
|
+
// 두 단계는 같은 입력(prd / priorHandoffs / diff)을 받고 컨텍스트가 독립이다.
|
|
215
|
+
// Promise.all 로 동시 호출 → codex CLI 호출 시간(가장 큰 비용)을 1회 비용으로 단축.
|
|
216
|
+
// stage 5 critical 시 stage 6 결과는 폐기 (직렬 동작과 의미 동일).
|
|
217
|
+
const wantChallenge = !noCodex && (secureRequested || sensitiveHit) && !fast;
|
|
218
|
+
if (noCodex) {
|
|
219
|
+
log('5+6 codex-review/codex-challenge skipped (--no-codex)');
|
|
220
|
+
} else {
|
|
221
|
+
log(wantChallenge
|
|
222
|
+
? `5+6 codex-review + codex-challenge (병렬, ${secureRequested ? '--secure' : 'sensitive 자동'})`
|
|
223
|
+
: '5 codex-review');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const codexCommonContext = { round: 1, prd, priorHandoffs: handoffs.slice(-3), diff: currentDiff };
|
|
227
|
+
if (!noCodex) {
|
|
228
|
+
const codexPromises = [
|
|
229
|
+
runWithFallback({
|
|
230
|
+
agent: 'codex-reviewer', stage: 'codex-review', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
231
|
+
context: codexCommonContext,
|
|
232
|
+
}),
|
|
233
|
+
];
|
|
234
|
+
if (wantChallenge) {
|
|
235
|
+
codexPromises.push(runWithFallback({
|
|
236
|
+
agent: 'codex-challenger', stage: 'codex-challenge', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
237
|
+
context: codexCommonContext,
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
const codexResults = await Promise.all(codexPromises);
|
|
241
|
+
const h5 = codexResults[0];
|
|
242
|
+
const h6 = codexResults[1] || null;
|
|
243
|
+
|
|
244
|
+
writeHandoff(h5);
|
|
245
|
+
if (hasCritical(h5.issues)) {
|
|
246
|
+
return humanGate(sessionDir, 'codex-review 에서 critical 발견', sessionId, handoffs, summaryBase);
|
|
247
|
+
}
|
|
248
|
+
if (opts.stopAfter === 'codex-review') {
|
|
249
|
+
const result = {
|
|
250
|
+
sessionId,
|
|
251
|
+
sessionDir,
|
|
252
|
+
mode: 'legacy-full-review-cycle',
|
|
253
|
+
handoffs,
|
|
254
|
+
files: dedupe(allFiles),
|
|
255
|
+
secureActive: wantChallenge,
|
|
256
|
+
verdict: h5.verdict || 'approve',
|
|
257
|
+
humanGate: false,
|
|
258
|
+
stoppedAt: 'codex-review',
|
|
259
|
+
targetProjectMutated: false,
|
|
260
|
+
};
|
|
261
|
+
writeReviewSummary(sessionDir, result, summaryBase);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (h6) {
|
|
266
|
+
writeHandoff(h6);
|
|
267
|
+
if (hasCritical(h6.issues)) {
|
|
268
|
+
return humanGate(sessionDir, 'codex-challenge 에서 critical 발견', sessionId, handoffs, summaryBase);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
log(`6 codex-challenge skipped${fast ? ' (--fast)' : ' (sensitive 미감지, --secure 미지정)'}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (live && currentDiff.trim()) {
|
|
276
|
+
try {
|
|
277
|
+
const applied = applyExecutionDiff(projectRoot, currentDiff);
|
|
278
|
+
if (applied) {
|
|
279
|
+
fs.writeFileSync(path.join(sessionDir, 'APPLIED_DIFF'), `applied_at: ${new Date().toISOString()}\n`);
|
|
280
|
+
targetProjectMutated = true;
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return humanGate(sessionDir, `live executor diff apply failed: ${e.message}`, sessionId, handoffs, summaryBase);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- 7. ship ----
|
|
288
|
+
if (noShip) {
|
|
289
|
+
log('7 ship skipped (--no-ship)');
|
|
290
|
+
} else {
|
|
291
|
+
log('7 ship');
|
|
292
|
+
const h7 = await runWithFallback({
|
|
293
|
+
agent: 'doc-writer', stage: 'ship', task: opts.task, live, harnessRoot, projectRoot, sessionDir, sessionId,
|
|
294
|
+
context: { prd, priorHandoffs: handoffs },
|
|
295
|
+
});
|
|
296
|
+
writeHandoff(h7);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const result = {
|
|
300
|
+
sessionId,
|
|
301
|
+
sessionDir,
|
|
302
|
+
mode: 'legacy-full-review-cycle',
|
|
303
|
+
handoffs,
|
|
304
|
+
files: dedupe(allFiles),
|
|
305
|
+
secureActive: wantChallenge,
|
|
306
|
+
verdict: 'approve',
|
|
307
|
+
humanGate: false,
|
|
308
|
+
stoppedAt: noShip ? 'codex-review' : 'ship',
|
|
309
|
+
targetProjectMutated,
|
|
310
|
+
};
|
|
311
|
+
writeReviewSummary(sessionDir, result, summaryBase);
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ----------------
|
|
316
|
+
|
|
317
|
+
async function runWithFallback({ agent, stage, task, live, harnessRoot, projectRoot, context, sessionDir, sessionId, executionMode }) {
|
|
318
|
+
try {
|
|
319
|
+
return await dispatch({ agent, stage, task, live, harnessRoot, projectRoot, context, sessionDir, sessionId, executionMode });
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (live) {
|
|
322
|
+
if (process.env.HARNESS_LIVE_ALLOW_MOCK_FALLBACK !== '1') {
|
|
323
|
+
throw new Error(`${agent}/${stage} live 실패: ${e.message}`);
|
|
324
|
+
}
|
|
325
|
+
console.error(`[review] ${agent}/${stage} live 실패 → mock 폴백(HARNESS_LIVE_ALLOW_MOCK_FALLBACK=1): ${e.message}`);
|
|
326
|
+
return await dispatch({
|
|
327
|
+
agent, stage, task, live: false, harnessRoot, projectRoot, context,
|
|
328
|
+
providerOverride: 'mock', sessionDir, sessionId, executionMode,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
throw e;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function runImplementStage({ agent, stage, task, live, harnessRoot, projectRoot, context, sessionDir, sessionId }) {
|
|
336
|
+
if (!live) {
|
|
337
|
+
const handoff = await runWithFallback({ agent, stage, task, live, harnessRoot, projectRoot, context, sessionDir, sessionId });
|
|
338
|
+
return { handoff, diff: null, files: [] };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const round = context?.round || 1;
|
|
342
|
+
const execution = await withExecutionWorkspace(
|
|
343
|
+
projectRoot,
|
|
344
|
+
sessionDir,
|
|
345
|
+
async (workspaceRoot) => runWithFallback({
|
|
346
|
+
agent,
|
|
347
|
+
stage,
|
|
348
|
+
task,
|
|
349
|
+
live,
|
|
350
|
+
harnessRoot,
|
|
351
|
+
projectRoot: workspaceRoot,
|
|
352
|
+
context,
|
|
353
|
+
sessionDir,
|
|
354
|
+
sessionId,
|
|
355
|
+
executionMode: 'workspace-write',
|
|
356
|
+
}),
|
|
357
|
+
{ sessionId, stage, round, baseDiff: context?.diff || '' },
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const handoff = execution.result;
|
|
361
|
+
handoff.files = dedupe([...(handoff.files || []), ...execution.files]);
|
|
362
|
+
if (execution.diffPath) handoff.diffPath = execution.diffPath;
|
|
363
|
+
if (execution.worktreeRoot) handoff.executionWorkspace = execution.worktreeRoot;
|
|
364
|
+
return { handoff, diff: execution.diff, files: execution.files };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function readPrd(sessionDir) {
|
|
368
|
+
const f = path.join(sessionDir, 'prd.json');
|
|
369
|
+
if (!fs.existsSync(f)) return null;
|
|
370
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function hasCritical(issues) {
|
|
374
|
+
return Array.isArray(issues) && issues.some(i => i.severity === 'critical');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function humanGate(sessionDir, reason, sessionId, handoffs, summaryBase = {}) {
|
|
378
|
+
const f = path.join(sessionDir, 'HUMAN_GATE');
|
|
379
|
+
fs.writeFileSync(f, `reason: ${reason}\nat: ${new Date().toISOString()}\n`);
|
|
380
|
+
console.error(`[review] HUMAN_GATE: ${reason}`);
|
|
381
|
+
const result = {
|
|
382
|
+
sessionId,
|
|
383
|
+
sessionDir,
|
|
384
|
+
mode: 'legacy-full-review-cycle',
|
|
385
|
+
handoffs,
|
|
386
|
+
humanGate: true,
|
|
387
|
+
reason,
|
|
388
|
+
verdict: 'block',
|
|
389
|
+
stoppedAt: 'human-gate',
|
|
390
|
+
targetProjectMutated: false,
|
|
391
|
+
};
|
|
392
|
+
writeReviewSummary(sessionDir, result, summaryBase);
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function writeReviewSummary(sessionDir, result, summary = {}) {
|
|
397
|
+
fs.writeFileSync(path.join(sessionDir, 'review-summary.json'), JSON.stringify({
|
|
398
|
+
sessionId: result.sessionId,
|
|
399
|
+
task: summary.task || null,
|
|
400
|
+
mode: 'legacy-full-review-cycle',
|
|
401
|
+
compatibility_command: 'review-cycle',
|
|
402
|
+
recommended_wrapper: 'run',
|
|
403
|
+
live: Boolean(summary.live),
|
|
404
|
+
fast: Boolean(summary.fast),
|
|
405
|
+
no_ship: Boolean(summary.noShip),
|
|
406
|
+
no_codex: Boolean(summary.noCodex),
|
|
407
|
+
secure_requested: Boolean(summary.secureRequested),
|
|
408
|
+
secure_active: Boolean(result.secureActive),
|
|
409
|
+
human_gate: Boolean(result.humanGate),
|
|
410
|
+
reason: result.reason || null,
|
|
411
|
+
stopped_at: result.stoppedAt || (result.humanGate ? 'human-gate' : 'ship'),
|
|
412
|
+
verdict: result.verdict || (result.humanGate ? 'block' : 'approve'),
|
|
413
|
+
handoff_count: result.handoffs?.length || 0,
|
|
414
|
+
stages: (result.handoffs || []).map(h => h.stage),
|
|
415
|
+
files: result.files || [],
|
|
416
|
+
target_project_mutated: Boolean(result.targetProjectMutated),
|
|
417
|
+
next_step: result.humanGate
|
|
418
|
+
? 'resolve the human gate before continuing'
|
|
419
|
+
: 'prefer run/work/verify/ship for new decomposed workflows',
|
|
420
|
+
}, null, 2));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function dedupe(arr) { return [...new Set(arr)]; }
|
|
424
|
+
|
|
425
|
+
function handoffBase(h) {
|
|
426
|
+
const nn = STAGE_INDEX[h.stage] || '00';
|
|
427
|
+
const round = Number(h.round || 1);
|
|
428
|
+
const roundSuffix = round > 1 ? `-r${round}` : '';
|
|
429
|
+
return `${nn}-${h.stage}${roundSuffix}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function renderHandoff(h) {
|
|
433
|
+
const lines = [];
|
|
434
|
+
lines.push(`# Handoff: ${h.stage} (round ${h.round || 1}, agent: ${h.agent}, ${h.provider}/${h.model})`);
|
|
435
|
+
lines.push('');
|
|
436
|
+
lines.push(`**Decided**: ${h.decided || ''}`);
|
|
437
|
+
if (h.rejected) lines.push(`**Rejected**: ${h.rejected}`);
|
|
438
|
+
if (h.risks) lines.push(`**Risks**: ${h.risks}`);
|
|
439
|
+
lines.push(`**Files**: ${(h.files || []).join(', ')}`);
|
|
440
|
+
if (h.remaining) lines.push(`**Remaining**: ${h.remaining}`);
|
|
441
|
+
if (h.verdict) lines.push(`**Verdict**: ${h.verdict}${h.confidence != null ? ` (confidence ${h.confidence})` : ''}`);
|
|
442
|
+
if (h.issues?.length) {
|
|
443
|
+
lines.push('');
|
|
444
|
+
lines.push('## Issues');
|
|
445
|
+
for (const i of h.issues) {
|
|
446
|
+
lines.push(`- [${i.severity}/${i.category}] ${i.file || ''}${i.line ? ':' + i.line : ''} — ${i.summary}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
lines.push(`<sub>provider=${h.provider} model=${h.model} duration_ms=${h.duration_ms}</sub>`);
|
|
451
|
+
return lines.join('\n') + '\n';
|
|
452
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { workCycle } from './work.js';
|
|
4
|
+
import { verifyCycle } from './verify.js';
|
|
5
|
+
import { shipCycle } from './ship.js';
|
|
6
|
+
import { applyCycle } from './apply.js';
|
|
7
|
+
|
|
8
|
+
export async function runCycle(opts) {
|
|
9
|
+
const harnessRoot = opts.harnessRoot || process.cwd();
|
|
10
|
+
const projectRoot = opts.projectRoot || harnessRoot;
|
|
11
|
+
if (!opts.task) throw new Error('run requires a task');
|
|
12
|
+
|
|
13
|
+
const sessionId = opts.sessionId || `run-${Date.now()}`;
|
|
14
|
+
const base = {
|
|
15
|
+
task: opts.task,
|
|
16
|
+
sessionId,
|
|
17
|
+
harnessRoot,
|
|
18
|
+
projectRoot,
|
|
19
|
+
live: !!opts.live,
|
|
20
|
+
profile: opts.profile,
|
|
21
|
+
strictQuality: !!opts.strictQuality,
|
|
22
|
+
dispatcher: opts.dispatcher,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const work = await workCycle(base);
|
|
26
|
+
const verify = await verifyCycle({
|
|
27
|
+
...base,
|
|
28
|
+
secure: !!opts.secure,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (verify.humanGate) {
|
|
32
|
+
return finishRun({
|
|
33
|
+
sessionId,
|
|
34
|
+
sessionDir: verify.sessionDir,
|
|
35
|
+
work,
|
|
36
|
+
verify,
|
|
37
|
+
ship: null,
|
|
38
|
+
apply: null,
|
|
39
|
+
applyRequested: !!opts.apply,
|
|
40
|
+
applySkippedReason: 'human gate from verify',
|
|
41
|
+
stoppedAt: 'verify',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ship = await shipCycle(base);
|
|
46
|
+
let apply = null;
|
|
47
|
+
let applySkippedReason = null;
|
|
48
|
+
|
|
49
|
+
if (opts.apply) {
|
|
50
|
+
if (!ship.shipReady) {
|
|
51
|
+
applySkippedReason = ship.reason || 'ship is not ready';
|
|
52
|
+
} else {
|
|
53
|
+
apply = applyCycle({
|
|
54
|
+
sessionId,
|
|
55
|
+
projectRoot,
|
|
56
|
+
allowDirty: !!opts.allowDirty,
|
|
57
|
+
force: !!opts.force,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return finishRun({
|
|
63
|
+
sessionId,
|
|
64
|
+
sessionDir: ship.sessionDir,
|
|
65
|
+
work,
|
|
66
|
+
verify,
|
|
67
|
+
ship,
|
|
68
|
+
apply,
|
|
69
|
+
applyRequested: !!opts.apply,
|
|
70
|
+
applySkippedReason,
|
|
71
|
+
stoppedAt: stoppedAt({ verify, ship, apply, applyRequested: !!opts.apply, applySkippedReason }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finishRun({ sessionId, sessionDir, work, verify, ship, apply, applyRequested, applySkippedReason, stoppedAt }) {
|
|
76
|
+
const result = {
|
|
77
|
+
sessionId,
|
|
78
|
+
sessionDir,
|
|
79
|
+
stoppedAt,
|
|
80
|
+
work,
|
|
81
|
+
verify,
|
|
82
|
+
ship,
|
|
83
|
+
apply,
|
|
84
|
+
applyRequested,
|
|
85
|
+
applySkippedReason,
|
|
86
|
+
humanGate: Boolean(verify?.humanGate || ship?.humanGate || apply?.humanGate),
|
|
87
|
+
noShip: Boolean(ship?.noShip || apply?.noShip),
|
|
88
|
+
shipReady: Boolean(ship?.shipReady),
|
|
89
|
+
applied: Boolean(apply?.applied),
|
|
90
|
+
verdict: apply?.applied
|
|
91
|
+
? 'applied'
|
|
92
|
+
: ship?.verdict || verify?.verdict || work?.handoff?.verdict || 'unknown',
|
|
93
|
+
};
|
|
94
|
+
writeSummary(sessionDir, result);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stoppedAt({ verify, ship, apply, applyRequested, applySkippedReason }) {
|
|
99
|
+
if (verify?.humanGate) return 'verify';
|
|
100
|
+
if (ship?.humanGate) return 'ship';
|
|
101
|
+
if (apply?.humanGate || apply?.noShip) return 'apply';
|
|
102
|
+
if (apply?.applied || apply?.alreadyApplied) return 'apply';
|
|
103
|
+
if (applyRequested && applySkippedReason) return 'ship';
|
|
104
|
+
return 'ship';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeSummary(sessionDir, result) {
|
|
108
|
+
if (!sessionDir) return;
|
|
109
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
110
|
+
fs.writeFileSync(path.join(sessionDir, 'run-summary.json'), JSON.stringify({
|
|
111
|
+
sessionId: result.sessionId,
|
|
112
|
+
stopped_at: result.stoppedAt,
|
|
113
|
+
work_round: result.work?.round || null,
|
|
114
|
+
work_files: result.work?.files || [],
|
|
115
|
+
acceptance_required: true,
|
|
116
|
+
acceptance_count: result.work?.handoff ? readAcceptanceCount(sessionDir) : 0,
|
|
117
|
+
profile: result.work?.handoff?.profile || result.verify?.profile || null,
|
|
118
|
+
strict_quality: Boolean(result.verify?.strictQuality),
|
|
119
|
+
strict_quality_blocked: Boolean(result.verify?.strictQualityBlocked),
|
|
120
|
+
verify_verdict: result.verify?.verdict || null,
|
|
121
|
+
verify_human_gate: Boolean(result.verify?.humanGate),
|
|
122
|
+
ship_ready: result.shipReady,
|
|
123
|
+
no_ship: result.noShip,
|
|
124
|
+
human_gate: result.humanGate,
|
|
125
|
+
apply_requested: result.applyRequested,
|
|
126
|
+
apply_skipped_reason: result.applySkippedReason,
|
|
127
|
+
applied: result.applied,
|
|
128
|
+
verdict: result.verdict,
|
|
129
|
+
target_project_mutated: result.applied,
|
|
130
|
+
next_step: nextStep(result),
|
|
131
|
+
}, null, 2));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readAcceptanceCount(sessionDir) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = fs.readFileSync(path.join(sessionDir, 'acceptance-criteria.json'), 'utf8');
|
|
137
|
+
const parsed = JSON.parse(raw);
|
|
138
|
+
return Array.isArray(parsed.criteria) ? parsed.criteria.length : 0;
|
|
139
|
+
} catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function nextStep(result) {
|
|
145
|
+
if (result.humanGate) return 'resolve the human gate before continuing';
|
|
146
|
+
if (result.noShip) return 'fix findings, rerun verify, then rerun run/ship';
|
|
147
|
+
if (result.applied) return 'review git diff, run project tests, then commit manually';
|
|
148
|
+
if (result.applyRequested && result.applySkippedReason) return 'resolve ship readiness before applying';
|
|
149
|
+
if (result.shipReady) return 'optionally run apply for live captured diffs';
|
|
150
|
+
return 'inspect run-summary.json';
|
|
151
|
+
}
|