tenbo-dashboard 0.8.0 → 0.10.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 (48) hide show
  1. package/README.md +14 -0
  2. package/bin/tenbo-dashboard.mjs +25 -0
  3. package/package.json +3 -2
  4. package/scripts/bin-output.test.ts +59 -0
  5. package/scripts/cliArgs.ts +30 -0
  6. package/scripts/cliResult.ts +59 -0
  7. package/scripts/commit-ready.test.ts +47 -0
  8. package/scripts/commit-ready.ts +69 -0
  9. package/scripts/context.test.ts +77 -0
  10. package/scripts/context.ts +29 -0
  11. package/scripts/item.test.ts +234 -0
  12. package/scripts/item.ts +148 -0
  13. package/scripts/items.test.ts +112 -0
  14. package/scripts/items.ts +55 -0
  15. package/scripts/next.test.ts +67 -0
  16. package/scripts/next.ts +34 -0
  17. package/scripts/work-queue.test.ts +65 -0
  18. package/scripts/work-queue.ts +58 -0
  19. package/src/api/lib/contextResolver.test.ts +161 -0
  20. package/src/api/lib/contextResolver.ts +415 -0
  21. package/src/api/lib/health/collectAll.test.ts +61 -0
  22. package/src/api/lib/health/collectAll.ts +48 -2
  23. package/src/api/lib/health/deadCode.test.ts +5 -1
  24. package/src/api/lib/health/deadCode.ts +28 -39
  25. package/src/api/lib/health/importGraph.test.ts +44 -0
  26. package/src/api/lib/health/importGraph.ts +23 -9
  27. package/src/api/lib/health/layerFiles.ts +10 -13
  28. package/src/api/lib/health/types.test.ts +9 -2
  29. package/src/api/lib/health/types.ts +2 -1
  30. package/src/api/lib/itemEvidence.ts +49 -0
  31. package/src/api/lib/itemProjection.ts +79 -0
  32. package/src/api/lib/metrics.ts +4 -1
  33. package/src/api/lib/metricsRefresh.ts +2 -1
  34. package/src/api/lib/metricsRefreshQueue.ts +183 -0
  35. package/src/api/lib/roadmapStore.test.ts +163 -0
  36. package/src/api/lib/roadmapStore.ts +314 -0
  37. package/src/api/lib/sourceIgnore.ts +23 -0
  38. package/src/api/lib/validator.ts +40 -0
  39. package/src/api/lib/validator.verification.test.ts +52 -0
  40. package/src/api/plugin.ts +2 -0
  41. package/src/api/routes/items.ts +1 -1
  42. package/src/api/routes/refreshMetrics.ts +40 -0
  43. package/src/api/routes/state.test.ts +148 -0
  44. package/src/api/routes/state.ts +3 -10
  45. package/src/types.ts +24 -0
  46. package/src/ui/HealthPage/HealthPage.module.css +18 -0
  47. package/src/ui/HealthPage/HealthPage.test.tsx +22 -0
  48. package/src/ui/HealthPage/HealthPage.tsx +24 -1
package/README.md CHANGED
@@ -38,6 +38,14 @@ Surfaces the things that would otherwise become tech debt nobody mentions: overs
38
38
  The same package ships commands the skill uses behind the scenes — also runnable directly:
39
39
 
40
40
  ```bash
41
+ npx tenbo-dashboard item show sk-030 --json
42
+ npx tenbo-dashboard item set-status sk-030 done
43
+ npx tenbo-dashboard item add-note sk-030 "Implemented; live verification pending"
44
+ npx tenbo-dashboard item verify sk-030 --status pending_live --evidence "npm test -- --run"
45
+ npx tenbo-dashboard item link-commit sk-030 7fc09a5
46
+ npx tenbo-dashboard items --status done --verification pending_live --json
47
+ npx tenbo-dashboard next --json
48
+ npx tenbo-dashboard context feature --query "help me build X" --json
41
49
  npx tenbo-dashboard validate # Schema + consistency checks
42
50
  npx tenbo-dashboard init-check # Strict completeness check after fresh init
43
51
  npx tenbo-dashboard metrics --all # Recompute scope metrics + health findings
@@ -45,6 +53,12 @@ npx tenbo-dashboard next-id <prefix> # Allocate next roadmap item ID
45
53
  npx tenbo-dashboard --version # Print the installed version
46
54
  ```
