archondev 0.1.0 → 1.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.
@@ -0,0 +1,878 @@
1
+ import {
2
+ ArchitectureParser
3
+ } from "./chunk-2CFO5GVH.js";
4
+ import {
5
+ ArchitectAgent,
6
+ createAtom,
7
+ validateAtom
8
+ } from "./chunk-MOZHC2GX.js";
9
+ import {
10
+ isAuthenticated
11
+ } from "./chunk-WCCBJSNI.js";
12
+ import {
13
+ AnthropicClient,
14
+ getDefaultModel
15
+ } from "./chunk-A7QU6JC6.js";
16
+
17
+ // src/cli/plan.ts
18
+ import chalk from "chalk";
19
+ import { existsSync as existsSync2 } from "fs";
20
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
21
+ import { join as join2 } from "path";
22
+ import { createInterface } from "readline";
23
+
24
+ // src/agents/sentinel.ts
25
+ var SYSTEM_PROMPT = `You are the Sentinel, a paranoid and skeptical code reviewer responsible for finding issues in implementation plans.
26
+
27
+ Your role:
28
+ - Critique the Architect's plan with extreme skepticism
29
+ - Identify security vulnerabilities, edge cases, and breaking changes
30
+ - Ensure the plan respects architecture boundaries and invariants
31
+ - Flag any missing steps or incomplete considerations
32
+ - Challenge assumptions and identify risks
33
+
34
+ You are NOT trying to approve plans. You are trying to find problems.
35
+
36
+ Output your validation in the following JSON format:
37
+ {
38
+ "passed": boolean,
39
+ "issues": [
40
+ {
41
+ "severity": "BLOCKER" | "WARNING" | "INFO",
42
+ "type": "SECURITY" | "ARCHITECTURE" | "PERFORMANCE" | "EDGE_CASE" | "BREAKING_CHANGE" | "MISSING_STEP",
43
+ "message": "Description of the issue",
44
+ "suggestion": "How to fix it (optional)"
45
+ }
46
+ ],
47
+ "suggestions": ["General improvement suggestion 1", ...]
48
+ }
49
+
50
+ Guidelines:
51
+ - BLOCKER issues must be resolved before implementation
52
+ - WARNING issues should be addressed but won't block
53
+ - INFO issues are nice-to-have improvements
54
+ - Be specific and actionable in your feedback
55
+ - Don't approve plans that violate architecture constraints
56
+ - Consider edge cases, error handling, and rollback scenarios`;
57
+ var SentinelAgent = class {
58
+ client;
59
+ config;
60
+ constructor(config, apiKey) {
61
+ this.config = {
62
+ provider: "anthropic",
63
+ model: config?.model ?? getDefaultModel("sentinel"),
64
+ maxTokens: config?.maxTokens ?? 4096,
65
+ temperature: config?.temperature ?? 0.3
66
+ // Lower temperature for more consistent critiques
67
+ };
68
+ this.client = new AnthropicClient(this.config, apiKey);
69
+ }
70
+ /**
71
+ * Validate an implementation plan
72
+ */
73
+ async validatePlan(atom, plan2, architecture) {
74
+ const userMessage = this.buildPrompt(atom, plan2, architecture);
75
+ const response = await this.client.chat(SYSTEM_PROMPT, userMessage, {
76
+ temperature: 0.3,
77
+ maxTokens: 4096
78
+ });
79
+ return this.parseValidationResponse(response.content, response.usage);
80
+ }
81
+ /**
82
+ * Build the validation prompt
83
+ */
84
+ buildPrompt(atom, plan2, architecture) {
85
+ const parts = [];
86
+ parts.push("# Atom Being Implemented");
87
+ parts.push(`**Title:** ${atom.title}`);
88
+ parts.push(`**Description:** ${atom.description ?? "No description"}`);
89
+ parts.push("");
90
+ parts.push("**Acceptance Criteria:**");
91
+ atom.acceptanceCriteria.forEach((ac, i) => parts.push(`${i + 1}. ${ac}`));
92
+ parts.push("");
93
+ parts.push("# Implementation Plan to Validate");
94
+ parts.push("");
95
+ parts.push("**Steps:**");
96
+ plan2.steps.forEach((step, i) => parts.push(`${i + 1}. ${step}`));
97
+ parts.push("");
98
+ parts.push("**Files to Modify:**");
99
+ plan2.files_to_modify.forEach((f) => parts.push(`- ${f}`));
100
+ parts.push("");
101
+ parts.push("**Dependencies:**");
102
+ plan2.dependencies.forEach((d) => parts.push(`- ${d}`));
103
+ parts.push("");
104
+ parts.push("**Identified Risks:**");
105
+ plan2.risks.forEach((r) => parts.push(`- ${r}`));
106
+ parts.push("");
107
+ parts.push(`**Estimated Complexity:** ${plan2.estimated_complexity}`);
108
+ parts.push("");
109
+ parts.push("# Architecture Constraints");
110
+ parts.push("");
111
+ if (architecture.invariants.length > 0) {
112
+ parts.push("**Invariants (MUST NOT violate):**");
113
+ architecture.invariants.forEach((inv) => {
114
+ parts.push(`- [${inv.severity}] ${inv.rule}${inv.scope ? ` (scope: ${inv.scope})` : ""}`);
115
+ });
116
+ parts.push("");
117
+ }
118
+ if (architecture.protectedPaths.length > 0) {
119
+ parts.push("**Protected Paths:**");
120
+ architecture.protectedPaths.forEach((p) => {
121
+ parts.push(`- ${p.pattern} (${p.level}): ${p.reason}`);
122
+ });
123
+ parts.push("");
124
+ }
125
+ if (architecture.components.length > 0) {
126
+ parts.push("**Components & Boundaries:**");
127
+ architecture.components.forEach((c) => {
128
+ parts.push(`- ${c.name}: ${c.boundary} (${c.stability})`);
129
+ if (c.forbiddenImports) {
130
+ c.forbiddenImports.forEach((fi) => {
131
+ parts.push(` - Cannot import: ${fi.pattern}`);
132
+ });
133
+ }
134
+ });
135
+ parts.push("");
136
+ }
137
+ parts.push("# Your Task");
138
+ parts.push("");
139
+ parts.push("Critically analyze this plan. Find every possible issue.");
140
+ parts.push("Consider: security, edge cases, error handling, rollback, performance.");
141
+ parts.push("Check that all acceptance criteria can be met by this plan.");
142
+ parts.push("Verify no architecture constraints are violated.");
143
+ parts.push("");
144
+ parts.push("Output your validation as valid JSON.");
145
+ return parts.join("\n");
146
+ }
147
+ /**
148
+ * Parse validation response from AI
149
+ */
150
+ parseValidationResponse(content, usage) {
151
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
152
+ if (!jsonMatch) {
153
+ throw new Error("Failed to parse validation: no JSON found in response");
154
+ }
155
+ try {
156
+ const parsed = JSON.parse(jsonMatch[0]);
157
+ const issues = (parsed.issues ?? []).filter((i) => i.message).map((i) => ({
158
+ severity: this.normalizeSeverity(i.severity),
159
+ type: this.normalizeType(i.type),
160
+ message: i.message ?? "",
161
+ suggestion: i.suggestion
162
+ }));
163
+ const hasBlockers = issues.some((i) => i.severity === "BLOCKER");
164
+ return {
165
+ passed: hasBlockers ? false : parsed.passed ?? true,
166
+ issues,
167
+ suggestions: parsed.suggestions ?? [],
168
+ usage
169
+ };
170
+ } catch (error) {
171
+ throw new Error(`Failed to parse validation JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
172
+ }
173
+ }
174
+ normalizeSeverity(value) {
175
+ const normalized = value?.toUpperCase();
176
+ if (normalized === "BLOCKER" || normalized === "WARNING" || normalized === "INFO") {
177
+ return normalized;
178
+ }
179
+ return "WARNING";
180
+ }
181
+ normalizeType(value) {
182
+ const normalized = value?.toUpperCase();
183
+ if (normalized === "SECURITY" || normalized === "ARCHITECTURE" || normalized === "PERFORMANCE" || normalized === "EDGE_CASE" || normalized === "BREAKING_CHANGE" || normalized === "MISSING_STEP") {
184
+ return normalized;
185
+ }
186
+ return "EDGE_CASE";
187
+ }
188
+ };
189
+
190
+ // src/agents/planner.ts
191
+ var DEFAULT_MAX_CYCLES = 3;
192
+ var AdversarialPlanner = class {
193
+ architect;
194
+ sentinel;
195
+ maxCycles;
196
+ constructor(config) {
197
+ this.architect = new ArchitectAgent(config?.architectConfig, config?.apiKey);
198
+ this.sentinel = new SentinelAgent(config?.sentinelConfig, config?.apiKey);
199
+ this.maxCycles = config?.maxCycles ?? DEFAULT_MAX_CYCLES;
200
+ }
201
+ /**
202
+ * Plan an atom through the adversarial loop
203
+ */
204
+ async planAtom(atom, architecture, context) {
205
+ const iterations = [];
206
+ const totalUsage = {
207
+ inputTokens: 0,
208
+ outputTokens: 0,
209
+ totalTokens: 0,
210
+ baseCost: 0,
211
+ markedUpCost: 0
212
+ };
213
+ let currentPlan = null;
214
+ let approved = false;
215
+ for (let cycle = 1; cycle <= this.maxCycles; cycle++) {
216
+ const architectContext = {
217
+ ...context
218
+ };
219
+ if (iterations.length > 0) {
220
+ const lastIteration2 = iterations[iterations.length - 1];
221
+ if (lastIteration2) {
222
+ architectContext.recentLearnings = [
223
+ ...context?.recentLearnings ?? [],
224
+ `Previous plan issues (cycle ${cycle - 1}):`,
225
+ ...lastIteration2.validation.issues.map(
226
+ (i) => `- [${i.severity}] ${i.message}${i.suggestion ? ` (suggestion: ${i.suggestion})` : ""}`
227
+ ),
228
+ ...lastIteration2.validation.suggestions.map((s) => `- Suggestion: ${s}`)
229
+ ];
230
+ }
231
+ }
232
+ const generatedPlan = await this.architect.generatePlan(atom, architecture, architectContext);
233
+ currentPlan = generatedPlan.plan;
234
+ this.accumulateUsage(totalUsage, generatedPlan.usage);
235
+ const validation = await this.sentinel.validatePlan(atom, currentPlan, architecture);
236
+ this.accumulateUsage(totalUsage, validation.usage);
237
+ iterations.push({
238
+ cycle,
239
+ plan: currentPlan,
240
+ reasoning: generatedPlan.reasoning,
241
+ validation,
242
+ usage: {
243
+ inputTokens: generatedPlan.usage.inputTokens + validation.usage.inputTokens,
244
+ outputTokens: generatedPlan.usage.outputTokens + validation.usage.outputTokens,
245
+ totalTokens: generatedPlan.usage.totalTokens + validation.usage.totalTokens,
246
+ baseCost: generatedPlan.usage.baseCost + validation.usage.baseCost,
247
+ markedUpCost: generatedPlan.usage.markedUpCost + validation.usage.markedUpCost
248
+ }
249
+ });
250
+ if (validation.passed) {
251
+ approved = true;
252
+ break;
253
+ }
254
+ const hasBlockers = validation.issues.some((i) => i.severity === "BLOCKER");
255
+ if (!hasBlockers) {
256
+ approved = true;
257
+ break;
258
+ }
259
+ }
260
+ const lastIteration = iterations[iterations.length - 1];
261
+ const blockers = lastIteration?.validation.issues.filter((i) => i.severity === "BLOCKER") ?? [];
262
+ return {
263
+ approved,
264
+ finalPlan: approved ? currentPlan : null,
265
+ iterations,
266
+ totalUsage,
267
+ blockers,
268
+ escalationReason: approved ? void 0 : `Failed to resolve ${blockers.length} blocking issues after ${this.maxCycles} cycles`
269
+ };
270
+ }
271
+ accumulateUsage(total, addition) {
272
+ total.inputTokens += addition.inputTokens;
273
+ total.outputTokens += addition.outputTokens;
274
+ total.totalTokens += addition.totalTokens;
275
+ total.baseCost += addition.baseCost;
276
+ total.markedUpCost += addition.markedUpCost;
277
+ }
278
+ };
279
+
280
+ // src/core/keys/manager.ts
281
+ import { homedir as homedir2 } from "os";
282
+ import { join } from "path";
283
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
284
+ import { existsSync } from "fs";
285
+
286
+ // src/core/keys/encryption.ts
287
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
288
+ import { homedir, hostname, userInfo } from "os";
289
+ var ALGORITHM = "aes-256-gcm";
290
+ var IV_LENGTH = 16;
291
+ var AUTH_TAG_LENGTH = 16;
292
+ var SALT_LENGTH = 32;
293
+ var KEY_LENGTH = 32;
294
+ function getDerivedKey(salt) {
295
+ const machineId = `${hostname()}-${userInfo().username}-${homedir()}`;
296
+ return scryptSync(machineId, salt, KEY_LENGTH);
297
+ }
298
+ function encrypt(plaintext) {
299
+ const salt = randomBytes(SALT_LENGTH);
300
+ const key = getDerivedKey(salt);
301
+ const iv = randomBytes(IV_LENGTH);
302
+ const cipher = createCipheriv(ALGORITHM, key, iv);
303
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
304
+ encrypted += cipher.final("hex");
305
+ const authTag = cipher.getAuthTag();
306
+ const combined = Buffer.concat([
307
+ salt,
308
+ iv,
309
+ authTag,
310
+ Buffer.from(encrypted, "hex")
311
+ ]);
312
+ return combined.toString("base64");
313
+ }
314
+ function decrypt(encryptedData) {
315
+ const combined = Buffer.from(encryptedData, "base64");
316
+ const salt = combined.subarray(0, SALT_LENGTH);
317
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
318
+ const authTag = combined.subarray(
319
+ SALT_LENGTH + IV_LENGTH,
320
+ SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
321
+ );
322
+ const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
323
+ const key = getDerivedKey(salt);
324
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
325
+ decipher.setAuthTag(authTag);
326
+ let decrypted = decipher.update(encrypted.toString("hex"), "hex", "utf8");
327
+ decrypted += decipher.final("utf8");
328
+ return decrypted;
329
+ }
330
+
331
+ // src/core/keys/manager.ts
332
+ var CONFIG_DIR = join(homedir2(), ".archon");
333
+ var KEYS_FILE = join(CONFIG_DIR, "keys.json");
334
+ var CURRENT_VERSION = 2;
335
+ var KeyManager = class {
336
+ async ensureConfigDir() {
337
+ if (!existsSync(CONFIG_DIR)) {
338
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
339
+ }
340
+ }
341
+ async loadKeys() {
342
+ try {
343
+ if (!existsSync(KEYS_FILE)) {
344
+ return { keys: [], version: CURRENT_VERSION };
345
+ }
346
+ const content = await readFile(KEYS_FILE, "utf-8");
347
+ const data = JSON.parse(content);
348
+ if (data.version === 1 && !Array.isArray(data.keys)) {
349
+ const oldKeys = data.keys;
350
+ const newKeys = Object.values(oldKeys).map((k) => ({
351
+ ...k,
352
+ label: k.label ?? "default",
353
+ isPrimary: k.isPrimary ?? true
354
+ }));
355
+ const migrated = { keys: newKeys, version: CURRENT_VERSION };
356
+ await this.saveKeys(migrated);
357
+ return migrated;
358
+ }
359
+ return data;
360
+ } catch {
361
+ return { keys: [], version: CURRENT_VERSION };
362
+ }
363
+ }
364
+ async saveKeys(stored) {
365
+ await this.ensureConfigDir();
366
+ const content = JSON.stringify(stored, null, 2);
367
+ await writeFile(KEYS_FILE, content, { mode: 384 });
368
+ await chmod(KEYS_FILE, 384);
369
+ }
370
+ async addKey(provider, apiKey, label = "default") {
371
+ try {
372
+ const stored = await this.loadKeys();
373
+ const now = (/* @__PURE__ */ new Date()).toISOString();
374
+ const existingIndex = stored.keys.findIndex((k) => k.provider === provider && k.label === label);
375
+ const providerKeys = stored.keys.filter((k) => k.provider === provider);
376
+ const isPrimary = providerKeys.length === 0 || existingIndex >= 0 && stored.keys[existingIndex]?.isPrimary === true;
377
+ const newKey = {
378
+ provider,
379
+ encryptedKey: encrypt(apiKey),
380
+ label,
381
+ isPrimary,
382
+ createdAt: now,
383
+ updatedAt: now
384
+ };
385
+ if (existingIndex >= 0) {
386
+ const existing = stored.keys[existingIndex];
387
+ if (existing) {
388
+ newKey.createdAt = existing.createdAt;
389
+ newKey.isPrimary = existing.isPrimary;
390
+ }
391
+ stored.keys[existingIndex] = newKey;
392
+ } else {
393
+ stored.keys.push(newKey);
394
+ }
395
+ await this.saveKeys(stored);
396
+ return { success: true };
397
+ } catch (error) {
398
+ return {
399
+ success: false,
400
+ error: `Failed to save key: ${error instanceof Error ? error.message : "Unknown error"}`
401
+ };
402
+ }
403
+ }
404
+ async getKey(provider, label) {
405
+ try {
406
+ const stored = await this.loadKeys();
407
+ let keyConfig;
408
+ if (label) {
409
+ keyConfig = stored.keys.find((k) => k.provider === provider && k.label === label);
410
+ } else {
411
+ keyConfig = stored.keys.find((k) => k.provider === provider && k.isPrimary);
412
+ if (!keyConfig) {
413
+ keyConfig = stored.keys.find((k) => k.provider === provider);
414
+ }
415
+ }
416
+ if (!keyConfig) {
417
+ return null;
418
+ }
419
+ return decrypt(keyConfig.encryptedKey);
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+ async removeKey(provider, label) {
425
+ try {
426
+ const stored = await this.loadKeys();
427
+ let index;
428
+ if (label) {
429
+ index = stored.keys.findIndex((k) => k.provider === provider && k.label === label);
430
+ } else {
431
+ index = stored.keys.findIndex((k) => k.provider === provider && k.isPrimary);
432
+ if (index < 0) {
433
+ index = stored.keys.findIndex((k) => k.provider === provider);
434
+ }
435
+ }
436
+ if (index < 0) {
437
+ const labelMsg = label ? ` with label "${label}"` : "";
438
+ return {
439
+ success: false,
440
+ error: `No API key found for provider: ${provider}${labelMsg}`
441
+ };
442
+ }
443
+ const removedKey = stored.keys[index];
444
+ stored.keys.splice(index, 1);
445
+ if (removedKey?.isPrimary) {
446
+ const nextKey = stored.keys.find((k) => k.provider === provider);
447
+ if (nextKey) {
448
+ nextKey.isPrimary = true;
449
+ }
450
+ }
451
+ await this.saveKeys(stored);
452
+ return { success: true };
453
+ } catch (error) {
454
+ return {
455
+ success: false,
456
+ error: `Failed to remove key: ${error instanceof Error ? error.message : "Unknown error"}`
457
+ };
458
+ }
459
+ }
460
+ async setPrimary(provider, label) {
461
+ try {
462
+ const stored = await this.loadKeys();
463
+ const targetKey = stored.keys.find((k) => k.provider === provider && k.label === label);
464
+ if (!targetKey) {
465
+ return {
466
+ success: false,
467
+ error: `No key found for ${provider} with label "${label}"`
468
+ };
469
+ }
470
+ for (const key of stored.keys) {
471
+ if (key.provider === provider) {
472
+ key.isPrimary = key.label === label;
473
+ }
474
+ }
475
+ await this.saveKeys(stored);
476
+ return { success: true };
477
+ } catch (error) {
478
+ return {
479
+ success: false,
480
+ error: `Failed to set primary: ${error instanceof Error ? error.message : "Unknown error"}`
481
+ };
482
+ }
483
+ }
484
+ async listProviders() {
485
+ try {
486
+ const stored = await this.loadKeys();
487
+ const providers = /* @__PURE__ */ new Set();
488
+ for (const key of stored.keys) {
489
+ providers.add(key.provider);
490
+ }
491
+ return Array.from(providers);
492
+ } catch {
493
+ return [];
494
+ }
495
+ }
496
+ async listKeys(provider) {
497
+ try {
498
+ const stored = await this.loadKeys();
499
+ if (provider) {
500
+ return stored.keys.filter((k) => k.provider === provider);
501
+ }
502
+ return stored.keys;
503
+ } catch {
504
+ return [];
505
+ }
506
+ }
507
+ async hasAnyKey() {
508
+ const stored = await this.loadKeys();
509
+ return stored.keys.length > 0;
510
+ }
511
+ async getKeyConfig(provider, label) {
512
+ try {
513
+ const stored = await this.loadKeys();
514
+ if (label) {
515
+ return stored.keys.find((k) => k.provider === provider && k.label === label) ?? null;
516
+ }
517
+ return stored.keys.find((k) => k.provider === provider && k.isPrimary) ?? stored.keys.find((k) => k.provider === provider) ?? null;
518
+ } catch {
519
+ return null;
520
+ }
521
+ }
522
+ async getAdversarialStatus() {
523
+ const providers = await this.listProviders();
524
+ const enabled = providers.length >= 2;
525
+ if (enabled) {
526
+ return {
527
+ enabled: true,
528
+ providerCount: providers.length,
529
+ providers,
530
+ message: `Adversarial features enabled (${providers.length} providers: ${providers.join(", ")})`
531
+ };
532
+ } else if (providers.length === 1) {
533
+ return {
534
+ enabled: false,
535
+ providerCount: 1,
536
+ providers,
537
+ message: `Adversarial features require keys from 2+ providers. Add another provider to unlock.`
538
+ };
539
+ } else {
540
+ return {
541
+ enabled: false,
542
+ providerCount: 0,
543
+ providers: [],
544
+ message: `No API keys configured. Add keys from 2+ providers to enable adversarial features.`
545
+ };
546
+ }
547
+ }
548
+ };
549
+ var keyManager = new KeyManager();
550
+
551
+ // src/core/keys/validator.ts
552
+ var KeyValidator = class {
553
+ async validateKey(provider, apiKey) {
554
+ switch (provider) {
555
+ case "anthropic":
556
+ return this.validateAnthropicKey(apiKey);
557
+ case "openai":
558
+ return this.validateOpenAIKey(apiKey);
559
+ case "google":
560
+ return this.validateGoogleKey(apiKey);
561
+ default:
562
+ return { valid: false, error: `Unknown provider: ${provider}`, provider };
563
+ }
564
+ }
565
+ async validateAnthropicKey(apiKey) {
566
+ try {
567
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
568
+ method: "POST",
569
+ headers: {
570
+ "Content-Type": "application/json",
571
+ "x-api-key": apiKey,
572
+ "anthropic-version": "2023-06-01"
573
+ },
574
+ body: JSON.stringify({
575
+ model: "claude-3-haiku-20240307",
576
+ max_tokens: 1,
577
+ messages: [{ role: "user", content: "Hi" }]
578
+ })
579
+ });
580
+ if (response.status === 401) {
581
+ return {
582
+ valid: false,
583
+ error: "Invalid API key. Please check your Anthropic API key.",
584
+ provider: "anthropic"
585
+ };
586
+ }
587
+ if (response.status === 400) {
588
+ const data = await response.json();
589
+ if (data.error?.message?.includes("credit")) {
590
+ return {
591
+ valid: false,
592
+ error: "API key valid but no credits. Please add credits to your Anthropic account.",
593
+ provider: "anthropic"
594
+ };
595
+ }
596
+ }
597
+ if (response.ok || response.status === 429 || response.status === 404) {
598
+ return { valid: true, provider: "anthropic" };
599
+ }
600
+ return {
601
+ valid: true,
602
+ provider: "anthropic"
603
+ };
604
+ } catch (error) {
605
+ return {
606
+ valid: false,
607
+ error: `Connection error: ${error instanceof Error ? error.message : "Unknown error"}`,
608
+ provider: "anthropic"
609
+ };
610
+ }
611
+ }
612
+ async validateOpenAIKey(apiKey) {
613
+ try {
614
+ const response = await fetch("https://api.openai.com/v1/models", {
615
+ method: "GET",
616
+ headers: {
617
+ Authorization: `Bearer ${apiKey}`
618
+ }
619
+ });
620
+ if (response.status === 401) {
621
+ return {
622
+ valid: false,
623
+ error: "Invalid API key. Please check your OpenAI API key.",
624
+ provider: "openai"
625
+ };
626
+ }
627
+ if (response.ok) {
628
+ return { valid: true, provider: "openai" };
629
+ }
630
+ return {
631
+ valid: false,
632
+ error: `Validation failed with status ${response.status}`,
633
+ provider: "openai"
634
+ };
635
+ } catch (error) {
636
+ return {
637
+ valid: false,
638
+ error: `Connection error: ${error instanceof Error ? error.message : "Unknown error"}`,
639
+ provider: "openai"
640
+ };
641
+ }
642
+ }
643
+ async validateGoogleKey(apiKey) {
644
+ try {
645
+ const response = await fetch(
646
+ `https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`,
647
+ { method: "GET" }
648
+ );
649
+ if (response.status === 400 || response.status === 401 || response.status === 403) {
650
+ return {
651
+ valid: false,
652
+ error: "Invalid API key. Please check your Google AI API key.",
653
+ provider: "google"
654
+ };
655
+ }
656
+ if (response.ok) {
657
+ return { valid: true, provider: "google" };
658
+ }
659
+ return {
660
+ valid: false,
661
+ error: `Validation failed with status ${response.status}`,
662
+ provider: "google"
663
+ };
664
+ } catch (error) {
665
+ return {
666
+ valid: false,
667
+ error: `Connection error: ${error instanceof Error ? error.message : "Unknown error"}`,
668
+ provider: "google"
669
+ };
670
+ }
671
+ }
672
+ };
673
+
674
+ // src/cli/plan.ts
675
+ var ATOMS_DIR = ".archon/atoms";
676
+ function createPrompt() {
677
+ const rl = createInterface({
678
+ input: process.stdin,
679
+ output: process.stdout
680
+ });
681
+ return {
682
+ ask: (question) => new Promise((resolve) => {
683
+ rl.question(question, resolve);
684
+ }),
685
+ close: () => rl.close()
686
+ };
687
+ }
688
+ async function plan(description, options) {
689
+ const prompt = createPrompt();
690
+ try {
691
+ if (!await isAuthenticated()) {
692
+ console.log(chalk.yellow('Not authenticated. Run "archon login" first.'));
693
+ console.log(chalk.dim("For local development, you can continue without authentication."));
694
+ }
695
+ const archPath = join2(process.cwd(), "ARCHITECTURE.md");
696
+ if (!existsSync2(archPath)) {
697
+ console.error(chalk.red('ARCHITECTURE.md not found. Run "archon init" first.'));
698
+ process.exit(1);
699
+ }
700
+ console.log(chalk.dim("Parsing ARCHITECTURE.md..."));
701
+ const parser = new ArchitectureParser(archPath);
702
+ const parseResult = await parser.parse();
703
+ if (!parseResult.success || !parseResult.schema) {
704
+ console.error(chalk.red("Failed to parse ARCHITECTURE.md:"));
705
+ for (const error of parseResult.errors) {
706
+ console.error(chalk.red(` - ${error.message}`));
707
+ }
708
+ process.exit(1);
709
+ }
710
+ console.log(chalk.dim("Creating atom from description..."));
711
+ const atomInput = parseAtomDescription(description, options);
712
+ const atom = createAtom(atomInput);
713
+ const validation = validateAtom(atom);
714
+ if (!validation.valid) {
715
+ console.error(chalk.red("Invalid atom:"));
716
+ for (const error of validation.errors) {
717
+ console.error(chalk.red(` - ${error.field}: ${error.message}`));
718
+ }
719
+ process.exit(1);
720
+ }
721
+ console.log(chalk.blue(`
722
+ Atom created: ${atom.externalId}`));
723
+ console.log(chalk.dim(`Title: ${atom.title}`));
724
+ console.log(chalk.dim(`Acceptance Criteria: ${atom.acceptanceCriteria.length} items`));
725
+ let apiKey = process.env["ANTHROPIC_API_KEY"];
726
+ if (!apiKey) {
727
+ const keyManager2 = new KeyManager();
728
+ const storedKey = await keyManager2.getKey("anthropic");
729
+ if (storedKey) {
730
+ apiKey = storedKey;
731
+ }
732
+ }
733
+ if (!apiKey) {
734
+ console.log(chalk.yellow("\nNo API key configured. Skipping adversarial planning."));
735
+ console.log(chalk.dim('Set ANTHROPIC_API_KEY or use "archon keys add anthropic"'));
736
+ await saveAtom(atom);
737
+ console.log(chalk.green(`
738
+ Atom saved: ${atom.externalId}`));
739
+ console.log(chalk.dim(`Next: Configure API key and run "archon plan ${atom.externalId} --continue"`));
740
+ return;
741
+ }
742
+ console.log(chalk.blue("\n\u{1F4CB} Starting adversarial planning..."));
743
+ console.log(chalk.dim("Architect will generate a plan, Sentinel will validate it.\n"));
744
+ const planner = new AdversarialPlanner({ apiKey });
745
+ const planResult = await planner.planAtom(atom, parseResult.schema);
746
+ if (!planResult.approved) {
747
+ console.log(chalk.red("\n\u274C Plan not approved after maximum iterations."));
748
+ console.log(chalk.yellow("Issues found:"));
749
+ for (const iteration of planResult.iterations) {
750
+ if (!iteration.validation.passed) {
751
+ for (const issue of iteration.validation.issues) {
752
+ console.log(chalk.yellow(` - [${issue.severity}] ${issue.message}`));
753
+ }
754
+ }
755
+ }
756
+ const answer2 = await prompt.ask("\nSave as draft anyway? (y/N): ");
757
+ if (answer2.toLowerCase() !== "y") {
758
+ console.log(chalk.dim("Atom discarded."));
759
+ return;
760
+ }
761
+ }
762
+ if (planResult.finalPlan) {
763
+ console.log(chalk.green("\n\u2705 Plan approved!"));
764
+ displayPlan(planResult.finalPlan);
765
+ } else {
766
+ console.log(chalk.yellow("\nNo approved plan available."));
767
+ }
768
+ console.log(chalk.dim(`
769
+ Token usage: ${planResult.totalUsage.inputTokens} input, ${planResult.totalUsage.outputTokens} output`));
770
+ const answer = await prompt.ask("\nApprove this plan? (y/N/e for edit): ");
771
+ if (answer.toLowerCase() === "e") {
772
+ console.log(chalk.yellow("Edit mode not yet implemented. Saving as draft."));
773
+ atom.status = "DRAFT";
774
+ } else if (answer.toLowerCase() !== "y") {
775
+ console.log(chalk.dim("Atom discarded."));
776
+ return;
777
+ } else {
778
+ atom.plan = planResult.finalPlan;
779
+ atom.status = "READY";
780
+ }
781
+ await saveAtom(atom);
782
+ console.log(chalk.green(`
783
+ \u2705 Atom saved: ${atom.externalId}`));
784
+ console.log(chalk.dim(`Status: ${atom.status}`));
785
+ console.log(chalk.dim(`
786
+ Next steps:`));
787
+ console.log(chalk.dim(` - Execute: archon execute ${atom.externalId}`));
788
+ console.log(chalk.dim(` - View: archon show ${atom.externalId}`));
789
+ } finally {
790
+ prompt.close();
791
+ }
792
+ }
793
+ function parseAtomDescription(description, options) {
794
+ const parts = description.split(" - ");
795
+ const title = parts[0]?.trim() ?? description;
796
+ let acceptanceCriteria = [];
797
+ if (parts.length > 1) {
798
+ acceptanceCriteria = (parts[1] ?? "").split(",").map((ac) => ac.trim());
799
+ }
800
+ if (acceptanceCriteria.length === 0) {
801
+ acceptanceCriteria = ["Feature implemented as described", "Typecheck passes", "Tests pass"];
802
+ }
803
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
804
+ return {
805
+ title,
806
+ description: description !== title ? description : void 0,
807
+ acceptanceCriteria,
808
+ priority: options.priority ?? 10,
809
+ tags
810
+ };
811
+ }
812
+ function displayPlan(plan2) {
813
+ console.log(chalk.bold("\n\u{1F4DD} Implementation Plan"));
814
+ console.log(chalk.dim("\u2500".repeat(40)));
815
+ console.log(chalk.bold("\nSteps:"));
816
+ plan2.steps.forEach((step, i) => {
817
+ console.log(` ${i + 1}. ${step}`);
818
+ });
819
+ console.log(chalk.bold("\nFiles to modify:"));
820
+ for (const file of plan2.files_to_modify) {
821
+ console.log(` - ${file}`);
822
+ }
823
+ if (plan2.dependencies.length > 0) {
824
+ console.log(chalk.bold("\nDependencies:"));
825
+ for (const dep of plan2.dependencies) {
826
+ console.log(` - ${dep}`);
827
+ }
828
+ }
829
+ if (plan2.risks.length > 0) {
830
+ console.log(chalk.bold("\nRisks:"));
831
+ for (const risk of plan2.risks) {
832
+ console.log(chalk.yellow(` \u26A0 ${risk}`));
833
+ }
834
+ }
835
+ console.log(chalk.bold("\nComplexity:"), plan2.estimated_complexity);
836
+ }
837
+ async function saveAtom(atom) {
838
+ const atomsDir = join2(process.cwd(), ATOMS_DIR);
839
+ if (!existsSync2(atomsDir)) {
840
+ await mkdir2(atomsDir, { recursive: true });
841
+ }
842
+ const atomFile = join2(atomsDir, `${atom.externalId}.json`);
843
+ await writeFile2(atomFile, JSON.stringify(atom, null, 2));
844
+ }
845
+ async function loadAtom(atomId) {
846
+ const atomFile = join2(process.cwd(), ATOMS_DIR, `${atomId}.json`);
847
+ if (!existsSync2(atomFile)) {
848
+ return null;
849
+ }
850
+ const content = await readFile2(atomFile, "utf-8");
851
+ return JSON.parse(content);
852
+ }
853
+ async function listLocalAtoms() {
854
+ const atomsDir = join2(process.cwd(), ATOMS_DIR);
855
+ if (!existsSync2(atomsDir)) {
856
+ return [];
857
+ }
858
+ const { readdir } = await import("fs/promises");
859
+ const files = await readdir(atomsDir);
860
+ const atoms = [];
861
+ for (const file of files) {
862
+ if (file.endsWith(".json")) {
863
+ const atom = await loadAtom(file.replace(".json", ""));
864
+ if (atom) {
865
+ atoms.push(atom);
866
+ }
867
+ }
868
+ }
869
+ return atoms;
870
+ }
871
+
872
+ export {
873
+ keyManager,
874
+ KeyValidator,
875
+ plan,
876
+ loadAtom,
877
+ listLocalAtoms
878
+ };