@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.
Files changed (203) hide show
  1. package/AGENTS.md +112 -0
  2. package/CLAUDE.md +81 -0
  3. package/LICENSE +21 -0
  4. package/README.md +283 -0
  5. package/REVIEW.md +96 -0
  6. package/RULES.md +51 -0
  7. package/SOUL.md +21 -0
  8. package/WORKING-CONTEXT.md +52 -0
  9. package/agent.yaml +219 -0
  10. package/agents/architect.md +57 -0
  11. package/agents/code-reviewer.md +60 -0
  12. package/agents/codex-challenger.md +53 -0
  13. package/agents/codex-reviewer.md +56 -0
  14. package/agents/debugger.md +33 -0
  15. package/agents/doc-writer.md +51 -0
  16. package/agents/executor.md +41 -0
  17. package/agents/planner.md +49 -0
  18. package/agents/research.md +50 -0
  19. package/agents/security-reviewer.md +47 -0
  20. package/agents/test-engineer.md +41 -0
  21. package/bridge/mcp-server.js +301 -0
  22. package/commands/claude-led-codex-review.md +29 -0
  23. package/docs/ADVANCED.md +321 -0
  24. package/docs/AI-DEVELOPMENT-LIFECYCLE.md +105 -0
  25. package/docs/ARCHITECTURE.md +205 -0
  26. package/docs/AUDIT.md +114 -0
  27. package/docs/AUTH-MIGRATION.md +282 -0
  28. package/docs/CHANGELOG.md +97 -0
  29. package/docs/CLI-STAGES.md +89 -0
  30. package/docs/CODEMAPS/README.md +15 -0
  31. package/docs/CODEMAPS/agents.md +22 -0
  32. package/docs/CODEMAPS/bridge.md +18 -0
  33. package/docs/CODEMAPS/hooks.md +28 -0
  34. package/docs/CODEMAPS/manifests.md +14 -0
  35. package/docs/CODEMAPS/rules.md +22 -0
  36. package/docs/CODEMAPS/schemas.md +21 -0
  37. package/docs/CODEMAPS/scripts.md +158 -0
  38. package/docs/CODEMAPS/skills.md +29 -0
  39. package/docs/CODEMAPS/tests.md +98 -0
  40. package/docs/CORE-INVARIANTS.md +38 -0
  41. package/docs/DEMO.md +110 -0
  42. package/docs/EXAMPLE-PROJECT.md +92 -0
  43. package/docs/PORTING.md +154 -0
  44. package/docs/PRODUCT-PRINCIPLES.md +303 -0
  45. package/docs/PUBLISH-ALPHA.md +106 -0
  46. package/docs/QUICKSTART.md +344 -0
  47. package/docs/RELEASE-READINESS.md +140 -0
  48. package/docs/RISK-CLASSIFIER.md +50 -0
  49. package/docs/RUNBOOK.md +146 -0
  50. package/docs/SECURITY.md +79 -0
  51. package/docs/SETUP.md +142 -0
  52. package/docs/WHY-NEKOWORK.md +64 -0
  53. package/docs/case-studies/README.md +16 -0
  54. package/docs/case-studies/SINDRESORHUS-IS-PLAIN-OBJ.md +141 -0
  55. package/docs/dev-log/2026-04-29-p1-recovery.md +142 -0
  56. package/docs/dev-log/2026-04-29-week1-4.md +81 -0
  57. package/docs/examples/GITHUB-ACTIONS-HARDENING.md +86 -0
  58. package/docs/examples/QUALITY-LIFECYCLE-SMOKE.md +32 -0
  59. package/docs/examples/TRADING-DASHBOARD-MOCK.md +65 -0
  60. package/docs/workflows-stash/README.md +32 -0
  61. package/docs/workflows-stash/harness-review.yml +166 -0
  62. package/docs/workflows-stash/harness-validate.yml +48 -0
  63. package/examples/github-actions-hardening/.github/workflows/hardened-validate.yml +38 -0
  64. package/examples/github-actions-hardening/README.md +31 -0
  65. package/examples/github-actions-hardening/case-study/ASK.md +26 -0
  66. package/examples/github-actions-hardening/case-study/GATE_STATUS.md +28 -0
  67. package/examples/github-actions-hardening/case-study/PLAN.md +25 -0
  68. package/examples/github-actions-hardening/case-study/SHIP_READY.md +21 -0
  69. package/examples/github-actions-hardening/case-study/TASK.md +30 -0
  70. package/examples/github-actions-hardening/case-study/TEAM_HANDOFFS.md +37 -0
  71. package/examples/github-actions-hardening/case-study/VERIFY_SUMMARY.md +35 -0
  72. package/examples/github-actions-hardening/case-study/WORK_SUMMARY.md +24 -0
  73. package/examples/github-actions-hardening/package.json +12 -0
  74. package/examples/github-actions-hardening/scripts/check.mjs +43 -0
  75. package/examples/quality-lifecycle-smoke/README.md +30 -0
  76. package/examples/quality-lifecycle-smoke/case-study/ASK.md +24 -0
  77. package/examples/quality-lifecycle-smoke/case-study/GATE_STATUS.md +10 -0
  78. package/examples/quality-lifecycle-smoke/case-study/PLAN.md +19 -0
  79. package/examples/quality-lifecycle-smoke/case-study/SHIP_READY.md +11 -0
  80. package/examples/quality-lifecycle-smoke/case-study/TASK.md +19 -0
  81. package/examples/quality-lifecycle-smoke/case-study/TEAM_HANDOFFS.md +21 -0
  82. package/examples/quality-lifecycle-smoke/case-study/VERIFY_SUMMARY.md +44 -0
  83. package/examples/quality-lifecycle-smoke/case-study/WORK_SUMMARY.md +19 -0
  84. package/examples/quality-lifecycle-smoke/package.json +8 -0
  85. package/examples/quality-lifecycle-smoke/scripts/check.mjs +44 -0
  86. package/examples/trading-dashboard-mock/README.md +33 -0
  87. package/examples/trading-dashboard-mock/case-study/ASK.md +24 -0
  88. package/examples/trading-dashboard-mock/case-study/GATE_STATUS.md +28 -0
  89. package/examples/trading-dashboard-mock/case-study/PLAN.md +23 -0
  90. package/examples/trading-dashboard-mock/case-study/SHIP_READY.md +21 -0
  91. package/examples/trading-dashboard-mock/case-study/TASK.md +29 -0
  92. package/examples/trading-dashboard-mock/case-study/TEAM_HANDOFFS.md +49 -0
  93. package/examples/trading-dashboard-mock/case-study/VERIFY_SUMMARY.md +35 -0
  94. package/examples/trading-dashboard-mock/case-study/WORK_SUMMARY.md +27 -0
  95. package/examples/trading-dashboard-mock/fixtures/market.json +9 -0
  96. package/examples/trading-dashboard-mock/index.html +76 -0
  97. package/examples/trading-dashboard-mock/package.json +9 -0
  98. package/examples/trading-dashboard-mock/scripts/check.mjs +54 -0
  99. package/examples/trading-dashboard-mock/src/app.js +83 -0
  100. package/examples/trading-dashboard-mock/src/styles.css +227 -0
  101. package/hooks/hooks.json +44 -0
  102. package/hooks/scripts/config-protection.js +34 -0
  103. package/hooks/scripts/gateguard-fact-force.js +146 -0
  104. package/hooks/scripts/persistent-mode.mjs +27 -0
  105. package/hooks/scripts/pre-bash-dispatcher.js +63 -0
  106. package/hooks/scripts/quality-gate.js +106 -0
  107. package/manifests/install-components.json +195 -0
  108. package/manifests/install-modules.json +101 -0
  109. package/manifests/install-profiles.json +134 -0
  110. package/package.json +96 -0
  111. package/rules/common/coding-style.md +71 -0
  112. package/rules/common/security.md +69 -0
  113. package/rules/common/testing.md +58 -0
  114. package/rules/python/coding-style.md +80 -0
  115. package/rules/python/testing.md +86 -0
  116. package/rules/typescript/coding-style.md +97 -0
  117. package/rules/typescript/security.md +67 -0
  118. package/rules/typescript/testing.md +78 -0
  119. package/schemas/agent-yaml.schema.json +168 -0
  120. package/schemas/agent.schema.json +32 -0
  121. package/schemas/handoff.schema.json +105 -0
  122. package/schemas/hooks.schema.json +35 -0
  123. package/schemas/install-components.schema.json +46 -0
  124. package/schemas/install-modules.schema.json +39 -0
  125. package/schemas/install-profiles.schema.json +32 -0
  126. package/schemas/install-state.schema.json +42 -0
  127. package/schemas/routing.schema.json +42 -0
  128. package/schemas/skill.schema.json +19 -0
  129. package/scripts/agents/dispatch.js +144 -0
  130. package/scripts/agents/runners/claude.js +214 -0
  131. package/scripts/agents/runners/codex.js +233 -0
  132. package/scripts/agents/runners/gemini.js +92 -0
  133. package/scripts/agents/runners/mock.js +107 -0
  134. package/scripts/auth/github-import-gh.js +52 -0
  135. package/scripts/auth/github-login.js +79 -0
  136. package/scripts/auth/github-logout.js +21 -0
  137. package/scripts/auth/github-status.js +46 -0
  138. package/scripts/build-claude.js +101 -0
  139. package/scripts/build-codemaps.js +286 -0
  140. package/scripts/build-codex.js +93 -0
  141. package/scripts/build-cursor.js +132 -0
  142. package/scripts/build-gemini.js +117 -0
  143. package/scripts/build-opencode.js +117 -0
  144. package/scripts/ci/catalog.js +120 -0
  145. package/scripts/ci/check-markers.js +48 -0
  146. package/scripts/ci/security-hardening.js +270 -0
  147. package/scripts/ci/validate-agents.js +88 -0
  148. package/scripts/ci/validate-hooks.js +99 -0
  149. package/scripts/ci/validate-manifests.js +128 -0
  150. package/scripts/ci/validate-skills.js +93 -0
  151. package/scripts/cli.js +1134 -0
  152. package/scripts/core/auth-guard.js +22 -0
  153. package/scripts/core/build-roots.js +11 -0
  154. package/scripts/core/cli-resolver.js +64 -0
  155. package/scripts/core/execution-workspace.js +84 -0
  156. package/scripts/core/git-mutation-guard.js +79 -0
  157. package/scripts/core/install-state.js +125 -0
  158. package/scripts/core/json-extractor.js +32 -0
  159. package/scripts/core/subprocess.js +74 -0
  160. package/scripts/daemon/wait.js +278 -0
  161. package/scripts/demo-external-project.js +222 -0
  162. package/scripts/demo-quick-run.js +193 -0
  163. package/scripts/demo-review.js +204 -0
  164. package/scripts/doctor.js +296 -0
  165. package/scripts/install-apply.js +185 -0
  166. package/scripts/install-plan.js +411 -0
  167. package/scripts/lib/acceptance-criteria.js +105 -0
  168. package/scripts/lib/costs.js +82 -0
  169. package/scripts/lib/instincts.js +194 -0
  170. package/scripts/lib/keychain.js +85 -0
  171. package/scripts/lib/profile-policy.js +134 -0
  172. package/scripts/lib/profile-safety.js +81 -0
  173. package/scripts/lib/risk-classifier.js +145 -0
  174. package/scripts/lib/router.js +138 -0
  175. package/scripts/lib/severity.js +99 -0
  176. package/scripts/lib/token-vault.js +136 -0
  177. package/scripts/orchestrators/apply.js +225 -0
  178. package/scripts/orchestrators/ask.js +143 -0
  179. package/scripts/orchestrators/gate.js +179 -0
  180. package/scripts/orchestrators/ralph.js +179 -0
  181. package/scripts/orchestrators/review.js +452 -0
  182. package/scripts/orchestrators/run.js +151 -0
  183. package/scripts/orchestrators/ship.js +339 -0
  184. package/scripts/orchestrators/team-lite.js +270 -0
  185. package/scripts/orchestrators/team.js +244 -0
  186. package/scripts/orchestrators/verify.js +306 -0
  187. package/scripts/orchestrators/work.js +207 -0
  188. package/scripts/portability/simulate-port.js +220 -0
  189. package/scripts/repair.js +184 -0
  190. package/scripts/sync-claude-md.js +220 -0
  191. package/scripts/verify/claude-live.js +30 -0
  192. package/scripts/verify/codex-live.js +60 -0
  193. package/scripts/verify/gemini-live.js +48 -0
  194. package/scripts/verify/runtime.js +105 -0
  195. package/skills/claude-led-codex-review/SKILL.md +133 -0
  196. package/skills/plan-eng-review/SKILL.md +51 -0
  197. package/skills/porting/SKILL.md +69 -0
  198. package/skills/ralph/SKILL.md +48 -0
  199. package/skills/release-readiness/SKILL.md +62 -0
  200. package/skills/review/SKILL.md +42 -0
  201. package/skills/security-hardening/SKILL.md +59 -0
  202. package/skills/ship/SKILL.md +44 -0
  203. 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
+ }