47
55
 
56
+ Installed packages also expose a shorter `tenbo` binary alias for the same commands.
57
+
58
+ `context feature` is the agent-facing read path. It returns likely scope/layers,
59
+ matching roadmap items, active work, goal refs, recommended files, and freshness warnings
60
+ as one JSON payload so agents do not have to re-read every roadmap file by hand.
61
+
48
62
  ## What is tenbo?
49
63
 
50
64
  An AI cofounder that gives your coding assistant persistent project memory — architecture docs, roadmaps, and health signals that survive across sessions. Available as a Claude Code skill and as a Cursor rule package; both editors share the same `.tenbo/` data and the same companion dashboard. The dashboard is the optional visual companion; the skill / rule is the always-on conversational brain.
@@ -41,6 +41,13 @@ const commands = {
41
41
  'init-check': 'scripts/init-check.ts',
42
42
  sync: 'scripts/sync.ts',
43
43
  archive: 'scripts/archive.ts',
44
+ 'commit-ready': 'scripts/commit-ready.ts',
45
+ item: 'scripts/item.ts',
46
+ items: 'scripts/items.ts',
47
+ list: 'scripts/items.ts',
48
+ next: 'scripts/next.ts',
49
+ 'work-queue': 'scripts/work-queue.ts',
50
+ context: 'scripts/context.ts',
44
51
  hook: 'scripts/hook.ts',
45
52
  };
46
53
 
@@ -78,6 +85,22 @@ Usage:
78
85
  tenbo-dashboard Launch the dashboard (http://localhost:5174)
79
86
  tenbo-dashboard serve Same as bare invocation (also: 'start', 'dev')
80
87
  tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
88
+ tenbo-dashboard item show <id> [--json]
89
+ tenbo-dashboard item set-status <id> <now|next|later|done|dropped> [--json]
90
+ tenbo-dashboard item add-note <id> "<note>" [--json]
91
+ tenbo-dashboard item verify <id> --status <not_required|pending_live|verified|failed>
92
+ [--evidence "<text>"] [--note "<text>"] [--json]
93
+ tenbo-dashboard item link-commit <id> <sha> [--json]
94
+ tenbo-dashboard items Query roadmap items [--status <status>]
95
+ [--verification <status>] [--goal <goal>]
96
+ [--type <type>] [--priority <p0|p1|p2|p3>]
97
+ [--layer <id>] [--fields <a,b>] [--summary] [--json]
98
+ tenbo-dashboard list Alias for items
99
+ tenbo-dashboard next Show next actionable roadmap items [--json]
100
+ tenbo-dashboard work-queue Compact agent task flow [--status <a,b>]
101
+ [--type <type>] [--json]
102
+ tenbo-dashboard context feature Fetch agent planning context for a natural-language request
103
+ --query "<request>" [--json]
81
104
  tenbo-dashboard validate Run validation rules
82
105
  tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
83
106
  tenbo-dashboard next-id <prefix> Allocate next roadmap item ID
@@ -87,6 +110,8 @@ Usage:
87
110
  tenbo-dashboard hook install Install opt-in pre-commit validation hook
88
111
  [--dry-run] [--force]
89
112
  tenbo-dashboard hook uninstall Remove the tenbo pre-commit hook (idempotent)
113
+ tenbo-dashboard commit-ready Report branch, diff, validation, and staging gates
114
+ without committing [--json]
90
115
  tenbo-dashboard --version Print package version
91
116
  tenbo-dashboard help Show this help
92
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tenbo-dashboard",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,8 @@
23
23
  "node": ">=18"
24
24
  },
