@nomos-arc/arc 0.1.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 (160) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.nomos-config.json +5 -0
  3. package/CLAUDE.md +108 -0
  4. package/LICENSE +190 -0
  5. package/README.md +569 -0
  6. package/dist/cli.js +21120 -0
  7. package/docs/auth/googel_plan.yaml +1093 -0
  8. package/docs/auth/google_task.md +235 -0
  9. package/docs/auth/hardened_blueprint.yaml +1658 -0
  10. package/docs/auth/red_team_report.yaml +336 -0
  11. package/docs/auth/session_state.yaml +162 -0
  12. package/docs/certificate/cer_enhance_plan.md +605 -0
  13. package/docs/certificate/certificate_report.md +338 -0
  14. package/docs/dev_overview.md +419 -0
  15. package/docs/feature_assessment.md +156 -0
  16. package/docs/how_it_works.md +78 -0
  17. package/docs/infrastructure/map.md +867 -0
  18. package/docs/init/master_plan.md +3581 -0
  19. package/docs/init/red_team_report.md +215 -0
  20. package/docs/init/report_phase_1a.md +304 -0
  21. package/docs/integrity-gate/enhance_drift.md +703 -0
  22. package/docs/integrity-gate/overview.md +108 -0
  23. package/docs/management/manger-task.md +99 -0
  24. package/docs/management/scafffold.md +76 -0
  25. package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
  26. package/docs/map/RED_TEAM_REPORT.md +159 -0
  27. package/docs/map/map_task.md +147 -0
  28. package/docs/map/semantic_graph_task.md +792 -0
  29. package/docs/map/semantic_master_plan.md +705 -0
  30. package/docs/phase7/TEAM_RED.md +249 -0
  31. package/docs/phase7/plan.md +1682 -0
  32. package/docs/phase7/task.md +275 -0
  33. package/docs/prompts/USAGE.md +312 -0
  34. package/docs/prompts/architect.md +165 -0
  35. package/docs/prompts/executer.md +190 -0
  36. package/docs/prompts/hardener.md +190 -0
  37. package/docs/prompts/red_team.md +146 -0
  38. package/docs/verification/goveranance-overview.md +396 -0
  39. package/docs/verification/governance-overview.md +245 -0
  40. package/docs/verification/verification-arc-ar.md +560 -0
  41. package/docs/verification/verification-architecture.md +560 -0
  42. package/docs/very_next.md +52 -0
  43. package/docs/whitepaper.md +89 -0
  44. package/overview.md +1469 -0
  45. package/package.json +63 -0
  46. package/src/adapters/__tests__/git.test.ts +296 -0
  47. package/src/adapters/__tests__/stdio.test.ts +70 -0
  48. package/src/adapters/git.ts +226 -0
  49. package/src/adapters/pty.ts +159 -0
  50. package/src/adapters/stdio.ts +113 -0
  51. package/src/cli.ts +83 -0
  52. package/src/commands/apply.ts +47 -0
  53. package/src/commands/auth.ts +301 -0
  54. package/src/commands/certificate.ts +89 -0
  55. package/src/commands/discard.ts +24 -0
  56. package/src/commands/drift.ts +116 -0
  57. package/src/commands/index.ts +78 -0
  58. package/src/commands/init.ts +121 -0
  59. package/src/commands/list.ts +75 -0
  60. package/src/commands/map.ts +55 -0
  61. package/src/commands/plan.ts +30 -0
  62. package/src/commands/review.ts +58 -0
  63. package/src/commands/run.ts +63 -0
  64. package/src/commands/search.ts +147 -0
  65. package/src/commands/show.ts +63 -0
  66. package/src/commands/status.ts +59 -0
  67. package/src/core/__tests__/budget.test.ts +213 -0
  68. package/src/core/__tests__/certificate.test.ts +385 -0
  69. package/src/core/__tests__/config.test.ts +191 -0
  70. package/src/core/__tests__/preflight.test.ts +24 -0
  71. package/src/core/__tests__/prompt.test.ts +358 -0
  72. package/src/core/__tests__/review.test.ts +161 -0
  73. package/src/core/__tests__/state.test.ts +362 -0
  74. package/src/core/auth/__tests__/manager.test.ts +166 -0
  75. package/src/core/auth/__tests__/server.test.ts +220 -0
  76. package/src/core/auth/gcp-projects.ts +160 -0
  77. package/src/core/auth/manager.ts +114 -0
  78. package/src/core/auth/server.ts +141 -0
  79. package/src/core/budget.ts +119 -0
  80. package/src/core/certificate.ts +502 -0
  81. package/src/core/config.ts +212 -0
  82. package/src/core/errors.ts +54 -0
  83. package/src/core/factory.ts +49 -0
  84. package/src/core/graph/__tests__/builder.test.ts +272 -0
  85. package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
  86. package/src/core/graph/__tests__/enricher.test.ts +299 -0
  87. package/src/core/graph/__tests__/parser.test.ts +200 -0
  88. package/src/core/graph/__tests__/pipeline.test.ts +202 -0
  89. package/src/core/graph/__tests__/renderer.test.ts +128 -0
  90. package/src/core/graph/__tests__/resolver.test.ts +185 -0
  91. package/src/core/graph/__tests__/scanner.test.ts +231 -0
  92. package/src/core/graph/__tests__/show.test.ts +134 -0
  93. package/src/core/graph/builder.ts +303 -0
  94. package/src/core/graph/constraints.ts +94 -0
  95. package/src/core/graph/contract-writer.ts +93 -0
  96. package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
  97. package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
  98. package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
  99. package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
  100. package/src/core/graph/drift/classifier.ts +165 -0
  101. package/src/core/graph/drift/comparator.ts +205 -0
  102. package/src/core/graph/drift/reporter.ts +77 -0
  103. package/src/core/graph/enricher.ts +251 -0
  104. package/src/core/graph/grammar-paths.ts +30 -0
  105. package/src/core/graph/html-template.ts +493 -0
  106. package/src/core/graph/map-schema.ts +137 -0
  107. package/src/core/graph/parser.ts +336 -0
  108. package/src/core/graph/pipeline.ts +209 -0
  109. package/src/core/graph/renderer.ts +92 -0
  110. package/src/core/graph/resolver.ts +195 -0
  111. package/src/core/graph/scanner.ts +145 -0
  112. package/src/core/logger.ts +46 -0
  113. package/src/core/orchestrator.ts +792 -0
  114. package/src/core/plan-file-manager.ts +66 -0
  115. package/src/core/preflight.ts +64 -0
  116. package/src/core/prompt.ts +173 -0
  117. package/src/core/review.ts +95 -0
  118. package/src/core/state.ts +294 -0
  119. package/src/core/worktree-coordinator.ts +77 -0
  120. package/src/search/__tests__/chunk-extractor.test.ts +339 -0
  121. package/src/search/__tests__/embedder-auth.test.ts +124 -0
  122. package/src/search/__tests__/embedder.test.ts +267 -0
  123. package/src/search/__tests__/graph-enricher.test.ts +178 -0
  124. package/src/search/__tests__/indexer.test.ts +518 -0
  125. package/src/search/__tests__/integration.test.ts +649 -0
  126. package/src/search/__tests__/query-engine.test.ts +334 -0
  127. package/src/search/__tests__/similarity.test.ts +78 -0
  128. package/src/search/__tests__/vector-store.test.ts +281 -0
  129. package/src/search/chunk-extractor.ts +167 -0
  130. package/src/search/embedder.ts +209 -0
  131. package/src/search/graph-enricher.ts +95 -0
  132. package/src/search/indexer.ts +483 -0
  133. package/src/search/lexical-searcher.ts +190 -0
  134. package/src/search/query-engine.ts +225 -0
  135. package/src/search/vector-store.ts +311 -0
  136. package/src/types/index.ts +572 -0
  137. package/src/utils/__tests__/ansi.test.ts +54 -0
  138. package/src/utils/__tests__/frontmatter.test.ts +79 -0
  139. package/src/utils/__tests__/sanitize.test.ts +229 -0
  140. package/src/utils/ansi.ts +19 -0
  141. package/src/utils/context.ts +44 -0
  142. package/src/utils/frontmatter.ts +27 -0
  143. package/src/utils/sanitize.ts +78 -0
  144. package/test/e2e/lifecycle.test.ts +330 -0
  145. package/test/fixtures/mock-planner-hang.ts +5 -0
  146. package/test/fixtures/mock-planner.ts +26 -0
  147. package/test/fixtures/mock-reviewer-bad.ts +8 -0
  148. package/test/fixtures/mock-reviewer-retry.ts +34 -0
  149. package/test/fixtures/mock-reviewer.ts +18 -0
  150. package/test/fixtures/sample-project/src/circular-a.ts +6 -0
  151. package/test/fixtures/sample-project/src/circular-b.ts +6 -0
  152. package/test/fixtures/sample-project/src/config.ts +15 -0
  153. package/test/fixtures/sample-project/src/main.ts +19 -0
  154. package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
  155. package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
  156. package/test/fixtures/sample-project/src/types.ts +14 -0
  157. package/test/fixtures/sample-project/src/utils/index.ts +14 -0
  158. package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
  159. package/tsconfig.json +20 -0
  160. package/vitest.config.ts +12 -0
