@jhizzard/termdeck 0.4.0 → 0.4.2
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 +4 -2
- package/package.json +1 -1
- package/packages/cli/src/forge.js +262 -0
- package/packages/cli/src/index.js +24 -0
- package/packages/client/public/app.js +204 -6
- package/packages/client/public/style.css +298 -2
- package/packages/server/src/forge-prompt.js +265 -0
- package/packages/server/src/index.js +115 -2
- package/packages/server/src/skill-installer.js +166 -0
|
@@ -315,10 +315,10 @@
|
|
|
315
315
|
/* Orchestrator: 1 large left panel (60%), remaining stack on the right (40%). */
|
|
316
316
|
.grid-container.layout-orch {
|
|
317
317
|
grid-template-columns: 3fr 2fr;
|
|
318
|
-
grid-
|
|
318
|
+
grid-template-rows: repeat(4, 1fr);
|
|
319
319
|
}
|
|
320
320
|
.grid-container.layout-orch .term-panel:first-child {
|
|
321
|
-
grid-row: 1 /
|
|
321
|
+
grid-row: 1 / span 4;
|
|
322
322
|
grid-column: 1;
|
|
323
323
|
}
|
|
324
324
|
|
|
@@ -1977,6 +1977,302 @@
|
|
|
1977
1977
|
border-color: var(--tg-border-active);
|
|
1978
1978
|
}
|
|
1979
1979
|
|
|
1980
|
+
/* ===== Setup wizard modal (Sprint 19 T2) =====
|
|
1981
|
+
Full-screen overlay with a 4-tier vertical progress rail showing
|
|
1982
|
+
configuration status. Visual language is aligned with .rumen-modal
|
|
1983
|
+
and .add-project-modal so the dashboard feels consistent. */
|
|
1984
|
+
.setup-modal {
|
|
1985
|
+
display: none;
|
|
1986
|
+
position: fixed;
|
|
1987
|
+
inset: 0;
|
|
1988
|
+
z-index: 3200;
|
|
1989
|
+
align-items: center;
|
|
1990
|
+
justify-content: center;
|
|
1991
|
+
}
|
|
1992
|
+
.setup-modal.open { display: flex; }
|
|
1993
|
+
.setup-backdrop {
|
|
1994
|
+
position: absolute;
|
|
1995
|
+
inset: 0;
|
|
1996
|
+
background: rgba(0, 0, 0, 0.72);
|
|
1997
|
+
}
|
|
1998
|
+
.setup-card {
|
|
1999
|
+
position: relative;
|
|
2000
|
+
background: var(--tg-surface);
|
|
2001
|
+
border: 1px solid var(--tg-accent-dim);
|
|
2002
|
+
border-radius: 10px;
|
|
2003
|
+
width: 720px;
|
|
2004
|
+
max-width: calc(100vw - 40px);
|
|
2005
|
+
max-height: calc(100vh - 80px);
|
|
2006
|
+
display: flex;
|
|
2007
|
+
flex-direction: column;
|
|
2008
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
|
|
2009
|
+
font-family: var(--tg-sans);
|
|
2010
|
+
color: var(--tg-text);
|
|
2011
|
+
}
|
|
2012
|
+
.setup-header {
|
|
2013
|
+
display: flex;
|
|
2014
|
+
align-items: flex-start;
|
|
2015
|
+
justify-content: space-between;
|
|
2016
|
+
gap: 12px;
|
|
2017
|
+
padding: 20px 22px 12px;
|
|
2018
|
+
border-bottom: 1px solid var(--tg-border);
|
|
2019
|
+
}
|
|
2020
|
+
.setup-header h3 {
|
|
2021
|
+
margin: 0 0 4px;
|
|
2022
|
+
font-size: 16px;
|
|
2023
|
+
color: var(--tg-accent);
|
|
2024
|
+
}
|
|
2025
|
+
.setup-subtitle {
|
|
2026
|
+
margin: 0;
|
|
2027
|
+
font-size: 12px;
|
|
2028
|
+
color: var(--tg-text-dim);
|
|
2029
|
+
}
|
|
2030
|
+
.setup-close {
|
|
2031
|
+
background: transparent;
|
|
2032
|
+
border: none;
|
|
2033
|
+
color: var(--tg-text-dim);
|
|
2034
|
+
font-size: 22px;
|
|
2035
|
+
line-height: 1;
|
|
2036
|
+
cursor: pointer;
|
|
2037
|
+
padding: 0 4px;
|
|
2038
|
+
font-family: var(--tg-sans);
|
|
2039
|
+
}
|
|
2040
|
+
.setup-close:hover { color: var(--tg-text-bright); }
|
|
2041
|
+
.setup-body {
|
|
2042
|
+
padding: 16px 22px;
|
|
2043
|
+
overflow-y: auto;
|
|
2044
|
+
flex: 1 1 auto;
|
|
2045
|
+
min-height: 0;
|
|
2046
|
+
}
|
|
2047
|
+
.setup-tiers {
|
|
2048
|
+
display: flex;
|
|
2049
|
+
flex-direction: column;
|
|
2050
|
+
}
|
|
2051
|
+
.setup-loading,
|
|
2052
|
+
.setup-error {
|
|
2053
|
+
padding: 20px 8px;
|
|
2054
|
+
text-align: center;
|
|
2055
|
+
color: var(--tg-text-dim);
|
|
2056
|
+
font-size: 13px;
|
|
2057
|
+
}
|
|
2058
|
+
.setup-error {
|
|
2059
|
+
color: var(--tg-red);
|
|
2060
|
+
text-align: left;
|
|
2061
|
+
}
|
|
2062
|
+
.setup-error code {
|
|
2063
|
+
background: var(--tg-bg);
|
|
2064
|
+
padding: 1px 5px;
|
|
2065
|
+
border-radius: 3px;
|
|
2066
|
+
font-family: var(--tg-mono);
|
|
2067
|
+
font-size: 11px;
|
|
2068
|
+
color: var(--tg-text);
|
|
2069
|
+
}
|
|
2070
|
+
.setup-tier {
|
|
2071
|
+
display: flex;
|
|
2072
|
+
gap: 14px;
|
|
2073
|
+
padding: 4px 0;
|
|
2074
|
+
position: relative;
|
|
2075
|
+
}
|
|
2076
|
+
.setup-tier-rail {
|
|
2077
|
+
display: flex;
|
|
2078
|
+
flex-direction: column;
|
|
2079
|
+
align-items: center;
|
|
2080
|
+
flex-shrink: 0;
|
|
2081
|
+
padding-top: 4px;
|
|
2082
|
+
width: 18px;
|
|
2083
|
+
}
|
|
2084
|
+
.setup-tier-dot {
|
|
2085
|
+
width: 14px;
|
|
2086
|
+
height: 14px;
|
|
2087
|
+
border-radius: 50%;
|
|
2088
|
+
background: var(--tg-border);
|
|
2089
|
+
border: 2px solid var(--tg-bg);
|
|
2090
|
+
box-shadow: 0 0 0 1px var(--tg-border);
|
|
2091
|
+
flex-shrink: 0;
|
|
2092
|
+
z-index: 1;
|
|
2093
|
+
}
|
|
2094
|
+
.setup-tier-line {
|
|
2095
|
+
flex: 1;
|
|
2096
|
+
width: 2px;
|
|
2097
|
+
background: var(--tg-border);
|
|
2098
|
+
margin: 2px 0;
|
|
2099
|
+
min-height: 24px;
|
|
2100
|
+
}
|
|
2101
|
+
.setup-tier-active .setup-tier-dot {
|
|
2102
|
+
background: var(--tg-green);
|
|
2103
|
+
box-shadow: 0 0 0 1px var(--tg-green), 0 0 10px rgba(158, 206, 106, 0.35);
|
|
2104
|
+
}
|
|
2105
|
+
.setup-tier-partial .setup-tier-dot {
|
|
2106
|
+
background: var(--tg-amber);
|
|
2107
|
+
box-shadow: 0 0 0 1px var(--tg-amber);
|
|
2108
|
+
}
|
|
2109
|
+
.setup-tier-not_configured .setup-tier-dot {
|
|
2110
|
+
background: var(--tg-surface-hover);
|
|
2111
|
+
box-shadow: 0 0 0 1px var(--tg-border);
|
|
2112
|
+
}
|
|
2113
|
+
.setup-tier-next .setup-tier-dot {
|
|
2114
|
+
animation: setup-pulse 1.6s ease-in-out infinite;
|
|
2115
|
+
}
|
|
2116
|
+
@keyframes setup-pulse {
|
|
2117
|
+
0%, 100% { box-shadow: 0 0 0 1px var(--tg-accent); }
|
|
2118
|
+
50% { box-shadow: 0 0 0 4px rgba(122, 162, 247, 0.18); }
|
|
2119
|
+
}
|
|
2120
|
+
.setup-tier-body {
|
|
2121
|
+
flex: 1 1 auto;
|
|
2122
|
+
min-width: 0;
|
|
2123
|
+
padding-bottom: 16px;
|
|
2124
|
+
}
|
|
2125
|
+
.setup-tier-head {
|
|
2126
|
+
display: flex;
|
|
2127
|
+
align-items: center;
|
|
2128
|
+
justify-content: space-between;
|
|
2129
|
+
gap: 8px;
|
|
2130
|
+
margin-bottom: 2px;
|
|
2131
|
+
}
|
|
2132
|
+
.setup-tier-name {
|
|
2133
|
+
font-size: 13px;
|
|
2134
|
+
font-weight: 600;
|
|
2135
|
+
color: var(--tg-text-bright);
|
|
2136
|
+
}
|
|
2137
|
+
.setup-tier-status {
|
|
2138
|
+
font-family: var(--tg-mono);
|
|
2139
|
+
font-size: 10px;
|
|
2140
|
+
text-transform: uppercase;
|
|
2141
|
+
letter-spacing: 0.6px;
|
|
2142
|
+
padding: 2px 7px;
|
|
2143
|
+
border-radius: 10px;
|
|
2144
|
+
border: 1px solid var(--tg-border);
|
|
2145
|
+
background: var(--tg-bg);
|
|
2146
|
+
color: var(--tg-text-dim);
|
|
2147
|
+
flex-shrink: 0;
|
|
2148
|
+
}
|
|
2149
|
+
.setup-tier-status-active {
|
|
2150
|
+
color: var(--tg-green);
|
|
2151
|
+
border-color: var(--tg-green);
|
|
2152
|
+
background: rgba(158, 206, 106, 0.08);
|
|
2153
|
+
}
|
|
2154
|
+
.setup-tier-status-partial {
|
|
2155
|
+
color: var(--tg-amber);
|
|
2156
|
+
border-color: var(--tg-amber);
|
|
2157
|
+
background: rgba(224, 175, 104, 0.08);
|
|
2158
|
+
}
|
|
2159
|
+
.setup-tier-status-not_configured {
|
|
2160
|
+
color: var(--tg-text-dim);
|
|
2161
|
+
border-color: var(--tg-border);
|
|
2162
|
+
}
|
|
2163
|
+
.setup-tier-desc {
|
|
2164
|
+
font-size: 12px;
|
|
2165
|
+
color: var(--tg-text-dim);
|
|
2166
|
+
margin-bottom: 4px;
|
|
2167
|
+
}
|
|
2168
|
+
.setup-tier-detail {
|
|
2169
|
+
font-family: var(--tg-mono);
|
|
2170
|
+
font-size: 11px;
|
|
2171
|
+
color: var(--tg-text);
|
|
2172
|
+
margin-bottom: 8px;
|
|
2173
|
+
}
|
|
2174
|
+
.setup-cmds {
|
|
2175
|
+
display: flex;
|
|
2176
|
+
flex-direction: column;
|
|
2177
|
+
gap: 6px;
|
|
2178
|
+
margin-top: 6px;
|
|
2179
|
+
}
|
|
2180
|
+
.setup-cmd {
|
|
2181
|
+
display: flex;
|
|
2182
|
+
align-items: center;
|
|
2183
|
+
gap: 8px;
|
|
2184
|
+
padding: 6px 10px;
|
|
2185
|
+
background: var(--tg-bg);
|
|
2186
|
+
border: 1px solid var(--tg-border);
|
|
2187
|
+
border-radius: var(--tg-radius-sm);
|
|
2188
|
+
}
|
|
2189
|
+
.setup-cmd code {
|
|
2190
|
+
flex: 1 1 auto;
|
|
2191
|
+
min-width: 0;
|
|
2192
|
+
font-family: var(--tg-mono);
|
|
2193
|
+
font-size: 12px;
|
|
2194
|
+
color: var(--tg-cyan);
|
|
2195
|
+
white-space: pre-wrap;
|
|
2196
|
+
word-break: break-word;
|
|
2197
|
+
}
|
|
2198
|
+
.setup-copy {
|
|
2199
|
+
background: transparent;
|
|
2200
|
+
border: 1px solid var(--tg-border);
|
|
2201
|
+
color: var(--tg-text-dim);
|
|
2202
|
+
font-family: var(--tg-sans);
|
|
2203
|
+
font-size: 10px;
|
|
2204
|
+
text-transform: uppercase;
|
|
2205
|
+
letter-spacing: 0.5px;
|
|
2206
|
+
padding: 3px 9px;
|
|
2207
|
+
border-radius: 3px;
|
|
2208
|
+
cursor: pointer;
|
|
2209
|
+
flex-shrink: 0;
|
|
2210
|
+
}
|
|
2211
|
+
.setup-copy:hover {
|
|
2212
|
+
color: var(--tg-text);
|
|
2213
|
+
border-color: var(--tg-border-active);
|
|
2214
|
+
}
|
|
2215
|
+
.setup-copy.copied {
|
|
2216
|
+
color: var(--tg-green);
|
|
2217
|
+
border-color: var(--tg-green);
|
|
2218
|
+
}
|
|
2219
|
+
.setup-footer {
|
|
2220
|
+
display: flex;
|
|
2221
|
+
align-items: center;
|
|
2222
|
+
justify-content: space-between;
|
|
2223
|
+
gap: 12px;
|
|
2224
|
+
padding: 14px 22px 16px;
|
|
2225
|
+
border-top: 1px solid var(--tg-border);
|
|
2226
|
+
flex-wrap: wrap;
|
|
2227
|
+
}
|
|
2228
|
+
.setup-hint {
|
|
2229
|
+
font-size: 11px;
|
|
2230
|
+
color: var(--tg-text-dim);
|
|
2231
|
+
flex: 1 1 auto;
|
|
2232
|
+
min-width: 0;
|
|
2233
|
+
}
|
|
2234
|
+
.setup-hint code {
|
|
2235
|
+
background: var(--tg-bg);
|
|
2236
|
+
padding: 1px 5px;
|
|
2237
|
+
border-radius: 3px;
|
|
2238
|
+
font-family: var(--tg-mono);
|
|
2239
|
+
font-size: 10px;
|
|
2240
|
+
color: var(--tg-text);
|
|
2241
|
+
}
|
|
2242
|
+
.setup-actions {
|
|
2243
|
+
display: flex;
|
|
2244
|
+
gap: 8px;
|
|
2245
|
+
flex-shrink: 0;
|
|
2246
|
+
}
|
|
2247
|
+
.setup-recheck,
|
|
2248
|
+
.setup-done {
|
|
2249
|
+
font-family: var(--tg-sans);
|
|
2250
|
+
font-size: 12px;
|
|
2251
|
+
font-weight: 600;
|
|
2252
|
+
padding: 6px 16px;
|
|
2253
|
+
border-radius: 4px;
|
|
2254
|
+
cursor: pointer;
|
|
2255
|
+
border: 1px solid var(--tg-border);
|
|
2256
|
+
}
|
|
2257
|
+
.setup-recheck {
|
|
2258
|
+
background: transparent;
|
|
2259
|
+
color: var(--tg-text);
|
|
2260
|
+
}
|
|
2261
|
+
.setup-recheck:hover {
|
|
2262
|
+
color: var(--tg-text-bright);
|
|
2263
|
+
border-color: var(--tg-border-active);
|
|
2264
|
+
}
|
|
2265
|
+
.setup-recheck:disabled {
|
|
2266
|
+
opacity: 0.5;
|
|
2267
|
+
cursor: not-allowed;
|
|
2268
|
+
}
|
|
2269
|
+
.setup-done {
|
|
2270
|
+
background: var(--tg-accent);
|
|
2271
|
+
color: var(--tg-bg);
|
|
2272
|
+
border-color: var(--tg-accent);
|
|
2273
|
+
}
|
|
2274
|
+
.setup-done:hover { filter: brightness(1.1); }
|
|
2275
|
+
|
|
1980
2276
|
/* ===== SCROLLBAR ===== */
|
|
1981
2277
|
::-webkit-scrollbar { width: 6px; }
|
|
1982
2278
|
::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// SkillForge — Opus prompt template for crystallizing Mnestra memories into skills.
|
|
2
|
+
//
|
|
3
|
+
// Contract:
|
|
4
|
+
// systemPrompt — 4-phase pipeline (audit → extract → generate → self-critique)
|
|
5
|
+
// buildUserPrompt(mems) — token-efficient, project-grouped memory bundle
|
|
6
|
+
// parseSkills(response) — extract validated skills with evidence + confidence
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const SYSTEM_PROMPT = `You are SkillForge, an autonomous knowledge crystallizer. You read a developer's long-term memory (Mnestra) and distill durable, high-signal skills a senior developer would actually reach for. You do NOT summarize memories. You produce skills.
|
|
11
|
+
|
|
12
|
+
A skill is a reusable playbook: a specific trigger, a concrete procedure, an evidence trail. A memory is raw data. Your job is the conversion.
|
|
13
|
+
|
|
14
|
+
You run a strict 4-phase pipeline. Skip no phase. Show the decision audit in your reasoning so it can be verified.
|
|
15
|
+
|
|
16
|
+
=== PHASE 1 — QUALITY AUDIT ===
|
|
17
|
+
|
|
18
|
+
Score each memory (or cluster of related memories) on actionability from 0.0 to 1.0:
|
|
19
|
+
- 0.9–1.0 reproducible error→fix pair, precise command sequence, non-obvious gotcha
|
|
20
|
+
- 0.6–0.8 multi-step procedure with enough detail that a new session could execute it
|
|
21
|
+
- 0.3–0.5 partial pattern or vague preference; probably needs more evidence
|
|
22
|
+
- 0.0–0.2 ephemeral task state, one-off observation, sprint meta-process noise
|
|
23
|
+
|
|
24
|
+
Discard anything below 0.3. Aggressively flag and drop:
|
|
25
|
+
- Sprint process meta ("T4 follows a gated workflow pattern", "terminal signed off with DONE")
|
|
26
|
+
- Status snapshots ("current version is v0.4.1") — these decay fast and belong in git/package.json
|
|
27
|
+
- Generic coding advice ("write tests", "check the config")
|
|
28
|
+
- Single-occurrence observations with no corroborating evidence
|
|
29
|
+
|
|
30
|
+
Keep only memories that represent real developer knowledge:
|
|
31
|
+
- Error → fix pairs (especially ones that burned time)
|
|
32
|
+
- Multi-step procedures executed 3+ times
|
|
33
|
+
- Cross-project patterns (a fix in project A applies to project B)
|
|
34
|
+
- Domain knowledge that is non-obvious to a competent senior dev
|
|
35
|
+
|
|
36
|
+
=== PHASE 2 — PATTERN EXTRACTION ===
|
|
37
|
+
|
|
38
|
+
From the surviving memories, identify:
|
|
39
|
+
- Same error class solved multiple times across projects (high value, high confidence)
|
|
40
|
+
- Multi-step procedures executed repeatedly (deploy sequences, config rituals, auth dances)
|
|
41
|
+
- Domain knowledge that is non-obvious (solver formulations, tuning sequences, platform quirks)
|
|
42
|
+
- Cross-project connections (solution in one repo rescues another)
|
|
43
|
+
|
|
44
|
+
A pattern must have at least 2 independent supporting memories OR one highly detailed, battle-tested entry. Note which it is.
|
|
45
|
+
|
|
46
|
+
=== PHASE 3 — SKILL GENERATION ===
|
|
47
|
+
|
|
48
|
+
For each validated pattern, emit a skill object with fields:
|
|
49
|
+
|
|
50
|
+
name kebab-case, 2–5 words, specific
|
|
51
|
+
description one line; explains WHEN this fires, not what it is
|
|
52
|
+
trigger specific activating context ("when deploying a Supabase Edge Function", NOT "when coding")
|
|
53
|
+
body exact steps, commands, error→fix mappings, gotchas; a senior dev should be able to follow this blind without reopening the source memory
|
|
54
|
+
evidence array of memory IDs (or short descriptors) used; the user must be able to audit
|
|
55
|
+
confidence 0.0–1.0; scales with number of independent sources × detail × recency
|
|
56
|
+
quality_score 0.0–1.0; your audit score from Phase 1 for the backing cluster
|
|
57
|
+
|
|
58
|
+
Writing the body:
|
|
59
|
+
- Lead with the trigger condition in one sentence.
|
|
60
|
+
- Then a numbered procedure OR an error→fix table.
|
|
61
|
+
- End with gotchas — the specific thing that bites people.
|
|
62
|
+
- No fluff. No "it is important to". No closing summary.
|
|
63
|
+
|
|
64
|
+
Reference skills derived from TermDeck history — these are the calibration bar. Your output should look like these in shape and density.
|
|
65
|
+
|
|
66
|
+
--- EXAMPLE SKILL 1: Supabase IPv4 toggle gotcha ---
|
|
67
|
+
{
|
|
68
|
+
"name": "supabase-ipv4-connect-toggle",
|
|
69
|
+
"description": "Fix Supabase Edge Function deploy failing with ECONNREFUSED / direct-connect timeouts on IPv6-only networks",
|
|
70
|
+
"trigger": "when deploying Supabase Edge Functions, running supabase db push, or hitting ECONNREFUSED against *.supabase.co",
|
|
71
|
+
"body": "Trigger: supabase CLI commands hang or fail with 'Connection refused' / ENETUNREACH against the direct Postgres host.\\n\\nFix:\\n1. Open Supabase Dashboard → Project → Connect modal (top-right).\\n2. Switch the connection type to 'Session pooler' (IPv4) OR enable the IPv4 add-on for direct connections.\\n3. Copy the pooler connection string (port 5432 → 6543 for transaction pooler, 5432 for session pooler).\\n4. Re-run the deploy / migration.\\n\\nGotcha: the modal's IPv4 toggle is not where it used to be — newer dashboards hide it under 'Connect → Direct connection → IPv4 compatibility'. Most residential ISPs and many CI runners are IPv6-only with broken NAT64, so the default 'Direct connection' silently fails. This bit the Rumen Edge Function deploy during Sprint 3.",
|
|
72
|
+
"evidence": ["termdeck:rumen-install-guide", "termdeck:sprint-3-deploy-notes", "mnestra:supabase-connect-modal"],
|
|
73
|
+
"confidence": 0.95,
|
|
74
|
+
"quality_score": 0.9
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
--- EXAMPLE SKILL 2: Mnestra startup ordering ---
|
|
78
|
+
{
|
|
79
|
+
"name": "mnestra-startup-sequence",
|
|
80
|
+
"description": "Bring Mnestra up cleanly when TermDeck's preflight reports the Mnestra healthz check as red",
|
|
81
|
+
"trigger": "when TermDeck preflight badge is red for Mnestra, or Mnestra /healthz returns 503, or RAG events are buffering locally without syncing",
|
|
82
|
+
"body": "Trigger: '[preflight] mnestra unreachable' in server logs, or dashboard health badge shows Mnestra red.\\n\\nSteps (in order — do NOT reorder):\\n1. Verify Postgres (Supabase) is reachable first: psql with the pooler URL from secrets.env. If this fails, stop and fix Postgres before touching Mnestra.\\n2. Confirm pgvector extension exists: SELECT extname FROM pg_extension WHERE extname='vector'. If missing, run the Mnestra migrations via 'termdeck init --mnestra'.\\n3. Start Mnestra server (MCP or HTTP mode per config.yaml rag.mnestra.mode).\\n4. curl http://localhost:<port>/healthz — must return 200 with {ok:true,pg:true,vector:true}.\\n5. Restart TermDeck; preflight re-runs on boot only.\\n\\nGotcha: TermDeck preflight caches its result for the server's lifetime. If you fix Mnestra while TermDeck is running, the badge stays red until a TermDeck restart. Also: the bridge supports three modes (direct/webhook/mcp) — 'direct' needs OPENAI_API_KEY in secrets.env for embedding, the other two do not.",
|
|
83
|
+
"evidence": ["termdeck:preflight.js", "termdeck:mnestra-bridge", "mnestra:healthz-contract"],
|
|
84
|
+
"confidence": 0.9,
|
|
85
|
+
"quality_score": 0.85
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
--- EXAMPLE SKILL 3: Version-drift prevention across the TermDeck/Mnestra/Rumen trio ---
|
|
89
|
+
{
|
|
90
|
+
"name": "termdeck-trio-version-bump",
|
|
91
|
+
"description": "Bump versions across the TermDeck + Mnestra + Rumen trio without creating cross-package drift",
|
|
92
|
+
"trigger": "when shipping a release that spans @jhizzard/termdeck, @jhizzard/mnestra, or @jhizzard/rumen, or when CLAUDE.md 'Current version' disagrees with package.json",
|
|
93
|
+
"body": "Trigger: any one of the three packages gets a new version, OR docs reference a version that no longer matches package.json.\\n\\nSteps:\\n1. Decide the semver bump for each package independently — they are NOT lockstep. Only bump a package if its own code changed.\\n2. Update each package.json version field.\\n3. Update every user-facing doc that cites a version: README.md, CLAUDE.md ('Current version' line), docs/GETTING-STARTED.md, docs/INSTALL.md, any launch/* files.\\n4. Grep for the OLD version string across the repo — never trust the 'obvious' list. 'rg -n \"0\\\\.4\\\\.0\"' catches references buried in sprint logs and examples.\\n5. If Mnestra schema changed, bump Mnestra's migration version AND TermDeck's minimum-required-mnestra check in preflight.js.\\n6. Publish in dependency order: mnestra → rumen → termdeck. Consumers break if termdeck ships first referencing an unpublished mnestra.\\n\\nGotcha: CLAUDE.md's 'Current version' line is the #1 source of drift because it's human-maintained and not part of any build step. Also: launch copy (docs/launch/*) freezes the version it was written for — intentionally leave historical launch posts alone, only update evergreen docs.",
|
|
94
|
+
"evidence": ["termdeck:CLAUDE.md", "termdeck:sprint-19-release", "mnestra:publish-sequence"],
|
|
95
|
+
"confidence": 0.85,
|
|
96
|
+
"quality_score": 0.85
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Use these as the density target. If your skills read blander than these, you are not done.
|
|
100
|
+
|
|
101
|
+
=== PHASE 4 — SELF-CRITIQUE ===
|
|
102
|
+
|
|
103
|
+
For every generated skill, answer honestly: "Would a senior developer find this genuinely useful, or is this obvious / generic / already in the docs?"
|
|
104
|
+
|
|
105
|
+
Discard on any YES:
|
|
106
|
+
- Is this generic coding advice? ("always validate input", "read the config")
|
|
107
|
+
- Would a senior dev guess this in under 30 seconds without this skill?
|
|
108
|
+
- Is this a sprint-process observation dressed up as a skill?
|
|
109
|
+
- Does the body lack a concrete command, path, or error string?
|
|
110
|
+
- Is the evidence a single vague memory?
|
|
111
|
+
|
|
112
|
+
Keep only skills where the body contains at least one of: a specific command, a specific file path, a specific error string, or a specific non-obvious configuration detail.
|
|
113
|
+
|
|
114
|
+
=== OUTPUT FORMAT ===
|
|
115
|
+
|
|
116
|
+
Emit ONLY a single JSON code block — no prose before or after. Shape:
|
|
117
|
+
|
|
118
|
+
\`\`\`json
|
|
119
|
+
{
|
|
120
|
+
"skills": [ /* skill objects, see Phase 3 */ ],
|
|
121
|
+
"discarded": [
|
|
122
|
+
{ "reason": "sprint process noise", "source": "memory-id-or-descriptor" }
|
|
123
|
+
],
|
|
124
|
+
"notes": "one-paragraph audit summary: how many memories reviewed, how many survived audit, how many skills emitted, overall signal quality"
|
|
125
|
+
}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
If zero skills pass self-critique, return an empty skills array with notes explaining why. Do NOT invent skills to hit a quota.`;
|
|
129
|
+
|
|
130
|
+
function buildUserPrompt(memories) {
|
|
131
|
+
if (!Array.isArray(memories) || memories.length === 0) {
|
|
132
|
+
return 'No memories supplied. Return {"skills":[],"discarded":[],"notes":"empty input"}.';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const byProject = new Map();
|
|
136
|
+
for (const m of memories) {
|
|
137
|
+
const project = (m && (m.project || m.source_project)) || 'unscoped';
|
|
138
|
+
if (!byProject.has(project)) byProject.set(project, []);
|
|
139
|
+
byProject.get(project).push(m);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lines = [];
|
|
143
|
+
lines.push(`You are reviewing ${memories.length} memory entries drawn from Mnestra across ${byProject.size} project(s).`);
|
|
144
|
+
lines.push('Memories are grouped by project. Each entry is: [id] (type, category, recency-days) — content.');
|
|
145
|
+
lines.push('Run the 4-phase pipeline. Emit the JSON block specified in the system prompt.');
|
|
146
|
+
lines.push('');
|
|
147
|
+
|
|
148
|
+
for (const [project, items] of byProject) {
|
|
149
|
+
lines.push(`## project: ${project} (${items.length} memories)`);
|
|
150
|
+
for (const m of items) {
|
|
151
|
+
const id = m.id || m.uuid || m.memory_id || '(no-id)';
|
|
152
|
+
const type = m.type || m.source_type || '-';
|
|
153
|
+
const category = m.category || '-';
|
|
154
|
+
const recency = typeof m.age_days === 'number'
|
|
155
|
+
? `${m.age_days}d`
|
|
156
|
+
: (m.created_at ? daysAgo(m.created_at) : '?');
|
|
157
|
+
const content = oneLine(m.content || m.body || m.text || '');
|
|
158
|
+
lines.push(`[${id}] (${type}, ${category}, ${recency}) — ${content}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lines.push('End of memory bundle. Begin Phase 1 audit.');
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function oneLine(s) {
|
|
168
|
+
return String(s).replace(/\s+/g, ' ').trim().slice(0, 600);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function daysAgo(iso) {
|
|
172
|
+
const t = Date.parse(iso);
|
|
173
|
+
if (Number.isNaN(t)) return '?';
|
|
174
|
+
const d = Math.round((Date.now() - t) / 86400000);
|
|
175
|
+
return `${d}d`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseSkills(response) {
|
|
179
|
+
if (!response || typeof response !== 'string') {
|
|
180
|
+
return { skills: [], discarded: [], notes: 'empty response', raw: response };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const block = extractJsonBlock(response);
|
|
184
|
+
if (!block) {
|
|
185
|
+
return { skills: [], discarded: [], notes: 'no JSON block found', raw: response };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let parsed;
|
|
189
|
+
try {
|
|
190
|
+
parsed = JSON.parse(block);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { skills: [], discarded: [], notes: `JSON parse error: ${err.message}`, raw: block };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const skills = Array.isArray(parsed.skills) ? parsed.skills : [];
|
|
196
|
+
const discarded = Array.isArray(parsed.discarded) ? parsed.discarded : [];
|
|
197
|
+
const notes = typeof parsed.notes === 'string' ? parsed.notes : '';
|
|
198
|
+
|
|
199
|
+
const validated = [];
|
|
200
|
+
const rejected = [...discarded];
|
|
201
|
+
for (const s of skills) {
|
|
202
|
+
const v = validateSkill(s);
|
|
203
|
+
if (v.ok) {
|
|
204
|
+
validated.push(v.skill);
|
|
205
|
+
} else {
|
|
206
|
+
rejected.push({ reason: v.reason, source: s && s.name });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { skills: validated, discarded: rejected, notes, raw: block };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function extractJsonBlock(text) {
|
|
214
|
+
const fenced = text.match(/```json\s*([\s\S]*?)```/i);
|
|
215
|
+
if (fenced) return fenced[1].trim();
|
|
216
|
+
const anyFence = text.match(/```\s*([\s\S]*?)```/);
|
|
217
|
+
if (anyFence && anyFence[1].trim().startsWith('{')) return anyFence[1].trim();
|
|
218
|
+
const first = text.indexOf('{');
|
|
219
|
+
const last = text.lastIndexOf('}');
|
|
220
|
+
if (first !== -1 && last > first) return text.slice(first, last + 1);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateSkill(s) {
|
|
225
|
+
if (!s || typeof s !== 'object') return { ok: false, reason: 'not an object' };
|
|
226
|
+
const required = ['name', 'description', 'trigger', 'body'];
|
|
227
|
+
for (const f of required) {
|
|
228
|
+
if (typeof s[f] !== 'string' || !s[f].trim()) {
|
|
229
|
+
return { ok: false, reason: `missing field: ${f}` };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(s.name)) {
|
|
233
|
+
return { ok: false, reason: 'name must be kebab-case' };
|
|
234
|
+
}
|
|
235
|
+
const confidence = clamp01(s.confidence);
|
|
236
|
+
const quality = clamp01(s.quality_score);
|
|
237
|
+
const evidence = Array.isArray(s.evidence) ? s.evidence.map(String) : [];
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
skill: {
|
|
242
|
+
name: s.name.trim(),
|
|
243
|
+
description: s.description.trim(),
|
|
244
|
+
trigger: s.trigger.trim(),
|
|
245
|
+
body: s.body,
|
|
246
|
+
evidence,
|
|
247
|
+
confidence,
|
|
248
|
+
quality_score: quality,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clamp01(v) {
|
|
254
|
+
const n = Number(v);
|
|
255
|
+
if (!Number.isFinite(n)) return 0;
|
|
256
|
+
if (n < 0) return 0;
|
|
257
|
+
if (n > 1) return 1;
|
|
258
|
+
return n;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
263
|
+
buildUserPrompt,
|
|
264
|
+
parseSkills,
|
|
265
|
+
};
|