@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,301 @@
1
+ import * as path from 'node:path';
2
+ import type { Command } from 'commander';
3
+ import * as readline from 'node:readline';
4
+ import { OAuth2Client } from 'google-auth-library';
5
+ import { loadConfig } from '../core/config.js';
6
+ import { createLogger } from '../core/logger.js';
7
+ import { NomosError } from '../core/errors.js';
8
+ import { AuthManager } from '../core/auth/manager.js';
9
+ import { startLoopbackServer } from '../core/auth/server.js';
10
+ import { listGcpProjects, promptProjectSelection, verifyGeminiAccess } from '../core/auth/gcp-projects.js';
11
+
12
+ const SCOPES = [
13
+ 'https://www.googleapis.com/auth/generative-language',
14
+ 'https://www.googleapis.com/auth/cloud-platform.read-only',
15
+ ];
16
+
17
+ /**
18
+ * Prompt the user for a secret value with masked input (shows * characters).
19
+ * Used as last-resort fallback when client_secret is not in config or env.
20
+ */
21
+ async function promptSecret(prompt: string): Promise<string> {
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout,
25
+ });
26
+ return new Promise((resolve) => {
27
+ // Mask input by writing * for each character
28
+ process.stdout.write(prompt);
29
+ let secret = '';
30
+ process.stdin.setRawMode?.(true);
31
+ process.stdin.resume();
32
+ process.stdin.on('data', (char: Buffer) => {
33
+ const c = char.toString('utf8');
34
+ if (c === '\n' || c === '\r') {
35
+ process.stdin.setRawMode?.(false);
36
+ rl.close();
37
+ process.stdout.write('\n');
38
+ resolve(secret);
39
+ } else if (c === '\u0003') { // Ctrl+C
40
+ process.stdin.setRawMode?.(false);
41
+ rl.close();
42
+ process.exit(1);
43
+ } else if (c === '\u007f') { // Backspace
44
+ if (secret.length > 0) {
45
+ secret = secret.slice(0, -1);
46
+ process.stdout.write('\b \b');
47
+ }
48
+ } else {
49
+ secret += c;
50
+ process.stdout.write('*');
51
+ }
52
+ });
53
+ });
54
+ }
55
+
56
+ export function registerAuthCommand(program: Command): void {
57
+ const auth = program
58
+ .command('auth')
59
+ .description('Manage Google OAuth authentication');
60
+
61
+ // ─── arc auth login ──────────────────────────────────────────────
62
+ auth
63
+ .command('login')
64
+ .description('Authenticate with Google via OAuth 2.0 and select a GCP billing project')
65
+ .option('--client-id <id>', 'Google OAuth client ID')
66
+ .option('--project <id>', 'GCP project ID for Gemini API quota (skip interactive picker)')
67
+ .action(async (opts) => {
68
+ try {
69
+ // 1. Load project config
70
+ const { config, projectRoot } = loadConfig();
71
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
72
+ const logger = createLogger(config.logging.level, logDir);
73
+
74
+ // 2. Resolve clientId: CLI flag > config file
75
+ const clientId = opts.clientId ?? config.auth.client_id;
76
+ if (!clientId) {
77
+ throw new NomosError(
78
+ 'auth_client_config_missing',
79
+ 'Google OAuth client_id not found. Provide via --client-id flag or set auth.client_id in .nomos-config.json',
80
+ );
81
+ }
82
+
83
+ // 3. Resolve clientSecret: env var > interactive prompt
84
+ // SECURITY: client_secret is NOT read from .nomos-config.json (project config is committed to git).
85
+ // NOTE: --client-secret CLI flag intentionally NOT supported (F-001: leaks to shell history / ps).
86
+ let clientSecret = process.env['NOMOS_GOOGLE_CLIENT_SECRET'];
87
+ if (!clientSecret) {
88
+ logger.info('[nomos:auth:info] Client secret not found in environment (NOMOS_GOOGLE_CLIENT_SECRET).');
89
+ clientSecret = await promptSecret('Enter Google OAuth client secret: ');
90
+ if (!clientSecret) {
91
+ throw new NomosError(
92
+ 'auth_client_config_missing',
93
+ 'Google OAuth client_secret not provided. Set NOMOS_GOOGLE_CLIENT_SECRET env var or enter it at the prompt.',
94
+ );
95
+ }
96
+ }
97
+
98
+ // 4. Create OAuth2Client
99
+ const oauth2Client = new OAuth2Client({ clientId, clientSecret });
100
+
101
+ // 5. Create AuthManager
102
+ const authManager = new AuthManager(config.auth, logger);
103
+
104
+ // 6. Start loopback server, open browser, wait for callback
105
+ console.log('Opening browser for Google authentication...');
106
+ const tokens = await startLoopbackServer(
107
+ oauth2Client, SCOPES, config.auth.redirect_port, logger,
108
+ );
109
+
110
+ // 7. Save credentials immediately (so login persists even if project selection fails)
111
+ tokens.client_id = clientId;
112
+ tokens.client_secret = clientSecret;
113
+ await authManager.saveCredentials(tokens);
114
+ console.log('✓ Authenticated successfully.\n');
115
+
116
+ // 8. GCP Project Selection
117
+ let quotaProjectId: string | undefined;
118
+
119
+ if (opts.project) {
120
+ // --project flag: skip interactive picker
121
+ quotaProjectId = opts.project;
122
+ console.log(`Using project from --project flag: ${quotaProjectId}`);
123
+ } else {
124
+ // Fetch user's GCP projects and let them pick
125
+ console.log('Fetching your Google Cloud projects...');
126
+ try {
127
+ const projects = await listGcpProjects(tokens.access_token);
128
+
129
+ if (projects.length === 0) {
130
+ console.log('No active GCP projects found. Skipping project selection.');
131
+ console.log('You can set a project later with: arc auth set-project <PROJECT_ID>');
132
+ } else if (projects.length === 1) {
133
+ quotaProjectId = projects[0]!.projectId;
134
+ console.log(`Auto-selected project: ${projects[0]!.displayName} (${quotaProjectId})`);
135
+ } else {
136
+ quotaProjectId = await promptProjectSelection(projects);
137
+ }
138
+ } catch (err) {
139
+ if (err instanceof NomosError) {
140
+ console.error(`[nomos:warn] ${err.message}`);
141
+ console.log('You can set a project later with: arc auth set-project <PROJECT_ID>');
142
+ } else {
143
+ throw err;
144
+ }
145
+ }
146
+ }
147
+
148
+ // 9. Verify Gemini API access on the selected project
149
+ if (quotaProjectId) {
150
+ try {
151
+ console.log(`\nVerifying Gemini API access on project "${quotaProjectId}"...`);
152
+ await verifyGeminiAccess(tokens.access_token, quotaProjectId);
153
+ console.log('✓ Gemini API is accessible.\n');
154
+ } catch (err) {
155
+ if (err instanceof NomosError && err.code === 'auth_gemini_not_enabled') {
156
+ console.error(`\n[nomos:warn] ${err.message}`);
157
+ console.log('\nLogin saved without project. After enabling the API, run: arc auth set-project ' + quotaProjectId);
158
+ quotaProjectId = undefined;
159
+ } else {
160
+ throw err;
161
+ }
162
+ }
163
+ }
164
+
165
+ // 10. Re-save credentials with quota_project_id
166
+ tokens.quota_project_id = quotaProjectId;
167
+ await authManager.saveCredentials(tokens);
168
+
169
+ // 11. Final confirmation
170
+ console.log(`Credentials saved to ${config.auth.credentials_path}`);
171
+ if (quotaProjectId) {
172
+ console.log(`Quota project: ${quotaProjectId}`);
173
+ }
174
+ console.log('\n✓ Auth complete.');
175
+ } catch (err) {
176
+ if (err instanceof NomosError) {
177
+ console.error(`[nomos:error] ${err.message}`);
178
+ } else {
179
+ console.error(`[nomos:error] Unexpected error: ${err}`);
180
+ }
181
+ process.exit(1);
182
+ }
183
+ });
184
+
185
+ // ─── arc auth set-project ────────────────────────────────────────
186
+ auth
187
+ .command('set-project <projectId>')
188
+ .description('Set or change the GCP project for Gemini API billing')
189
+ .action(async (projectId: string) => {
190
+ try {
191
+ const { config, projectRoot } = loadConfig();
192
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
193
+ const logger = createLogger(config.logging.level, logDir);
194
+ const authManager = new AuthManager(config.auth, logger);
195
+
196
+ const creds = authManager.loadCredentials();
197
+ if (!creds) {
198
+ throw new NomosError('auth_not_logged_in', 'Not logged in. Run `arc auth login` first.');
199
+ }
200
+
201
+ // Verify Gemini API access
202
+ const token = await authManager.getAccessToken();
203
+ console.log(`Verifying Gemini API access on project "${projectId}"...`);
204
+ await verifyGeminiAccess(token, projectId);
205
+ console.log('✓ Gemini API is accessible.\n');
206
+
207
+ // Update credentials
208
+ creds.quota_project_id = projectId;
209
+ await authManager.saveCredentials(creds);
210
+ console.log(`✓ Quota project set to "${projectId}".`);
211
+ } catch (err) {
212
+ if (err instanceof NomosError) {
213
+ console.error(`[nomos:error] ${err.message}`);
214
+ } else {
215
+ console.error(`[nomos:error] Unexpected error: ${err}`);
216
+ }
217
+ process.exit(1);
218
+ }
219
+ });
220
+
221
+ // ─── arc auth logout ─────────────────────────────────────────────
222
+ auth
223
+ .command('logout')
224
+ .description('Remove stored OAuth credentials')
225
+ .action(async () => {
226
+ try {
227
+ // 1. Load config, create AuthManager
228
+ const { config, projectRoot } = loadConfig();
229
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
230
+ const logger = createLogger(config.logging.level, logDir);
231
+ const authManager = new AuthManager(config.auth, logger);
232
+
233
+ // 2. Delete credentials file
234
+ await authManager.clearCredentials();
235
+
236
+ // 3. Confirm
237
+ console.log('✓ Logged out. Credentials removed.');
238
+ } catch (err) {
239
+ if (err instanceof NomosError) {
240
+ console.error(`[nomos:error] ${err.message}`);
241
+ } else {
242
+ console.error(`[nomos:error] Unexpected error: ${err}`);
243
+ }
244
+ process.exit(1);
245
+ }
246
+ });
247
+
248
+ // ─── arc auth status ─────────────────────────────────────────────
249
+ auth
250
+ .command('status')
251
+ .description('Show current authentication state')
252
+ .action(async () => {
253
+ try {
254
+ // 1. Load config, create AuthManager
255
+ const { config, projectRoot } = loadConfig();
256
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
257
+ const logger = createLogger(config.logging.level, logDir);
258
+ const authManager = new AuthManager(config.auth, logger);
259
+
260
+ // 2. Check login state
261
+ const loggedIn = authManager.isLoggedIn();
262
+
263
+ // 3. If logged in, show token info
264
+ if (loggedIn) {
265
+ const creds = authManager.loadCredentials()!;
266
+ const expiryDate = new Date(creds.expiry_date);
267
+ const isExpired = creds.expiry_date < Date.now();
268
+ console.log(`OAuth: Logged in (token ${isExpired ? 'EXPIRED' : 'valid until ' + expiryDate.toLocaleString()})`);
269
+
270
+ // 4. Show quota project
271
+ if (creds.quota_project_id) {
272
+ console.log(`GCP Billing Project: ${creds.quota_project_id}`);
273
+ } else {
274
+ console.log('GCP Billing Project: Not set (run `arc auth login` or `arc auth set-project <ID>`)');
275
+ }
276
+ } else {
277
+ console.log('OAuth: Not logged in');
278
+ }
279
+
280
+ // 5. Check API key
281
+ const hasApiKey = !!process.env['GEMINI_API_KEY'];
282
+ console.log(`API Key: ${hasApiKey ? 'Set (GEMINI_API_KEY)' : 'Not set'}`);
283
+
284
+ // 6. Show credential chain resolution
285
+ if (hasApiKey) {
286
+ console.log('Active credential: GEMINI_API_KEY (takes priority over OAuth)');
287
+ } else if (loggedIn) {
288
+ console.log('Active credential: OAuth access token');
289
+ } else {
290
+ console.log('Active credential: None — run `arc auth login` or set GEMINI_API_KEY');
291
+ }
292
+ } catch (err) {
293
+ if (err instanceof NomosError) {
294
+ console.error(`[nomos:error] ${err.message}`);
295
+ } else {
296
+ console.error(`[nomos:error] Unexpected error: ${err}`);
297
+ }
298
+ process.exit(1);
299
+ }
300
+ });
301
+ }
@@ -0,0 +1,89 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { Command } from 'commander';
4
+ import { createOrchestrator } from '../core/factory.js';
5
+ import { CertificateEngine } from '../core/certificate.js';
6
+ import { NomosError } from '../core/errors.js';
7
+
8
+ export function registerCertificateCommand(program: Command): void {
9
+ program
10
+ .command('certificate <task>')
11
+ .description('Generate or verify a Certificate of AI Engineering Integrity')
12
+ .option('--output <path>', 'Output path for the certificate file')
13
+ .option('--format <fmt>', 'Output format', 'json')
14
+ .option('--verify', 'Verify an existing certificate instead of generating')
15
+ .action(async (task: string, opts: { output?: string; format: string; verify?: boolean }) => {
16
+ try {
17
+ const { orchestrator, projectRoot } = await createOrchestrator();
18
+ const engine = new CertificateEngine();
19
+
20
+ const defaultPath = path.join(
21
+ projectRoot, 'tasks-management', 'certificates', `${task}.certificate.json`,
22
+ );
23
+ const certPath = opts.output ?? defaultPath;
24
+
25
+ if (opts.verify) {
26
+ // ── Verify mode ──────────────────────────────────────────────
27
+ if (!fs.existsSync(certPath)) {
28
+ throw new NomosError(
29
+ 'certificate_not_found',
30
+ `Certificate file not found: ${certPath}`,
31
+ );
32
+ }
33
+
34
+ const raw = fs.readFileSync(certPath, 'utf-8');
35
+ const certificate = engine.parse(raw);
36
+ const result = engine.verify(certificate);
37
+
38
+ console.log(`\nCertificate Verification: ${task}`);
39
+ console.log('─'.repeat(50));
40
+
41
+ for (const check of result.checks) {
42
+ const icon = check.passed ? 'PASS' : 'FAIL';
43
+ console.log(` [${icon}] ${check.name}`);
44
+ console.log(` ${check.detail}`);
45
+ }
46
+
47
+ console.log('─'.repeat(50));
48
+ console.log(`Result: ${result.valid ? 'VALID' : 'INVALID'}`);
49
+
50
+ process.exit(result.valid ? 0 : 1);
51
+ }
52
+
53
+ // ── Generate mode ────────────────────────────────────────────
54
+ const state = await orchestrator.status(task);
55
+ const certificate = engine.generate(state);
56
+
57
+ // Ensure output directory exists
58
+ fs.mkdirSync(path.dirname(certPath), { recursive: true });
59
+ fs.writeFileSync(certPath, JSON.stringify(certificate, null, 2), 'utf-8');
60
+
61
+ const finalScore = certificate.final_review.score;
62
+ const iterCount = certificate.iterations.length;
63
+
64
+ console.log(
65
+ `\nCertificate Generated\n` +
66
+ `─`.repeat(50) + `\n` +
67
+ `Task: ${certificate.task_id}\n` +
68
+ `Status: ${certificate.task_status}\n` +
69
+ `Iterations: ${iterCount}\n` +
70
+ `Final Score: ${finalScore.toFixed(2)}\n` +
71
+ `Approval: ${certificate.final_review.approval_reason}\n` +
72
+ `Chain Hash: ${certificate.integrity.chain_hash}\n` +
73
+ `Certificate: ${certificate.certificate_hash}\n` +
74
+ `─`.repeat(50) + `\n` +
75
+ `Saved to: ${certPath}\n` +
76
+ `\nVerify with: arc certificate ${task} --verify`,
77
+ );
78
+
79
+ process.exit(0);
80
+ } catch (err) {
81
+ if (err instanceof NomosError) {
82
+ console.error(`[nomos:error] ${err.message}`);
83
+ } else {
84
+ console.error(`[nomos:error] Unexpected error: ${err}`);
85
+ }
86
+ process.exit(1);
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,24 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+
5
+ export function registerDiscardCommand(program: Command): void {
6
+ program
7
+ .command('discard <task>')
8
+ .description('Discard a task: remove worktree, delete shadow branch, and mark as discarded')
9
+ .action(async (task: string) => {
10
+ try {
11
+ const { orchestrator } = await createOrchestrator();
12
+ await orchestrator.discard(task);
13
+ console.log(`Task '${task}' discarded.`);
14
+ process.exit(0);
15
+ } catch (err) {
16
+ if (err instanceof NomosError) {
17
+ console.error(`[nomos:error] ${err.message}`);
18
+ } else {
19
+ console.error(`[nomos:error] Unexpected error: ${err}`);
20
+ }
21
+ process.exit(1);
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,116 @@
1
+ import * as path from 'node:path';
2
+ import type { Command } from 'commander';
3
+ import { simpleGit } from 'simple-git';
4
+ import { loadConfig } from '../core/config.js';
5
+ import { createLogger } from '../core/logger.js';
6
+ import { NomosError } from '../core/errors.js';
7
+ import { readProjectMap, migrateProjectMap } from '../core/graph/map-schema.js';
8
+ import { compare } from '../core/graph/drift/comparator.js';
9
+ import { classify } from '../core/graph/drift/classifier.js';
10
+ import { render } from '../core/graph/drift/reporter.js';
11
+
12
+ export function registerDriftCommand(program: Command): void {
13
+ program
14
+ .command('drift')
15
+ .description('Detect structural drift between the current project map and a baseline')
16
+ .option('--baseline <path>', 'Path to a baseline project_map.json file')
17
+ .option('--json', 'Output drift report as JSON', false)
18
+ .option('--breaking-only', 'Only report breaking changes', false)
19
+ .action(
20
+ async (opts: { baseline?: string; json: boolean; breakingOnly: boolean }) => {
21
+ const { config, projectRoot } = loadConfig();
22
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
23
+ const logger = createLogger(config.logging.level, logDir);
24
+
25
+ const currentMapPath = path.join(
26
+ projectRoot,
27
+ config.graph.output_dir,
28
+ 'project_map.json',
29
+ );
30
+
31
+ // ── Step 3: Resolve baseline map ────────────────────────────────────────
32
+ let baseline;
33
+
34
+ if (opts.baseline) {
35
+ // --baseline <path> provided: read from file
36
+ const result = await readProjectMap(opts.baseline);
37
+ if (result === null) {
38
+ throw new NomosError(
39
+ 'drift_baseline_not_found',
40
+ `Baseline map not found at '${opts.baseline}'. Ensure the file exists and is readable.`,
41
+ );
42
+ }
43
+ baseline = result;
44
+ } else {
45
+ // No --baseline: read HEAD:project_map.json from git history
46
+ const git = simpleGit(projectRoot);
47
+ const relativeMapPath = path.relative(projectRoot, currentMapPath).split(path.sep).join('/');
48
+
49
+ let rawGitContent: string;
50
+ try {
51
+ rawGitContent = await git.show([`HEAD:${relativeMapPath}`]);
52
+ } catch {
53
+ throw new NomosError(
54
+ 'drift_baseline_not_found',
55
+ 'No baseline map found. Commit a project_map.json first, or use --baseline <path>.',
56
+ );
57
+ }
58
+
59
+ let parsedBaseline: unknown;
60
+ try {
61
+ parsedBaseline = JSON.parse(rawGitContent);
62
+ } catch {
63
+ throw new NomosError(
64
+ 'drift_parse_error',
65
+ 'Baseline map from git history is not valid JSON. Ensure the committed project_map.json is well-formed.',
66
+ );
67
+ }
68
+
69
+ baseline = migrateProjectMap(parsedBaseline);
70
+ }
71
+
72
+ // ── Step 4: Load current map ─────────────────────────────────────────────
73
+ const current = await readProjectMap(currentMapPath);
74
+ if (current === null) {
75
+ throw new NomosError(
76
+ 'graph_map_not_found',
77
+ 'No current project map. Run: arc map',
78
+ );
79
+ }
80
+
81
+ // ── Steps 5-7: Compare → Classify → Render ───────────────────────────────
82
+ logger.debug('Running drift comparison...');
83
+ const report = compare(baseline, current);
84
+ const classified = classify(report);
85
+ const output = render(classified, baseline.generated_at, current.generated_at, {
86
+ json: opts.json,
87
+ breakingOnly: opts.breakingOnly,
88
+ });
89
+
90
+ // ── Step 8: Output ────────────────────────────────────────────────────────
91
+ console.log(output);
92
+
93
+ // ── Step 9: Exit code — DO NOT call process.exit() (ref: F-001) ──────────
94
+ // process.exitCode is set and main() in cli.ts handles termination.
95
+ const s = report.summary;
96
+ const hasAnything =
97
+ s.files_added > 0 ||
98
+ s.files_removed > 0 ||
99
+ s.files_modified > 0 ||
100
+ s.symbols_added > 0 ||
101
+ s.symbols_removed > 0 ||
102
+ s.symbols_changed > 0 ||
103
+ s.imports_added > 0 ||
104
+ s.imports_removed > 0 ||
105
+ s.depth_changes > 0 ||
106
+ s.stale_enrichments > 0;
107
+
108
+ if (classified.has_breaking) {
109
+ process.exitCode = 3;
110
+ } else if (hasAnything) {
111
+ process.exitCode = 2;
112
+ }
113
+ // else: default exitCode = 0 (no drift)
114
+ },
115
+ );
116
+ }
@@ -0,0 +1,78 @@
1
+ import * as path from 'node:path';
2
+ import type { Command } from 'commander';
3
+ import { loadConfig } from '../core/config.js';
4
+ import { createLogger } from '../core/logger.js';
5
+ import { NomosError } from '../core/errors.js';
6
+ import { SearchIndexer } from '../search/indexer.js';
7
+ import { AuthManager } from '../core/auth/manager.js';
8
+
9
+ export function registerIndexCommand(program: Command): void {
10
+ program
11
+ .command('index')
12
+ .description('Build or rebuild the vector search index from project map')
13
+ .option('--incremental', 'Only re-index files changed since last indexing run')
14
+ .option('--force', 'Force full re-index (ignore incremental metadata)')
15
+ .option('--dry-run', 'Count chunks without embedding (no API calls, no writes)')
16
+ .action(async (opts: { incremental?: boolean; force?: boolean; dryRun?: boolean }) => {
17
+ try {
18
+ const { config, projectRoot } = loadConfig();
19
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
20
+ const logger = createLogger(config.logging.level, logDir);
21
+ const authManager = new AuthManager(config.auth, logger);
22
+ const indexer = new SearchIndexer(projectRoot, config, logger, authManager);
23
+
24
+ // --dry-run: count chunks only, no API calls, no writes [S-2]
25
+ if (opts.dryRun) {
26
+ logger.info('[nomos:search:info] DRY RUN — no API calls, no writes.');
27
+ const { fileChunks, symbolChunks, totalChunks } = await indexer.dryRun();
28
+ const batchSize = config.search.batch_size;
29
+ const batchCount = Math.ceil(totalChunks / batchSize);
30
+ logger.info(
31
+ `[nomos:search:info] Would index: ${totalChunks} chunks (${fileChunks} file-level, ${symbolChunks} symbol-level)`,
32
+ );
33
+ logger.info(
34
+ `[nomos:search:info] Estimated API calls: ${batchCount} batches × ${batchSize} chunks`,
35
+ );
36
+ process.exit(0);
37
+ }
38
+
39
+ const cancellationFlag = { cancelled: false };
40
+
41
+ process.on('SIGINT', () => {
42
+ cancellationFlag.cancelled = true;
43
+ logger.warn('[nomos:search:warn] SIGINT received. Finishing current batch...');
44
+ });
45
+
46
+ logger.info('[nomos:search:info] Extracting chunks from project map...');
47
+
48
+ const startTime = Date.now();
49
+
50
+ // --incremental (without --force) → incremental index; everything else → full index
51
+ const meta = (opts.incremental && !opts.force)
52
+ ? await indexer.incrementalIndex(cancellationFlag)
53
+ : await indexer.fullIndex(cancellationFlag);
54
+
55
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
56
+
57
+ console.log(
58
+ `Indexed ${meta.total_chunks} chunks (${meta.total_files_indexed} files, ${meta.total_symbols_indexed} symbols) in ${duration}s`,
59
+ );
60
+ console.log(`Vector index stored at: ${config.search.vector_store_path}`);
61
+
62
+ if (meta.failed_files.length > 0) {
63
+ console.error(
64
+ `⚠ ${meta.failed_files.length} files failed embedding. They will be retried on next incremental index.`,
65
+ );
66
+ }
67
+
68
+ process.exit(0);
69
+ } catch (err) {
70
+ if (err instanceof NomosError) {
71
+ console.error(`[nomos:error] ${err.message}`);
72
+ } else {
73
+ console.error(`[nomos:error] Unexpected error: ${err}`);
74
+ }
75
+ process.exit(1);
76
+ }
77
+ });
78
+ }