25
25
  "bin": {
26
- "tenbo-dashboard": "./bin/tenbo-dashboard.mjs"
26
+ "tenbo-dashboard": "./bin/tenbo-dashboard.mjs",
27
+ "tenbo": "./bin/tenbo-dashboard.mjs"
27
28
  },
28
29
  "files": [
29
30
  "bin/",
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ let dir: string;
8
+
9
+ beforeEach(() => {
10
+ dir = mkdtempSync(path.join(tmpdir(), 'tenbo-bin-output-'));
11
+ mkdirSync(path.join(dir, '.git'));
12
+ mkdirSync(path.join(dir, '.tenbo/scopes/editor/layers/app'), { recursive: true });
13
+ writeFileSync(path.join(dir, '.tenbo/workspace.yaml'), 'scopes:\n - id: editor\n path: apps/editor\n description: editor scope\ncross_cutting: []\n');
14
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/architecture.yaml'), 'layers:\n - id: app\n name: App\n description: application layer\n files: ["src/**"]\n');
15
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/layers/app/README.md'), '# App\n');
16
+ writeFileSync(path.join(dir, '.tenbo/overview.md'), '# Overview\n');
17
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
18
+ 'items:',
19
+ ' - id: ed-001',
20
+ ' title: Large Output',
21
+ ' layer: app',
22
+ ' status: done',
23
+ ' description: item with a large field',
24
+ ' risks:',
25
+ ` - ${'x'.repeat(12_000)}`,
26
+ '',
27
+ ].join('\n'));
28
+ });
29
+
30
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
31
+
32
+ describe('bin output', () => {
33
+ it('flushes large JSON output before exiting', () => {
34
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
35
+ const result = spawnSync(process.execPath, [bin, 'items', '--status', 'done', '--json'], {
36
+ cwd: dir,
37
+ encoding: 'utf8',
38
+ maxBuffer: 1024 * 1024,
39
+ });
40
+
41
+ expect(result.status).toBe(0);
42
+ const payload = JSON.parse(result.stdout);
43
+ expect(payload.items[0].item.risks[0]).toHaveLength(12_000);
44
+ });
45
+
46
+ it('supports list as an alias for items', () => {
47
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
48
+ const result = spawnSync(process.execPath, [bin, 'list', '--status', 'done', '--fields', 'id,title,status', '--json'], {
49
+ cwd: dir,
50
+ encoding: 'utf8',
51
+ maxBuffer: 1024 * 1024,
52
+ });
53
+
54
+ expect(result.status).toBe(0);
55
+ expect(JSON.parse(result.stdout).items).toEqual([
56
+ { id: 'ed-001', title: 'Large Output', status: 'done' },
57
+ ]);
58
+ });
59
+ });
@@ -0,0 +1,30 @@
1
+ export function hasFlag(args: string[], flag: string): boolean {
2
+ return args.includes(flag);
3
+ }
4
+
5
+ export function readOption(args: string[], flag: string): string | undefined {
6
+ const i = args.indexOf(flag);
7
+ if (i === -1) return undefined;
8
+ return args[i + 1];
9
+ }
10
+
11
+ export function readRepeated(args: string[], flag: string): string[] {
12
+ const out: string[] = [];
13
+ for (let i = 0; i < args.length; i++) {
14
+ if (args[i] === flag && args[i + 1]) out.push(args[i + 1]);
15
+ }
16
+ return out;
17
+ }
18
+
19
+ export function positionalArgs(args: string[]): string[] {
20
+ const out: string[] = [];
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (arg.startsWith('--')) {
24
+ if (args[i + 1] && !args[i + 1].startsWith('--')) i++;
25
+ continue;
26
+ }
27
+ out.push(arg);
28
+ }
29
+ return out;
30
+ }
@@ -0,0 +1,59 @@
1
+ import { findRepoRoot } from '../src/api/lib/repoRoot';
2
+ import { isRoadmapStoreError } from '../src/api/lib/roadmapStore';
3
+
4
+ export interface CliResult {
5
+ stdout: string;
6
+ stderr: string;
7
+ exitCode: number;
8
+ }
9
+
10
+ export function ok(stdout: string): CliResult {
11
+ return { stdout, stderr: '', exitCode: 0 };
12
+ }
13
+
14
+ export function fail(error: string, message: string, json: boolean, retryable = false, exitCode = 1): CliResult {
15
+ if (json) {
16
+ return {
17
+ stdout: '',
18
+ stderr: `${JSON.stringify({ ok: false, error, message, retryable })}\n`,
19
+ exitCode,
20
+ };
21
+ }
22
+ return { stdout: '', stderr: `${message}\n`, exitCode };
23
+ }
24
+
25
+ export function misuse(message: string, json: boolean): CliResult {
26
+ return fail('invalid_args', message, json, false, 2);
27
+ }
28
+
29
+ export function serialize(payload: unknown, json: boolean, text: string): CliResult {
30
+ return ok(json ? `${JSON.stringify(payload, null, 2)}\n` : text);
31
+ }
32
+
33
+ export function handleCliError(err: unknown, json: boolean): CliResult {
34
+ if (isRoadmapStoreError(err)) {
35
+ return fail(err.code, err.message, json, err.retryable);
36
+ }
37
+ const message = err instanceof Error ? err.message : String(err);
38
+ return fail('error', message, json);
39
+ }
40
+
41
+ export function repoRootFromCwd(cwd = process.cwd()): string {
42
+ return findRepoRoot(cwd) ?? cwd;
43
+ }
44
+
45
+ export function runMain(result: CliResult): void {
46
+ if (result.stdout) process.stdout.write(result.stdout);
47
+ if (result.stderr) process.stderr.write(result.stderr);
48
+ process.exitCode = result.exitCode;
49
+ }
50
+
51
+ export function isMain(importMetaUrl: string): boolean {
52
+ try {
53
+ const invoked = process.argv[1] ? new URL(`file://${process.argv[1]}`).pathname : '';
54
+ const here = new URL(importMetaUrl).pathname;
55
+ return invoked === here;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { runCommitReadyCli } from './commit-ready';
7
+
8
+ let dir: string;
9
+
10
+ beforeEach(() => {
11
+ dir = mkdtempSync(path.join(tmpdir(), 'tenbo-commit-ready-cli-'));
12
+ spawnSync('git', ['init', '-q'], { cwd: dir });
13
+ spawnSync('git', ['checkout', '-q', '-b', 'main'], { cwd: dir });
14
+ mkdirSync(path.join(dir, '.tenbo/scopes/editor/layers/app'), { recursive: true });
15
+ writeFileSync(path.join(dir, '.tenbo/workspace.yaml'), 'scopes:\n - id: editor\n path: apps/editor\n description: editor scope\ncross_cutting: []\n');
16
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/architecture.yaml'), 'layers:\n - id: app\n name: App\n description: application layer\n files: ["src/**"]\n');
17
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/layers/app/README.md'), '# App\n');
18
+ writeFileSync(path.join(dir, '.tenbo/overview.md'), '# Overview\n');
19
+ writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
20
+ 'items:',
21
+ ' - id: ed-001',
22
+ ' title: First',
23
+ ' layer: app',
24
+ ' status: next',
25
+ ' description: first item',
26
+ '',
27
+ ].join('\n'));
28
+ });
29
+
30
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
31
+
32
+ describe('commit-ready CLI', () => {
33
+ it('reports branch, dirty files, diff check, and validation without committing', () => {
34
+ const result = runCommitReadyCli(dir, ['--json']);
35
+
36
+ expect(result.exitCode).toBe(0);
37
+ const payload = JSON.parse(result.stdout);
38
+ expect(payload).toMatchObject({
39
+ ok: true,
40
+ branch: expect.objectContaining({ current: 'main' }),
41
+ diff_check: expect.objectContaining({ exitCode: 0 }),
42
+ validation: expect.objectContaining({ errors: 0 }),
43
+ });
44
+ expect(payload.dirty_files.length).toBeGreaterThan(0);
45
+ expect(payload.recommended_gates).toContain('git diff --check');
46
+ });
47
+ });
@@ -0,0 +1,69 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readState } from '../src/api/lib/tenboFs';
3
+ import { validate } from '../src/api/lib/validator';
4
+ import { hasFlag } from './cliArgs';
5
+ import { handleCliError, isMain, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
6
+
7
+ function runGit(repoRoot: string, args: string[]) {
8
+ const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });
9
+ return {
10
+ exitCode: result.status ?? 1,
11
+ stdout: result.stdout ?? '',
12
+ stderr: result.stderr ?? '',
13
+ };
14
+ }
15
+
16
+ function parseStatus(stdout: string): { current: string | null; upstream?: string; dirtyFiles: string[] } {
17
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
18
+ const header = lines[0] ?? '';
19
+ const unbornMatch = header.match(/^## No commits yet on (.+)$/);
20
+ if (unbornMatch) {
21
+ return {
22
+ current: unbornMatch[1],
23
+ dirtyFiles: lines.slice(1),
24
+ };
25
+ }
26
+ const branchMatch = header.match(/^## ([^.\s]+)(?:\.\.\.([^\s]+))?/);
27
+ return {
28
+ current: branchMatch?.[1] ?? null,
29
+ ...(branchMatch?.[2] ? { upstream: branchMatch[2] } : {}),
30
+ dirtyFiles: lines.slice(1),
31
+ };
32
+ }
33
+
34
+ export function runCommitReadyCli(repoRoot: string, args: string[]): CliResult {
35
+ const json = hasFlag(args, '--json');
36
+ try {
37
+ const status = runGit(repoRoot, ['status', '--short', '--branch']);
38
+ const parsed = parseStatus(status.stdout);
39
+ const diffCheck = runGit(repoRoot, ['diff', '--check']);
40
+ const staged = runGit(repoRoot, ['diff', '--cached', '--name-only']);
41
+ const validation = validate(readState(repoRoot));
42
+ const payload = {
43
+ ok: true,
44
+ branch: {
45
+ current: parsed.current,
46
+ upstream: parsed.upstream ?? null,
47
+ },
48
+ dirty_files: parsed.dirtyFiles,
49
+ staged_files: staged.stdout.split(/\r?\n/).filter(Boolean),
50
+ diff_check: diffCheck,
51
+ validation: {
52
+ errors: validation.errors.length,
53
+ warnings: validation.warnings.length,
54
+ },
55
+ recommended_gates: [
56
+ 'git diff --check',
57
+ 'tenbo-dashboard validate',
58
+ 'git diff --cached --check',
59
+ ],
60
+ };
61
+ return serialize(payload, json, `branch: ${payload.branch.current ?? 'unknown'}\ndirty files: ${payload.dirty_files.length}\nvalidation: ${payload.validation.errors} errors, ${payload.validation.warnings} warnings\n`);
62
+ } catch (err) {
63
+ return handleCliError(err, json);
64
+ }
65
+ }
66
+
67
+ if (isMain(import.meta.url)) {
68
+ runMain(runCommitReadyCli(repoRootFromCwd(), process.argv.slice(2)));
69
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { runContextCli } from './context';
6
+
7
+ let dir: string;
8
+
9
+ beforeEach(() => {
10
+ dir = mkdtempSync(path.join(tmpdir(), 'tenbo-context-cli-'));
11
+ mkdirSync(path.join(dir, '.git'));
12
+ mkdirSync(path.join(dir, '.tenbo/scopes/dashboard/layers/cli-tools'), { recursive: true });
13
+ writeFileSync(path.join(dir, '.tenbo/workspace.yaml'), [
14
+ 'scopes:',
15
+ ' - id: dashboard',
16
+ ' path: tenbo-dashboard',
17
+ ' description: dashboard and CLI package',
18
+ 'cross_cutting: []',
19
+ '',
20
+ ].join('\n'));
21
+ writeFileSync(path.join(dir, '.tenbo/overview.md'), [
22
+ '# Overview',
23
+ '',
24
+ '## Product goals',
25
+ '- **g1**: Give agents holistic context.',
26
+ '',
27
+ ].join('\n'));
28
+ writeFileSync(path.join(dir, '.tenbo/scopes/dashboard/architecture.yaml'), [
29
+ 'layers:',
30
+ ' - id: cli-tools',
31
+ ' name: CLI Tools',
32
+ ' description: Commands for agents to fetch context.',
33
+ ' files: ["bin/**", "scripts/**"]',
34
+ '',
35
+ ].join('\n'));
36
+ writeFileSync(path.join(dir, '.tenbo/scopes/dashboard/layers/cli-tools/intent.md'), '# CLI Tools\n');
37
+ writeFileSync(path.join(dir, '.tenbo/scopes/dashboard/roadmap.yaml'), [
38
+ 'items:',
39
+ ' - id: td-010',
40
+ ' title: Add context command',
41
+ ' layer: cli-tools',
42
+ ' status: next',
43
+ ' goal_ref: [g1]',
44
+ ' description: Add a context command for feature planning.',
45
+ '',
46
+ ].join('\n'));
47
+ });
48
+
49
+ afterEach(() => rmSync(dir, { recursive: true, force: true }));
50
+
51
+ describe('context CLI', () => {
52
+ it('prints feature context as JSON', () => {
53
+ const result = runContextCli(dir, ['feature', '--query', 'build a context command for agents', '--json']);
54
+
55
+ expect(result.exitCode).toBe(0);
56
+ const payload = JSON.parse(result.stdout);
57
+ expect(payload).toMatchObject({
58
+ ok: true,
59
+ intent: 'feature',
60
+ recommendation: {
61
+ scope: 'dashboard',
62
+ layers: ['cli-tools'],
63
+ },
64
+ });
65
+ expect(payload.roadmap.matching_items.map((entry: { item: { id: string } }) => entry.item.id)).toEqual(['td-010']);
66
+ });
67
+
68
+ it('rejects feature context requests without a query', () => {
69
+ const result = runContextCli(dir, ['feature', '--json']);
70
+
71
+ expect(result.exitCode).toBe(2);
72
+ expect(JSON.parse(result.stderr)).toMatchObject({
73
+ ok: false,
74
+ error: 'invalid_args',
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,29 @@
1
+ import { resolveFeatureContext } from '../src/api/lib/contextResolver';
2
+ import { hasFlag, readOption } from './cliArgs';
3
+ import { handleCliError, isMain, misuse, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
4
+
5
+ export function runContextCli(repoRoot: string, args: string[]): CliResult {
6
+ const json = hasFlag(args, '--json');
7
+ const command = args.find((arg) => !arg.startsWith('--'));
8
+
9
+ try {
10
+ if (command === 'feature') {
11
+ const query = readOption(args, '--query');
12
+ if (!query) return misuse('Usage: tenbo context feature --query "<request>" [--json]', json);
13
+ const bundle = resolveFeatureContext(repoRoot, query);
14
+ return serialize(
15
+ bundle,
16
+ json,
17
+ `${bundle.recommendation.confidence} confidence: ${bundle.recommendation.scope ?? 'unclassified'}${bundle.recommendation.layers.length ? `/${bundle.recommendation.layers.join(',')}` : ''}\n`,
18
+ );
19
+ }
20
+
21
+ return misuse('Usage: tenbo context feature --query "<request>" [--json]', json);
22
+ } catch (err) {
23
+ return handleCliError(err, json);
24
+ }
25
+ }
26
+
27
+ if (isMain(import.meta.url)) {
28
+ runMain(runContextCli(repoRootFromCwd(), process.argv.slice(2)));
29
+ }