@opensassi/opencode 0.1.3 → 0.1.5

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 (39) hide show
  1. package/AGENTS.md +3 -2
  2. package/dashboard/dashboard.e2e.test.ts +247 -0
  3. package/dashboard/dist/index.d.ts +9 -0
  4. package/dashboard/dist/index.js +36 -0
  5. package/dashboard/dist/routes/api.d.ts +2 -0
  6. package/dashboard/dist/routes/api.js +215 -0
  7. package/dashboard/dist/services/cache.d.ts +13 -0
  8. package/dashboard/dist/services/cache.js +29 -0
  9. package/dashboard/dist/services/experiments.d.ts +11 -0
  10. package/dashboard/dist/services/experiments.js +108 -0
  11. package/dashboard/dist/services/git.d.ts +12 -0
  12. package/dashboard/dist/services/git.js +149 -0
  13. package/dashboard/dist/services/sessions.d.ts +25 -0
  14. package/dashboard/dist/services/sessions.js +208 -0
  15. package/dashboard/dist/services/specs.d.ts +9 -0
  16. package/dashboard/dist/services/specs.js +102 -0
  17. package/dashboard/dist/types.d.ts +173 -0
  18. package/dashboard/dist/types.js +1 -0
  19. package/dashboard/opencode.e2e.test.ts +100 -0
  20. package/dashboard/playwright.config.ts +11 -0
  21. package/dashboard/public/app.js +961 -0
  22. package/dashboard/public/index.html +29 -0
  23. package/dashboard/public/style.css +231 -0
  24. package/dashboard/src/index.ts +53 -0
  25. package/dashboard/src/routes/api.ts +235 -0
  26. package/dashboard/src/services/cache.ts +38 -0
  27. package/dashboard/src/services/experiments.ts +117 -0
  28. package/dashboard/src/services/git.ts +139 -0
  29. package/dashboard/src/services/sessions.ts +216 -0
  30. package/dashboard/src/services/specs.ts +95 -0
  31. package/dashboard/src/types.ts +168 -0
  32. package/dashboard/technical-specification.md +414 -0
  33. package/dashboard/test-api.sh +127 -0
  34. package/dashboard/tsconfig.json +16 -0
  35. package/lib/util/paths.js +9 -1
  36. package/package.json +10 -1
  37. package/scripts/dashboard.js +17 -0
  38. package/scripts/generate-daily-summaries.js +190 -0
  39. package/skills/opensassi/SKILL.md +8 -5
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { join, resolve, basename } from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ const SESSIONS_DIR = resolve(process.cwd(), 'sessions');
7
+
8
+ function parseMdField(content, label) {
9
+ const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+?)(?:\\n\\n|\\n(?=\\*\\*)|$)`, 's');
10
+ const m = content.match(re);
11
+ return m ? m[1].trim() : null;
12
+ }
13
+
14
+ function parseDurationMinutes(content) {
15
+ // Try **Duration:** HH<something>
16
+ const durMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*min/);
17
+ if (durMatch) return parseInt(durMatch[1], 10);
18
+
19
+ // Fallback: from .json.bz2 (handled below)
20
+ return 0;
21
+ }
22
+
23
+ function parsePrompterMinutes(content) {
24
+ const totalMatch = content.match(/\*\*Total:\*\*\s*([\d.]+)\s*hours?/);
25
+ if (totalMatch) return Math.round(parseFloat(totalMatch[1]) * 60);
26
+ // also try: prompter active ≈ X hours
27
+ const activeMatch = content.match(/prompter active\s*[≈~]?\s*([\d.]+)\s*hours?/);
28
+ if (activeMatch) return Math.round(parseFloat(activeMatch[1]) * 60);
29
+ return 30;
30
+ }
31
+
32
+ function parseSmeHours(content) {
33
+ const smeMatch = content.match(/\*\*Model-Equivalent SME Time Estimate:\*\*\s*(?:~|≈)?\s*([\d.]+)\s*hours?/);
34
+ if (smeMatch) return Math.round(parseFloat(smeMatch[1]) * 60);
35
+ return 60;
36
+ }
37
+
38
+ function parseTags(content) {
39
+ const tagMatch = content.match(/\*\*Aggregation Tags:\*\*\s*(.+?)(?:\\n\\n|$)/s);
40
+ if (tagMatch) {
41
+ return tagMatch[1].split(',').map(t => t.trim()).filter(Boolean);
42
+ }
43
+ return [];
44
+ }
45
+
46
+ function parseSessionId(content) {
47
+ return parseMdField(content, 'Session ID') || '';
48
+ }
49
+
50
+ function parseDescription(content) {
51
+ return parseMdField(content, 'Top-Level Component') || '';
52
+ }
53
+
54
+ function parseConfidence(/*content*/) {
55
+ return 'medium';
56
+ }
57
+
58
+ function extractDate(sessionId, filename) {
59
+ if (sessionId) {
60
+ const m = sessionId.match(/^(\d{4}-\d{2}-\d{2})/);
61
+ if (m) return m[1];
62
+ }
63
+ const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
64
+ return m ? m[1] : null;
65
+ }
66
+
67
+ function getBz2Duration(sessionDir, sessionId) {
68
+ if (!sessionId) return null;
69
+ const files = readdirSync(sessionDir);
70
+ const match = files.find(f => f.startsWith(sessionId) && f.endsWith('.json.bz2'));
71
+ if (!match) return null;
72
+ try {
73
+ const buf = execFileSync('bzcat', [join(sessionDir, match)], {
74
+ encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024, stdio: ['pipe', 'pipe', 'ignore'],
75
+ });
76
+ const d = JSON.parse(buf);
77
+ const info = d.info || {};
78
+ const t = info.time || {};
79
+ const created = t.created || 0;
80
+ const updated = t.updated || 0;
81
+ if (updated && created) return Math.round((updated - created) / 60000);
82
+ return null;
83
+ } catch { return null; }
84
+ }
85
+
86
+ async function main() {
87
+ if (!existsSync(SESSIONS_DIR)) {
88
+ console.error(`Sessions directory not found: ${SESSIONS_DIR}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ const files = readdirSync(SESSIONS_DIR);
93
+ const mdFiles = files.filter(f => f.endsWith('.md') && !f.endsWith('.spec.md') && f !== 'README.md' && f !== 'export-session.sh');
94
+
95
+ // Group sessions by date
96
+ const byDate = {};
97
+
98
+ for (const mdFile of mdFiles) {
99
+ const content = readFileSync(join(SESSIONS_DIR, mdFile), 'utf-8');
100
+ const sessionId = parseSessionId(content);
101
+ const date = extractDate(sessionId, mdFile);
102
+ if (!date) { console.error(`Could not extract date from ${mdFile}`); continue; }
103
+
104
+ const prompterMin = parsePrompterMinutes(content);
105
+ const smeMin = parseSmeHours(content);
106
+ const bz2Duration = getBz2Duration(SESSIONS_DIR, sessionId);
107
+ const durationMin = bz2Duration || prompterMin || parseDurationMinutes(content);
108
+ const tags = parseTags(content);
109
+ const description = parseDescription(content);
110
+ const confidence = parseConfidence(content);
111
+
112
+ if (!byDate[date]) byDate[date] = [];
113
+ byDate[date].push({
114
+ session_id: sessionId || mdFile.replace(/\.md$/, ''),
115
+ duration_minutes: durationMin,
116
+ prompter_time_minutes: Math.min(prompterMin, durationMin || prompterMin),
117
+ sme_time_minutes: smeMin,
118
+ top_component_summary: description,
119
+ tags,
120
+ human_confidence: confidence,
121
+ });
122
+ }
123
+
124
+ // Generate daily files
125
+ const dailyDir = join(SESSIONS_DIR, 'daily');
126
+ if (!existsSync(dailyDir)) {
127
+ mkdirSync(dailyDir, { recursive: true });
128
+ }
129
+
130
+ for (const [date, sessions] of Object.entries(byDate).sort()) {
131
+ let totalPrompter = 0, totalSme = 0;
132
+ for (const s of sessions) {
133
+ totalPrompter += s.prompter_time_minutes;
134
+ totalSme += s.sme_time_minutes;
135
+ }
136
+ const totalPrompterHrs = Math.round((totalPrompter / 60) * 10) / 10;
137
+ const totalSmeHrs = Math.round((totalSme / 60) * 10) / 10;
138
+
139
+ // Compute per-tag aggregates
140
+ const tagMap = {};
141
+ for (const s of sessions) {
142
+ const perTagSme = s.sme_time_minutes / (s.tags.length || 1);
143
+ const perTagPrompter = s.prompter_time_minutes / (s.tags.length || 1);
144
+ for (const tag of s.tags) {
145
+ if (!tagMap[tag]) tagMap[tag] = { prompter: 0, sme: 0 };
146
+ tagMap[tag].prompter += perTagPrompter;
147
+ tagMap[tag].sme += perTagSme;
148
+ }
149
+ }
150
+ const topSubjectAreas = Object.entries(tagMap)
151
+ .map(([name, v]) => ({
152
+ name,
153
+ prompter_time_hours: Math.round((v.prompter / 60) * 100) / 100,
154
+ sme_time_hours: Math.round((v.sme / 60) * 100) / 100,
155
+ ai_multiplier: v.prompter > 0 ? Math.round((v.sme / v.prompter) * 10) / 10 : 0,
156
+ }))
157
+ .sort((a, b) => b.sme_time_hours - a.sme_time_hours);
158
+
159
+ const aiMultiplier = totalPrompterHrs > 0
160
+ ? Math.round((totalSmeHrs / totalPrompterHrs) * 10) / 10
161
+ : 0;
162
+
163
+ const daily = {
164
+ dashboard: {
165
+ metadata: {
166
+ generated_at: new Date().toISOString(),
167
+ audited: false,
168
+ audit_note: 'Auto-generated from session evaluation files',
169
+ },
170
+ daily_summary: {
171
+ date,
172
+ total_prompter_time_hours: totalPrompterHrs,
173
+ total_sme_time_hours: totalSmeHrs,
174
+ ai_multiplier: aiMultiplier,
175
+ total_sessions: sessions.length,
176
+ top_subject_areas: topSubjectAreas,
177
+ },
178
+ session_breakdown: sessions,
179
+ },
180
+ };
181
+
182
+ const outPath = join(dailyDir, `${date}.json`);
183
+ writeFileSync(outPath, JSON.stringify(daily, null, 2) + '\n');
184
+ console.log(`Wrote ${outPath} (${sessions.length} sessions, ${totalPrompterHrs}h prompter, ${totalSmeHrs}h SME)`);
185
+ }
186
+
187
+ console.log(`\nDone. Generated ${Object.keys(byDate).length} daily summaries.`);
188
+ }
189
+
190
+ main().catch(e => { console.error(e); process.exit(1); });
@@ -5,13 +5,16 @@ description: Root skill ecosystem — loads system-design + spec tree, routes su
5
5
 
