@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.
- package/.claude/settings.local.json +10 -0
- package/.nomos-config.json +5 -0
- package/CLAUDE.md +108 -0
- package/LICENSE +190 -0
- package/README.md +569 -0
- package/dist/cli.js +21120 -0
- package/docs/auth/googel_plan.yaml +1093 -0
- package/docs/auth/google_task.md +235 -0
- package/docs/auth/hardened_blueprint.yaml +1658 -0
- package/docs/auth/red_team_report.yaml +336 -0
- package/docs/auth/session_state.yaml +162 -0
- package/docs/certificate/cer_enhance_plan.md +605 -0
- package/docs/certificate/certificate_report.md +338 -0
- package/docs/dev_overview.md +419 -0
- package/docs/feature_assessment.md +156 -0
- package/docs/how_it_works.md +78 -0
- package/docs/infrastructure/map.md +867 -0
- package/docs/init/master_plan.md +3581 -0
- package/docs/init/red_team_report.md +215 -0
- package/docs/init/report_phase_1a.md +304 -0
- package/docs/integrity-gate/enhance_drift.md +703 -0
- package/docs/integrity-gate/overview.md +108 -0
- package/docs/management/manger-task.md +99 -0
- package/docs/management/scafffold.md +76 -0
- package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
- package/docs/map/RED_TEAM_REPORT.md +159 -0
- package/docs/map/map_task.md +147 -0
- package/docs/map/semantic_graph_task.md +792 -0
- package/docs/map/semantic_master_plan.md +705 -0
- package/docs/phase7/TEAM_RED.md +249 -0
- package/docs/phase7/plan.md +1682 -0
- package/docs/phase7/task.md +275 -0
- package/docs/prompts/USAGE.md +312 -0
- package/docs/prompts/architect.md +165 -0
- package/docs/prompts/executer.md +190 -0
- package/docs/prompts/hardener.md +190 -0
- package/docs/prompts/red_team.md +146 -0
- package/docs/verification/goveranance-overview.md +396 -0
- package/docs/verification/governance-overview.md +245 -0
- package/docs/verification/verification-arc-ar.md +560 -0
- package/docs/verification/verification-architecture.md +560 -0
- package/docs/very_next.md +52 -0
- package/docs/whitepaper.md +89 -0
- package/overview.md +1469 -0
- package/package.json +63 -0
- package/src/adapters/__tests__/git.test.ts +296 -0
- package/src/adapters/__tests__/stdio.test.ts +70 -0
- package/src/adapters/git.ts +226 -0
- package/src/adapters/pty.ts +159 -0
- package/src/adapters/stdio.ts +113 -0
- package/src/cli.ts +83 -0
- package/src/commands/apply.ts +47 -0
- package/src/commands/auth.ts +301 -0
- package/src/commands/certificate.ts +89 -0
- package/src/commands/discard.ts +24 -0
- package/src/commands/drift.ts +116 -0
- package/src/commands/index.ts +78 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/list.ts +75 -0
- package/src/commands/map.ts +55 -0
- package/src/commands/plan.ts +30 -0
- package/src/commands/review.ts +58 -0
- package/src/commands/run.ts +63 -0
- package/src/commands/search.ts +147 -0
- package/src/commands/show.ts +63 -0
- package/src/commands/status.ts +59 -0
- package/src/core/__tests__/budget.test.ts +213 -0
- package/src/core/__tests__/certificate.test.ts +385 -0
- package/src/core/__tests__/config.test.ts +191 -0
- package/src/core/__tests__/preflight.test.ts +24 -0
- package/src/core/__tests__/prompt.test.ts +358 -0
- package/src/core/__tests__/review.test.ts +161 -0
- package/src/core/__tests__/state.test.ts +362 -0
- package/src/core/auth/__tests__/manager.test.ts +166 -0
- package/src/core/auth/__tests__/server.test.ts +220 -0
- package/src/core/auth/gcp-projects.ts +160 -0
- package/src/core/auth/manager.ts +114 -0
- package/src/core/auth/server.ts +141 -0
- package/src/core/budget.ts +119 -0
- package/src/core/certificate.ts +502 -0
- package/src/core/config.ts +212 -0
- package/src/core/errors.ts +54 -0
- package/src/core/factory.ts +49 -0
- package/src/core/graph/__tests__/builder.test.ts +272 -0
- package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
- package/src/core/graph/__tests__/enricher.test.ts +299 -0
- package/src/core/graph/__tests__/parser.test.ts +200 -0
- package/src/core/graph/__tests__/pipeline.test.ts +202 -0
- package/src/core/graph/__tests__/renderer.test.ts +128 -0
- package/src/core/graph/__tests__/resolver.test.ts +185 -0
- package/src/core/graph/__tests__/scanner.test.ts +231 -0
- package/src/core/graph/__tests__/show.test.ts +134 -0
- package/src/core/graph/builder.ts +303 -0
- package/src/core/graph/constraints.ts +94 -0
- package/src/core/graph/contract-writer.ts +93 -0
- package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
- package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
- package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
- package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
- package/src/core/graph/drift/classifier.ts +165 -0
- package/src/core/graph/drift/comparator.ts +205 -0
- package/src/core/graph/drift/reporter.ts +77 -0
- package/src/core/graph/enricher.ts +251 -0
- package/src/core/graph/grammar-paths.ts +30 -0
- package/src/core/graph/html-template.ts +493 -0
- package/src/core/graph/map-schema.ts +137 -0
- package/src/core/graph/parser.ts +336 -0
- package/src/core/graph/pipeline.ts +209 -0
- package/src/core/graph/renderer.ts +92 -0
- package/src/core/graph/resolver.ts +195 -0
- package/src/core/graph/scanner.ts +145 -0
- package/src/core/logger.ts +46 -0
- package/src/core/orchestrator.ts +792 -0
- package/src/core/plan-file-manager.ts +66 -0
- package/src/core/preflight.ts +64 -0
- package/src/core/prompt.ts +173 -0
- package/src/core/review.ts +95 -0
- package/src/core/state.ts +294 -0
- package/src/core/worktree-coordinator.ts +77 -0
- package/src/search/__tests__/chunk-extractor.test.ts +339 -0
- package/src/search/__tests__/embedder-auth.test.ts +124 -0
- package/src/search/__tests__/embedder.test.ts +267 -0
- package/src/search/__tests__/graph-enricher.test.ts +178 -0
- package/src/search/__tests__/indexer.test.ts +518 -0
- package/src/search/__tests__/integration.test.ts +649 -0
- package/src/search/__tests__/query-engine.test.ts +334 -0
- package/src/search/__tests__/similarity.test.ts +78 -0
- package/src/search/__tests__/vector-store.test.ts +281 -0
- package/src/search/chunk-extractor.ts +167 -0
- package/src/search/embedder.ts +209 -0
- package/src/search/graph-enricher.ts +95 -0
- package/src/search/indexer.ts +483 -0
- package/src/search/lexical-searcher.ts +190 -0
- package/src/search/query-engine.ts +225 -0
- package/src/search/vector-store.ts +311 -0
- package/src/types/index.ts +572 -0
- package/src/utils/__tests__/ansi.test.ts +54 -0
- package/src/utils/__tests__/frontmatter.test.ts +79 -0
- package/src/utils/__tests__/sanitize.test.ts +229 -0
- package/src/utils/ansi.ts +19 -0
- package/src/utils/context.ts +44 -0
- package/src/utils/frontmatter.ts +27 -0
- package/src/utils/sanitize.ts +78 -0
- package/test/e2e/lifecycle.test.ts +330 -0
- package/test/fixtures/mock-planner-hang.ts +5 -0
- package/test/fixtures/mock-planner.ts +26 -0
- package/test/fixtures/mock-reviewer-bad.ts +8 -0
- package/test/fixtures/mock-reviewer-retry.ts +34 -0
- package/test/fixtures/mock-reviewer.ts +18 -0
- package/test/fixtures/sample-project/src/circular-a.ts +6 -0
- package/test/fixtures/sample-project/src/circular-b.ts +6 -0
- package/test/fixtures/sample-project/src/config.ts +15 -0
- package/test/fixtures/sample-project/src/main.ts +19 -0
- package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
- package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
- package/test/fixtures/sample-project/src/types.ts +14 -0
- package/test/fixtures/sample-project/src/utils/index.ts +14 -0
- package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
- package/tsconfig.json +20 -0
- 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
|
+
}
|