tenbo-dashboard 0.9.0 → 0.10.1
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/bin/tenbo-dashboard.mjs +11 -1
- package/package.json +1 -1
- package/scripts/bin-output.test.ts +29 -0
- package/scripts/cliArgs.ts +30 -0
- package/scripts/cliResult.ts +7 -3
- package/scripts/commit-ready.test.ts +47 -0
- package/scripts/commit-ready.ts +69 -0
- package/scripts/context.test.ts +1 -1
- package/scripts/context.ts +4 -13
- package/scripts/item.test.ts +156 -1
- package/scripts/item.ts +98 -26
- package/scripts/items.test.ts +86 -0
- package/scripts/items.ts +38 -15
- package/scripts/next-id.test.ts +1 -1
- package/scripts/next-id.ts +1 -5
- package/scripts/next.test.ts +23 -0
- package/scripts/next.ts +19 -1
- package/scripts/work-queue.test.ts +65 -0
- package/scripts/work-queue.ts +58 -0
- package/src/api/lib/itemEvidence.ts +49 -0
- package/src/api/lib/itemProjection.ts +82 -0
- package/src/api/lib/roadmapStore.test.ts +29 -0
- package/src/api/lib/roadmapStore.ts +67 -4
- package/tsconfig.json +1 -1
package/bin/tenbo-dashboard.mjs
CHANGED
|
@@ -41,9 +41,12 @@ 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',
|
|
44
45
|
item: 'scripts/item.ts',
|
|
45
46
|
items: 'scripts/items.ts',
|
|
47
|
+
list: 'scripts/items.ts',
|
|
46
48
|
next: 'scripts/next.ts',
|
|
49
|
+
'work-queue': 'scripts/work-queue.ts',
|
|
47
50
|
context: 'scripts/context.ts',
|
|
48
51
|
hook: 'scripts/hook.ts',
|
|
49
52
|
};
|
|
@@ -89,8 +92,13 @@ Usage:
|
|
|
89
92
|
[--evidence "<text>"] [--note "<text>"] [--json]
|
|
90
93
|
tenbo-dashboard item link-commit <id> <sha> [--json]
|
|
91
94
|
tenbo-dashboard items Query roadmap items [--status <status>]
|
|
92
|
-
[--verification <status>] [--goal <goal>]
|
|
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
|
|
93
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]
|
|
94
102
|
tenbo-dashboard context feature Fetch agent planning context for a natural-language request
|
|
95
103
|
--query "<request>" [--json]
|
|
96
104
|
tenbo-dashboard validate Run validation rules
|
|
@@ -102,6 +110,8 @@ Usage:
|
|
|
102
110
|
tenbo-dashboard hook install Install opt-in pre-commit validation hook
|
|
103
111
|
[--dry-run] [--force]
|
|
104
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]
|
|
105
115
|
tenbo-dashboard --version Print package version
|
|
106
116
|
tenbo-dashboard help Show this help
|
|
107
117
|
|
package/package.json
CHANGED
|
@@ -42,4 +42,33 @@ describe('bin output', () => {
|
|
|
42
42
|
const payload = JSON.parse(result.stdout);
|
|
43
43
|
expect(payload.items[0].item.risks[0]).toHaveLength(12_000);
|
|
44
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
|
+
|
|
60
|
+
it('prints items help without running the query', () => {
|
|
61
|
+
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
62
|
+
const result = spawnSync(process.execPath, [bin, 'items', '--help'], {
|
|
63
|
+
cwd: dir,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
maxBuffer: 1024 * 1024,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.status).toBe(0);
|
|
69
|
+
expect(result.stderr).toBe('');
|
|
70
|
+
expect(result.stdout).toContain('Usage: tenbo-dashboard items');
|
|
71
|
+
expect(result.stdout).toContain('--fields <a,b>');
|
|
72
|
+
expect(result.stdout).not.toContain('ed-001');
|
|
73
|
+
});
|
|
45
74
|
});
|
|
@@ -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
|
+
}
|
package/scripts/cliResult.ts
CHANGED
|
@@ -11,15 +11,19 @@ export function ok(stdout: string): CliResult {
|
|
|
11
11
|
return { stdout, stderr: '', exitCode: 0 };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function fail(error: string, message: string, json: boolean, retryable = false): CliResult {
|
|
14
|
+
export function fail(error: string, message: string, json: boolean, retryable = false, exitCode = 1): CliResult {
|
|
15
15
|
if (json) {
|
|
16
16
|
return {
|
|
17
17
|
stdout: '',
|
|
18
18
|
stderr: `${JSON.stringify({ ok: false, error, message, retryable })}\n`,
|
|
19
|
-
exitCode
|
|
19
|
+
exitCode,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
return { stdout: '', stderr: `${message}\n`, exitCode
|
|
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);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function serialize(payload: unknown, json: boolean, text: string): CliResult {
|
|
@@ -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
|
+
}
|
package/scripts/context.test.ts
CHANGED
|
@@ -68,7 +68,7 @@ describe('context CLI', () => {
|
|
|
68
68
|
it('rejects feature context requests without a query', () => {
|
|
69
69
|
const result = runContextCli(dir, ['feature', '--json']);
|
|
70
70
|
|
|
71
|
-
expect(result.exitCode).toBe(
|
|
71
|
+
expect(result.exitCode).toBe(2);
|
|
72
72
|
expect(JSON.parse(result.stderr)).toMatchObject({
|
|
73
73
|
ok: false,
|
|
74
74
|
error: 'invalid_args',
|
package/scripts/context.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { resolveFeatureContext } from '../src/api/lib/contextResolver';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
5
|
-
return args.includes(flag);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function readOption(args: string[], flag: string): string | undefined {
|
|
9
|
-
const i = args.indexOf(flag);
|
|
10
|
-
if (i === -1) return undefined;
|
|
11
|
-
return args[i + 1];
|
|
12
|
-
}
|
|
2
|
+
import { hasFlag, readOption } from './cliArgs';
|
|
3
|
+
import { handleCliError, isMain, misuse, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
|
|
13
4
|
|
|
14
5
|
export function runContextCli(repoRoot: string, args: string[]): CliResult {
|
|
15
6
|
const json = hasFlag(args, '--json');
|
|
@@ -18,7 +9,7 @@ export function runContextCli(repoRoot: string, args: string[]): CliResult {
|
|
|
18
9
|
try {
|
|
19
10
|
if (command === 'feature') {
|
|
20
11
|
const query = readOption(args, '--query');
|
|
21
|
-
if (!query) return
|
|
12
|
+
if (!query) return misuse('Usage: tenbo context feature --query "<request>" [--json]', json);
|
|
22
13
|
const bundle = resolveFeatureContext(repoRoot, query);
|
|
23
14
|
return serialize(
|
|
24
15
|
bundle,
|
|
@@ -27,7 +18,7 @@ export function runContextCli(repoRoot: string, args: string[]): CliResult {
|
|
|
27
18
|
);
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
return
|
|
21
|
+
return misuse('Usage: tenbo context feature --query "<request>" [--json]', json);
|
|
31
22
|
} catch (err) {
|
|
32
23
|
return handleCliError(err, json);
|
|
33
24
|
}
|
package/scripts/item.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { runItemCli } from './item';
|
|
@@ -21,6 +21,13 @@ beforeEach(() => {
|
|
|
21
21
|
' layer: app',
|
|
22
22
|
' status: next',
|
|
23
23
|
' description: first item',
|
|
24
|
+
' type: refactor',
|
|
25
|
+
' done_when:',
|
|
26
|
+
' - First item lands.',
|
|
27
|
+
' files_to_read:',
|
|
28
|
+
' - src/app.ts',
|
|
29
|
+
' risks:',
|
|
30
|
+
' - May overlap active work.',
|
|
24
31
|
'',
|
|
25
32
|
].join('\n'));
|
|
26
33
|
});
|
|
@@ -49,6 +56,67 @@ describe('item CLI', () => {
|
|
|
49
56
|
});
|
|
50
57
|
});
|
|
51
58
|
|
|
59
|
+
it('prints an active task brief as compact JSON', () => {
|
|
60
|
+
const result = runItemCli(dir, ['brief', 'ed-001', '--json']);
|
|
61
|
+
|
|
62
|
+
expect(result.exitCode).toBe(0);
|
|
63
|
+
const payload = JSON.parse(result.stdout);
|
|
64
|
+
expect(payload).toMatchObject({
|
|
65
|
+
ok: true,
|
|
66
|
+
item: {
|
|
67
|
+
id: 'ed-001',
|
|
68
|
+
title: 'First',
|
|
69
|
+
status: 'next',
|
|
70
|
+
type: 'refactor',
|
|
71
|
+
layer: 'app',
|
|
72
|
+
},
|
|
73
|
+
files_to_read: ['src/app.ts'],
|
|
74
|
+
acceptance_criteria: ['First item lands.'],
|
|
75
|
+
risks: ['May overlap active work.'],
|
|
76
|
+
});
|
|
77
|
+
expect(payload.item).not.toHaveProperty('description');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('prints a handoff prompt for another agent', () => {
|
|
81
|
+
const result = runItemCli(dir, ['handoff', 'ed-001', '--json']);
|
|
82
|
+
|
|
83
|
+
expect(result.exitCode).toBe(0);
|
|
84
|
+
const payload = JSON.parse(result.stdout);
|
|
85
|
+
expect(payload).toMatchObject({
|
|
86
|
+
ok: true,
|
|
87
|
+
item_id: 'ed-001',
|
|
88
|
+
});
|
|
89
|
+
expect(payload.prompt).toContain('Files to read');
|
|
90
|
+
expect(payload.prompt).toContain('src/app.ts');
|
|
91
|
+
expect(payload.prompt).toContain('Acceptance criteria');
|
|
92
|
+
expect(payload.prompt).toContain('First item lands.');
|
|
93
|
+
expect(payload.prompt).toContain('Non-goals');
|
|
94
|
+
expect(payload.prompt).toContain('Completion rules');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('adds a note when existing notes are a YAML sequence', () => {
|
|
98
|
+
writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
|
|
99
|
+
'items:',
|
|
100
|
+
' - id: ed-001',
|
|
101
|
+
' title: First',
|
|
102
|
+
' layer: app',
|
|
103
|
+
' status: next',
|
|
104
|
+
' description: first item',
|
|
105
|
+
' notes:',
|
|
106
|
+
' - Existing list note',
|
|
107
|
+
'',
|
|
108
|
+
].join('\n'));
|
|
109
|
+
|
|
110
|
+
const result = runItemCli(dir, ['add-note', 'ed-001', 'Added from CLI', '--json']);
|
|
111
|
+
const roadmap = readFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), 'utf8');
|
|
112
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
113
|
+
|
|
114
|
+
expect(result.exitCode).toBe(0);
|
|
115
|
+
expect(JSON.parse(result.stdout).item.notes).toContain('- Existing list note');
|
|
116
|
+
expect(JSON.parse(result.stdout).item.notes).toContain(`- ${today}: Added from CLI`);
|
|
117
|
+
expect(roadmap).toContain(`- ${today}: Added from CLI`);
|
|
118
|
+
});
|
|
119
|
+
|
|
52
120
|
it('sets verification status and evidence', () => {
|
|
53
121
|
const result = runItemCli(dir, [
|
|
54
122
|
'verify',
|
|
@@ -67,6 +135,79 @@ describe('item CLI', () => {
|
|
|
67
135
|
});
|
|
68
136
|
});
|
|
69
137
|
|
|
138
|
+
it('checks item evidence without mutating verification metadata', () => {
|
|
139
|
+
const result = runItemCli(dir, ['verify', 'ed-001', '--check', '--json']);
|
|
140
|
+
|
|
141
|
+
expect(result.exitCode).toBe(0);
|
|
142
|
+
const payload = JSON.parse(result.stdout);
|
|
143
|
+
expect(payload).toMatchObject({
|
|
144
|
+
ok: true,
|
|
145
|
+
item_id: 'ed-001',
|
|
146
|
+
verdict: 'likely_open',
|
|
147
|
+
});
|
|
148
|
+
expect(findRoadmap()).not.toContain('verification:');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('reports inconsistent evidence for done items missing hygiene fields', () => {
|
|
152
|
+
writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
|
|
153
|
+
'items:',
|
|
154
|
+
' - id: ed-001',
|
|
155
|
+
' title: First',
|
|
156
|
+
' layer: app',
|
|
157
|
+
' status: done',
|
|
158
|
+
' description: first item',
|
|
159
|
+
'',
|
|
160
|
+
].join('\n'));
|
|
161
|
+
|
|
162
|
+
const result = runItemCli(dir, ['verify', 'ed-001', '--check', '--json']);
|
|
163
|
+
|
|
164
|
+
expect(result.exitCode).toBe(0);
|
|
165
|
+
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
166
|
+
verdict: 'inconsistent',
|
|
167
|
+
missing: expect.arrayContaining(['verification', 'doc_update']),
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('sets doc_update from the CLI', () => {
|
|
172
|
+
const dated = runItemCli(dir, ['doc-update', 'ed-001', '--date', 'today', '--json']);
|
|
173
|
+
const skipped = runItemCli(dir, ['doc-update', 'ed-001', '--skipped', '--reason', 'No docs changed.', '--json']);
|
|
174
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
175
|
+
|
|
176
|
+
expect(dated.exitCode).toBe(0);
|
|
177
|
+
expect(JSON.parse(dated.stdout).item.doc_update).toBe(today);
|
|
178
|
+
expect(skipped.exitCode).toBe(0);
|
|
179
|
+
expect(JSON.parse(skipped.stdout).item.doc_update).toBe('skipped — No docs changed.');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('completes an item from the CLI in one roadmap transaction', () => {
|
|
183
|
+
const result = runItemCli(dir, [
|
|
184
|
+
'complete',
|
|
185
|
+
'ed-001',
|
|
186
|
+
'--evidence',
|
|
187
|
+
'npm test -- --run',
|
|
188
|
+
'--doc-update',
|
|
189
|
+
'today',
|
|
190
|
+
'--commit',
|
|
191
|
+
'abc123',
|
|
192
|
+
'--json',
|
|
193
|
+
]);
|
|
194
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
195
|
+
|
|
196
|
+
expect(result.exitCode).toBe(0);
|
|
197
|
+
const payload = JSON.parse(result.stdout);
|
|
198
|
+
expect(payload.item).toMatchObject({
|
|
199
|
+
status: 'done',
|
|
200
|
+
doc_update: today,
|
|
201
|
+
links: ['commit:abc123'],
|
|
202
|
+
verification: {
|
|
203
|
+
status: 'verified',
|
|
204
|
+
evidence: ['npm test -- --run'],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
expect(payload.item.notes).toContain(`- ${today}: Completed with evidence: npm test -- --run`);
|
|
208
|
+
expect(payload.validation).toHaveProperty('errors');
|
|
209
|
+
});
|
|
210
|
+
|
|
70
211
|
it('returns a structured error for missing items', () => {
|
|
71
212
|
const result = runItemCli(dir, ['show', 'ed-999', '--json']);
|
|
72
213
|
|
|
@@ -76,4 +217,18 @@ describe('item CLI', () => {
|
|
|
76
217
|
error: 'not_found',
|
|
77
218
|
});
|
|
78
219
|
});
|
|
220
|
+
|
|
221
|
+
it('returns misuse exit code for unknown item commands', () => {
|
|
222
|
+
const result = runItemCli(dir, ['unknown', 'ed-001', '--json']);
|
|
223
|
+
|
|
224
|
+
expect(result.exitCode).toBe(2);
|
|
225
|
+
expect(JSON.parse(result.stderr)).toMatchObject({
|
|
226
|
+
ok: false,
|
|
227
|
+
error: 'invalid_args',
|
|
228
|
+
});
|
|
229
|
+
});
|
|
79
230
|
});
|
|
231
|
+
|
|
232
|
+
function findRoadmap(): string {
|
|
233
|
+
return readFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), 'utf8');
|
|
234
|
+
}
|
package/scripts/item.ts
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addItemNote,
|
|
3
|
+
completeItem,
|
|
3
4
|
findItem,
|
|
4
5
|
linkItemCommit,
|
|
6
|
+
setItemDocUpdate,
|
|
5
7
|
setItemStatus,
|
|
6
8
|
setItemVerification,
|
|
7
9
|
} from '../src/api/lib/roadmapStore';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readOption(args: string[], flag: string): string | undefined {
|
|
15
|
-
const i = args.indexOf(flag);
|
|
16
|
-
if (i === -1) return undefined;
|
|
17
|
-
return args[i + 1];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function readRepeated(args: string[], flag: string): string[] {
|
|
21
|
-
const out: string[] = [];
|
|
22
|
-
for (let i = 0; i < args.length; i++) {
|
|
23
|
-
if (args[i] === flag && args[i + 1]) out.push(args[i + 1]);
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
10
|
+
import { summarizeItem } from '../src/api/lib/itemProjection';
|
|
11
|
+
import { checkItemEvidence } from '../src/api/lib/itemEvidence';
|
|
12
|
+
import { hasFlag, positionalArgs, readOption, readRepeated } from './cliArgs';
|
|
13
|
+
import { handleCliError, isMain, misuse, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
|
|
27
14
|
|
|
28
15
|
export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
29
16
|
const json = hasFlag(args, '--json');
|
|
30
|
-
const [command, itemId, value] = args
|
|
31
|
-
if (!command || !itemId) return
|
|
17
|
+
const [command, itemId, value] = positionalArgs(args);
|
|
18
|
+
if (!command || !itemId) return misuse('Usage: tenbo item <show|set-status|add-note|verify|link-commit> <item-id>', json);
|
|
32
19
|
|
|
33
20
|
try {
|
|
34
21
|
if (command === 'show') {
|
|
@@ -36,21 +23,77 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
36
23
|
return serialize({ ok: true, scope: located.scopeId, item: located.item }, json, `${itemId}: ${located.item.title} (${located.item.status})\n`);
|
|
37
24
|
}
|
|
38
25
|
|
|
26
|
+
if (command === 'brief') {
|
|
27
|
+
const located = findItem(repoRoot, itemId);
|
|
28
|
+
const item = located.item;
|
|
29
|
+
return serialize({
|
|
30
|
+
ok: true,
|
|
31
|
+
scope: located.scopeId,
|
|
32
|
+
item: summarizeItem(located),
|
|
33
|
+
files_to_read: item.files_to_read ?? [],
|
|
34
|
+
acceptance_criteria: item.done_when ?? [],
|
|
35
|
+
risks: item.risks ?? [],
|
|
36
|
+
verification_commands: [
|
|
37
|
+
`tenbo-dashboard item verify ${item.id} --check --json`,
|
|
38
|
+
],
|
|
39
|
+
completion_commands: [
|
|
40
|
+
`tenbo-dashboard item complete ${item.id} --evidence "<evidence>" --doc-update today --json`,
|
|
41
|
+
],
|
|
42
|
+
}, json, `${item.id}: ${item.title} (${item.status})\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (command === 'handoff') {
|
|
46
|
+
const located = findItem(repoRoot, itemId);
|
|
47
|
+
const item = located.item;
|
|
48
|
+
const files = item.files_to_read?.length ? item.files_to_read.map((file) => `- ${file}`).join('\n') : '- Read the linked item/spec first.';
|
|
49
|
+
const criteria = item.done_when?.length ? item.done_when.map((entry) => `- ${entry}`).join('\n') : '- Satisfy the roadmap item description.';
|
|
50
|
+
const risks = item.risks?.length ? item.risks.map((entry) => `- ${entry}`).join('\n') : '- No known overlap risks recorded.';
|
|
51
|
+
const prompt = [
|
|
52
|
+
`Implement ${item.id}: ${item.title}`,
|
|
53
|
+
'',
|
|
54
|
+
'Files to read',
|
|
55
|
+
files,
|
|
56
|
+
'',
|
|
57
|
+
'Non-goals',
|
|
58
|
+
'- Do not broaden scope beyond this roadmap item.',
|
|
59
|
+
'- Do not edit unrelated roadmap items or user changes.',
|
|
60
|
+
'',
|
|
61
|
+
'Overlap risks',
|
|
62
|
+
risks,
|
|
63
|
+
'',
|
|
64
|
+
'Acceptance criteria',
|
|
65
|
+
criteria,
|
|
66
|
+
'',
|
|
67
|
+
'Verification commands',
|
|
68
|
+
`- tenbo-dashboard item verify ${item.id} --check --json`,
|
|
69
|
+
'- Run focused tests for touched code.',
|
|
70
|
+
'',
|
|
71
|
+
'Completion rules',
|
|
72
|
+
`- Use tenbo-dashboard item complete ${item.id} --evidence "<evidence>" --doc-update today --json when the item is actually complete.`,
|
|
73
|
+
'- Report tests run and any residual risks.',
|
|
74
|
+
].join('\n');
|
|
75
|
+
return serialize({ ok: true, item_id: item.id, scope: located.scopeId, prompt }, json, `${prompt}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
if (command === 'set-status') {
|
|
40
|
-
if (!value) return
|
|
79
|
+
if (!value) return misuse('Usage: tenbo item set-status <item-id> <status>', json);
|
|
41
80
|
const result = setItemStatus(repoRoot, itemId, value);
|
|
42
81
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} status: ${result.item.status}\n`);
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
if (command === 'add-note') {
|
|
46
|
-
if (!value) return
|
|
85
|
+
if (!value) return misuse('Usage: tenbo item add-note <item-id> <note>', json);
|
|
47
86
|
const result = addItemNote(repoRoot, itemId, value);
|
|
48
87
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} note added\n`);
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
if (command === 'verify') {
|
|
91
|
+
if (hasFlag(args, '--check')) {
|
|
92
|
+
const report = checkItemEvidence(repoRoot, itemId);
|
|
93
|
+
return serialize(report, json, `${itemId} evidence: ${report.verdict}\n`);
|
|
94
|
+
}
|
|
52
95
|
const status = readOption(args, '--status');
|
|
53
|
-
if (!status) return
|
|
96
|
+
if (!status) return misuse('Usage: tenbo item verify <item-id> --status <status> [--evidence <text>] [--note <text>]', json);
|
|
54
97
|
const result = setItemVerification(repoRoot, itemId, {
|
|
55
98
|
status,
|
|
56
99
|
evidence: readRepeated(args, '--evidence'),
|
|
@@ -59,13 +102,42 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
59
102
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} verification: ${result.item.verification?.status}\n`);
|
|
60
103
|
}
|
|
61
104
|
|
|
105
|
+
if (command === 'doc-update') {
|
|
106
|
+
const date = readOption(args, '--date');
|
|
107
|
+
const skipped = hasFlag(args, '--skipped');
|
|
108
|
+
const reason = readOption(args, '--reason');
|
|
109
|
+
if (!date && !skipped) return misuse('Usage: tenbo item doc-update <item-id> --date <today|YYYY-MM-DD> OR --skipped --reason "<text>"', json);
|
|
110
|
+
if (skipped && !reason) return misuse('Usage: tenbo item doc-update <item-id> --skipped --reason "<text>"', json);
|
|
111
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
112
|
+
const docUpdate = skipped ? `skipped — ${reason}` : date === 'today' ? today : date;
|
|
113
|
+
const result = setItemDocUpdate(repoRoot, itemId, docUpdate ?? today);
|
|
114
|
+
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} doc_update: ${result.item.doc_update}\n`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (command === 'complete') {
|
|
118
|
+
const evidence = readRepeated(args, '--evidence');
|
|
119
|
+
const docUpdateArg = readOption(args, '--doc-update');
|
|
120
|
+
const commit = readOption(args, '--commit');
|
|
121
|
+
const verificationStatus = readOption(args, '--verification');
|
|
122
|
+
if (evidence.length === 0) return misuse('Usage: tenbo item complete <item-id> --evidence "<text>" [--doc-update today|YYYY-MM-DD] [--commit <sha>] [--verification <status>]', json);
|
|
123
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
124
|
+
const docUpdate = docUpdateArg === 'today' ? today : docUpdateArg;
|
|
125
|
+
const result = completeItem(repoRoot, itemId, {
|
|
126
|
+
evidence,
|
|
127
|
+
...(docUpdate ? { docUpdate } : {}),
|
|
128
|
+
...(commit ? { commit } : {}),
|
|
129
|
+
...(verificationStatus ? { verificationStatus } : {}),
|
|
130
|
+
});
|
|
131
|
+
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} completed\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
62
134
|
if (command === 'link-commit') {
|
|
63
|
-
if (!value) return
|
|
135
|
+
if (!value) return misuse('Usage: tenbo item link-commit <item-id> <sha>', json);
|
|
64
136
|
const result = linkItemCommit(repoRoot, itemId, value);
|
|
65
137
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} linked commit: ${value}\n`);
|
|
66
138
|
}
|
|
67
139
|
|
|
68
|
-
return
|
|
140
|
+
return misuse(`unknown item command: ${command}`, json);
|
|
69
141
|
} catch (err) {
|
|
70
142
|
return handleCliError(err, json);
|
|
71
143
|
}
|