@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,1093 @@
1
+ plan:
2
+ task_id: "auth-oauth-login"
3
+ task_title: "Frictionless OAuth 2.0 Google Login for arc CLI"
4
+ created_at: "2026-04-06T00:00:00Z"
5
+
6
+ context_analysis:
7
+ tech_stack:
8
+ language: "TypeScript (strict mode)"
9
+ framework: "Commander.js CLI, Zod validation"
10
+ runtime: "Node.js >= 20 (ESM)"
11
+ package_manager: "npm"
12
+ architecture_pattern: "Modular CLI — Commander.js registerXCommand(program) pattern, Zod config schemas with per-field defaults, NomosError typed error codes, Winston logger injection, no singletons"
13
+ touch_zone:
14
+ - "package.json" # Add google-auth-library dependency (open already present)
15
+ - "src/types/index.ts" # Add AuthCredentials interface, auth config section to NomosConfig
16
+ - "src/core/errors.ts" # Add auth_* error codes to NomosErrorCode union
17
+ - "src/core/config.ts" # Add AuthSchema with Zod defaults to NomosConfigSchema
18
+ - "src/core/auth/manager.ts" # CREATE — AuthManager class
19
+ - "src/core/auth/server.ts" # CREATE — OAuth loopback HTTP server
20
+ - "src/commands/auth.ts" # CREATE — arc auth login/logout/status
21
+ - "src/cli.ts" # Register registerAuthCommand
22
+ - "src/search/embedder.ts" # Credential chain fallback (sync→async factory)
23
+ - "src/core/graph/enricher.ts" # Credential chain fallback
24
+ - "src/search/indexer.ts" # Update Embedder instantiation to use factory
25
+ - "src/search/query-engine.ts" # Update Embedder instantiation to use factory
26
+ - "src/core/graph/pipeline.ts" # Update SemanticEnricher instantiation
27
+ - "src/commands/index.ts" # Pass authManager or config to indexer
28
+ - "src/commands/search.ts" # Pass authManager or config to query engine
29
+ - "src/commands/map.ts" # Pass authManager or config to pipeline
30
+ fragile_zone:
31
+ - "src/core/state.ts" # Task state management — no changes
32
+ - "src/core/orchestrator.ts" # Orchestration loop — no changes
33
+ - "src/core/budget.ts" # Budget tracking — no changes
34
+ - "src/search/vector-store.ts" # Vector storage — no changes
35
+ - "src/search/chunk-extractor.ts" # Chunk extraction — no changes
36
+ - "src/utils/sanitize.ts" # Sanitization — review only (ensure token coverage)
37
+ dependencies:
38
+ - "google-auth-library (new npm dependency)"
39
+ - "open (already in package.json ^11.0.0)"
40
+ - "@google/generative-ai (existing ^0.24.1)"
41
+ - "commander (existing ^14.0.3)"
42
+ - "winston (existing ^3.19.0)"
43
+ - "zod (existing ^4.3.6)"
44
+ - "Node.js built-in: http, net, fs, path, os, crypto"
45
+
46
+ steps:
47
+ # ═══════════════════════════════════════════════════════════════════════════
48
+ # PHASE 1: CONTRACT FIRST — Types, Error Codes, Config Schema
49
+ # ═══════════════════════════════════════════════════════════════════════════
50
+
51
+ - step_id: "1.1"
52
+ title: "Install google-auth-library dependency"
53
+ action: "CONFIGURE"
54
+ file_path: "package.json"
55
+ description: |
56
+ Run `npm install google-auth-library`.
57
+ This adds the Google OAuth2 client library needed for token exchange,
58
+ refresh, and auth URL generation. The `open` package is already installed.
59
+ Also update the esbuild build script in package.json to add
60
+ `--external:google-auth-library` to the existing externals list.
61
+ inputs:
62
+ - "Current package.json with existing dependencies"
63
+ outputs:
64
+ - "package.json updated with google-auth-library dependency"
65
+ - "package-lock.json updated"
66
+ - "esbuild externals list includes google-auth-library"
67
+ validation: |
68
+ `npm ls google-auth-library` exits 0.
69
+ `grep 'google-auth-library' package.json` returns a match.
70
+ Build script contains `--external:google-auth-library`.
71
+ depends_on: []
72
+ can_parallel: true
73
+ risk_level: "low"
74
+ rollback: |
75
+ `npm uninstall google-auth-library`. Revert esbuild script change.
76
+
77
+ - step_id: "1.2"
78
+ title: "Add AuthCredentials interface and auth config to NomosConfig"
79
+ action: "MODIFY"
80
+ file_path: "src/types/index.ts"
81
+ description: |
82
+ Add the following AFTER the existing `ExecutionMode` type declaration block
83
+ and BEFORE the `NomosConfig` interface:
84
+
85
+ ```typescript
86
+ // ─── Auth Types ──────────────────────────────────────────────────────────────
87
+ export interface AuthCredentials {
88
+ access_token: string;
89
+ refresh_token: string;
90
+ expiry_date: number;
91
+ token_type: string;
92
+ scope: string;
93
+ }
94
+ ```
95
+
96
+ Then add an `auth` section to the `NomosConfig` interface, after the `search`
97
+ block:
98
+
99
+ ```typescript
100
+ auth: {
101
+ client_id?: string;
102
+ client_secret?: string;
103
+ credentials_path: string;
104
+ redirect_port: number;
105
+ };
106
+ ```
107
+
108
+ The `credentials_path` defaults to `~/.nomos/credentials.json`.
109
+ The `redirect_port` defaults to `3000` (fixed port for Google Cloud Console
110
+ redirect URI matching, with fallback logic handled in the server module).
111
+ inputs:
112
+ - "Existing NomosConfig interface (lines 15-75 of src/types/index.ts)"
113
+ outputs:
114
+ - "AuthCredentials interface available for import"
115
+ - "NomosConfig.auth section defined"
116
+ validation: |
117
+ `npx tsc --noEmit` passes. AuthCredentials and NomosConfig['auth'] are
118
+ importable from '../types/index.js'.
119
+ depends_on: []
120
+ can_parallel: true
121
+ risk_level: "low"
122
+ rollback: |
123
+ Remove the added interface and config section from src/types/index.ts.
124
+
125
+ - step_id: "1.3"
126
+ title: "Register auth error codes in NomosErrorCode union"
127
+ action: "MODIFY"
128
+ file_path: "src/core/errors.ts"
129
+ description: |
130
+ Add four new error codes to the `NomosErrorCode` union type, after the
131
+ existing `search_api_key_missing` entry:
132
+
133
+ ```typescript
134
+ | 'auth_not_logged_in'
135
+ | 'auth_token_expired'
136
+ | 'auth_login_failed'
137
+ | 'auth_client_config_missing';
138
+ ```
139
+
140
+ These codes are used by AuthManager and the auth commands to throw
141
+ typed NomosError instances.
142
+ inputs:
143
+ - "Existing NomosErrorCode union (lines 1-38 of src/core/errors.ts)"
144
+ outputs:
145
+ - "Four new error codes available in the NomosErrorCode type"
146
+ validation: |
147
+ `npx tsc --noEmit` passes. Code `new NomosError('auth_not_logged_in', '...')`
148
+ compiles without error.
149
+ depends_on: []
150
+ can_parallel: true
151
+ risk_level: "low"
152
+ rollback: |
153
+ Remove the four added lines from the NomosErrorCode union.
154
+
155
+ - step_id: "1.4"
156
+ title: "Add AuthSchema to Zod config with defaults"
157
+ action: "MODIFY"
158
+ file_path: "src/core/config.ts"
159
+ description: |
160
+ Add a new `AuthSchema` Zod object AFTER the existing `SearchConfigSchema`
161
+ (around line 105) and BEFORE `NomosConfigSchema`:
162
+
163
+ ```typescript
164
+ const AuthSchema = z.object({
165
+ client_id: z.string().optional(),
166
+ client_secret: z.string().optional(),
167
+ credentials_path: z.string().default(
168
+ path.join(os.homedir(), '.nomos', 'credentials.json'),
169
+ ),
170
+ redirect_port: z.number().int().positive().default(3000),
171
+ });
172
+ ```
173
+
174
+ Add `import * as os from 'node:os';` to the top imports.
175
+
176
+ Then add the `auth` key to `NomosConfigSchema`:
177
+
178
+ ```typescript
179
+ auth: AuthSchema.default(() => AuthSchema.parse({})),
180
+ ```
181
+
182
+ This follows the exact same pattern as every other config section
183
+ (e.g., `search: SearchConfigSchema.default(...)`).
184
+ inputs:
185
+ - "Existing NomosConfigSchema (lines 118-129 of src/core/config.ts)"
186
+ - "Step 1.2 must be complete (NomosConfig type has auth section)"
187
+ outputs:
188
+ - "Auth config section parsed and defaulted by Zod"
189
+ - ".nomos-config.json can optionally contain auth.client_id, auth.client_secret, auth.redirect_port"
190
+ validation: |
191
+ `npx tsc --noEmit` passes. `getDefaultConfig().auth.credentials_path` ends
192
+ with `.nomos/credentials.json`.
193
+ depends_on: ["1.2"]
194
+ can_parallel: false
195
+ risk_level: "low"
196
+ rollback: |
197
+ Remove AuthSchema definition and the auth key from NomosConfigSchema.
198
+ Remove os import.
199
+
200
+ - step_id: "1.5"
201
+ title: "Create src/core/auth/ directory"
202
+ action: "CREATE"
203
+ file_path: "src/core/auth/"
204
+ description: |
205
+ Create the directory `src/core/auth/`. This is where manager.ts and
206
+ server.ts will live. No index barrel file needed — direct imports are
207
+ used throughout the project (e.g., `import { Embedder } from './embedder.js'`).
208
+ inputs: []
209
+ outputs:
210
+ - "Directory src/core/auth/ exists"
211
+ validation: |
212
+ `ls src/core/auth/` succeeds.
213
+ depends_on: []
214
+ can_parallel: true
215
+ risk_level: "low"
216
+ rollback: |
217
+ `rm -rf src/core/auth/`
218
+
219
+ # ═══════════════════════════════════════════════════════════════════════════
220
+ # PHASE 2: ISOLATED LOGIC — AuthManager, Loopback Server
221
+ # ═══════════════════════════════════════════════════════════════════════════
222
+
223
+ - step_id: "2.1"
224
+ title: "Implement AuthManager class"
225
+ action: "CREATE"
226
+ file_path: "src/core/auth/manager.ts"
227
+ description: |
228
+ Create `AuthManager` class with the following contract:
229
+
230
+ **Constructor:** `constructor(authConfig: NomosConfig['auth'], logger: Logger)`
231
+ - Stores config and logger. Does NOT read files or perform I/O in constructor.
232
+ - Logger type: `import type { Logger } from 'winston'`.
233
+
234
+ **Methods:**
235
+
236
+ 1. `async saveCredentials(tokens: AuthCredentials): Promise<void>`
237
+ - Ensure `~/.nomos/` directory exists (`fs.mkdir(dir, { recursive: true })`).
238
+ - Write JSON to `authConfig.credentials_path`.
239
+ - Set file permissions: `fs.chmod(path, 0o600)`.
240
+ - Log success via `logger.info()`. Never log token values.
241
+
242
+ 2. `loadCredentials(): AuthCredentials | null`
243
+ - Synchronous read. Return `null` if file doesn't exist or is invalid JSON.
244
+ - Parse and validate shape matches `AuthCredentials` interface.
245
+
246
+ 3. `async getAuthenticatedClient(): Promise<OAuth2Client>`
247
+ - Load credentials via `loadCredentials()`.
248
+ - If null, throw `NomosError('auth_not_logged_in', ...)`.
249
+ - Create `OAuth2Client` from `google-auth-library`, set credentials.
250
+ - Check `expiry_date < Date.now()`:
251
+ - If expired, call `oauth2Client.refreshAccessToken()`.
252
+ - Persist refreshed tokens via `saveCredentials()`.
253
+ - Return the configured `OAuth2Client`.
254
+
255
+ 4. `isLoggedIn(): boolean`
256
+ - Synchronous. Check if credentials file exists AND contains `refresh_token`.
257
+
258
+ 5. `async clearCredentials(): Promise<void>`
259
+ - Delete credentials file if it exists (`fs.unlink`).
260
+ - Log confirmation.
261
+
262
+ 6. `async getAccessToken(): Promise<string>`
263
+ - Convenience method. Calls `getAuthenticatedClient()`, then
264
+ `client.getAccessToken()`. Returns the token string.
265
+ - This is the method `Embedder` and `SemanticEnricher` will use
266
+ to get a bearer token when `GEMINI_API_KEY` is absent.
267
+
268
+ **Imports:** `fs` (node:fs and node:fs/promises), `path`, `OAuth2Client`
269
+ from `google-auth-library`, `NomosError`, `AuthCredentials`, `NomosConfig`.
270
+
271
+ **Security:** Never log `access_token`, `refresh_token`, or `client_secret`.
272
+ Use `logger.info('[nomos:auth:info] Credentials saved.')` style messages.
273
+ inputs:
274
+ - "AuthCredentials interface from step 1.2"
275
+ - "NomosConfig['auth'] type from step 1.2 + 1.4"
276
+ - "NomosError auth codes from step 1.3"
277
+ outputs:
278
+ - "AuthManager class with full token lifecycle management"
279
+ validation: |
280
+ `npx tsc --noEmit` passes. All methods have correct return types.
281
+ No `console.log` calls — only `logger.*`.
282
+ depends_on: ["1.2", "1.3", "1.4", "1.5"]
283
+ can_parallel: false
284
+ risk_level: "medium"
285
+ rollback: |
286
+ Delete src/core/auth/manager.ts.
287
+
288
+ - step_id: "2.2"
289
+ title: "Implement OAuth loopback server"
290
+ action: "CREATE"
291
+ file_path: "src/core/auth/server.ts"
292
+ description: |
293
+ Create a single exported function:
294
+
295
+ ```typescript
296
+ export async function startLoopbackServer(
297
+ oauth2Client: OAuth2Client,
298
+ scopes: string[],
299
+ port: number,
300
+ logger: Logger,
301
+ ): Promise<AuthCredentials>
302
+ ```
303
+
304
+ **Implementation details:**
305
+
306
+ 1. **Server creation:** Use Node.js built-in `http.createServer()`.
307
+
308
+ 2. **Port binding:** Attempt to listen on `port` (default 3000 from config).
309
+ If EADDRINUSE, try `listen(0)` for a random available port.
310
+ Log the actual port used.
311
+
312
+ 3. **Auth URL generation:**
313
+ ```typescript
314
+ const redirectUri = `http://localhost:${actualPort}`;
315
+ oauth2Client.redirectUri = redirectUri; // must be set before generateAuthUrl
316
+ const authUrl = oauth2Client.generateAuthUrl({
317
+ access_type: 'offline',
318
+ scope: scopes,
319
+ redirect_uri: redirectUri,
320
+ });
321
+ ```
322
+
323
+ 4. **Browser launch:** `import open from 'open'`. Call `await open(authUrl)`.
324
+ Wrap in try/catch — if browser launch fails (headless), log the URL:
325
+ `logger.info('[nomos:auth:info] Open this URL in your browser: <url>')`.
326
+
327
+ 5. **Callback handler:** On incoming GET request:
328
+ - Parse `req.url` for `code` query parameter.
329
+ - If no code, respond 400 and continue listening.
330
+ - Exchange code: `const { tokens } = await oauth2Client.getToken(code)`.
331
+ - Respond with success HTML: simple page saying "Login successful! You can close this tab."
332
+ - Map tokens to `AuthCredentials` shape and resolve the Promise.
333
+
334
+ 6. **Timeout:** 120-second timeout via `setTimeout`. On timeout:
335
+ - Destroy all tracked sockets.
336
+ - `server.close()`.
337
+ - Reject with `NomosError('auth_login_failed', 'Login timed out after 120 seconds.')`.
338
+
339
+ 7. **Socket tracking:** Maintain `const sockets = new Set<net.Socket>()`.
340
+ Track on `server.on('connection', socket => sockets.add(socket))`.
341
+ On cleanup, iterate and `socket.destroy()`.
342
+
343
+ 8. **Cleanup:** After receiving callback OR on timeout, destroy all sockets
344
+ and close server. The function must never leave a dangling server.
345
+
346
+ **Imports:** `http`, `net`, `url` (node:url for URL parsing), `open`,
347
+ `OAuth2Client` from google-auth-library.
348
+ inputs:
349
+ - "OAuth2Client instance (created by caller with client_id/client_secret)"
350
+ - "AuthCredentials interface from step 1.2"
351
+ outputs:
352
+ - "AuthCredentials object with access_token, refresh_token, expiry_date, etc."
353
+ validation: |
354
+ `npx tsc --noEmit` passes. Function signature matches spec.
355
+ Server cleanup is guaranteed (finally block or equivalent).
356
+ depends_on: ["1.2", "1.3", "1.5"]
357
+ can_parallel: true
358
+ risk_level: "high"
359
+ rollback: |
360
+ Delete src/core/auth/server.ts.
361
+
362
+ # ═══════════════════════════════════════════════════════════════════════════
363
+ # PHASE 3: CLI COMMANDS — arc auth login/logout/status
364
+ # ═══════════════════════════════════════════════════════════════════════════
365
+
366
+ - step_id: "3.1"
367
+ title: "Create arc auth command group"
368
+ action: "CREATE"
369
+ file_path: "src/commands/auth.ts"
370
+ description: |
371
+ Create `registerAuthCommand(program: Command): void` following the exact
372
+ same pattern as every other command in `src/commands/`.
373
+
374
+ **Structure:**
375
+
376
+ ```typescript
377
+ import type { Command } from 'commander';
378
+ import { OAuth2Client } from 'google-auth-library';
379
+ import { loadConfig } from '../core/config.js';
380
+ import { createLogger } from '../core/logger.js';
381
+ import { NomosError } from '../core/errors.js';
382
+ import { AuthManager } from '../core/auth/manager.js';
383
+ import { startLoopbackServer } from '../core/auth/server.js';
384
+
385
+ const SCOPES = ['https://www.googleapis.com/auth/generative-language'];
386
+
387
+ export function registerAuthCommand(program: Command): void {
388
+ const auth = program
389
+ .command('auth')
390
+ .description('Manage Google OAuth authentication');
391
+
392
+ // ─── arc auth login ──────────────────────────────────────────────
393
+ auth
394
+ .command('login')
395
+ .description('Authenticate with Google via OAuth 2.0')
396
+ .option('--client-id <id>', 'Google OAuth client ID')
397
+ .option('--client-secret <secret>', 'Google OAuth client secret')
398
+ .action(async (opts) => { ... });
399
+
400
+ // ─── arc auth logout ─────────────────────────────────────────────
401
+ auth
402
+ .command('logout')
403
+ .description('Remove stored OAuth credentials')
404
+ .action(async () => { ... });
405
+
406
+ // ─── arc auth status ─────────────────────────────────────────────
407
+ auth
408
+ .command('status')
409
+ .description('Show current authentication state')
410
+ .action(async () => { ... });
411
+ }
412
+ ```
413
+
414
+ **`arc auth login` flow:**
415
+ 1. Load config via `loadConfig()`.
416
+ 2. Resolve `client_id` and `client_secret`:
417
+ - Priority: CLI flags `--client-id`/`--client-secret` > config `auth.client_id`/`auth.client_secret`.
418
+ - If neither available, throw `NomosError('auth_client_config_missing', ...)` with
419
+ message explaining how to provide them.
420
+ 3. Create `OAuth2Client` with `{ clientId, clientSecret }`.
421
+ 4. Create `AuthManager` with config.auth and logger.
422
+ 5. Call `startLoopbackServer(oauth2Client, SCOPES, config.auth.redirect_port, logger)`.
423
+ 6. On success: `authManager.saveCredentials(tokens)`.
424
+ 7. Print: `✓ Logged in successfully. Credentials saved to <path>`.
425
+
426
+ **`arc auth logout` flow:**
427
+ 1. Load config, create AuthManager.
428
+ 2. Call `authManager.clearCredentials()`.
429
+ 3. Print: `✓ Logged out. Credentials removed.`
430
+
431
+ **`arc auth status` flow:**
432
+ 1. Load config, create AuthManager.
433
+ 2. Check `authManager.isLoggedIn()`.
434
+ 3. If logged in: load credentials, display token expiry (human-readable).
435
+ 4. Check `process.env['GEMINI_API_KEY']` — display whether API key is set.
436
+ 5. Print credential chain resolution order being used.
437
+
438
+ **Error handling:** Wrap each action in try/catch, matching the pattern
439
+ in `src/commands/index.ts` (lines 67-75): NomosError → `console.error` + exit 1.
440
+ inputs:
441
+ - "AuthManager from step 2.1"
442
+ - "startLoopbackServer from step 2.2"
443
+ - "Config with auth section from step 1.4"
444
+ outputs:
445
+ - "`arc auth login`, `arc auth logout`, `arc auth status` commands functional"
446
+ validation: |
447
+ `npx tsc --noEmit` passes.
448
+ `npx tsx src/cli.ts auth --help` lists login, logout, status.
449
+ `npx tsx src/cli.ts auth login --help` shows --client-id and --client-secret options.
450
+ depends_on: ["2.1", "2.2"]
451
+ can_parallel: false
452
+ risk_level: "medium"
453
+ rollback: |
454
+ Delete src/commands/auth.ts.
455
+
456
+ - step_id: "3.2"
457
+ title: "Register auth command in CLI entrypoint"
458
+ action: "MODIFY"
459
+ file_path: "src/cli.ts"
460
+ description: |
461
+ 1. Add import at the top (after the existing command imports, around line 16):
462
+ ```typescript
463
+ import { registerAuthCommand } from './commands/auth.js';
464
+ ```
465
+
466
+ 2. Add `registerAuthCommand` to the registration array (after
467
+ `registerSearchCommand`, around line 59):
468
+ ```typescript
469
+ registerAuthCommand,
470
+ ```
471
+ inputs:
472
+ - "registerAuthCommand function from step 3.1"
473
+ outputs:
474
+ - "`arc auth` appears in `arc --help` output"
475
+ validation: |
476
+ `npx tsc --noEmit` passes.
477
+ `npx tsx src/cli.ts --help` lists 'auth' command.
478
+ depends_on: ["3.1"]
479
+ can_parallel: false
480
+ risk_level: "low"
481
+ rollback: |
482
+ Remove the import line and the array entry from src/cli.ts.
483
+
484
+ # ═══════════════════════════════════════════════════════════════════════════
485
+ # PHASE 4: CREDENTIAL CHAIN INTEGRATION
486
+ # ═══════════════════════════════════════════════════════════════════════════
487
+
488
+ - step_id: "4.1"
489
+ title: "Add async factory method to Embedder with credential chain"
490
+ action: "MODIFY"
491
+ file_path: "src/search/embedder.ts"
492
+ description: |
493
+ The Embedder constructor is currently synchronous and reads GEMINI_API_KEY
494
+ from process.env. The OAuth fallback requires async operations (file I/O,
495
+ token refresh). To avoid breaking the constructor contract, we introduce
496
+ a static factory method while keeping the constructor functional for the
497
+ API key path.
498
+
499
+ **Changes:**
500
+
501
+ 1. Add a private constructor overload that accepts an API key string
502
+ directly (instead of reading from env):
503
+
504
+ Change the constructor to accept an optional `apiKey` parameter:
505
+ ```typescript
506
+ constructor(
507
+ private readonly config: NomosConfig['search'],
508
+ private readonly logger: Logger,
509
+ apiKey?: string,
510
+ ) {
511
+ const key = apiKey ?? process.env['GEMINI_API_KEY'];
512
+ if (!key) {
513
+ throw new NomosError(
514
+ 'search_api_key_missing',
515
+ 'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
516
+ );
517
+ }
518
+ this.client = new GoogleGenerativeAI(key);
519
+ }
520
+ ```
521
+
522
+ 2. Add a static async factory method:
523
+ ```typescript
524
+ static async create(
525
+ config: NomosConfig['search'],
526
+ logger: Logger,
527
+ authManager?: AuthManager | null,
528
+ ): Promise<Embedder> {
529
+ // Priority 1: GEMINI_API_KEY
530
+ const envKey = process.env['GEMINI_API_KEY'];
531
+ if (envKey) {
532
+ return new Embedder(config, logger, envKey);
533
+ }
534
+
535
+ // Priority 2: OAuth credentials
536
+ if (authManager?.isLoggedIn()) {
537
+ const token = await authManager.getAccessToken();
538
+ return new Embedder(config, logger, token);
539
+ }
540
+
541
+ // Priority 3: Neither — throw
542
+ throw new NomosError(
543
+ 'search_api_key_missing',
544
+ 'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
545
+ );
546
+ }
547
+ ```
548
+
549
+ 3. Add import for AuthManager:
550
+ ```typescript
551
+ import { AuthManager } from '../core/auth/manager.js';
552
+ ```
553
+
554
+ **Backward compatibility:** The existing `new Embedder(config, logger)`
555
+ constructor still works for any caller that doesn't need OAuth. The factory
556
+ method is the new preferred path.
557
+ inputs:
558
+ - "Existing Embedder class (src/search/embedder.ts)"
559
+ - "AuthManager from step 2.1"
560
+ outputs:
561
+ - "Embedder.create() factory method available"
562
+ - "Existing constructor still works for API key path"
563
+ validation: |
564
+ `npx tsc --noEmit` passes.
565
+ `new Embedder(config, logger)` still compiles (backward compat).
566
+ `Embedder.create(config, logger, authManager)` compiles.
567
+ depends_on: ["2.1"]
568
+ can_parallel: true
569
+ risk_level: "high"
570
+ rollback: |
571
+ Revert src/search/embedder.ts to original constructor.
572
+ Remove AuthManager import and create() method.
573
+
574
+ - step_id: "4.2"
575
+ title: "Add credential chain to SemanticEnricher"
576
+ action: "MODIFY"
577
+ file_path: "src/core/graph/enricher.ts"
578
+ description: |
579
+ Apply the same credential chain pattern to SemanticEnricher.
580
+
581
+ **Changes:**
582
+
583
+ 1. Modify constructor to accept optional `apiKey` parameter:
584
+ ```typescript
585
+ constructor(
586
+ private readonly projectRoot: string,
587
+ private readonly config: NomosConfig['graph'],
588
+ private readonly logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
589
+ apiKey?: string,
590
+ ) {
591
+ const key = apiKey ?? process.env['GEMINI_API_KEY'];
592
+ if (!key && config.ai_enrichment) {
593
+ throw new NomosError(
594
+ 'graph_ai_key_missing',
595
+ 'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
596
+ );
597
+ }
598
+ this.client = new GoogleGenerativeAI(key ?? '');
599
+ this.limit = pLimit(config.ai_concurrency);
600
+ }
601
+ ```
602
+
603
+ 2. Add static async factory method:
604
+ ```typescript
605
+ static async create(
606
+ projectRoot: string,
607
+ config: NomosConfig['graph'],
608
+ logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
609
+ authManager?: AuthManager | null,
610
+ ): Promise<SemanticEnricher> {
611
+ const envKey = process.env['GEMINI_API_KEY'];
612
+ if (envKey) {
613
+ return new SemanticEnricher(projectRoot, config, logger, envKey);
614
+ }
615
+ if (authManager?.isLoggedIn()) {
616
+ const token = await authManager.getAccessToken();
617
+ return new SemanticEnricher(projectRoot, config, logger, token);
618
+ }
619
+ // No key and enrichment enabled — let constructor throw
620
+ return new SemanticEnricher(projectRoot, config, logger);
621
+ }
622
+ ```
623
+
624
+ 3. Add import for AuthManager:
625
+ ```typescript
626
+ import { AuthManager } from '../auth/manager.js';
627
+ ```
628
+
629
+ **Backward compatibility:** `new SemanticEnricher(root, config, logger)`
630
+ still works exactly as before.
631
+ inputs:
632
+ - "Existing SemanticEnricher class (src/core/graph/enricher.ts)"
633
+ - "AuthManager from step 2.1"
634
+ outputs:
635
+ - "SemanticEnricher.create() factory method available"
636
+ - "Existing constructor still works"
637
+ validation: |
638
+ `npx tsc --noEmit` passes.
639
+ `new SemanticEnricher(root, config, logger)` still compiles.
640
+ depends_on: ["2.1"]
641
+ can_parallel: true
642
+ risk_level: "high"
643
+ rollback: |
644
+ Revert src/core/graph/enricher.ts to original constructor.
645
+
646
+ - step_id: "4.3"
647
+ title: "Update SearchIndexer to use Embedder.create() factory"
648
+ action: "MODIFY"
649
+ file_path: "src/search/indexer.ts"
650
+ description: |
651
+ The SearchIndexer uses a lazy `get embedder()` accessor that calls
652
+ `new Embedder(config, logger)`. Update it to support the credential chain.
653
+
654
+ **Changes:**
655
+
656
+ 1. Add `AuthManager` to constructor parameters (optional):
657
+ ```typescript
658
+ constructor(
659
+ private readonly projectRoot: string,
660
+ private readonly config: NomosConfig,
661
+ private readonly logger: Logger,
662
+ private readonly authManager?: AuthManager | null,
663
+ )
664
+ ```
665
+
666
+ 2. Change the lazy embedder accessor to async. Replace:
667
+ ```typescript
668
+ private get embedder(): Embedder {
669
+ if (!this._embedder) {
670
+ this._embedder = new Embedder(this.config.search, this.logger);
671
+ }
672
+ return this._embedder;
673
+ }
674
+ ```
675
+ With:
676
+ ```typescript
677
+ private async getEmbedder(): Promise<Embedder> {
678
+ if (!this._embedder) {
679
+ this._embedder = await Embedder.create(
680
+ this.config.search, this.logger, this.authManager,
681
+ );
682
+ }
683
+ return this._embedder;
684
+ }
685
+ ```
686
+
687
+ 3. Update all call sites within indexer.ts:
688
+ - `void this.embedder;` → `await this.getEmbedder();` (pre-check)
689
+ - `this.embedder.embedBatch(...)` → `(await this.getEmbedder()).embedBatch(...)`
690
+
691
+ **Backward compatibility:** Callers that don't pass `authManager` get the
692
+ same behavior as before (API key only).
693
+ inputs:
694
+ - "Existing SearchIndexer class (src/search/indexer.ts)"
695
+ - "Embedder.create() from step 4.1"
696
+ outputs:
697
+ - "SearchIndexer uses credential chain when authManager is provided"
698
+ validation: |
699
+ `npx tsc --noEmit` passes.
700
+ `new SearchIndexer(root, config, logger)` still compiles (authManager optional).
701
+ depends_on: ["4.1"]
702
+ can_parallel: false
703
+ risk_level: "medium"
704
+ rollback: |
705
+ Revert the lazy accessor and constructor changes in indexer.ts.
706
+
707
+ - step_id: "4.4"
708
+ title: "Update QueryEngine to use Embedder.create() factory"
709
+ action: "MODIFY"
710
+ file_path: "src/search/query-engine.ts"
711
+ description: |
712
+ Same pattern as step 4.3. The QueryEngine has an identical lazy `get embedder()`
713
+ accessor.
714
+
715
+ **Changes:**
716
+
717
+ 1. Add `AuthManager` to constructor (optional):
718
+ ```typescript
719
+ constructor(
720
+ private readonly projectRoot: string,
721
+ private readonly config: NomosConfig,
722
+ private readonly logger: Logger,
723
+ private readonly authManager?: AuthManager | null,
724
+ )
725
+ ```
726
+
727
+ 2. Change lazy accessor to async `getEmbedder()`.
728
+
729
+ 3. Update `this.embedder.embedOne(query.trim())` in `search()` method to
730
+ `(await this.getEmbedder()).embedOne(query.trim())`.
731
+ inputs:
732
+ - "Existing QueryEngine class (src/search/query-engine.ts)"
733
+ - "Embedder.create() from step 4.1"
734
+ outputs:
735
+ - "QueryEngine uses credential chain when authManager is provided"
736
+ validation: |
737
+ `npx tsc --noEmit` passes.
738
+ depends_on: ["4.1"]
739
+ can_parallel: true
740
+ risk_level: "medium"
741
+ rollback: |
742
+ Revert query-engine.ts changes.
743
+
744
+ - step_id: "4.5"
745
+ title: "Update MapPipeline to use SemanticEnricher.create() factory"
746
+ action: "MODIFY"
747
+ file_path: "src/core/graph/pipeline.ts"
748
+ description: |
749
+ In the `run()` method, the enricher is instantiated at line 146:
750
+ ```typescript
751
+ const enricher = new SemanticEnricher(this.projectRoot, this.config.graph, this.logger);
752
+ ```
753
+
754
+ **Changes:**
755
+
756
+ 1. Add `authManager` to MapPipeline constructor (optional):
757
+ ```typescript
758
+ constructor(
759
+ private readonly config: NomosConfig,
760
+ private readonly projectRoot: string,
761
+ private readonly logger: Logger,
762
+ private readonly authManager?: AuthManager | null,
763
+ )
764
+ ```
765
+
766
+ 2. Replace direct instantiation with factory:
767
+ ```typescript
768
+ const enricher = await SemanticEnricher.create(
769
+ this.projectRoot, this.config.graph, this.logger, this.authManager,
770
+ );
771
+ ```
772
+
773
+ 3. Add import:
774
+ ```typescript
775
+ import { AuthManager } from '../auth/manager.js';
776
+ ```
777
+ (Note: relative path from `src/core/graph/` to `src/core/auth/` is `../auth/`)
778
+ inputs:
779
+ - "Existing MapPipeline class (src/core/graph/pipeline.ts)"
780
+ - "SemanticEnricher.create() from step 4.2"
781
+ outputs:
782
+ - "MapPipeline passes authManager to SemanticEnricher"
783
+ validation: |
784
+ `npx tsc --noEmit` passes.
785
+ depends_on: ["4.2"]
786
+ can_parallel: false
787
+ risk_level: "medium"
788
+ rollback: |
789
+ Revert pipeline.ts to direct `new SemanticEnricher(...)` instantiation.
790
+
791
+ - step_id: "4.6"
792
+ title: "Wire AuthManager into command call sites"
793
+ action: "MODIFY"
794
+ file_path: "src/commands/index.ts"
795
+ description: |
796
+ Update the `arc index` command to create an `AuthManager` and pass it
797
+ to `SearchIndexer`.
798
+
799
+ **Changes to src/commands/index.ts:**
800
+
801
+ 1. Add import:
802
+ ```typescript
803
+ import { AuthManager } from '../core/auth/manager.js';
804
+ ```
805
+
806
+ 2. After `const logger = createLogger(...)`, create AuthManager:
807
+ ```typescript
808
+ const authManager = new AuthManager(config.auth, logger);
809
+ ```
810
+
811
+ 3. Pass to SearchIndexer:
812
+ ```typescript
813
+ const indexer = new SearchIndexer(projectRoot, config, logger, authManager);
814
+ ```
815
+
816
+ **Also update src/commands/search.ts:**
817
+
818
+ Same pattern — create `AuthManager` and pass to `QueryEngine` constructor.
819
+
820
+ **Also update src/commands/map.ts:**
821
+
822
+ Same pattern — create `AuthManager` and pass to `MapPipeline` constructor.
823
+ inputs:
824
+ - "AuthManager from step 2.1"
825
+ - "Updated SearchIndexer from step 4.3"
826
+ - "Updated QueryEngine from step 4.4"
827
+ - "Updated MapPipeline from step 4.5"
828
+ outputs:
829
+ - "All Gemini-dependent commands use the credential chain"
830
+ validation: |
831
+ `npx tsc --noEmit` passes.
832
+ `arc index`, `arc search`, `arc map` commands work with GEMINI_API_KEY set
833
+ (backward compat).
834
+ depends_on: ["4.3", "4.4", "4.5"]
835
+ can_parallel: false
836
+ risk_level: "medium"
837
+ rollback: |
838
+ Remove AuthManager creation and parameter passing from all three command files.
839
+
840
+ # ═══════════════════════════════════════════════════════════════════════════
841
+ # PHASE 5: SECURITY — Sanitization Coverage
842
+ # ═══════════════════════════════════════════════════════════════════════════
843
+
844
+ - step_id: "5.1"
845
+ title: "Verify sanitize.ts covers auth tokens in env deny list"
846
+ action: "MODIFY"
847
+ file_path: "src/utils/sanitize.ts"
848
+ description: |
849
+ Review the `ALWAYS_DENY` regex array in `sanitizeEnv()` (line 57-61).
850
+ The existing pattern already covers `GOOGLE_.*?(KEY|SECRET|TOKEN|...)`.
851
+ This would catch env vars like `GOOGLE_ACCESS_TOKEN` but NOT the
852
+ internal OAuth tokens (which are in a JSON file, not env vars).
853
+
854
+ **No code change needed if** the existing patterns already cover Google
855
+ credential env vars. Verify and confirm.
856
+
857
+ **If the patterns do NOT cover a scenario where tokens could leak into
858
+ env vars**, add coverage. But per the design, OAuth tokens live in
859
+ `~/.nomos/credentials.json` (file, not env) and are never set as env
860
+ vars by the auth module.
861
+
862
+ **Action:** Read the file, verify coverage, document finding. No modification
863
+ expected unless a gap is found.
864
+ inputs:
865
+ - "Existing sanitize.ts ALWAYS_DENY patterns"
866
+ outputs:
867
+ - "Confirmation that auth tokens are not leaked through env sanitization gaps"
868
+ validation: |
869
+ Manual review confirms ALWAYS_DENY covers GOOGLE_* secrets.
870
+ Auth tokens stored in file only — no env var leak path.
871
+ depends_on: []
872
+ can_parallel: true
873
+ risk_level: "low"
874
+ rollback: |
875
+ N/A — review-only step.
876
+
877
+ # ═══════════════════════════════════════════════════════════════════════════
878
+ # PHASE 6: VALIDATION & TESTING
879
+ # ═══════════════════════════════════════════════════════════════════════════
880
+
881
+ - step_id: "6.1"
882
+ title: "TypeScript compilation check"
883
+ action: "TEST"
884
+ file_path: ""
885
+ description: |
886
+ Run `npx tsc --noEmit` (or `npm run lint`) to verify all new and
887
+ modified files compile without errors. This catches:
888
+ - Missing imports
889
+ - Type mismatches between AuthCredentials, NomosConfig, NomosErrorCode
890
+ - Incorrect async/await usage in factory methods
891
+ inputs:
892
+ - "All files from steps 1.1 through 5.1"
893
+ outputs:
894
+ - "Zero TypeScript errors"
895
+ validation: |
896
+ `npm run lint` exits 0.
897
+ depends_on: ["1.1", "1.2", "1.3", "1.4", "2.1", "2.2", "3.1", "3.2", "4.1", "4.2", "4.3", "4.4", "4.5", "4.6"]
898
+ can_parallel: false
899
+ risk_level: "low"
900
+ rollback: |
901
+ Fix compilation errors identified.
902
+
903
+ - step_id: "6.2"
904
+ title: "Run existing test suite for regression"
905
+ action: "TEST"
906
+ file_path: ""
907
+ description: |
908
+ Run `npm test` to ensure all existing tests still pass. Key test files
909
+ to watch:
910
+ - `src/search/__tests__/embedder.test.ts` — may need update if constructor
911
+ signature changed (apiKey parameter added)
912
+ - `src/core/graph/__tests__/enricher.test.ts` — same concern
913
+ - `src/core/graph/__tests__/pipeline.test.ts` — if MapPipeline constructor
914
+ changed
915
+ - `src/search/__tests__/indexer.test.ts` — if SearchIndexer constructor changed
916
+
917
+ If tests fail due to constructor signature changes (new optional `apiKey`
918
+ or `authManager` params), these are expected and must be fixed:
919
+ - Tests using `new Embedder(config, logger)` → still valid (apiKey is optional)
920
+ - Tests using `new SearchIndexer(root, config, logger)` → still valid (authManager optional)
921
+
922
+ If any test fails for an unexpected reason, investigate before proceeding.
923
+ inputs:
924
+ - "All source changes from previous steps"
925
+ outputs:
926
+ - "All existing tests pass (or failures are expected and documented)"
927
+ validation: |
928
+ `npm test` exits 0 (or with only expected, documented failures).
929
+ depends_on: ["6.1"]
930
+ can_parallel: false
931
+ risk_level: "medium"
932
+ rollback: |
933
+ Fix test failures. If a test fails due to constructor changes,
934
+ update the test to pass the new optional parameter or verify
935
+ the default behavior hasn't changed.
936
+
937
+ - step_id: "6.3"
938
+ title: "Unit tests for AuthManager"
939
+ action: "CREATE"
940
+ file_path: "src/core/auth/__tests__/manager.test.ts"
941
+ description: |
942
+ Create unit tests for AuthManager covering:
943
+
944
+ 1. **saveCredentials** — writes JSON to temp path, verifies file exists
945
+ and contains correct data, verifies file permissions are 0600.
946
+ 2. **loadCredentials** — returns parsed object when file exists,
947
+ returns null when file doesn't exist, returns null on invalid JSON.
948
+ 3. **isLoggedIn** — true when credentials file has refresh_token,
949
+ false when file missing.
950
+ 4. **clearCredentials** — deletes file, doesn't throw if file missing.
951
+ 5. **getAccessToken** — throws `auth_not_logged_in` when no credentials.
952
+
953
+ Use `vitest` (project's test runner). Mock the filesystem or use
954
+ a temp directory (`os.tmpdir()`) with cleanup in afterEach.
955
+
956
+ Do NOT mock google-auth-library for unit tests — test only the
957
+ file I/O and credential management logic. Integration with Google
958
+ is tested manually via `arc auth login`.
959
+ inputs:
960
+ - "AuthManager from step 2.1"
961
+ outputs:
962
+ - "Unit tests for AuthManager pass"
963
+ validation: |
964
+ `npx vitest run src/core/auth/__tests__/manager.test.ts` exits 0.
965
+ depends_on: ["2.1"]
966
+ can_parallel: true
967
+ risk_level: "low"
968
+ rollback: |
969
+ Delete test file.
970
+
971
+ - step_id: "6.4"
972
+ title: "Unit tests for loopback server"
973
+ action: "CREATE"
974
+ file_path: "src/core/auth/__tests__/server.test.ts"
975
+ description: |
976
+ Create unit tests for the loopback server covering:
977
+
978
+ 1. **Port binding** — server starts on specified port.
979
+ 2. **Timeout** — server rejects after 120s timeout (use fake timers
980
+ via `vi.useFakeTimers()`).
981
+ 3. **Callback handling** — mock an HTTP GET with `?code=test_code`,
982
+ verify the function resolves (mock `oauth2Client.getToken`).
983
+ 4. **Cleanup** — server is closed after callback.
984
+
985
+ Mock `OAuth2Client.getToken()` to return fake tokens.
986
+ Mock `open` to prevent actual browser launch.
987
+ inputs:
988
+ - "startLoopbackServer from step 2.2"
989
+ outputs:
990
+ - "Unit tests for loopback server pass"
991
+ validation: |
992
+ `npx vitest run src/core/auth/__tests__/server.test.ts` exits 0.
993
+ depends_on: ["2.2"]
994
+ can_parallel: true
995
+ risk_level: "low"
996
+ rollback: |
997
+ Delete test file.
998
+
999
+ - step_id: "6.5"
1000
+ title: "Integration test — Embedder credential chain"
1001
+ action: "CREATE"
1002
+ file_path: "src/search/__tests__/embedder-auth.test.ts"
1003
+ description: |
1004
+ Test the credential chain in Embedder.create():
1005
+
1006
+ 1. **API key takes priority** — set GEMINI_API_KEY env var, call
1007
+ `Embedder.create(config, logger, authManager)`. Verify Embedder
1008
+ is created using the env var (mock AuthManager, verify it's NOT called).
1009
+ 2. **OAuth fallback** — unset GEMINI_API_KEY, mock
1010
+ `authManager.isLoggedIn()` → true, `authManager.getAccessToken()` →
1011
+ 'fake-token'. Verify Embedder is created.
1012
+ 3. **Neither available** — unset GEMINI_API_KEY, mock
1013
+ `authManager.isLoggedIn()` → false. Verify `search_api_key_missing`
1014
+ error is thrown with message mentioning both `GEMINI_API_KEY` and
1015
+ `arc auth login`.
1016
+ inputs:
1017
+ - "Embedder.create() from step 4.1"
1018
+ - "AuthManager from step 2.1"
1019
+ outputs:
1020
+ - "Credential chain works correctly in all priority scenarios"
1021
+ validation: |
1022
+ `npx vitest run src/search/__tests__/embedder-auth.test.ts` exits 0.
1023
+ depends_on: ["4.1"]
1024
+ can_parallel: true
1025
+ risk_level: "low"
1026
+ rollback: |
1027
+ Delete test file.
1028
+
1029
+ risk_assessment:
1030
+ overall_risk: "high"
1031
+ critical_steps: ["2.2", "4.1", "4.2"]
1032
+ failure_scenarios:
1033
+ - scenario: "Google SDK rejects access_token passed as API key string"
1034
+ impact: "OAuth tokens cannot authenticate Gemini API calls. Entire credential chain is non-functional."
1035
+ mitigation: |
1036
+ Step 4.1 description notes this is Risk 2 from the task spec. Start with
1037
+ spike test: pass access_token as apiKey to GoogleGenerativeAI constructor.
1038
+ If it fails, fall back to custom Authorization header injection (option 3
1039
+ from task spec). The plan's architecture (factory method returning Embedder)
1040
+ supports either approach without further refactoring.
1041
+ - scenario: "Dynamic port breaks Google OAuth redirect URI matching"
1042
+ impact: "OAuth callback fails with 'redirect_uri_mismatch' error."
1043
+ mitigation: |
1044
+ Fixed default port (3000) with configurable override via
1045
+ `config.auth.redirect_port`. Users register `http://localhost:3000` in
1046
+ Google Cloud Console. Fallback to random port only if 3000 is in use —
1047
+ with clear warning that the user must register the actual port.
1048
+ - scenario: "Sync-to-async constructor migration breaks existing callers"
1049
+ impact: "SearchIndexer, QueryEngine, MapPipeline fail to compile or behave differently."
1050
+ mitigation: |
1051
+ Optional apiKey parameter preserves backward compatibility. The `new Embedder(config, logger)`
1052
+ path is unchanged. Factory method is additive. Existing tests that use
1053
+ the constructor directly continue to work. Step 6.2 explicitly validates this.
1054
+ - scenario: "Token refresh fails silently, stale token used for API calls"
1055
+ impact: "Gemini API returns 401, user sees cryptic error."
1056
+ mitigation: |
1057
+ AuthManager.getAuthenticatedClient() checks expiry_date and refreshes
1058
+ proactively. If refresh fails, it throws auth_token_expired with clear
1059
+ message to re-run `arc auth login`.
1060
+ - scenario: "Loopback server hangs, blocking CLI process"
1061
+ impact: "User's terminal is stuck, requires manual kill."
1062
+ mitigation: |
1063
+ 120-second hard timeout with socket tracking and forced destroy.
1064
+ Step 2.2 explicitly tracks sockets in a Set and destroys all on cleanup.
1065
+
1066
+ compliance:
1067
+ rules_checked: true
1068
+ violations: []
1069
+ notes: |
1070
+ - JSON source of truth: Token state in ~/.nomos/credentials.json (JSON). ✓
1071
+ - NomosError codes: All auth failures use typed codes. ✓
1072
+ - Commander.js pattern: registerAuthCommand(program) matches existing commands. ✓
1073
+ - Config-driven: Auth config in .nomos-config.json with Zod schema. ✓
1074
+ - Security: client_secret never logged. Sanitize pipeline reviewed. ✓
1075
+ - No global state: AuthManager instantiated per-command. ✓
1076
+ - Winston logging: All auth logs via injected logger. ✓
1077
+ - Open/Closed: Existing constructor signatures preserved; factory methods are additive. ✓
1078
+ - Backward compatibility: GEMINI_API_KEY path unchanged; OAuth is fallback only. ✓
1079
+
1080
+ summary:
1081
+ total_steps: 18
1082
+ estimated_files_changed: 10
1083
+ estimated_files_created: 5
1084
+ approach_justification: |
1085
+ The plan uses additive factory methods (Embedder.create, SemanticEnricher.create)
1086
+ rather than replacing synchronous constructors, preserving full backward
1087
+ compatibility. The credential chain (env var → OAuth → error) is implemented
1088
+ at the factory level, keeping the core Embedder/Enricher classes unaware of
1089
+ auth complexity. AuthManager is instantiated per-command (no singleton), matching
1090
+ the project's existing patterns. The loopback server uses a fixed default port
1091
+ (3000) to simplify Google Cloud Console configuration while supporting fallback
1092
+ to random ports. All new code follows existing conventions: NomosError codes,
1093
+ Zod config schemas, Commander.js registration, Winston logging.