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.
@@ -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>] [--json]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tenbo-dashboard",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
+ }
@@ -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: 1,
19
+ exitCode,
20
20
  };
21
21
  }
22
- return { stdout: '', stderr: `${message}\n`, exitCode: 1 };
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
+ }
@@ -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(1);
71
+ expect(result.exitCode).toBe(2);
72
72
  expect(JSON.parse(result.stderr)).toMatchObject({
73
73
  ok: false,
74
74
  error: 'invalid_args',
@@ -1,15 +1,6 @@
1
1
  import { resolveFeatureContext } from '../src/api/lib/contextResolver';
2
- import { fail, handleCliError, isMain, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
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 fail('invalid_args', 'Usage: tenbo context feature --query "<request>" [--json]', json);
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 fail('invalid_args', 'Usage: tenbo context feature --query "<request>" [--json]', json);
21
+ return misuse('Usage: tenbo context feature --query "<request>" [--json]', json);
31
22
  } catch (err) {
32
23
  return handleCliError(err, json);
33
24
  }
@@ -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 { fail, handleCliError, isMain, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
9
-
10
- function hasFlag(args: string[], flag: string): boolean {
11
- return args.includes(flag);
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.filter((arg) => arg !== '--json');
31
- if (!command || !itemId) return fail('invalid_args', 'Usage: tenbo item <show|set-status|add-note|verify|link-commit> <item-id>', json);
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 fail('invalid_args', 'Usage: tenbo item set-status <item-id> <status>', json);
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 fail('invalid_args', 'Usage: tenbo item add-note <item-id> <note>', json);
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 fail('invalid_args', 'Usage: tenbo item verify <item-id> --status <status> [--evidence <text>] [--note <text>]', json);
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 fail('invalid_args', 'Usage: tenbo item link-commit <item-id> <sha>', json);
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 fail('invalid_args', `unknown item command: ${command}`, json);
140
+ return misuse(`unknown item command: ${command}`, json);
69
141
  } catch (err) {
70
142
  return handleCliError(err, json);
71
143
  }