@@ -0,0 +1,330 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
5
+ import simpleGit from 'simple-git';
6
+ import { createOrchestrator } from '../../src/core/factory.js';
7
+ import { NomosError } from '../../src/core/errors.js';
8
+ import type { Orchestrator } from '../../src/core/orchestrator.js';
9
+
10
+ // E5 fix: use path.resolve(import.meta.url pathname, ...) to get fixture paths.
11
+ // fixturesDir is in the REPO, not in tempDir. These are separate locations.
12
+ const fixturesDir = path.resolve(new URL(import.meta.url).pathname, '../../fixtures');
13
+
14
+ // Use the local tsx binary via absolute path so subprocesses spawned in the worktree CWD
15
+ // (which has no node_modules) do not trigger an npx download prompt.
16
+ const projectRoot = path.resolve(new URL(import.meta.url).pathname, '../../..');
17
+ const tsxBin = path.join(projectRoot, 'node_modules', '.bin', 'tsx');
18
+
19
+ // Counter file path must match the one used in mock-reviewer-retry.ts
20
+ const counterFile = path.join(os.tmpdir(), 'nomos-mock-reviewer-retry-count.txt');
21
+
22
+ vi.setConfig({ testTimeout: 60000 });
23
+
24
+ let tempDir: string;
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Initialise a project in dir, overwrite config, and return a ready orchestrator.
30
+ * Avoids process.chdir() — passes projectRoot explicitly (E5 fix).
31
+ *
32
+ * Also commits all scaffold files to main and extends .gitignore so that
33
+ * state/log files don't appear as untracked. This is required for apply()
34
+ * to pass the dirty-tree check (simple-git's isClean() includes untracked files).
35
+ */
36
+ async function setupProject(dir: string, config: object): Promise<Orchestrator> {
37
+ const { orchestrator: initOrch } = await createOrchestrator({ skipConfig: true, projectRoot: dir });
38
+ await initOrch.initProject();
39
+
40
+ // Overwrite default config with test-specific mock config
41
+ fs.writeFileSync(path.join(dir, '.nomos-config.json'), JSON.stringify(config, null, 2));
42
+
43
+ // Extend .gitignore so state/lock/log files created at runtime don't show as untracked
44
+ const gitignorePath = path.join(dir, '.gitignore');
45
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
46
+ const extra = ['tasks-management/state/', 'tasks-management/state/*.lock']
47
+ .filter(e => !existing.includes(e))
48
+ .join('\n');
49
+ if (extra) fs.appendFileSync(gitignorePath, '\n' + extra + '\n');
50
+
51
+ // Commit all scaffold files so main's working tree is clean for apply()
52
+ const git = simpleGit(dir);
53
+ await git.add('.');
54
+ await git.commit('arc init: scaffold project');
55
+
56
+ const { orchestrator } = await createOrchestrator({ projectRoot: dir });
57
+ return orchestrator;
58
+ }
59
+
60
+ function makePlannerBinary(fixtureName: string, overrides: Record<string, unknown> = {}) {
61
+ return {
62
+ cmd: tsxBin,
63
+ args: [path.join(fixturesDir, fixtureName)],
64
+ pty: true,
65
+ total_timeout_ms: 30000,
66
+ heartbeat_timeout_ms: 20000,
67
+ max_output_bytes: 1048576,
68
+ usage_pattern: 'Tokens used:\\s*(\\d+)',
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ function makeReviewerBinary(fixtureName: string) {
74
+ return {
75
+ cmd: tsxBin,
76
+ args: [path.join(fixturesDir, fixtureName)],
77
+ pty: false,
78
+ total_timeout_ms: 30000,
79
+ heartbeat_timeout_ms: 20000,
80
+ max_output_bytes: 524288,
81
+ usage_pattern: null,
82
+ };
83
+ }
84
+
85
+ // ── Lifecycle ─────────────────────────────────────────────────────────────────
86
+
87
+ beforeEach(async () => {
88
+ // Clean retry counter so mock-reviewer-retry starts fresh in every test
89
+ try { fs.unlinkSync(counterFile); } catch {}
90
+
91
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nomos-e2e-'));
92
+
93
+ // E5 fix: NEVER use process.chdir() — pass projectRoot explicitly everywhere.
94
+ const git = simpleGit(tempDir);
95
+ await git.init();
96
+ await git.addConfig('user.email', 'test@test.com');
97
+ await git.addConfig('user.name', 'Test');
98
+ fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test Project');
99
+ fs.mkdirSync(path.join(tempDir, 'src'));
100
+ await git.add('.');
101
+ await git.commit('initial commit');
102
+ // Ensure branch is always 'main' — mergeToMain checks for this branch by name
103
+ await git.raw(['branch', '-M', 'main']);
104
+ });
105
+
106
+ afterEach(() => {
107
+ try { fs.unlinkSync(counterFile); } catch {}
108
+ fs.rmSync(tempDir, { recursive: true, force: true });
109
+ });
110
+
111
+ // ── Tests ─────────────────────────────────────────────────────────────────────
112
+
113
+ describe('E2E: Full Lifecycle', () => {
114
+
115
+ // Test 1 — Happy path: init → plan → review → apply
116
+ it('runs the full init → plan → review → apply lifecycle', async () => {
117
+ const orchestrator = await setupProject(tempDir, {
118
+ binaries: {
119
+ planner: makePlannerBinary('mock-planner.ts'),
120
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
121
+ },
122
+ });
123
+ const git = simpleGit(tempDir);
124
+
125
+ // Verify initProject created required artifacts
126
+ expect(fs.existsSync(path.join(tempDir, 'tasks-management/rules/global.md'))).toBe(true);
127
+ expect(fs.existsSync(path.join(tempDir, '.nomos-config.json'))).toBe(true);
128
+
129
+ // Init task
130
+ await orchestrator.initTask('my-task');
131
+ expect(fs.existsSync(path.join(tempDir, 'tasks-management/tasks/my-task.md'))).toBe(true);
132
+ expect(fs.existsSync(path.join(tempDir, 'tasks-management/state/my-task.json'))).toBe(true);
133
+ // Commit task file to main so working tree is clean for apply().
134
+ // The shadow branch also has this file (same content) → clean merge.
135
+ await git.add('tasks-management/tasks/my-task.md');
136
+ await git.commit('test: commit task definition to main');
137
+
138
+ // Plan
139
+ await orchestrator.plan('my-task', 'auto');
140
+ const stateAfterPlan = await orchestrator.status('my-task');
141
+ expect(stateAfterPlan.meta.status).toBe('pending_review');
142
+ expect(stateAfterPlan.history).toHaveLength(1);
143
+ expect(stateAfterPlan.history[0].tokens_source).toBe('metered'); // RTV-4
144
+ // Commit plan diff to main so working tree is clean for apply().
145
+ // The shadow branch also has this file (same content) → clean merge.
146
+ await git.add('tasks-management/plans/my-task-v1.diff');
147
+ await git.commit('test: commit plan diff to main');
148
+
149
+ // Review
150
+ await orchestrator.review('my-task', 'auto');
151
+ const stateAfterReview = await orchestrator.status('my-task');
152
+ expect(stateAfterReview.meta.status).toBe('approved');
153
+ expect(stateAfterReview.meta.approval_reason).toBe('score_threshold'); // RTV-6
154
+ expect(stateAfterReview.history[stateAfterReview.history.length - 1]?.review?.score).toBe(0.92);
155
+
156
+ // Apply — working tree is clean; shadow branch merges cleanly into main
157
+ await orchestrator.apply('my-task');
158
+ const finalState = await orchestrator.status('my-task');
159
+ expect(finalState.meta.status).toBe('merged');
160
+ });
161
+
162
+ // Test 2 — Subprocess timeout → stalled or failed
163
+ it('transitions to stalled when planner heartbeat times out', async () => {
164
+ const orchestrator = await setupProject(tempDir, {
165
+ binaries: {
166
+ planner: makePlannerBinary('mock-planner-hang.ts', { heartbeat_timeout_ms: 2000 }),
167
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
168
+ },
169
+ });
170
+
171
+ await orchestrator.initTask('hang-task');
172
+ await orchestrator.plan('hang-task', 'auto'); // returns (does not throw) per M-7 pattern
173
+
174
+ const state = await orchestrator.status('hang-task');
175
+ expect(['stalled', 'failed']).toContain(state.meta.status);
176
+ });
177
+
178
+ // Test 3 — Budget guard: second plan throws budget_exceeded
179
+ it('rejects second plan when token budget is exhausted', async () => {
180
+ // score_threshold: 0.99 forces refinement (mock-reviewer returns 0.92 < 0.99)
181
+ // max_tokens_per_task: 1000 is exceeded by mock-planner's 5000 metered tokens
182
+ const orchestrator = await setupProject(tempDir, {
183
+ binaries: {
184
+ planner: makePlannerBinary('mock-planner.ts'),
185
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
186
+ },
187
+ convergence: { score_threshold: 0.99, max_iterations: 3 },
188
+ budget: { max_tokens_per_task: 1000 },
189
+ });
190
+
191
+ await orchestrator.initTask('budget-task');
192
+
193
+ // First plan succeeds and records 5000 metered tokens
194
+ await orchestrator.plan('budget-task', 'auto');
195
+ const afterPlan = await orchestrator.status('budget-task');
196
+ expect(afterPlan.meta.status).toBe('pending_review');
197
+
198
+ // Review → 0.92 < 0.99 threshold and version 1 < max_iterations 3 → refinement
199
+ await orchestrator.review('budget-task', 'auto');
200
+ const afterReview = await orchestrator.status('budget-task');
201
+ expect(afterReview.meta.status).toBe('refinement');
202
+
203
+ // Second plan → budget check fires (5000+ tokens used >= 1000 limit)
204
+ const planError = await orchestrator.plan('budget-task', 'auto').catch(e => e);
205
+ expect(planError).toBeInstanceOf(NomosError);
206
+ expect((planError as NomosError).code).toBe('budget_exceeded');
207
+
208
+ const finalState = await orchestrator.status('budget-task');
209
+ expect(finalState.meta.status).toBe('failed');
210
+ });
211
+
212
+ // Test 4 — Discard lifecycle
213
+ it('discard removes worktree, branch, and sets state to discarded', async () => {
214
+ const orchestrator = await setupProject(tempDir, {
215
+ binaries: {
216
+ planner: makePlannerBinary('mock-planner.ts'),
217
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
218
+ },
219
+ });
220
+
221
+ await orchestrator.initTask('task2');
222
+ await orchestrator.discard('task2');
223
+
224
+ const state = await orchestrator.status('task2');
225
+ expect(state.meta.status).toBe('discarded');
226
+ });
227
+
228
+ // Test 5 — Apply guard: cannot apply a task that is not approved
229
+ it('throws invalid_transition when applying a non-approved task', async () => {
230
+ const orchestrator = await setupProject(tempDir, {
231
+ binaries: {
232
+ planner: makePlannerBinary('mock-planner.ts'),
233
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
234
+ },
235
+ });
236
+
237
+ await orchestrator.initTask('task3');
238
+ // task3 is in 'init' status — apply should throw
239
+ const err = await orchestrator.apply('task3').catch(e => e);
240
+ expect(err).toBeInstanceOf(NomosError);
241
+ expect((err as NomosError).code).toBe('invalid_transition');
242
+ expect((err as NomosError).message).toMatch(/init/);
243
+ });
244
+
245
+ // Test 6 — Supervised mode TTY guard (BLK-1)
246
+ it('throws no_tty when supervised mode is used without a TTY', async () => {
247
+ const orchestrator = await setupProject(tempDir, {
248
+ binaries: {
249
+ planner: makePlannerBinary('mock-planner.ts'),
250
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
251
+ },
252
+ });
253
+
254
+ await orchestrator.initTask('task-tty');
255
+ // vitest runs without a TTY → PtyAdapter.execute throws no_tty for supervised mode
256
+ const err = await orchestrator.plan('task-tty', 'supervised').catch(e => e);
257
+ expect(err).toBeInstanceOf(NomosError);
258
+ expect((err as NomosError).code).toBe('no_tty');
259
+ });
260
+
261
+ // Test 7 — Review retry on bad JSON → failed
262
+ it('transitions to failed when reviewer always returns invalid JSON', async () => {
263
+ const orchestrator = await setupProject(tempDir, {
264
+ binaries: {
265
+ planner: makePlannerBinary('mock-planner.ts'),
266
+ reviewer: makeReviewerBinary('mock-reviewer-bad.ts'),
267
+ },
268
+ });
269
+
270
+ await orchestrator.initTask('bad-review-task');
271
+ await orchestrator.plan('bad-review-task', 'auto');
272
+
273
+ // review() returns state, never throws (M-7 fix)
274
+ await orchestrator.review('bad-review-task', 'auto');
275
+
276
+ const state = await orchestrator.status('bad-review-task');
277
+ expect(state.meta.status).toBe('failed');
278
+
279
+ // Raw review log must exist even on parse failure
280
+ const version = state.current_version;
281
+ const rawLogPath = path.join(
282
+ tempDir, 'tasks-management', 'logs',
283
+ `bad-review-task-v${version}-review-raw.log`,
284
+ );
285
+ expect(fs.existsSync(rawLogPath)).toBe(true);
286
+ });
287
+
288
+ // Test 8 — Review retry succeeds on second attempt (via temp-file counter)
289
+ it('succeeds on retry when reviewer returns valid JSON on second attempt', async () => {
290
+ // beforeEach already deleted counterFile — clean slate guaranteed
291
+ const orchestrator = await setupProject(tempDir, {
292
+ binaries: {
293
+ planner: makePlannerBinary('mock-planner.ts'),
294
+ reviewer: makeReviewerBinary('mock-reviewer-retry.ts'),
295
+ },
296
+ });
297
+
298
+ await orchestrator.initTask('retry-task');
299
+ await orchestrator.plan('retry-task', 'auto');
300
+ await orchestrator.review('retry-task', 'auto');
301
+
302
+ const state = await orchestrator.status('retry-task');
303
+ // mock-reviewer-retry returns score: 0.85 on retry
304
+ // 0.85 < default threshold 0.9 and version 1 < max_iterations 3 → refinement
305
+ // (approved would also be valid if threshold were lower — spec allows both)
306
+ expect(['approved', 'refinement']).toContain(state.meta.status);
307
+ });
308
+
309
+ // Test 9 — approval_reason is max_iterations_reached when iterations exhausted (RTV-6)
310
+ it('sets approval_reason to max_iterations_reached when max iterations hit', async () => {
311
+ // max_iterations: 1 — after one plan cycle (version becomes 1), review fires
312
+ // max_iterations_reached because current_version (1) >= maxVersion (1)
313
+ const orchestrator = await setupProject(tempDir, {
314
+ binaries: {
315
+ planner: makePlannerBinary('mock-planner.ts'),
316
+ reviewer: makeReviewerBinary('mock-reviewer.ts'),
317
+ },
318
+ convergence: { score_threshold: 0.99, max_iterations: 1 },
319
+ });
320
+
321
+ await orchestrator.initTask('iter-task');
322
+ await orchestrator.plan('iter-task', 'auto');
323
+ await orchestrator.review('iter-task', 'auto');
324
+
325
+ const state = await orchestrator.status('iter-task');
326
+ expect(state.meta.status).toBe('approved');
327
+ expect(state.meta.approval_reason).toBe('max_iterations_reached');
328
+ });
329
+
330
+ });
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Writes initial output then hangs — used to test heartbeat/total timeout
3
+ process.stdout.write('Starting plan...\n');
4
+ // Never exits — timeout test relies on this hanging indefinitely
5
+ setInterval(() => {}, 60000);
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // Simulates: claude -p "<prompt>" running inside a worktree
3
+ // Reads prompt from -p arg, creates a file, commits it, prints token usage.
4
+ import simpleGit from 'simple-git';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+
8
+ const pIdx = process.argv.indexOf('-p');
9
+ const prompt = pIdx !== -1 ? process.argv[pIdx + 1] : '';
10
+
11
+ // Write ANSI-colored output (to test ANSI stripping in PtyAdapter)
12
+ process.stdout.write('\x1b[32m✓\x1b[0m Generating plan...\n');
13
+ process.stdout.write(`Received prompt (${prompt.length} chars)\n`);
14
+
15
+ // Create a file in the current working directory (the worktree)
16
+ const outputFile = path.join(process.cwd(), 'src', 'mock-output.ts');
17
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
18
+ fs.writeFileSync(outputFile, `// mock implementation\nexport const result = 'plan';\n`);
19
+
20
+ // L-5 fix: commit using simple-git, not shell git commands
21
+ const git = simpleGit(process.cwd());
22
+ await git.add('.');
23
+ await git.commit('mock planner: add implementation');
24
+
25
+ process.stdout.write('Tokens used: 5000\n');
26
+ process.exit(0);
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Always returns invalid JSON — used to test review retry and failure handling
3
+ let input = '';
4
+ process.stdin.on('data', (chunk: Buffer) => { input += chunk.toString(); });
5
+ process.stdin.on('end', () => {
6
+ process.stdout.write('I cannot review this because reasons');
7
+ process.exit(0);
8
+ });
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // First call: returns invalid JSON. Second call (retry): returns valid JSON.
3
+ // Uses a temp file as an invocation counter — since each retry is a NEW subprocess,
4
+ // in-process state cannot be used. The temp file path is derived from the process CWD.
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+
9
+ const counterFile = path.join(os.tmpdir(), 'nomos-mock-reviewer-retry-count.txt');
10
+
11
+ let input = '';
12
+ process.stdin.on('data', (chunk: Buffer) => { input += chunk.toString(); });
13
+ process.stdin.on('end', () => {
14
+ // Read and increment call counter
15
+ let count = 0;
16
+ try { count = parseInt(fs.readFileSync(counterFile, 'utf8'), 10); } catch {}
17
+ count++;
18
+ fs.writeFileSync(counterFile, String(count));
19
+
20
+ if (count === 1) {
21
+ // First invocation: return bad JSON
22
+ process.stdout.write('I cannot review this because reasons');
23
+ } else {
24
+ // Second invocation (retry): clean up counter and return valid JSON
25
+ try { fs.unlinkSync(counterFile); } catch {}
26
+ const review = {
27
+ score: 0.85,
28
+ summary: "Plan looks good after retry. Implementation steps are clear.",
29
+ issues: []
30
+ };
31
+ process.stdout.write(JSON.stringify(review));
32
+ }
33
+ process.exit(0);
34
+ });
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // Simulates: codex -q --full-auto receiving review prompt via stdin
3
+ let input = '';
4
+ process.stdin.on('data', (chunk: Buffer) => { input += chunk.toString(); });
5
+ process.stdin.on('end', () => {
6
+ const review = {
7
+ score: 0.92,
8
+ summary: "Plan is well-structured with clear implementation steps and good error handling.",
9
+ issues: [{
10
+ severity: "low",
11
+ category: "maintainability",
12
+ description: "Consider adding more inline documentation.",
13
+ suggestion: "Add JSDoc comments to exported functions."
14
+ }]
15
+ };
16
+ process.stdout.write(JSON.stringify(review));
17
+ process.exit(0);
18
+ });
@@ -0,0 +1,6 @@
1
+ // Circular dependency test: circular-a ↔ circular-b
2
+ import { fromB } from './circular-b';
3
+
4
+ export function fromA(): string {
5
+ return `A(${fromB()})`;
6
+ }
@@ -0,0 +1,6 @@
1
+ // Circular dependency test: circular-b ↔ circular-a
2
+ import { fromA } from './circular-a';
3
+
4
+ export function fromB(): string {
5
+ return `B(${fromA()})`;
6
+ }
@@ -0,0 +1,15 @@
1
+ // App configuration
2
+ import type { Result } from './types';
3
+ import { ok, fail } from './utils/index';
4
+
5
+ export interface AppConfig {
6
+ port: number;
7
+ dbUrl: string;
8
+ }
9
+
10
+ export function loadAppConfig(): Result<AppConfig> {
11
+ const port = Number(process.env['PORT'] ?? '3000');
12
+ const dbUrl = process.env['DATABASE_URL'];
13
+ if (!dbUrl) return fail('DATABASE_URL is required');
14
+ return ok({ port, dbUrl });
15
+ }
@@ -0,0 +1,19 @@
1
+ // Entry point — imports everything, no one imports this
2
+ import { createUser, getUser } from './services/user-service';
3
+ import { createProduct, getProduct } from './services/product-service';
4
+ import { loadAppConfig } from './config';
5
+ import { formatDate } from './utils/index';
6
+
7
+ async function main() {
8
+ const cfg = loadAppConfig();
9
+ if (!cfg.ok) {
10
+ console.error(cfg.error);
11
+ process.exit(1);
12
+ }
13
+ console.log(`Starting on port ${cfg.value.port}, date: ${formatDate(new Date())}`);
14
+ const user = createUser({ id: '1', name: 'Alice', email: 'alice@example.com' });
15
+ const product = createProduct({ id: 'p1', name: 'Widget', price: 9.99 });
16
+ console.log(getUser('1'), getProduct('p1'), user, product);
17
+ }
18
+
19
+ main();
@@ -0,0 +1,20 @@
1
+ // Product service
2
+ import type { Product, Result } from '../types';
3
+ import { ok, fail } from '../utils/index';
4
+
5
+ const store = new Map<string, Product>();
6
+
7
+ export function createProduct(data: unknown): Result<Product> {
8
+ const obj = data as Record<string, unknown>;
9
+ if (typeof obj['id'] !== 'string') return fail('Missing id');
10
+ if (typeof obj['name'] !== 'string') return fail('Missing name');
11
+ if (typeof obj['price'] !== 'number') return fail('Missing price');
12
+ const product: Product = { id: obj['id'], name: obj['name'], price: obj['price'] };
13
+ store.set(product.id, product);
14
+ return ok(product);
15
+ }
16
+
17
+ export function getProduct(id: string): Result<Product> {
18
+ const product = store.get(id);
19
+ return product ? ok(product) : fail(`Product ${id} not found`);
20
+ }
@@ -0,0 +1,18 @@
1
+ // User service
2
+ import type { User, Result } from '../types';
3
+ import { ok, fail } from '../utils/index';
4
+ import { validateUser } from '../utils/validate';
5
+
6
+ const store = new Map<string, User>();
7
+
8
+ export function createUser(data: unknown): Result<User> {
9
+ const validated = validateUser(data);
10
+ if (!validated.ok) return fail(validated.error);
11
+ store.set(validated.value.id, validated.value);
12
+ return ok(validated.value);
13
+ }
14
+
15
+ export function getUser(id: string): Result<User> {
16
+ const user = store.get(id);
17
+ return user ? ok(user) : fail(`User ${id} not found`);
18
+ }
@@ -0,0 +1,14 @@
1
+ // Core types used throughout the project
2
+ export interface User {
3
+ id: string;
4
+ name: string;
5
+ email: string;
6
+ }
7
+
8
+ export interface Product {
9
+ id: string;
10
+ name: string;
11
+ price: number;
12
+ }
13
+
14
+ export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
@@ -0,0 +1,14 @@
1
+ // Shared utilities
2
+ import type { Result } from '../types';
3
+
4
+ export function ok<T>(value: T): Result<T> {
5
+ return { ok: true, value };
6
+ }
7
+
8
+ export function fail<T>(error: string): Result<T> {
9
+ return { ok: false, error };
10
+ }
11
+
12
+ export function formatDate(date: Date): string {
13
+ return date.toISOString().split('T')[0]!;
14
+ }
@@ -0,0 +1,12 @@
1
+ // Validation helpers
2
+ import type { User, Result } from '../types';
3
+ import { fail, ok } from './index';
4
+
5
+ export function validateUser(u: unknown): Result<User> {
6
+ if (typeof u !== 'object' || u === null) return fail('Not an object');
7
+ const obj = u as Record<string, unknown>;
8
+ if (typeof obj['id'] !== 'string') return fail('Missing id');
9
+ if (typeof obj['name'] !== 'string') return fail('Missing name');
10
+ if (typeof obj['email'] !== 'string') return fail('Missing email');
11
+ return ok(obj as unknown as User);
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "types": ["node"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "test"]
20
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: [
6
+ 'src/**/__tests__/**/*.test.ts',
7
+ 'test/**/*.test.ts',
8
+ ],
9
+ testTimeout: 10000,
10
+ hookTimeout: 10000,
11
+ },
12
+ });