@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,159 @@
1
+ import * as pty from 'node-pty';
2
+ import type { Logger } from 'winston';
3
+ import { NomosError } from '../core/errors.js';
4
+ import { stripAnsi } from '../utils/ansi.js';
5
+ import type { PtySpawnOptions, ExecutionResult } from '../types/index.js';
6
+
7
+ export class PtyAdapter {
8
+ constructor(private readonly logger: Logger) {}
9
+
10
+ async execute(options: PtySpawnOptions): Promise<ExecutionResult> {
11
+ // C5 fix: TTY pre-check for supervised mode
12
+ if (options.mode === 'supervised' && !process.stdin.isTTY) {
13
+ throw new NomosError(
14
+ 'no_tty',
15
+ 'Supervised mode requires an interactive terminal. ' +
16
+ 'Use --mode=auto for non-interactive environments (CI, vitest).',
17
+ );
18
+ }
19
+
20
+ const startTime = Date.now();
21
+ const outputBuffer: string[] = [];
22
+ let bytesBuffered = 0;
23
+ let bytesDropped = 0;
24
+ let killed = false;
25
+ let killReason: ExecutionResult['killReason'];
26
+
27
+ // Phase 1a: Tee Stream — no pattern matching, no response_map.
28
+ // PTY output is piped directly to developer terminal and captured for logging.
29
+
30
+ // C1 fix: cmd and args always separate — never shell-interpolated
31
+ // RT2-3.1 fix: Process Group Killing — use detached process group so we can
32
+ // kill the entire process tree (including grandchildren like bash, editors).
33
+ const proc = pty.spawn(options.cmd, options.args, {
34
+ name: 'xterm-256color',
35
+ cols: 120,
36
+ rows: 40,
37
+ cwd: options.cwd,
38
+ env: options.env,
39
+ });
40
+
41
+ // RT2-3.1 fix: Helper to kill the entire process group.
42
+ // Using -proc.pid sends SIGTERM to the process GROUP (negative PID),
43
+ // which terminates all children spawned by the PTY subprocess.
44
+ const killProcessGroup = () => {
45
+ try {
46
+ process.kill(-proc.pid, 'SIGTERM');
47
+ } catch {
48
+ // Process group may already be dead — fall back to direct kill
49
+ try { proc.kill(); } catch {}
50
+ }
51
+ };
52
+
53
+ // RT2-3.1 fix: stdin cleanup extracted into a helper so it runs on ALL exit paths
54
+ // (normal exit, timeout kill, AND unexpected throw). This resolves the stdin
55
+ // listener leak audit finding — previously, an error between listener registration
56
+ // and onExit would leave the listener attached, causing erratic terminal behavior.
57
+ let stdinListener: ((data: Buffer) => void) | null = null;
58
+ const cleanupStdin = () => {
59
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {}
60
+ process.stdin.pause();
61
+ if (stdinListener) {
62
+ process.stdin.removeListener('data', stdinListener);
63
+ stdinListener = null;
64
+ }
65
+ };
66
+
67
+ return new Promise<ExecutionResult>((resolve, reject) => {
68
+ let heartbeatTimer: ReturnType<typeof setTimeout>;
69
+ let totalTimer: ReturnType<typeof setTimeout>;
70
+
71
+ // RT2-3.1 fix: try/catch wrapper around entire Promise body.
72
+ // On unexpected throw: kill process group, restore stdin, reject promise.
73
+ try {
74
+ const resetHeartbeat = () => {
75
+ clearTimeout(heartbeatTimer);
76
+ heartbeatTimer = setTimeout(() => {
77
+ this.logger.warn(
78
+ `Heartbeat timeout (${options.heartbeat_timeout_ms}ms). Killing process group.`,
79
+ );
80
+ killProcessGroup();
81
+ killed = true;
82
+ killReason = 'heartbeat_timeout';
83
+ }, options.heartbeat_timeout_ms);
84
+ };
85
+
86
+ totalTimer = setTimeout(() => {
87
+ this.logger.warn(
88
+ `Total timeout (${options.total_timeout_ms}ms). Killing process group.`,
89
+ );
90
+ killProcessGroup();
91
+ killed = true;
92
+ killReason = 'total_timeout';
93
+ }, options.total_timeout_ms);
94
+
95
+ resetHeartbeat();
96
+
97
+ proc.onData((data: string) => {
98
+ // Forward to developer in real-time (supervised and auto modes both show output)
99
+ process.stdout.write(data);
100
+
101
+ // Buffer for log capture
102
+ const bytes = Buffer.byteLength(data, 'utf8');
103
+ if (bytesBuffered < options.max_output_bytes) {
104
+ if (bytesBuffered + bytes > options.max_output_bytes) {
105
+ // E3 fix: warn on overflow, track dropped bytes
106
+ const remaining = options.max_output_bytes - bytesBuffered;
107
+ outputBuffer.push(data.slice(0, remaining));
108
+ bytesDropped += bytes - remaining;
109
+ bytesBuffered = options.max_output_bytes;
110
+ this.logger.warn(
111
+ `Output exceeded ${options.max_output_bytes} byte limit. ` +
112
+ `Further output will not be captured. Consider increasing max_output_bytes in config.`,
113
+ );
114
+ } else {
115
+ outputBuffer.push(data);
116
+ bytesBuffered += bytes;
117
+ }
118
+ } else {
119
+ bytesDropped += bytes;
120
+ }
121
+
122
+ resetHeartbeat();
123
+ });
124
+
125
+ // Bidirectional piping for supervised mode
126
+ if (process.stdin.isTTY) {
127
+ process.stdin.setRawMode(true);
128
+ }
129
+ process.stdin.resume();
130
+ stdinListener = (data: Buffer) => { proc.write(data.toString()); };
131
+ process.stdin.on('data', stdinListener);
132
+
133
+ proc.onExit(({ exitCode }) => {
134
+ clearTimeout(heartbeatTimer);
135
+ clearTimeout(totalTimer);
136
+ cleanupStdin();
137
+
138
+ const rawOutput = outputBuffer.join('');
139
+ resolve({
140
+ exitCode: exitCode ?? 1,
141
+ rawOutput,
142
+ strippedOutput: stripAnsi(rawOutput),
143
+ duration_ms: Date.now() - startTime,
144
+ killed,
145
+ killReason,
146
+ });
147
+ });
148
+
149
+ } catch (err) {
150
+ // RT2-3.1 fix: unexpected throw — kill process group, clean up stdin, reject
151
+ clearTimeout(heartbeatTimer!);
152
+ clearTimeout(totalTimer!);
153
+ killProcessGroup();
154
+ cleanupStdin();
155
+ reject(err);
156
+ }
157
+ });
158
+ }
159
+ }
@@ -0,0 +1,113 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import type { Logger } from 'winston';
3
+ import { stripAnsi } from '../utils/ansi.js';
4
+ import { NomosError } from '../core/errors.js';
5
+ import type { StdioSpawnOptions, ExecutionResult } from '../types/index.js';
6
+
7
+ export class StdioAdapter {
8
+ constructor(private readonly logger: Logger) {}
9
+
10
+ async execute(options: StdioSpawnOptions): Promise<ExecutionResult> {
11
+ const startTime = Date.now();
12
+ let killed = false;
13
+ let killReason: ExecutionResult['killReason'];
14
+
15
+ // C1 fix: shell is NEVER set to true — args passed directly, never interpolated
16
+ const proc = spawn(options.cmd, options.args, {
17
+ cwd: options.cwd,
18
+ env: options.env,
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ shell: false,
21
+ });
22
+
23
+ return new Promise<ExecutionResult>((resolve, reject) => {
24
+ const stdoutChunks: Buffer[] = [];
25
+ const stderrChunks: Buffer[] = [];
26
+ let bytesBuffered = 0;
27
+
28
+ const killProcess = (p: ChildProcess) => {
29
+ // W4 fix: platform-aware kill
30
+ if (process.platform === 'win32') {
31
+ spawn('taskkill', ['/pid', String(p.pid), '/f', '/t'], { shell: false });
32
+ } else {
33
+ p.kill('SIGTERM');
34
+ setTimeout(() => { if (!p.killed) p.kill('SIGKILL'); }, 3000);
35
+ }
36
+ };
37
+
38
+ let heartbeatTimer: ReturnType<typeof setTimeout>;
39
+ let totalTimer: ReturnType<typeof setTimeout>;
40
+
41
+ const resetHeartbeat = () => {
42
+ clearTimeout(heartbeatTimer);
43
+ heartbeatTimer = setTimeout(() => {
44
+ killed = true;
45
+ killReason = 'heartbeat_timeout';
46
+ killProcess(proc);
47
+ }, options.heartbeat_timeout_ms);
48
+ };
49
+
50
+ totalTimer = setTimeout(() => {
51
+ killed = true;
52
+ killReason = 'total_timeout';
53
+ killProcess(proc);
54
+ }, options.total_timeout_ms);
55
+
56
+ resetHeartbeat();
57
+
58
+ proc.stdout!.on('data', (chunk: Buffer) => {
59
+ resetHeartbeat();
60
+ if (bytesBuffered < options.max_output_bytes) {
61
+ stdoutChunks.push(chunk);
62
+ bytesBuffered += chunk.length;
63
+ }
64
+ });
65
+
66
+ proc.stderr!.on('data', (chunk: Buffer) => { stderrChunks.push(chunk); });
67
+
68
+ // M-5 fix: Handle stdin errors (broken pipe) and backpressure (large prompts)
69
+ proc.stdin!.on('error', (err) => {
70
+ reject(new NomosError(
71
+ 'review_failed',
72
+ `Failed to write to reviewer stdin: ${err.message}`,
73
+ ));
74
+ });
75
+
76
+ const ok = proc.stdin!.write(options.stdinData);
77
+ if (!ok) {
78
+ // Backpressure: large review prompts (200KB+ diff + rules) can exceed the stdin
79
+ // buffer. Without drain handling, data is silently truncated.
80
+ proc.stdin!.once('drain', () => { proc.stdin!.end(); });
81
+ } else {
82
+ proc.stdin!.end();
83
+ }
84
+
85
+ proc.on('close', (exitCode) => {
86
+ clearTimeout(heartbeatTimer);
87
+ clearTimeout(totalTimer);
88
+
89
+ const rawOutput = Buffer.concat(stdoutChunks).toString('utf8');
90
+ const stderrOutput = Buffer.concat(stderrChunks).toString('utf8');
91
+
92
+ if (stderrOutput && (exitCode ?? 0) > 0) {
93
+ this.logger.error(`Reviewer stderr: ${stderrOutput.trim()}`);
94
+ }
95
+
96
+ resolve({
97
+ exitCode: exitCode ?? 1,
98
+ rawOutput,
99
+ strippedOutput: stripAnsi(rawOutput),
100
+ duration_ms: Date.now() - startTime,
101
+ killed,
102
+ killReason,
103
+ });
104
+ });
105
+
106
+ proc.on('error', (err) => {
107
+ clearTimeout(heartbeatTimer);
108
+ clearTimeout(totalTimer);
109
+ reject(new NomosError('review_failed', `Reviewer process error: ${err.message}`));
110
+ });
111
+ });
112
+ }
113
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { Command } from 'commander';
2
+ import { createRequire } from 'module';
3
+ import { NomosError } from './core/errors.js';
4
+ import { registerInitCommand } from './commands/init.js';
5
+ import { registerPlanCommand } from './commands/plan.js';
6
+ import { registerReviewCommand } from './commands/review.js';
7
+ import { registerRunCommand } from './commands/run.js';
8
+ import { registerStatusCommand } from './commands/status.js';
9
+ import { registerApplyCommand } from './commands/apply.js';
10
+ import { registerDiscardCommand } from './commands/discard.js';
11
+ import { registerListCommand } from './commands/list.js';
12
+ import { registerCertificateCommand } from './commands/certificate.js';
13
+ import { registerMapCommand } from './commands/map.js';
14
+ import { registerShowCommand } from './commands/show.js';
15
+ import { registerIndexCommand } from './commands/index.js';
16
+ import { registerSearchCommand } from './commands/search.js';
17
+ import { registerAuthCommand } from './commands/auth.js';
18
+ import { registerDriftCommand } from './commands/drift.js';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const pkg = require('../package.json');
22
+
23
+ const program = new Command();
24
+ program.name('arc').description('The Architect — AI Orchestrator CLI').version(pkg.version);
25
+
26
+ // RTV-5 fix: SIGINT handler uses process.exitCode (not process.exit) to avoid racing
27
+ // with the orchestrator's state-transition catch block.
28
+ //
29
+ // Flow on Ctrl+C:
30
+ // 1. SIGINT fires → stdin restored, process.exitCode = 130
31
+ // 2. Child process receives SIGINT → PTY onExit fires → ptyAdapter.execute() resolves
32
+ // 3. Orchestrator's catch block runs: await stateManager.transition(taskId, 'stalled', ...)
33
+ // 4. State written successfully (no race — process.exit() was NOT called yet)
34
+ // 5. catch block re-throws error → main() catches it
35
+ // 6. main() sees process.exitCode === 130 → calls process.exit(130)
36
+ //
37
+ // This is a non-destructive exit: state is persisted, task is recoverable via arc plan.
38
+ process.on('SIGINT', () => {
39
+ process.exitCode = 130; // mark the intended exit code
40
+ try {
41
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
42
+ } catch {}
43
+ process.stdin.pause();
44
+ // Do NOT call process.exit() here. Let the orchestrator's catch block complete first.
45
+ });
46
+
47
+ [
48
+ registerInitCommand,
49
+ registerPlanCommand,
50
+ registerReviewCommand,
51
+ registerRunCommand,
52
+ registerStatusCommand,
53
+ registerApplyCommand,
54
+ registerDiscardCommand,
55
+ registerListCommand,
56
+ registerCertificateCommand,
57
+ registerMapCommand,
58
+ registerShowCommand,
59
+ registerIndexCommand,
60
+ registerSearchCommand,
61
+ registerAuthCommand,
62
+ registerDriftCommand,
63
+ ].forEach(fn => fn(program));
64
+
65
+ async function main() {
66
+ try {
67
+ await program.parseAsync();
68
+ } catch (err) {
69
+ // RTV-5: If SIGINT was received, honour the 130 exit code
70
+ if (process.exitCode === 130) {
71
+ console.error('\n[nomos:warn] Interrupted. Task state preserved (stalled).');
72
+ process.exit(130);
73
+ }
74
+ if (err instanceof NomosError) {
75
+ console.error(`[nomos:error] ${err.message}`);
76
+ process.exit(1);
77
+ }
78
+ console.error(`[nomos:error] Unexpected error: ${err}`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ main();
@@ -0,0 +1,47 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+
5
+ export function registerApplyCommand(program: Command): void {
6
+ program
7
+ .command('apply <task>')
8
+ .description('Merge approved task shadow branch into main and clean up')
9
+ .action(async (task: string) => {
10
+ try {
11
+ const { orchestrator } = await createOrchestrator();
12
+
13
+ // orchestrator.apply() returns void and never throws on merge conflict —
14
+ // it transitions the task to 'merge_conflict' status and returns cleanly.
15
+ // We re-read state to detect the outcome.
16
+ await orchestrator.apply(task);
17
+ const finalState = await orchestrator.status(task);
18
+
19
+ if (finalState.meta.status === 'merge_conflict') {
20
+ // RTV-15 fix: exit code 3 for merge conflict — recoverable, not an error.
21
+ // Conflict details were logged by the orchestrator.
22
+ console.error(
23
+ `[nomos:conflict] Merge conflict detected in task '${task}'.\n` +
24
+ ` Resolve conflicts in your working tree, then run: arc apply ${task}`
25
+ );
26
+ process.exit(3);
27
+ }
28
+
29
+ console.log(`Task '${task}' merged to main. Shadow branch cleaned up.`);
30
+ process.exit(0);
31
+ } catch (err) {
32
+ if (err instanceof NomosError) {
33
+ if (err.code === 'invalid_transition') {
34
+ console.error(
35
+ `[nomos:error] ${err.message}\n` +
36
+ ` Check current status with: arc status ${task}`
37
+ );
38
+ } else {
39
+ console.error(`[nomos:error] ${err.message}`);
40
+ }
41
+ } else {
42
+ console.error(`[nomos:error] Unexpected error: ${err}`);
43
+ }
44
+ process.exit(1);
45
+ }
46
+ });
47
+ }