6
6
  # Skill: opensassi — Root Skill Ecosystem
7
7
 
8
+ > **Invocation note:** Within this project, use `npm run opencode -- <cmd>`.
9
+ > Consumers of the published `@opensassi/opencode` package use `npx @opensassi/opencode <cmd>` instead.
10
+
8
11
  ## Entry Point
9
12
 
10
13
  | Input | Action |
11
14
  |-------|--------|
12
15
  | `/opensassi` | Load `skill system-design`, read `technical-specification.md` + spec tree depth 2 (root + facade specs). Report ready. |
13
16
  | `/opensassi init` | Run `env-check.sh`. Parse JSON result: if node+git+FlameGraph+deps all present → "Already bootstrapped". Otherwise run full bootstrap sequence (env-check → install → flamegraph → npm-deps → gitignore). |
14
- | `/opensassi <skill> <command> [args]` | Load `<skill>` from npm via `npx @opensassi/opencode <skill>`, then run `<command>` with `[args]`. Return result. |
17
+ | `/opensassi <skill> <command> [args]` | Load `<skill>` from npm via `npm run opencode -- <skill>`, then run `<command>` with `[args]`. Return result. |
15
18
 
16
19
  ### Spec tree depth
17
20
 
@@ -23,7 +26,7 @@ Depth is controlled by `--depth` flag on `load spec`:
23
26
 
