@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,121 @@
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 { GitAdapter } from '../adapters/git.js';
6
+ import { createLogger } from '../core/logger.js';
7
+ import { NomosError } from '../core/errors.js';
8
+
9
+ export function registerInitCommand(program: Command): void {
10
+ program
11
+ .command('init [task]')
12
+ .description('Initialize a new project (no task) or a new task (with task name)')
13
+ .option('--force', 'Force re-initialize: prune zombie worktrees, delete task branch, and reset state')
14
+ .action(async (task?: string, options?: { force?: boolean }) => {
15
+ try {
16
+ if (!task) {
17
+ // Project initialization — no config exists yet
18
+ const { orchestrator } = await createOrchestrator({ skipConfig: true });
19
+ await orchestrator.initProject();
20
+ console.log(
21
+ "Project initialized. Next steps:\n" +
22
+ " 1. Edit tasks-management/rules/global.md with your engineering standards.\n" +
23
+ " 2. Run: arc init <task-name> to create your first task."
24
+ );
25
+ } else if (options?.force) {
26
+ // Force Recovery — zombie worktree cleanup + state reset
27
+ // GAP-3 fix: handles SIGKILL mid-init, crash mid-worktree-creation,
28
+ // or any state where git metadata and filesystem are out of sync.
29
+ //
30
+ // RT2-6.1 fix: The previous 5-step sequence had a fatal ordering bug.
31
+ // Step 1 was `git worktree prune`, which only removes metadata for worktrees
32
+ // whose directories NO LONGER EXIST. In the most common crash scenario
33
+ // (SIGKILL mid-creation), the directory STILL EXISTS, so prune is a no-op.
34
+ // Then branch deletion fails because git says the branch is "checked out"
35
+ // in the still-registered worktree.
36
+ //
37
+ // Correct sequence: unlock → remove directory → prune metadata → delete branch.
38
+ const { orchestrator, config, projectRoot } = await createOrchestrator();
39
+ // RT2-6.1 fix: Use GitAdapter (already imported via factory), not raw simpleGit.
40
+ // The previous code called `simpleGit(projectRoot)` but simpleGit was never
41
+ // imported in init.ts — a compile-time error hidden by the --help verification.
42
+ const gitAdapter = new GitAdapter(projectRoot, config,
43
+ createLogger(config.logging.level));
44
+
45
+ const branchName = `${config.execution.shadow_branch_prefix}${task}`;
46
+ const worktreePath = path.join(
47
+ config.execution.worktree_base,
48
+ path.basename(projectRoot),
49
+ task,
50
+ );
51
+
52
+ // Step 1: Unlock the worktree if it was locked (prevents removal)
53
+ try {
54
+ await gitAdapter.raw(['worktree', 'unlock', worktreePath]);
55
+ } catch {
56
+ // Worktree may not exist or not be locked — not an error
57
+ }
58
+
59
+ // Step 2: Remove the worktree directory from filesystem
60
+ // This MUST happen before prune — prune only cleans metadata for
61
+ // directories that no longer exist on disk.
62
+ if (fs.existsSync(worktreePath)) {
63
+ fs.rmSync(worktreePath, { recursive: true, force: true });
64
+ }
65
+
66
+ // Step 3: Prune git worktree metadata (now that directory is gone)
67
+ await gitAdapter.raw(['worktree', 'prune']);
68
+
69
+ // Step 4: Force-delete the task's shadow branch
70
+ // This now succeeds because the worktree referencing it was pruned.
71
+ try {
72
+ await gitAdapter.raw(['branch', '-D', branchName]);
73
+ } catch {
74
+ // Branch may not exist — not an error
75
+ }
76
+
77
+ // Step 5: Remove stale state file
78
+ const statePath = path.join(projectRoot, 'tasks-management', 'state', `${task}.json`);
79
+ if (fs.existsSync(statePath)) {
80
+ fs.rmSync(statePath);
81
+ }
82
+
83
+ // Step 6: Re-initialize the task cleanly
84
+ await orchestrator.initTask(task);
85
+ console.log(
86
+ `[nomos:force] Zombie worktree and stale state cleared for '${task}'.\n` +
87
+ `Task re-initialized.\n` +
88
+ ` Edit: tasks-management/tasks/${task}.md\n` +
89
+ ` Then: arc plan ${task}`
90
+ );
91
+ } else {
92
+ // Task initialization — config MUST exist.
93
+ // RTV-8: If config is missing, loadConfig() throws NomosError('config_not_found').
94
+ // The error handler below catches this and provides a targeted recovery message.
95
+ const { orchestrator } = await createOrchestrator();
96
+ await orchestrator.initTask(task);
97
+ console.log(
98
+ `Task '${task}' initialized.\n` +
99
+ ` Edit: tasks-management/tasks/${task}.md\n` +
100
+ ` Then: arc plan ${task}`
101
+ );
102
+ }
103
+ } catch (err) {
104
+ if (err instanceof NomosError) {
105
+ if (err.code === 'config_not_found') {
106
+ // RTV-8 fix: specific message when arc init <task> is run without a project
107
+ console.error(
108
+ `[nomos:error] No project found. To create a task, you must first initialize a project.\n` +
109
+ ` Run: arc init (to scaffold a new project in the current directory)\n` +
110
+ ` Then: arc init ${task}`
111
+ );
112
+ } else {
113
+ console.error(`[nomos:error] ${err.message}`);
114
+ }
115
+ process.exit(1);
116
+ }
117
+ console.error(`[nomos:error] Unexpected error: ${err}`);
118
+ process.exit(1);
119
+ }
120
+ });
121
+ }
@@ -0,0 +1,75 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+
5
+ export function registerListCommand(program: Command): void {
6
+ program
7
+ .command('list')
8
+ .description('List all tasks and their current status')
9
+ .action(async () => {
10
+ try {
11
+ const { orchestrator } = await createOrchestrator();
12
+ const tasks = await orchestrator.listTasks();
13
+
14
+ if (tasks.length === 0) {
15
+ console.log('No tasks found. Run: arc init <task-name> to create one.');
16
+ process.exit(0);
17
+ }
18
+
19
+ // Column widths
20
+ const COL_TASK = 16;
21
+ const COL_STATUS = 14;
22
+ const COL_VERSION = 7;
23
+ const COL_SCORE = 10;
24
+ const COL_TOKENS = 6;
25
+
26
+ const pad = (s: string, n: number): string => s.padEnd(n);
27
+
28
+ const header =
29
+ pad('Task', COL_TASK) +
30
+ pad('Status', COL_STATUS) +
31
+ pad('Version', COL_VERSION) +
32
+ pad('Last Score', COL_SCORE) +
33
+ 'Tokens';
34
+
35
+ const divider =
36
+ '─'.repeat(COL_TASK - 1) + ' ' +
37
+ '─'.repeat(COL_STATUS - 1) + ' ' +
38
+ '─'.repeat(COL_VERSION - 1) + ' ' +
39
+ '─'.repeat(COL_SCORE - 1) + ' ' +
40
+ '─'.repeat(COL_TOKENS);
41
+
42
+ console.log(header);
43
+ console.log(divider);
44
+
45
+ for (const task of tasks) {
46
+ const lastReview = task.history
47
+ .slice()
48
+ .reverse()
49
+ .find(h => h.review !== null)?.review;
50
+
51
+ const score = lastReview?.score != null
52
+ ? lastReview.score.toFixed(2)
53
+ : 'N/A';
54
+
55
+ const row =
56
+ pad(task.task_id, COL_TASK) +
57
+ pad(task.meta.status, COL_STATUS) +
58
+ pad(String(task.current_version), COL_VERSION) +
59
+ pad(score, COL_SCORE) +
60
+ String(task.budget.tokens_used);
61
+
62
+ console.log(row);
63
+ }
64
+
65
+ process.exit(0);
66
+ } catch (err) {
67
+ if (err instanceof NomosError) {
68
+ console.error(`[nomos:error] ${err.message}`);
69
+ } else {
70
+ console.error(`[nomos:error] Unexpected error: ${err}`);
71
+ }
72
+ process.exit(1);
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,55 @@
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 { MapPipeline } from '../core/graph/pipeline.js';
6
+ import { AuthManager } from '../core/auth/manager.js';
7
+ import { NomosError } from '../core/errors.js';
8
+
9
+ export function registerMapCommand(program: Command): void {
10
+ program
11
+ .command('map [patterns...]')
12
+ .description('Scan project files and build a semantic dependency map')
13
+ .option('--no-ai', 'Skip AI enrichment (structural map only)')
14
+ .option('--force', 'Re-parse all files even if unchanged')
15
+ .action(async (patterns: string[], opts: { ai: boolean; force: boolean }) => {
16
+ try {
17
+ const { config, projectRoot } = loadConfig();
18
+
19
+ // --no-ai flag disables enrichment for this run only
20
+ if (!opts.ai) {
21
+ config.graph.ai_enrichment = false;
22
+ }
23
+
24
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
25
+ const logger = createLogger(config.logging.level, logDir);
26
+
27
+ const authManager = new AuthManager(config.auth, logger);
28
+ const pipeline = new MapPipeline(config, projectRoot, logger, authManager);
29
+
30
+ const result = await pipeline.run({
31
+ noAi: !opts.ai,
32
+ force: opts.force ?? false,
33
+ patterns: patterns.length > 0 ? patterns : undefined,
34
+ });
35
+
36
+ const { total_files, structural_only, semantically_enriched, indexed } = result.map.stats;
37
+
38
+ console.error(`[nomos:info] Map complete.`);
39
+ console.log(`Mapped ${total_files} files: ${structural_only} structural, ${semantically_enriched} semantic, ${indexed} indexed`);
40
+
41
+ // [AMB-6 FIX] Exit codes: 0 = success, 1 = fatal, 10 = partial AI failure (NOT 2)
42
+ if (result.aiFailures > 0) {
43
+ process.exit(10);
44
+ }
45
+ process.exit(0);
46
+ } catch (err) {
47
+ if (err instanceof NomosError) {
48
+ console.error(`[nomos:error] ${err.message}`);
49
+ } else {
50
+ console.error(`[nomos:error] Unexpected error: ${err}`);
51
+ }
52
+ process.exit(1);
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,30 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+ import type { ExecutionMode } from '../types/index.js';
5
+
6
+ export function registerPlanCommand(program: Command): void {
7
+ program
8
+ .command('plan <task>')
9
+ .description('Run the planner for a task and save the resulting plan')
10
+ .option('--mode <mode>', 'Execution mode: supervised or dry-run')
11
+ .action(async (task: string, options: { mode?: string }) => {
12
+ try {
13
+ const { orchestrator, config } = await createOrchestrator();
14
+
15
+ // Resolve mode: flag > config.execution.default_mode
16
+ const mode: ExecutionMode = (options.mode as ExecutionMode) ?? config.execution.default_mode;
17
+
18
+ const state = await orchestrator.plan(task, mode);
19
+ const version = state.current_version;
20
+ console.log(`Plan v${version} saved to tasks-management/plans/${task}-v${version}.diff`);
21
+ } catch (err) {
22
+ if (err instanceof NomosError) {
23
+ console.error(`[nomos:error] ${err.message}`);
24
+ } else {
25
+ console.error(`[nomos:error] Unexpected error: ${err}`);
26
+ }
27
+ process.exit(1);
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,58 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+ import type { ExecutionMode } from '../types/index.js';
5
+
6
+ export function registerReviewCommand(program: Command): void {
7
+ program
8
+ .command('review <task>')
9
+ .description('Run the reviewer for a task and record the review result')
10
+ .option('--mode <mode>', 'Execution mode: supervised or dry-run')
11
+ .action(async (task: string, options: { mode?: string }) => {
12
+ try {
13
+ const { orchestrator, config } = await createOrchestrator();
14
+
15
+ // Resolve mode: flag > config.execution.default_mode
16
+ const mode: ExecutionMode = (options.mode as ExecutionMode) ?? config.execution.default_mode;
17
+
18
+ // M-7 fix: exit codes derived from state inspection, NOT exception catching.
19
+ // orchestrator.review() returns state and never throws on review outcomes.
20
+ const state = await orchestrator.review(task, mode);
21
+ const lastReview = state.history[state.history.length - 1]?.review;
22
+
23
+ if (lastReview) {
24
+ const version = state.current_version;
25
+ const score = lastReview.score;
26
+ const status = state.meta.status;
27
+ const issues = lastReview.issues ?? [];
28
+ const highCount = issues.filter(i => i.severity === 'high').length;
29
+ const medCount = issues.filter(i => i.severity === 'medium').length;
30
+ const lowCount = issues.filter(i => i.severity === 'low').length;
31
+ const summary = lastReview.summary ?? '';
32
+
33
+ console.log(
34
+ `Review complete (v${version}):\n` +
35
+ ` Score: ${score}\n` +
36
+ ` Status: ${status}\n` +
37
+ ` Issues: ${issues.length} (${highCount} high, ${medCount} medium, ${lowCount} low)\n` +
38
+ ` Summary: ${summary}`
39
+ );
40
+ }
41
+
42
+ if (state.meta.status === 'approved') {
43
+ process.exit(0);
44
+ } else if (state.meta.status === 'refinement') {
45
+ process.exit(2); // expected non-success — not an error
46
+ } else {
47
+ process.exit(1); // failed, or other unexpected state
48
+ }
49
+ } catch (err) {
50
+ if (err instanceof NomosError) {
51
+ console.error(`[nomos:error] ${err.message}`);
52
+ } else {
53
+ console.error(`[nomos:error] Unexpected error: ${err}`);
54
+ }
55
+ process.exit(1);
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,63 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+ import type { ExecutionMode } from '../types/index.js';
5
+
6
+ export function registerRunCommand(program: Command): void {
7
+ program
8
+ .command('run <task>')
9
+ .description('Run full plan/review cycle until convergence or max iterations')
10
+ .option('--mode <mode>', 'Execution mode: supervised or dry-run')
11
+ .option('--iterations <n>', 'Maximum number of plan/review iterations', parseInt)
12
+ .action(async (task: string, options: { mode?: string; iterations?: number }) => {
13
+ try {
14
+ const { orchestrator, config } = await createOrchestrator();
15
+
16
+ // Resolve mode: flag > config.execution.default_mode
17
+ const mode: ExecutionMode = (options.mode as ExecutionMode) ?? config.execution.default_mode;
18
+ const iterations = options.iterations;
19
+
20
+ // RTV-6 fix: exit code determination via meta.approval_reason inspection.
21
+ // NomosError('convergence_failed') is NOT thrown anywhere — this is the M-7 pattern
22
+ // applied to run: state inspection, not exception catching.
23
+ const finalState = await orchestrator.run(task, mode, iterations);
24
+
25
+ const lastEntry = finalState.history[finalState.history.length - 1];
26
+ const lastScore = lastEntry?.review?.score ?? 'N/A';
27
+ const version = finalState.current_version;
28
+ const status = finalState.meta.status;
29
+ const tokensUsed = finalState.budget.tokens_used;
30
+ const estimatedCost = finalState.budget.estimated_cost_usd.toFixed(4);
31
+
32
+ console.log(
33
+ `Run complete (v${version}):\n` +
34
+ ` Status: ${status}\n` +
35
+ ` Final score: ${lastScore}\n` +
36
+ ` Tokens used: ${tokensUsed}\n` +
37
+ ` Estimated cost: $${estimatedCost}`
38
+ );
39
+
40
+ if (finalState.meta.status === 'approved') {
41
+ if (finalState.meta.approval_reason === 'max_iterations_reached') {
42
+ // Converged by iteration limit, not by score threshold — signal as non-convergence
43
+ console.error(
44
+ `[nomos:warn] Max iterations reached without meeting score threshold. ` +
45
+ `Final score: ${lastScore}. ` +
46
+ `Consider increasing max_iterations or adjusting score_threshold in config.`
47
+ );
48
+ process.exit(2); // expected non-convergence
49
+ }
50
+ process.exit(0); // genuine convergence
51
+ } else {
52
+ process.exit(1); // error (failed state, budget exceeded, etc.)
53
+ }
54
+ } catch (err) {
55
+ if (err instanceof NomosError) {
56
+ console.error(`[nomos:error] ${err.message}`);
57
+ } else {
58
+ console.error(`[nomos:error] Unexpected error: ${err}`);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,147 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import type { Command } from 'commander';
4
+ import { loadConfig } from '../core/config.js';
5
+ import { createLogger } from '../core/logger.js';
6
+ import { NomosError } from '../core/errors.js';
7
+ import { QueryEngine } from '../search/query-engine.js';
8
+ import { AuthManager } from '../core/auth/manager.js';
9
+ import type { SearchResult } from '../types/index.js';
10
+
11
+ export function registerSearchCommand(program: Command): void {
12
+ program
13
+ .command('search <query>')
14
+ .description('Semantically search the codebase using the vector index')
15
+ .option('--top <n>', 'Maximum number of results', (v) => parseInt(v, 10), 5)
16
+ .option('--threshold <score>', 'Minimum similarity score (0.0–1.0)', (v) => parseFloat(v), 0.7)
17
+ .option('--json', 'Output raw JSON instead of formatted table')
18
+ .action(async (query: string, opts: { top: number; threshold: number; json?: boolean }) => {
19
+ try {
20
+ if (!query.trim()) {
21
+ console.error('Usage: arc search <query> [--top <n>] [--threshold <score>] [--json]');
22
+ process.exit(1);
23
+ }
24
+
25
+ const { config, projectRoot } = loadConfig();
26
+ const logDir = path.join(projectRoot, 'tasks-management', 'logs');
27
+
28
+ // In --json mode: suppress info/warn output to keep stdout machine-parseable [S-5]
29
+ const logLevel = opts.json ? 'error' : config.logging.level;
30
+ const logger = createLogger(logLevel, opts.json ? undefined : logDir);
31
+
32
+ const authManager = new AuthManager(config.auth, logger);
33
+ const engine = new QueryEngine(projectRoot, config, logger, authManager);
34
+
35
+ let results: SearchResult[];
36
+ try {
37
+ results = await engine.search(query, { topK: opts.top, threshold: opts.threshold });
38
+ } catch (err) {
39
+ if (err instanceof NomosError && err.code === 'search_index_not_found') {
40
+ if (opts.json) {
41
+ console.error(`[nomos:error] ${err.message}`);
42
+ } else {
43
+ console.error(`[nomos:error] ${err.message}`);
44
+ console.error('Run: arc map');
45
+ }
46
+ process.exit(1);
47
+ }
48
+ throw err;
49
+ }
50
+
51
+ if (opts.json) {
52
+ // Read index age from metadata (best-effort) [S-5]
53
+ let indexAge: string | null = null;
54
+ try {
55
+ const metaPath = path.join(config.search.vector_store_path, 'index-meta.json');
56
+ const raw = await fs.readFile(metaPath, 'utf-8');
57
+ const meta = JSON.parse(raw) as { last_full_index: string };
58
+ indexAge = meta.last_full_index;
59
+ } catch { /* non-fatal */ }
60
+
61
+ // Verify no vector field leaks [S-5] — SearchResult type excludes it by design
62
+ const output = {
63
+ query,
64
+ results: results.map((r) => ({
65
+ id: r.id,
66
+ type: r.type,
67
+ file_path: r.file_path,
68
+ symbol_name: r.symbol_name,
69
+ symbol_type: r.symbol_type,
70
+ line_start: r.line_start,
71
+ line_end: r.line_end,
72
+ purpose: r.purpose,
73
+ similarity_score: r.similarity_score,
74
+ graph_depth: r.graph_depth,
75
+ dependents_count: r.dependents_count,
76
+ is_core_module: r.is_core_module,
77
+ is_stale: r.is_stale,
78
+ })),
79
+ metadata: {
80
+ top_k: opts.top,
81
+ threshold: opts.threshold,
82
+ total_results: results.length,
83
+ index_age: indexAge,
84
+ },
85
+ };
86
+ console.log(JSON.stringify(output, null, 2));
87
+ process.exit(0);
88
+ }
89
+
90
+ // Human-readable output
91
+ if (results.length === 0) {
92
+ console.log(`No results found above threshold ${opts.threshold.toFixed(2)}.`);
93
+ process.exit(0);
94
+ }
95
+
96
+ console.log(`\nResults for: "${query}"\n`);
97
+
98
+ for (let i = 0; i < results.length; i++) {
99
+ const r = results[i]!;
100
+ const rank = i + 1;
101
+ const score = `[${r.similarity_score.toFixed(2)}]`;
102
+
103
+ // Header line: file + optional symbol + score + optional line range
104
+ let header = ` ${rank}. ${r.file_path}`;
105
+ if (r.symbol_name) {
106
+ const symbolSig = r.symbol_type ? `${r.symbol_name}()` : r.symbol_name;
107
+ header += ` :: ${symbolSig}`;
108
+ }
109
+ // Pad score to column 60
110
+ const headerPad = header.length < 60 ? ' '.repeat(60 - header.length) : ' ';
111
+ header += `${headerPad}${score}`;
112
+ if (r.line_start !== null && r.line_end !== null) {
113
+ header += ` L${r.line_start}-${r.line_end}`;
114
+ }
115
+ console.log(header);
116
+
117
+ // Purpose line
118
+ console.log(` "${r.purpose}"`);
119
+
120
+ // Depth / stale line [TRAP-4]: never print graph_depth === -1 numerically
121
+ if (r.is_stale) {
122
+ console.log(` ⚠ Stale — file removed since last index. Run: arc index --incremental`);
123
+ } else if (r.is_core_module) {
124
+ console.log(
125
+ ` ⚠ Core Module (depth ${r.graph_depth}) — modifying this affects ${r.dependents_count} dependents`,
126
+ );
127
+ } else {
128
+ console.log(
129
+ ` Leaf Module (depth ${r.graph_depth}) — ${r.dependents_count} dependents`,
130
+ );
131
+ }
132
+
133
+ if (i < results.length - 1) console.log('');
134
+ }
135
+
136
+ console.log(`\nFound ${results.length} results (threshold: ${opts.threshold.toFixed(2)}, top: ${opts.top})`);
137
+ process.exit(0);
138
+ } catch (err) {
139
+ if (err instanceof NomosError) {
140
+ console.error(`[nomos:error] ${err.message}`);
141
+ } else {
142
+ console.error(`[nomos:error] Unexpected error: ${err}`);
143
+ }
144
+ process.exit(1);
145
+ }
146
+ });
147
+ }
@@ -0,0 +1,63 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs/promises';
3
+ import type { Command } from 'commander';
4
+ import { loadConfig } from '../core/config.js';
5
+ import { readProjectMap, migrateProjectMap } from '../core/graph/map-schema.js';
6
+ import { MapRenderer } from '../core/graph/renderer.js';
7
+ import { generateHtml } from '../core/graph/html-template.js';
8
+ import { NomosError } from '../core/errors.js';
9
+
10
+ export function registerShowCommand(program: Command): void {
11
+ const show = program
12
+ .command('show')
13
+ .description('Display visual outputs');
14
+
15
+ show
16
+ .command('map')
17
+ .description('Open the interactive dependency graph in your browser')
18
+ .option('--no-open', 'Generate index.html without opening browser')
19
+ .action(async (opts: { open: boolean }) => {
20
+ try {
21
+ const { config, projectRoot } = loadConfig();
22
+
23
+ const outputDir = path.resolve(projectRoot, config.graph.output_dir);
24
+ const mapPath = path.join(outputDir, 'project_map.json');
25
+
26
+ const rawMap = await readProjectMap(mapPath);
27
+ if (rawMap === null) {
28
+ console.error('[nomos:error] No project map found. Run: arc map first.');
29
+ process.exit(1);
30
+ }
31
+
32
+ const map = migrateProjectMap(rawMap);
33
+ const renderer = new MapRenderer();
34
+ const { nodes, edges } = renderer.render(map);
35
+
36
+ const html = generateHtml(
37
+ JSON.stringify(map),
38
+ JSON.stringify(nodes),
39
+ JSON.stringify(edges),
40
+ );
41
+
42
+ const htmlPath = path.join(outputDir, 'index.html');
43
+ await fs.mkdir(outputDir, { recursive: true });
44
+ await fs.writeFile(htmlPath, html, 'utf-8');
45
+
46
+ console.log(`Graph visualization written to: ${htmlPath}`);
47
+
48
+ if (opts.open !== false) {
49
+ const { default: open } = await import('open');
50
+ await open(htmlPath);
51
+ }
52
+
53
+ process.exit(0);
54
+ } catch (err) {
55
+ if (err instanceof NomosError) {
56
+ console.error(`[nomos:error] ${err.message}`);
57
+ } else {
58
+ console.error(`[nomos:error] Unexpected error: ${err}`);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,59 @@
1
+ import type { Command } from 'commander';
2
+ import { createOrchestrator } from '../core/factory.js';
3
+ import { NomosError } from '../core/errors.js';
4
+
5
+ export function registerStatusCommand(program: Command): void {
6
+ program
7
+ .command('status <task>')
8
+ .description('Show current status and metadata for a task')
9
+ .action(async (task: string) => {
10
+ try {
11
+ const { orchestrator } = await createOrchestrator();
12
+ const state = await orchestrator.status(task);
13
+
14
+ // RTV-4 fix: derive hasMeteredTokens from history entries — never from an undeclared variable
15
+ const hasMeteredTokens = state.history.some(h => h.tokens_source === 'metered');
16
+ const lastReview = state.history
17
+ .slice()
18
+ .reverse()
19
+ .find(h => h.review !== null)?.review;
20
+
21
+ console.log(
22
+ `Task: ${state.task_id}\n` +
23
+ `Status: ${state.meta.status}\n` +
24
+ `Version: ${state.current_version}\n` +
25
+ `Last Score: ${lastReview?.score?.toFixed(2) ?? 'N/A'}\n` +
26
+ `Shadow Branch: ${state.shadow_branch.branch}\n` +
27
+ `Worktree: ${state.shadow_branch.worktree}\n` +
28
+ `Tokens Used: ${state.budget.tokens_used} (${hasMeteredTokens ? 'metered' : '~estimated'})\n` +
29
+ `Estimated Cost: $${state.budget.estimated_cost_usd.toFixed(4)}`
30
+ );
31
+
32
+ // RTV-7 fix: recovery hint for every non-terminal status
33
+ const recoveryHints: Partial<Record<typeof state.meta.status, string>> = {
34
+ init: `Run: arc plan ${task}`,
35
+ planning: `(in progress — if stuck, run: arc discard ${task} && arc init ${task})`,
36
+ pending_review: `Run: arc review ${task}`,
37
+ reviewing: `(in progress — if stuck, run: arc discard ${task} && arc init ${task})`,
38
+ refinement: `Run: arc plan ${task} (to address review feedback)`,
39
+ approved: `Run: arc apply ${task} (to merge) or: arc discard ${task}`,
40
+ merge_conflict: `Resolve conflicts manually, then: arc apply ${task}`,
41
+ stalled: `Run: arc plan ${task} (to retry from stalled state)`,
42
+ failed: `Run: arc plan ${task} (to retry from failed state)`,
43
+ merged: `(terminal — task complete)`,
44
+ discarded: `(terminal — task discarded)`,
45
+ };
46
+ const hint = recoveryHints[state.meta.status];
47
+ if (hint) console.log(`\nNext step: ${hint}`);
48
+
49
+ process.exit(0);
50
+ } catch (err) {
51
+ if (err instanceof NomosError) {
52
+ console.error(`[nomos:error] ${err.message}`);
53
+ } else {
54
+ console.error(`[nomos:error] Unexpected error: ${err}`);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ });
59
+ }