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