tenbo-dashboard 0.2.0 → 0.3.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/README.md CHANGED
@@ -47,9 +47,9 @@ npx tenbo-dashboard --version # Print the installed version
47
47
 
48
48
  ## What is tenbo?
49
49
 
50
- A Claude Code skill that gives your AI coding assistant persistent project memory — architecture docs, roadmaps, and health signals that survive across sessions. The dashboard is the optional visual companion; the skill is the always-on conversational brain.
50
+ 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.
51
51
 
52
- Install the skill: [github.com/poyi/tenbo](https://github.com/poyi/tenbo)
52
+ Install: [github.com/poyi/tenbo](https://github.com/poyi/tenbo) (instructions for both Claude Code and Cursor)
53
53
 
54
54
  ## License
55
55
 
@@ -38,6 +38,7 @@ const commands = {
38
38
  'next-id': 'scripts/next-id.ts',
39
39
  metrics: 'scripts/compute-metrics.ts',
40
40
  'init-check': 'scripts/init-check.ts',
41
+ sync: 'scripts/sync.ts',
41
42
  };
42
43
 
43
44
  function run(script, scriptArgs) {
@@ -65,6 +66,7 @@ tenbo-dashboard — local architecture dashboard for .tenbo/ repos
65
66
 
66
67
  Usage:
67
68
  tenbo-dashboard Launch the dashboard (http://localhost:5174)
69
+ tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
68
70
  tenbo-dashboard validate Run validation rules
69
71
  tenbo-dashboard init-check Strict completeness check for fresh init (errors on missing skeletons, file_count:0, etc)
70
72
  tenbo-dashboard next-id <prefix> Allocate next roadmap item ID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tenbo-dashboard",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,144 @@
1
+ /**
2
+ * sync.ts — single command for "make tenbo state fresh after a change".
3
+ *
4
+ * Runs metrics --all (or --scope <id>) + init-check + validate, then prints
5
+ * a unified summary of NEW findings (severity >= warning) introduced since
6
+ * the prior metrics.json snapshot. Replaces three separate commands the
7
+ * skill used to invoke at init and completion time, eliminating the
8
+ * silent-staleness failure mode where an agent forgot one of the three.
9
+ *
10
+ * Exit codes:
11
+ * 0 — everything refreshed cleanly, no new errors
12
+ * 1 — an error in any step (validation failure, init defect, metrics throw)
13
+ * 2 — could not locate repo root
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { findRepoRoot } from '../src/api/lib/repoRoot';
18
+ import { runComputeMetrics } from './compute-metrics';
19
+ import { runInitCheck } from './init-check';
20
+ import { readState } from '../src/api/lib/tenboFs';
21
+ import { validate } from '../src/api/lib/validator';
22
+ import type { ScopeMetrics } from '../src/types';
23
+
24
+ interface NewFinding {
25
+ scope: string;
26
+ layer: string;
27
+ severity: string;
28
+ signal: string;
29
+ headline: string;
30
+ }
31
+
32
+ function readMetrics(metricsPath: string): ScopeMetrics | null {
33
+ try {
34
+ if (!fs.existsSync(metricsPath)) return null;
35
+ return JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function findingKey(scope: string, f: { signal: string; layer: string; headline: string }): string {
42
+ return `${scope}::${f.signal}::${f.layer}::${f.headline}`;
43
+ }
44
+
45
+ interface SyncArgs {
46
+ repoRoot: string;
47
+ scope?: string; // single-scope refresh; omit to refresh all
48
+ }
49
+
50
+ export async function runSync(args: SyncArgs): Promise<number> {
51
+ const { repoRoot, scope } = args;
52
+
53
+ // Snapshot prior findings so we can diff after the refresh.
54
+ const state = readState(repoRoot);
55
+ const targetScopes = scope ? [scope] : state.scopes.map((s) => s.id);
56
+ const priorFindingKeys = new Set<string>();
57
+ for (const id of targetScopes) {
58
+ const m = readMetrics(path.join(repoRoot, '.tenbo', 'scopes', id, 'metrics.json'));
59
+ for (const f of m?.findings ?? []) priorFindingKeys.add(findingKey(id, f));
60
+ }
61
+
62
+ // 1. metrics
63
+ const metricsCode = await runComputeMetrics({
64
+ repoRoot,
65
+ all: !scope,
66
+ scope,
67
+ });
68
+ if (metricsCode !== 0) {
69
+ process.stderr.write('sync: metrics step failed\n');
70
+ return 1;
71
+ }
72
+
73
+ // 2. init-check
74
+ const { defects } = runInitCheck(repoRoot);
75
+ if (defects.length > 0) {
76
+ process.stderr.write(`sync: init-check FAILED — ${defects.length} init defect(s):\n`);
77
+ for (const d of defects) {
78
+ const loc = d.scope ? ` [${d.scope}${d.layerId ? `/${d.layerId}` : ''}]` : '';
79
+ process.stderr.write(` ❌${loc} ${d.message}\n`);
80
+ }
81
+ return 1;
82
+ }
83
+
84
+ // 3. validate (re-read state after metrics may have changed scope dirs)
85
+ const stateAfter = readState(repoRoot);
86
+ const result = validate(stateAfter);
87
+ if (result.errors.length > 0) {
88
+ process.stderr.write(`sync: validate FAILED — ${result.errors.length} error(s):\n`);
89
+ for (const e of result.errors) process.stderr.write(` ❌ ${e.message}\n`);
90
+ return 1;
91
+ }
92
+
93
+ // 4. Diff findings: surface NEW critical/warning findings introduced this run.
94
+ const newFindings: NewFinding[] = [];
95
+ for (const id of targetScopes) {
96
+ const m = readMetrics(path.join(repoRoot, '.tenbo', 'scopes', id, 'metrics.json'));
97
+ for (const f of m?.findings ?? []) {
98
+ if (f.severity === 'info') continue;
99
+ if (priorFindingKeys.has(findingKey(id, f))) continue;
100
+ newFindings.push({
101
+ scope: id,
102
+ layer: f.layer,
103
+ severity: f.severity,
104
+ signal: f.signal,
105
+ headline: f.headline,
106
+ });
107
+ }
108
+ }
109
+
110
+ // 5. Summary line + (optional) new-finding lines.
111
+ const scopeLabel = scope ? `scope "${scope}"` : `${targetScopes.length} scope(s)`;
112
+ if (newFindings.length === 0) {
113
+ process.stdout.write(`sync: ${scopeLabel} fresh. No new errors, no new warning/critical findings.\n`);
114
+ return 0;
115
+ }
116
+ process.stdout.write(`sync: ${scopeLabel} fresh. ${newFindings.length} new finding(s) since last refresh:\n`);
117
+ for (const f of newFindings) {
118
+ const icon = f.severity === 'critical' ? '🔴' : '⚠️';
119
+ process.stdout.write(` ${icon} [${f.scope}/${f.layer}] ${f.signal}: ${f.headline}\n`);
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ function isMain(): boolean {
125
+ try {
126
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : '';
127
+ const here = path.resolve(new URL(import.meta.url).pathname);
128
+ return invoked === here;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ if (isMain()) {
135
+ const args = process.argv.slice(2);
136
+ const scopeIdx = args.indexOf('--scope');
137
+ const scope = scopeIdx >= 0 ? args[scopeIdx + 1] : undefined;
138
+ const repoRoot = findRepoRoot(process.cwd());
139
+ if (!repoRoot) {
140
+ process.stderr.write('sync: unable to find repo root (no .git in any parent directory)\n');
141
+ process.exit(2);
142
+ }
143
+ runSync({ repoRoot, scope }).then((code) => process.exit(code));
144
+ }