24
27
  ## Init
25
28
 
26
- Single shell command: `npx @opensassi/opencode run --skill opensassi env-check.sh`
29
+ Single shell command: `npm run opencode -- run --skill opensassi env-check.sh`
27
30
 
28
31
  Returns JSON: `{"os": ..., "distro": ..., "node_version": ..., "git_version": ..., ...}`
29
32
 
@@ -37,11 +40,11 @@ bootstrapped = (node_version != "" && git_version != ""
37
40
  If bootstrapped → report "Environment ready." + show node/git versions.
38
41
  If not → run full bootstrap:
39
42
 
40
- 1. `npx @opensassi/opencode run --skill opensassi env-check.sh` — install git + Node.js LTS if missing, write `.nvmrc`
43
+ 1. `npm run opencode -- run --skill opensassi env-check.sh` — install git + Node.js LTS if missing, write `.nvmrc`
41
44
  2. `init install` — run platform-specific installer (cmake, nasm, gdb, ripgrep, perf, htop, etc.) or report none found
42
45
  3. `init flamegraph` — clone FlameGraph v1.0 to `scripts/FlameGraph/`
43
- 4. `npx @opensassi/opencode run --skill opensassi install-npm-deps.sh` — `npm install`
44
- 5. `npx @opensassi/opencode run --skill opensassi ensure-gitignore.sh` — append common patterns
46
+ 4. `npm run opencode -- run --skill opensassi install-npm-deps.sh` — `npm install`
47
+ 5. `npm run opencode -- run --skill opensassi ensure-gitignore.sh` — append common patterns
45
48
 
46
49
  ## Lexicon
47
50