@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,143 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildDefaultAcceptanceCriteria } from '../lib/acceptance-criteria.js';
4
+ import { classifyRisk } from '../lib/risk-classifier.js';
5
+ import { profilePolicy } from '../lib/profile-policy.js';
6
+
7
+ export function classifyAskTask(task = '') {
8
+ const classification = classifyRisk({ task, files: [] });
9
+ return {
10
+ risk: classification.risk,
11
+ tags: classification.tags,
12
+ requiresCodexChallenge: classification.requiresCodexChallenge,
13
+ requiresHumanGate: classification.requiresHumanGate,
14
+ };
15
+ }
16
+
17
+ export function buildQuestionGate(task = '', opts = {}) {
18
+ const classification = classifyAskTask(task);
19
+ const policy = profilePolicy(opts.profile);
20
+ const questions = [
21
+ 'What outcome should count as done?',
22
+ 'Who is the target user or operator, and what problem should this solve?',
23
+ 'What is the smallest MVP scope for this cycle?',
24
+ 'What is explicitly out of scope?',
25
+ 'What files, surfaces, or user flows are allowed to change?',
26
+ 'What launch or readiness risk should block ship?',
27
+ ];
28
+
29
+ if (task.trim().split(/\s+/).filter(Boolean).length <= 6) {
30
+ questions.push('Should NEKOWORK proceed with reasonable assumptions, or wait for more detail?');
31
+ }
32
+ if (classification.tags.includes('product-ui')) {
33
+ questions.push('Is this a mock/demo UI, or should it be production-ready behavior?');
34
+ questions.push('What target user and required screen states should guide the UI?');
35
+ questions.push('What UX confusion or unsafe user assumption should the design prevent?');
36
+ }
37
+ if (classification.tags.includes('financial')) {
38
+ questions.push('Must all broker/order/payment behavior stay mock-only for this cycle?');
39
+ questions.push('What warning or demo-only labeling is required to avoid real-money confusion?');
40
+ }
41
+ if (classification.tags.includes('security')) {
42
+ questions.push('Which auth, token, permission, or secret boundaries are allowed to change?');
43
+ questions.push('Should Codex challenge and human gate be mandatory even if review passes?');
44
+ }
45
+ if (classification.tags.includes('deploy')) {
46
+ questions.push('Is production deployment explicitly allowed, or should this stop at release readiness?');
47
+ }
48
+ if (classification.tags.includes('data')) {
49
+ questions.push('Is data loss possible, and what backup or rollback condition is required?');
50
+ }
51
+ if (policy.profile === 'product') {
52
+ questions.push('What non-goal should be protected if scope pressure appears?');
53
+ questions.push('What QA acceptance criteria would make this launch-ready?');
54
+ }
55
+ if (policy.profile === 'quality') {
56
+ questions.push('What test-first plan should exist before implementation starts?');
57
+ questions.push('What evidence should prove each acceptance criterion passed?');
58
+ }
59
+
60
+ return {
61
+ stage: 'question-gate',
62
+ agent: 'question-gate',
63
+ round: 1,
64
+ timestamp: new Date().toISOString(),
65
+ provider: 'local',
66
+ model: 'deterministic',
67
+ profile: policy.profile || undefined,
68
+ decided: 'Question gate only. No provider call, shell command, or project file mutation is required.',
69
+ rejected: 'Implementation, live provider execution, shipping, and multi-worker file writes are out of scope for ask.',
70
+ risks: `risk=${classification.risk}; tags=${classification.tags.length ? classification.tags.join(',') : 'none'}; human_gate=${classification.requiresHumanGate ? 'required-if-continuing' : 'not-required-by-ask'}`,
71
+ files: [],
72
+ remaining: 'Answer the blocking questions or continue to plan with documented assumptions.',
73
+ risk_level: classification.risk,
74
+ questions,
75
+ success_criteria: buildDefaultAcceptanceCriteria(task, 3),
76
+ assumptions: [
77
+ 'All ambiguous work defaults to no-ship.',
78
+ 'Multi-worker phases are read-only unless a later work/review cycle grants one executor write authority.',
79
+ 'Secure, financial, deploy, or destructive data work cannot bypass Codex verification and human gate policy.',
80
+ ],
81
+ requires_human_gate: classification.requiresHumanGate,
82
+ };
83
+ }
84
+
85
+ export async function askGate(opts) {
86
+ const harnessRoot = opts.harnessRoot || process.cwd();
87
+ const projectRoot = opts.projectRoot || harnessRoot;
88
+ const sessionId = opts.sessionId || `ask-${Date.now()}`;
89
+ const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
90
+ const handoffDir = path.join(sessionDir, 'handoffs');
91
+ fs.mkdirSync(handoffDir, { recursive: true });
92
+
93
+ const handoff = buildQuestionGate(opts.task || '', { profile: opts.profile });
94
+ handoff.session_id = sessionId;
95
+ writeAskArtifacts(sessionDir, handoffDir, sessionId, opts.task || '', handoff);
96
+
97
+ return {
98
+ sessionId,
99
+ sessionDir,
100
+ handoff,
101
+ };
102
+ }
103
+
104
+ function writeAskArtifacts(sessionDir, handoffDir, sessionId, task, handoff) {
105
+ const policy = profilePolicy(handoff.profile);
106
+ fs.writeFileSync(path.join(sessionDir, 'ask.json'), JSON.stringify({
107
+ sessionId,
108
+ task,
109
+ generated_at: handoff.timestamp,
110
+ risk_level: handoff.risk_level,
111
+ profile: handoff.profile || null,
112
+ profile_checklist: policy.checklist,
113
+ requires_human_gate: handoff.requires_human_gate,
114
+ questions: handoff.questions,
115
+ success_criteria: handoff.success_criteria,
116
+ assumptions: handoff.assumptions,
117
+ }, null, 2));
118
+
119
+ fs.writeFileSync(path.join(handoffDir, '00-question-gate.json'), JSON.stringify(handoff, null, 2));
120
+ fs.writeFileSync(path.join(handoffDir, '00-question-gate.md'), renderQuestionGate(handoff));
121
+ }
122
+
123
+ function renderQuestionGate(h) {
124
+ return [
125
+ '# Handoff: question-gate',
126
+ '',
127
+ `Decided: ${h.decided}`,
128
+ `Rejected: ${h.rejected}`,
129
+ `Risks: ${h.risks}`,
130
+ 'Files: ',
131
+ `Remaining: ${h.remaining}`,
132
+ '',
133
+ 'Questions:',
134
+ ...h.questions.map((q, i) => `${i + 1}. ${q}`),
135
+ '',
136
+ 'Draft success criteria:',
137
+ ...h.success_criteria.map(ac => `- ${ac.id}: ${ac.desc}`),
138
+ '',
139
+ 'Assumptions:',
140
+ ...h.assumptions.map((a, i) => `${i + 1}. ${a}`),
141
+ '',
142
+ ].join('\n');
143
+ }
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const MARKERS = {
5
+ human: 'HUMAN_GATE',
6
+ approved: 'GATE_APPROVED',
7
+ blocked: 'GATE_BLOCKED',
8
+ };
9
+
10
+ export function gateCommand(opts) {
11
+ const action = opts.action || 'status';
12
+ if (action === 'status') return gateStatus(opts);
13
+ if (action === 'approve') return approveGate(opts);
14
+ if (action === 'block') return blockGate(opts);
15
+ throw new Error(`unknown gate action: ${action}`);
16
+ }
17
+
18
+ export function gateStatus(opts) {
19
+ const projectRoot = opts.projectRoot || process.cwd();
20
+ if (!opts.sessionId) throw new Error('gate requires --session <id>');
21
+
22
+ const sessionId = opts.sessionId;
23
+ const sessionDir = sessionPath(projectRoot, sessionId);
24
+ if (!fs.existsSync(sessionDir)) {
25
+ return {
26
+ sessionId,
27
+ sessionDir,
28
+ status: 'missing',
29
+ humanGate: false,
30
+ approved: false,
31
+ blocked: false,
32
+ reason: 'session not found',
33
+ humanGateReason: null,
34
+ approvalReason: null,
35
+ blockReason: null,
36
+ };
37
+ }
38
+
39
+ const human = readMarker(sessionDir, MARKERS.human);
40
+ const approval = readMarker(sessionDir, MARKERS.approved);
41
+ const block = readMarker(sessionDir, MARKERS.blocked);
42
+
43
+ let status = 'clear';
44
+ let reason = null;
45
+ if (block) {
46
+ status = 'blocked';
47
+ reason = block.reason;
48
+ } else if (human && (!approval || markerTime(approval) < markerTime(human))) {
49
+ status = 'open';
50
+ reason = human.reason;
51
+ } else if (approval && (!human || markerTime(approval) >= markerTime(human))) {
52
+ status = 'approved';
53
+ reason = approval.reason;
54
+ }
55
+
56
+ return {
57
+ sessionId,
58
+ sessionDir,
59
+ status,
60
+ humanGate: Boolean(human) && status !== 'approved',
61
+ approved: status === 'approved',
62
+ blocked: status === 'blocked',
63
+ reason,
64
+ humanGateReason: human?.reason || null,
65
+ approvalReason: approval?.reason || null,
66
+ blockReason: block?.reason || null,
67
+ humanGateAt: human?.at || null,
68
+ approvalAt: approval?.at || null,
69
+ blockAt: block?.at || null,
70
+ };
71
+ }
72
+
73
+ export function approveGate(opts) {
74
+ const base = gateStatus(opts);
75
+ if (base.status === 'missing') throw new Error('gate approve requires an existing session');
76
+ if (base.status === 'blocked') throw new Error('gate approve cannot override an explicit gate block');
77
+ if (base.status !== 'open') throw new Error('gate approve requires an open HUMAN_GATE');
78
+ if (!String(opts.reason || '').trim()) throw new Error('gate approve requires --reason <text>');
79
+
80
+ const sessionDir = base.sessionDir;
81
+ writeMarker(sessionDir, MARKERS.approved, {
82
+ reason: opts.reason,
83
+ humanGateReason: base.humanGateReason,
84
+ });
85
+ appendEvent(sessionDir, {
86
+ event: 'approve',
87
+ reason: opts.reason,
88
+ humanGateReason: base.humanGateReason,
89
+ });
90
+ const result = gateStatus(opts);
91
+ writeSummary(sessionDir, result, 'approve');
92
+ return result;
93
+ }
94
+
95
+ export function blockGate(opts) {
96
+ const projectRoot = opts.projectRoot || process.cwd();
97
+ if (!opts.sessionId) throw new Error('gate requires --session <id>');
98
+ if (!String(opts.reason || '').trim()) throw new Error('gate block requires --reason <text>');
99
+
100
+ const sessionDir = sessionPath(projectRoot, opts.sessionId);
101
+ if (!fs.existsSync(sessionDir)) throw new Error('gate block requires an existing session');
102
+
103
+ const reason = opts.reason;
104
+ writeMarker(sessionDir, MARKERS.human, { reason: `manual block: ${reason}` });
105
+ writeMarker(sessionDir, MARKERS.blocked, { reason });
106
+ appendEvent(sessionDir, { event: 'block', reason });
107
+ const result = gateStatus(opts);
108
+ writeSummary(sessionDir, result, 'block');
109
+ return result;
110
+ }
111
+
112
+ function sessionPath(projectRoot, sessionId) {
113
+ return path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
114
+ }
115
+
116
+ function readMarker(sessionDir, name) {
117
+ const file = path.join(sessionDir, name);
118
+ if (!fs.existsSync(file)) return null;
119
+ const raw = fs.readFileSync(file, 'utf8');
120
+ return {
121
+ kind: name,
122
+ file,
123
+ raw,
124
+ reason: raw.match(/^reason:\s*(.+)$/m)?.[1] || null,
125
+ at: raw.match(/^at:\s*(.+)$/m)?.[1] || null,
126
+ humanGateReason: raw.match(/^human_gate_reason:\s*(.+)$/m)?.[1] || null,
127
+ };
128
+ }
129
+
130
+ function writeMarker(sessionDir, name, fields) {
131
+ const lines = [];
132
+ lines.push(`reason: ${fields.reason}`);
133
+ if (fields.humanGateReason) lines.push(`human_gate_reason: ${fields.humanGateReason}`);
134
+ lines.push(`at: ${new Date().toISOString()}`);
135
+ fs.writeFileSync(path.join(sessionDir, name), lines.join('\n') + '\n');
136
+ }
137
+
138
+ function appendEvent(sessionDir, event) {
139
+ const row = {
140
+ ...event,
141
+ at: new Date().toISOString(),
142
+ };
143
+ fs.appendFileSync(path.join(sessionDir, 'gate-events.jsonl'), JSON.stringify(row) + '\n');
144
+ }
145
+
146
+ function writeSummary(sessionDir, result, action) {
147
+ fs.writeFileSync(path.join(sessionDir, 'gate-summary.json'), JSON.stringify({
148
+ sessionId: result.sessionId,
149
+ action,
150
+ status: result.status,
151
+ human_gate: result.humanGate,
152
+ approved: result.approved,
153
+ blocked: result.blocked,
154
+ reason: result.reason,
155
+ human_gate_reason: result.humanGateReason,
156
+ approval_reason: result.approvalReason,
157
+ block_reason: result.blockReason,
158
+ target_project_mutated: false,
159
+ next_step: nextStep(result),
160
+ }, null, 2));
161
+ }
162
+
163
+ function nextStep(result) {
164
+ if (result.status === 'approved') return 'rerun ship for readiness';
165
+ if (result.status === 'blocked') return 'do not ship; revise work or start a new cycle';
166
+ if (result.status === 'open') return 'human must approve or block the gate';
167
+ if (result.status === 'missing') return 'create a work/verify session first';
168
+ return 'continue to verify or ship';
169
+ }
170
+
171
+ function markerTime(marker) {
172
+ const time = Date.parse(marker?.at || '');
173
+ return Number.isFinite(time) ? time : 0;
174
+ }
175
+
176
+ export {
177
+ readMarker as _readMarker,
178
+ markerTime as _markerTime,
179
+ };
@@ -0,0 +1,179 @@
1
+ // Persistent Ralph loop. It repeats an execution engine until PRD acceptance
2
+ // criteria pass, a human gate stops the run, cost cap is hit, or max-iter ends.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { reviewCycle } from './review.js';
7
+ import { runCycle } from './run.js';
8
+ import { list as costList, summarize as costSummarize } from '../lib/costs.js';
9
+
10
+ const DEFAULT_MAX_ITER = Number(process.env.HARNESS_RALPH_MAX_ITER || 5);
11
+ const DAILY_CAP_USD = Number(process.env.HARNESS_DAILY_COST_CAP_USD || 0);
12
+ const VALID_ENGINES = new Set(['review', 'legacy-review', 'run']);
13
+
14
+ export async function ralphLoop(opts) {
15
+ const harnessRoot = opts.harnessRoot || process.cwd();
16
+ const projectRoot = opts.projectRoot || harnessRoot;
17
+ const sessionId = opts.sessionId || `ralph-${Date.now()}`;
18
+ const maxIter = Math.max(1, Number(opts.maxIter || DEFAULT_MAX_ITER));
19
+ const engine = normalizeEngine(opts.engine || 'review');
20
+
21
+ const sessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', sessionId);
22
+ fs.mkdirSync(sessionDir, { recursive: true });
23
+ fs.writeFileSync(path.join(sessionDir, 'active'), renderActiveFile({
24
+ started_at: new Date().toISOString(),
25
+ mode: 'ralph',
26
+ engine,
27
+ task: opts.task,
28
+ max_iter: maxIter,
29
+ live: Boolean(opts.live),
30
+ secure: Boolean(opts.secure),
31
+ }));
32
+
33
+ const log = (m) => console.log(`[ralph:${sessionId}] ${m}`);
34
+ const progress = (line) => fs.appendFileSync(path.join(sessionDir, 'progress.txt'), `[${new Date().toISOString()}] ${line}\n`);
35
+
36
+ log(`start: max-iter=${maxIter}, engine=${engine}, task="${opts.task}"`);
37
+
38
+ let iter = 0;
39
+ let result;
40
+ const iterationSessions = [];
41
+ while (iter < maxIter) {
42
+ iter++;
43
+ log(`iter ${iter}/${maxIter}`);
44
+ progress(`iter ${iter} start (${engine})`);
45
+
46
+ if (DAILY_CAP_USD > 0) {
47
+ const today = costList({ since: '24h' });
48
+ const sum = costSummarize(today);
49
+ if (sum.total_usd >= DAILY_CAP_USD) {
50
+ log(`STOP: daily cost cap $${DAILY_CAP_USD} reached (current $${sum.total_usd})`);
51
+ progress('STOP cost_cap');
52
+ try { fs.unlinkSync(path.join(sessionDir, 'active')); } catch {}
53
+ return finish(sessionDir, 'cost_cap', { engine, iter, iteration_sessions: iterationSessions, sum });
54
+ }
55
+ }
56
+
57
+ const iterSessionId = `${sessionId}-i${iter}`;
58
+ iterationSessions.push(iterSessionId);
59
+ result = await runIteration({
60
+ engine,
61
+ opts,
62
+ harnessRoot,
63
+ projectRoot,
64
+ sessionDir,
65
+ iterSessionId,
66
+ });
67
+
68
+ if (result.humanGate) {
69
+ log(`STOP: HUMAN_GATE - ${result.reason}`);
70
+ progress(`STOP human_gate ${result.reason}`);
71
+ try { fs.unlinkSync(path.join(sessionDir, 'active')); } catch {}
72
+ fs.writeFileSync(path.join(sessionDir, 'HUMAN_GATE'), `reason: ${result.reason}\nfrom: ${result.sessionId}\n`);
73
+ return finish(sessionDir, 'human_gate', { engine, iter, iteration_sessions: iterationSessions, last: result });
74
+ }
75
+
76
+ const ralphPrd = path.join(sessionDir, 'prd.json');
77
+ if (!fs.existsSync(ralphPrd)) {
78
+ const iterationPrd = path.join(result.sessionDir, 'prd.json');
79
+ if (fs.existsSync(iterationPrd)) fs.copyFileSync(iterationPrd, ralphPrd);
80
+ else fs.writeFileSync(ralphPrd, JSON.stringify(defaultPrd(opts.task), null, 2));
81
+ }
82
+
83
+ const prd = JSON.parse(fs.readFileSync(ralphPrd, 'utf8'));
84
+ const next = (prd.acceptance || []).find(a => !a.passes);
85
+ if (next) {
86
+ next.passes = true;
87
+ fs.writeFileSync(ralphPrd, JSON.stringify(prd, null, 2));
88
+ progress(`AC ${next.id} -> passes:true`);
89
+ }
90
+
91
+ const total = (prd.acceptance || []).length;
92
+ const passed = (prd.acceptance || []).filter(a => a.passes).length;
93
+ log(`AC progress: ${passed}/${total} PASS`);
94
+ if (passed === total && total > 0) {
95
+ log('all AC PASS -> done');
96
+ progress('STOP all_ac_passed');
97
+ try { fs.unlinkSync(path.join(sessionDir, 'active')); } catch {}
98
+ return finish(sessionDir, 'all_passed', { engine, iter, iteration_sessions: iterationSessions, last: result });
99
+ }
100
+ }
101
+
102
+ log(`STOP: max-iter ${maxIter} reached`);
103
+ progress('STOP max_iter');
104
+ try { fs.unlinkSync(path.join(sessionDir, 'active')); } catch {}
105
+ return finish(sessionDir, 'max_iter', { engine, iter, iteration_sessions: iterationSessions, last: result });
106
+ }
107
+
108
+ async function runIteration({ engine, opts, harnessRoot, projectRoot, sessionDir, iterSessionId }) {
109
+ if (engine === 'run') {
110
+ const iterSessionDir = path.join(projectRoot, '.harness', 'state', 'sessions', iterSessionId);
111
+ fs.mkdirSync(iterSessionDir, { recursive: true });
112
+ syncRalphPrdToIteration(sessionDir, iterSessionDir, opts.task);
113
+ return runCycle({
114
+ task: opts.task,
115
+ live: opts.live,
116
+ secure: opts.secure,
117
+ apply: false,
118
+ sessionId: iterSessionId,
119
+ harnessRoot,
120
+ projectRoot,
121
+ dispatcher: opts.dispatcher,
122
+ });
123
+ }
124
+
125
+ return reviewCycle({
126
+ task: opts.task,
127
+ live: opts.live,
128
+ secure: opts.secure,
129
+ noShip: true,
130
+ sessionId: iterSessionId,
131
+ harnessRoot,
132
+ projectRoot,
133
+ });
134
+ }
135
+
136
+ function normalizeEngine(engine) {
137
+ const value = String(engine || '').trim();
138
+ if (!VALID_ENGINES.has(value)) {
139
+ throw new Error(`unknown ralph engine: ${engine}. Use "review" or "run".`);
140
+ }
141
+ return value === 'legacy-review' ? 'review' : value;
142
+ }
143
+
144
+ function syncRalphPrdToIteration(ralphSessionDir, iterSessionDir, task) {
145
+ const ralphPrd = path.join(ralphSessionDir, 'prd.json');
146
+ if (!fs.existsSync(ralphPrd)) {
147
+ fs.writeFileSync(ralphPrd, JSON.stringify(defaultPrd(task), null, 2));
148
+ }
149
+ fs.copyFileSync(ralphPrd, path.join(iterSessionDir, 'prd.json'));
150
+ }
151
+
152
+ function defaultPrd(task) {
153
+ return {
154
+ task,
155
+ acceptance: [
156
+ { id: 'AC-001', desc: 'Primary happy path is handled', passes: false },
157
+ { id: 'AC-002', desc: 'Failure or edge cases are handled', passes: false },
158
+ { id: 'AC-003', desc: 'Verification and handoff artifacts are present', passes: false },
159
+ ],
160
+ non_goals: [],
161
+ };
162
+ }
163
+
164
+ function finish(sessionDir, reason, extras) {
165
+ const summary = { reason, finished_at: new Date().toISOString(), ...extras };
166
+ fs.writeFileSync(path.join(sessionDir, 'ralph-summary.json'), JSON.stringify(summary, null, 2));
167
+ return summary;
168
+ }
169
+
170
+ function renderActiveFile(fields) {
171
+ return Object.entries(fields)
172
+ .map(([key, value]) => `${key}: ${typeof value === 'string' ? JSON.stringify(value) : value}`)
173
+ .join('\n') + '\n';
174
+ }
175
+
176
+ export {
177
+ normalizeEngine as _normalizeEngine,
178
+ defaultPrd as _defaultPrd,
179
+ };