bossbuild 0.97.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/LICENSE +21 -0
- package/PRINCIPLES.md +70 -0
- package/README.md +213 -0
- package/VERSION +1 -0
- package/bin/boss +3 -0
- package/library/README.md +19 -0
- package/library/agents/.gitkeep +0 -0
- package/library/agents/mentor-venture.md +57 -0
- package/library/hooks/.gitkeep +0 -0
- package/library/hooks/auto-log.js +133 -0
- package/library/hooks/memory-cue.js +82 -0
- package/library/hooks/secrets-guard.js +87 -0
- package/library/memory-seed/README.md +29 -0
- package/library/memory-seed/durable-facts-example.md +16 -0
- package/library/practices/.gitkeep +0 -0
- package/library/practices/agent-security.md +111 -0
- package/library/practices/ai-adoption-culture.md +104 -0
- package/library/practices/ai-ux-patterns.md +246 -0
- package/library/practices/celebration-of-done.md +100 -0
- package/library/practices/conscience-voicing.md +121 -0
- package/library/practices/context-discipline.md +116 -0
- package/library/practices/design-system.md +152 -0
- package/library/practices/git-workflow.md +119 -0
- package/library/practices/harm-taxonomy.md +45 -0
- package/library/practices/quality-ratchet.md +48 -0
- package/library/practices/revalidation.md +57 -0
- package/library/practices/scalable-architecture.md +111 -0
- package/library/practices/ship-it-live.md +149 -0
- package/library/practices/skill-authoring.md +70 -0
- package/library/skills/.gitkeep +0 -0
- package/library/skills/boss-learn/SKILL.md +63 -0
- package/library/skills/boss-sync/SKILL.md +48 -0
- package/package.json +49 -0
- package/registry/CHANGELOG.md +2737 -0
- package/src/board.js +655 -0
- package/src/brain.js +288 -0
- package/src/cli.js +542 -0
- package/src/conscience.js +426 -0
- package/src/insights.js +147 -0
- package/src/learn.js +92 -0
- package/src/map.js +103 -0
- package/src/modes.js +82 -0
- package/src/paths.js +36 -0
- package/src/registry.js +34 -0
- package/src/scaffold.js +138 -0
- package/src/sync.js +292 -0
- package/src/team.js +103 -0
- package/stages/L0-quickstart/manifest.json +12 -0
- package/stages/L0-quickstart/template/.claude/agents/coder-generalist.md +31 -0
- package/stages/L0-quickstart/template/.claude/agents/mentor-venture.md +57 -0
- package/stages/L0-quickstart/template/.claude/agents/pm.md +28 -0
- package/stages/L0-quickstart/template/.claude/hooks/conscience.js +89 -0
- package/stages/L0-quickstart/template/.claude/hooks/lib/loop-runtime.js +507 -0
- package/stages/L0-quickstart/template/.claude/hooks/lib/yaml.js +163 -0
- package/stages/L0-quickstart/template/.claude/hooks/memory-cue.js +82 -0
- package/stages/L0-quickstart/template/.claude/hooks/secrets-guard.js +87 -0
- package/stages/L0-quickstart/template/.claude/rules/your-app-code.md +17 -0
- package/stages/L0-quickstart/template/.claude/settings.json +36 -0
- package/stages/L0-quickstart/template/.claude/skills/boss/SKILL.md +161 -0
- package/stages/L0-quickstart/template/.claude/skills/boss-learn/SKILL.md +63 -0
- package/stages/L0-quickstart/template/.claude/skills/boss-sync/SKILL.md +55 -0
- package/stages/L0-quickstart/template/.claude/skills/canvas/SKILL.md +112 -0
- package/stages/L0-quickstart/template/.claude/skills/comprehend/SKILL.md +72 -0
- package/stages/L0-quickstart/template/.claude/skills/decide/SKILL.md +122 -0
- package/stages/L0-quickstart/template/.claude/skills/feedback/SKILL.md +68 -0
- package/stages/L0-quickstart/template/.claude/skills/import/SKILL.md +73 -0
- package/stages/L0-quickstart/template/.claude/skills/persona/SKILL.md +92 -0
- package/stages/L0-quickstart/template/.claude/skills/prototype/SKILL.md +114 -0
- package/stages/L0-quickstart/template/.claude/skills/triage/SKILL.md +104 -0
- package/stages/L0-quickstart/template/.claude/skills/welcome/SKILL.md +262 -0
- package/stages/L0-quickstart/template/AGENTS.md +31 -0
- package/stages/L0-quickstart/template/CLAUDE.md +57 -0
- package/stages/L0-quickstart/template/docs/IDS.md +42 -0
- package/stages/L0-quickstart/template/docs/ideas/INDEX.md +24 -0
- package/stages/L0-quickstart/template/docs/loops/canvas-loop.md +90 -0
- package/stages/L0-quickstart/template/docs/loops/capture-loop.md +64 -0
- package/stages/L1-mvp/manifest.json +12 -0
- package/stages/L1-mvp/template/.claude/agents/mentor-architect.md +124 -0
- package/stages/L1-mvp/template/.claude/agents/mentor-cofounder.md +85 -0
- package/stages/L1-mvp/template/.claude/agents/mentor-gtm.md +49 -0
- package/stages/L1-mvp/template/.claude/agents/program-manager.md +46 -0
- package/stages/L1-mvp/template/.claude/agents/tester.md +42 -0
- package/stages/L1-mvp/template/.claude/hooks/auto-log.js +133 -0
- package/stages/L1-mvp/template/.claude/rules/feature-context.md +18 -0
- package/stages/L1-mvp/template/.claude/skills/ai-cost/SKILL.md +249 -0
- package/stages/L1-mvp/template/.claude/skills/ai-failure-states/SKILL.md +226 -0
- package/stages/L1-mvp/template/.claude/skills/ai-first-init/SKILL.md +227 -0
- package/stages/L1-mvp/template/.claude/skills/close/SKILL.md +170 -0
- package/stages/L1-mvp/template/.claude/skills/consult/SKILL.md +72 -0
- package/stages/L1-mvp/template/.claude/skills/cost-review/SKILL.md +204 -0
- package/stages/L1-mvp/template/.claude/skills/design-tokens-init/SKILL.md +192 -0
- package/stages/L1-mvp/template/.claude/skills/drift-deep/SKILL.md +170 -0
- package/stages/L1-mvp/template/.claude/skills/evals/SKILL.md +154 -0
- package/stages/L1-mvp/template/.claude/skills/extract/SKILL.md +209 -0
- package/stages/L1-mvp/template/.claude/skills/judge-traces/SKILL.md +68 -0
- package/stages/L1-mvp/template/.claude/skills/log/SKILL.md +64 -0
- package/stages/L1-mvp/template/.claude/skills/practice/SKILL.md +92 -0
- package/stages/L1-mvp/template/.claude/skills/pretotype/SKILL.md +95 -0
- package/stages/L1-mvp/template/.claude/skills/red-team/SKILL.md +137 -0
- package/stages/L1-mvp/template/.claude/skills/revalidate/SKILL.md +51 -0
- package/stages/L1-mvp/template/.claude/skills/ship/SKILL.md +105 -0
- package/stages/L1-mvp/template/.claude/skills/smoke/SKILL.md +43 -0
- package/stages/L1-mvp/template/.claude/skills/spec/SKILL.md +145 -0
- package/stages/L1-mvp/template/claude-append.md +122 -0
- package/stages/L1-mvp/template/docs/loops/ai-failure-state-loop.md +107 -0
- package/stages/L1-mvp/template/docs/loops/coordination-loop.md +116 -0
- package/stages/L1-mvp/template/docs/loops/cost-budget-loop.md +117 -0
- package/stages/L1-mvp/template/docs/loops/cost-review-loop.md +113 -0
- package/stages/L1-mvp/template/docs/loops/design-tokens-loop.md +98 -0
- package/stages/L1-mvp/template/docs/loops/drift-loop.md +149 -0
- package/stages/L1-mvp/template/docs/loops/extraction-loop.md +128 -0
- package/stages/L1-mvp/template/docs/loops/focus-loop.md +106 -0
- package/stages/L1-mvp/template/docs/loops/pretotype-loop.md +88 -0
- package/stages/L1-mvp/template/docs/loops/spec-loop.md +83 -0
- package/stages/L2-v1/manifest.json +12 -0
- package/stages/L2-v1/template/.claude/agents/db-architect.md +91 -0
- package/stages/L2-v1/template/.claude/agents/mentor-business.md +124 -0
- package/stages/L2-v1/template/.claude/agents/mentor-fundraising.md +72 -0
- package/stages/L2-v1/template/.claude/agents/mentor-pitch.md +84 -0
- package/stages/L2-v1/template/.claude/agents/mentor-talent.md +84 -0
- package/stages/L2-v1/template/.claude/agents/ui-designer.md +81 -0
- package/stages/L2-v1/template/.claude/agents/ux-designer.md +87 -0
- package/stages/L2-v1/template/.claude/skills/board/SKILL.md +98 -0
- package/stages/L2-v1/template/.claude/skills/design-review/SKILL.md +77 -0
- package/stages/L2-v1/template/.claude/skills/ux-check/SKILL.md +93 -0
- package/stages/L2-v1/template/claude-append.md +59 -0
- package/stages/L2-v1/template/docs/loops/design-drift-loop.md +108 -0
- package/stages/L3-scale/README.md +13 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// Conscience-state inspect for the BOSS CLI (v0.20.0+).
|
|
2
|
+
//
|
|
3
|
+
// Human-readable surface for what the conscience hook does machine-readably.
|
|
4
|
+
// Loads docs/loops/*.md in the current project, classifies each loop, formats
|
|
5
|
+
// open/closed/unopenable + what would close the open ones + any recent
|
|
6
|
+
// overrides recorded in the devlog. Asked-for by eng-builder / indie-hacker /
|
|
7
|
+
// vibe-virtuoso personas (v0.19 reactions) — "I want to see what fired and why."
|
|
8
|
+
//
|
|
9
|
+
// The runtime is the same one the hook uses — imported from the Quickstart
|
|
10
|
+
// template's hook lib so there's one source of truth. The path is awkward but
|
|
11
|
+
// the alternative (duplicating ~250 lines) is worse.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { loadLoops, classifyLoop, readPauseState, readMuteState } from '../stages/L0-quickstart/template/.claude/hooks/lib/loop-runtime.js';
|
|
16
|
+
|
|
17
|
+
// Parse a duration spec for `boss conscience pause --for <spec>`.
|
|
18
|
+
// Accepted: <N>m / <N>h / <N>d (minutes / hours / days). Returns ms or throws.
|
|
19
|
+
function parseDuration(spec) {
|
|
20
|
+
const m = String(spec).match(/^(\d+)([mhd])$/);
|
|
21
|
+
if (!m) throw new Error(`bad duration: '${spec}'. Use 30m, 8h, or 2d.`);
|
|
22
|
+
const n = parseInt(m[1], 10);
|
|
23
|
+
const mult = { m: 60e3, h: 3600e3, d: 86400e3 }[m[2]];
|
|
24
|
+
return n * mult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readConfigOrFail(projectDir) {
|
|
28
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
29
|
+
if (!existsSync(f)) {
|
|
30
|
+
throw new Error('not a BOSS project (no .boss/config.json here).');
|
|
31
|
+
}
|
|
32
|
+
return { path: f, cfg: JSON.parse(readFileSync(f, 'utf8')) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeConfig(path, cfg) {
|
|
36
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `boss conscience pause [--for <duration> | --until-resume] [--reason "..."]`
|
|
40
|
+
// Default duration: 8h. Records the pause in `.boss/config.json` per IDEA-011's
|
|
41
|
+
// fractal-override discipline (the IDEA-008 override grammar applied at the
|
|
42
|
+
// session level instead of per-loop).
|
|
43
|
+
export function consciencePause(flags) {
|
|
44
|
+
const { path, cfg } = readConfigOrFail(process.cwd());
|
|
45
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : '';
|
|
46
|
+
|
|
47
|
+
let expires = null;
|
|
48
|
+
if (flags['until-resume']) {
|
|
49
|
+
expires = null;
|
|
50
|
+
} else {
|
|
51
|
+
const spec = typeof flags.for === 'string' ? flags.for : '8h';
|
|
52
|
+
const ms = parseDuration(spec);
|
|
53
|
+
expires = new Date(Date.now() + ms).toISOString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cfg.conscience = {
|
|
57
|
+
mode: 'paused',
|
|
58
|
+
since: new Date().toISOString(),
|
|
59
|
+
expires,
|
|
60
|
+
reason,
|
|
61
|
+
};
|
|
62
|
+
writeConfig(path, cfg);
|
|
63
|
+
|
|
64
|
+
console.log(`\n ✦ Conscience paused.`);
|
|
65
|
+
if (expires) {
|
|
66
|
+
console.log(` auto-resumes: ${expires}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(` no expiry — \`boss conscience resume\` to end.`);
|
|
69
|
+
}
|
|
70
|
+
if (reason) console.log(` reason: ${reason}`);
|
|
71
|
+
console.log('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `boss conscience resume` — explicit un-pause. (Also happens automatically when
|
|
75
|
+
// the recorded expiry passes, via the hook's auto-clear.)
|
|
76
|
+
export function conscienceResume() {
|
|
77
|
+
const { path, cfg } = readConfigOrFail(process.cwd());
|
|
78
|
+
if (!cfg.conscience || cfg.conscience.mode !== 'paused') {
|
|
79
|
+
console.log('\n Conscience is already active.\n');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
cfg.conscience = { mode: 'active' };
|
|
83
|
+
writeConfig(path, cfg);
|
|
84
|
+
console.log(`\n ✦ Conscience resumed.\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The moments this project can actually fire — derived from the loops present
|
|
88
|
+
// (each hook-loop's `drift_moment`), so mute validates against reality rather than
|
|
89
|
+
// a hardcoded list that rots as moments are added. Used to catch typos and to show
|
|
90
|
+
// the founder what's muteable.
|
|
91
|
+
function availableMoments(projectDir) {
|
|
92
|
+
try {
|
|
93
|
+
return [...new Set(loadLoops(projectDir).map((l) => l.drift_moment).filter(Boolean))].sort();
|
|
94
|
+
} catch { return []; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `boss conscience mute <moment> [--for <duration> | --until-resume] [--reason "..."]`
|
|
98
|
+
// Default duration: 7d. The surgical companion to pause — silence ONE moment, not
|
|
99
|
+
// the whole conscience. This is "voice the tension, never filter the menu" made
|
|
100
|
+
// operational: the founder consents to (or declines) each moment individually.
|
|
101
|
+
// Stored under `conscienceMutes` so pause/resume never clobber it (orthogonal).
|
|
102
|
+
export function conscienceMute(flags) {
|
|
103
|
+
const { path, cfg } = readConfigOrFail(process.cwd());
|
|
104
|
+
const moment = (flags._ && flags._[0]) || '';
|
|
105
|
+
if (!moment) {
|
|
106
|
+
throw new Error('which moment? e.g. `boss conscience mute drift`. `boss conscience status` lists what can fire.');
|
|
107
|
+
}
|
|
108
|
+
const moments = availableMoments(process.cwd());
|
|
109
|
+
if (moments.length && !moments.includes(moment)) {
|
|
110
|
+
throw new Error(`no '${moment}' moment in this project's loops. Available: ${moments.join(', ')}.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : '';
|
|
114
|
+
let until = null;
|
|
115
|
+
if (!flags['until-resume']) {
|
|
116
|
+
const spec = typeof flags.for === 'string' ? flags.for : '7d';
|
|
117
|
+
until = new Date(Date.now() + parseDuration(spec)).toISOString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cfg.conscienceMutes = cfg.conscienceMutes || {};
|
|
121
|
+
cfg.conscienceMutes[moment] = { until, since: new Date().toISOString(), reason };
|
|
122
|
+
writeConfig(path, cfg);
|
|
123
|
+
|
|
124
|
+
console.log(`\n ✦ Muted the '${moment}' moment.`);
|
|
125
|
+
if (until) console.log(` auto-unmutes: ${until}`);
|
|
126
|
+
else console.log(` no expiry — \`boss conscience unmute ${moment}\` to end.`);
|
|
127
|
+
if (reason) console.log(` reason: ${reason}`);
|
|
128
|
+
console.log(` ${dim('Other moments still speak; `boss conscience pause` silences all of them.')}`);
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// `boss conscience unmute <moment>` (or `--all`) — the explicit un-silence.
|
|
133
|
+
// (A mute with an expiry also lapses on its own, via the hook's clearExpiredMutes.)
|
|
134
|
+
export function conscienceUnmute(flags) {
|
|
135
|
+
const { path, cfg } = readConfigOrFail(process.cwd());
|
|
136
|
+
const mutes = cfg.conscienceMutes || {};
|
|
137
|
+
const moment = (flags._ && flags._[0]) || '';
|
|
138
|
+
|
|
139
|
+
if (flags.all === true) {
|
|
140
|
+
if (Object.keys(mutes).length === 0) { console.log('\n No moments are muted.\n'); return; }
|
|
141
|
+
delete cfg.conscienceMutes;
|
|
142
|
+
writeConfig(path, cfg);
|
|
143
|
+
console.log('\n ✦ Unmuted all moments.\n');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!moment) throw new Error('which moment? e.g. `boss conscience unmute drift` (or `--all`).');
|
|
147
|
+
if (!mutes[moment]) { console.log(`\n '${moment}' isn't muted.\n`); return; }
|
|
148
|
+
|
|
149
|
+
delete mutes[moment];
|
|
150
|
+
if (Object.keys(mutes).length === 0) delete cfg.conscienceMutes;
|
|
151
|
+
else cfg.conscienceMutes = mutes;
|
|
152
|
+
writeConfig(path, cfg);
|
|
153
|
+
console.log(`\n ✦ Unmuted the '${moment}' moment.\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Read the optional cohort declaration from .boss/config.json. Returns null if
|
|
157
|
+
// no config or no cohort field — Claude composes the conscience voice generically
|
|
158
|
+
// when cohort is null.
|
|
159
|
+
function readCohort(projectDir) {
|
|
160
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
161
|
+
if (!existsSync(f)) return null;
|
|
162
|
+
try {
|
|
163
|
+
const cfg = JSON.parse(readFileSync(f, 'utf8'));
|
|
164
|
+
return cfg.cohort || null;
|
|
165
|
+
} catch { return null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Recent override entries in docs/devlog.md, per IDEA-008's grammar:
|
|
169
|
+
// - **OVERRIDE:** <action> `<loop-id>` — rationale: <text>
|
|
170
|
+
function readOverrides(projectDir) {
|
|
171
|
+
const f = join(projectDir, 'docs', 'devlog.md');
|
|
172
|
+
if (!existsSync(f)) return [];
|
|
173
|
+
try {
|
|
174
|
+
const lines = readFileSync(f, 'utf8').split('\n');
|
|
175
|
+
const re = /^- \*\*OVERRIDE:\*\*\s+(\w+)\s+`([^`]+)`\s+—\s+rationale:\s+(.+)$/;
|
|
176
|
+
return lines.map((l) => l.match(re))
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.map((m) => ({ action: m[1], loop: m[2], rationale: m[3].trim() }));
|
|
179
|
+
} catch { return []; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Render a single closed-loop predicate result as a one-line "what's needed."
|
|
183
|
+
function exitSummary(result) {
|
|
184
|
+
const e = result.evidence || {};
|
|
185
|
+
if (typeof e.count === 'number' && typeof e.min === 'number') {
|
|
186
|
+
return `count: ${e.count} / threshold: ${e.min}${e.count >= e.min ? ' (met)' : ` (need ${e.min - e.count} more)`}`;
|
|
187
|
+
}
|
|
188
|
+
if (typeof e.matched_files === 'number') {
|
|
189
|
+
return `${e.matched_files} / ${e.total_files} file(s) match`;
|
|
190
|
+
}
|
|
191
|
+
if (e.type === 'exists') {
|
|
192
|
+
return `expects: ${e.path}`;
|
|
193
|
+
}
|
|
194
|
+
return JSON.stringify(e);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Read the frequency ledger (v0.34) the hook appends to on every fire.
|
|
198
|
+
function readActivity(projectDir) {
|
|
199
|
+
const f = join(projectDir, '.boss', 'conscience-log.jsonl');
|
|
200
|
+
if (!existsSync(f)) return [];
|
|
201
|
+
try {
|
|
202
|
+
return readFileSync(f, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)).filter(Boolean);
|
|
203
|
+
} catch { return []; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The OUTCOME side of the ledger (RVW-021, the humane alternative to a hard
|
|
207
|
+
// notification cap). The frequency log says how OFTEN the conscience fired; the
|
|
208
|
+
// relationship log (.boss/brain/relationship.md, written at /close) says whether
|
|
209
|
+
// those fires LANDED. A persistently-low acted-on rate is the honest over-fire
|
|
210
|
+
// smell — better than a raw count, and it never muzzles a load-bearing warning.
|
|
211
|
+
// Heuristic read of the founder-owned prose (the tags /close is told to use);
|
|
212
|
+
// returns null when there's no relationship log yet.
|
|
213
|
+
function readRelationshipOutcomes(projectDir) {
|
|
214
|
+
const f = join(projectDir, '.boss', 'brain', 'relationship.md');
|
|
215
|
+
if (!existsSync(f)) return null;
|
|
216
|
+
let text;
|
|
217
|
+
try { text = readFileSync(f, 'utf8').toLowerCase(); } catch { return null; }
|
|
218
|
+
const n = (re) => (text.match(re) || []).length;
|
|
219
|
+
const landed = n(/\blanded\b/g);
|
|
220
|
+
const overrode = n(/\boverr(?:ode|ide)\b/g);
|
|
221
|
+
const pushedBack = n(/\bpushed[-\s]back\b/g);
|
|
222
|
+
const ignored = n(/\bignored\b/g);
|
|
223
|
+
const total = landed + overrode + pushedBack + ignored;
|
|
224
|
+
if (total === 0) return null;
|
|
225
|
+
// "Acted on" = the founder engaged: it landed, they deliberately overrode, or it
|
|
226
|
+
// was wrong and they pushed back (the conscience learns). Only `ignored` is noise.
|
|
227
|
+
const actedOn = landed + overrode + pushedBack;
|
|
228
|
+
return { total, landed, overrode, pushedBack, ignored, actedOnRate: Math.round((actedOn / total) * 100) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const median = (xs) => {
|
|
232
|
+
if (!xs.length) return 0;
|
|
233
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
234
|
+
const m = Math.floor(s.length / 2);
|
|
235
|
+
return s.length % 2 ? s[m] : Math.round((s[m - 1] + s[m]) / 2);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Count fires per moment within the last `hours`.
|
|
239
|
+
function firesWithin(rows, hours) {
|
|
240
|
+
const cutoff = Date.now() - hours * 3600e3;
|
|
241
|
+
const counts = {};
|
|
242
|
+
for (const r of rows) {
|
|
243
|
+
const t = Date.parse(r.ts);
|
|
244
|
+
if (!Number.isNaN(t) && t >= cutoff) {
|
|
245
|
+
for (const m of r.moments || []) counts[m.moment] = (counts[m.moment] || 0) + 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return counts;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// `boss conscience activity` (alias: `cost`) — the frequency view.
|
|
252
|
+
//
|
|
253
|
+
// Deliberately NOT a token/dollar number. The conscience hook never calls a
|
|
254
|
+
// model, so it cannot honestly estimate tokens (the induced bounded reads are
|
|
255
|
+
// invisible to it). What it CAN measure — and what actually tells you the
|
|
256
|
+
// conscience is becoming costly/annoying — is how often it fires. Over-firing,
|
|
257
|
+
// not the token bill, is the failure mode. Facts, not estimates.
|
|
258
|
+
export function conscienceActivity(projectDir = process.cwd(), { asCost = false } = {}) {
|
|
259
|
+
const rows = readActivity(projectDir);
|
|
260
|
+
|
|
261
|
+
if (asCost) {
|
|
262
|
+
console.log(`\n conscience cost → measured as FREQUENCY, not tokens.`);
|
|
263
|
+
console.log(` A hook that never calls a model can't honestly price tokens; over-firing (not the`);
|
|
264
|
+
console.log(` bill) is how a conscience becomes costly. Facts, not estimates.`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (rows.length === 0) {
|
|
268
|
+
console.log(`\n No conscience activity logged yet (.boss/conscience-log.jsonl absent or empty).`);
|
|
269
|
+
console.log(` The ledger fills one line per fire. Nothing has fired in this project.\n`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const total = rows.length;
|
|
274
|
+
const first = rows[0].ts, last = rows[rows.length - 1].ts;
|
|
275
|
+
const perMoment = {};
|
|
276
|
+
let judgeFires = 0;
|
|
277
|
+
const chars = [];
|
|
278
|
+
for (const r of rows) {
|
|
279
|
+
for (const m of r.moments || []) perMoment[m.moment] = (perMoment[m.moment] || 0) + 1;
|
|
280
|
+
if (r.judge) judgeFires++;
|
|
281
|
+
if (typeof r.injected_chars === 'number') chars.push(r.injected_chars);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`\n conscience activity (.boss/conscience-log.jsonl)`);
|
|
285
|
+
console.log(` fires: ${total} ${first.slice(0, 10)} → ${last.slice(0, 10)}`);
|
|
286
|
+
console.log(` judge-moments: ${judgeFires}/${total} fires induced a model bounded-read (drift / caution)`);
|
|
287
|
+
console.log(` injected ctx: ${median(chars)} chars median per fire ${dim('(chars are a fact; tokens would be a guess)')}`);
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(` by moment:`);
|
|
290
|
+
for (const [m, n] of Object.entries(perMoment).sort((a, b) => b[1] - a[1])) {
|
|
291
|
+
console.log(` ${String(n).padStart(4)} ${m}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Over-fire smell — the signal that actually matters. No per-prompt denominator
|
|
295
|
+
// (the hook only logs fires, to stay instant), so we flag clustering instead.
|
|
296
|
+
const last1h = firesWithin(rows, 1);
|
|
297
|
+
const last24h = firesWithin(rows, 24);
|
|
298
|
+
const smells = [];
|
|
299
|
+
for (const [m, n] of Object.entries(last1h)) if (n >= 4) smells.push(`${m} fired ${n}× in the last hour`);
|
|
300
|
+
for (const [m, n] of Object.entries(last24h)) if (n >= 8 && !(last1h[m] >= 4)) smells.push(`${m} fired ${n}× in the last 24h`);
|
|
301
|
+
console.log('');
|
|
302
|
+
if (smells.length) {
|
|
303
|
+
console.log(` ⚠ over-fire smell — the conscience may be talking too often:`);
|
|
304
|
+
for (const s of smells) console.log(` • ${s}`);
|
|
305
|
+
console.log(` ${dim('A moment firing this often erodes trust like a false alarm. Worth a look —')}`);
|
|
306
|
+
console.log(` ${dim('tune the loop, `boss conscience mute <moment>` to turn down just that one,')}`);
|
|
307
|
+
console.log(` ${dim('or `boss conscience pause` to silence everything for a while.')}`);
|
|
308
|
+
} else {
|
|
309
|
+
console.log(` No over-fire smell — fires are spread out.`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Outcome ledger (RVW-021): did the fires LAND? The deeper over-fire signal —
|
|
313
|
+
// frequency says how often it spoke; this says whether it was worth listening to.
|
|
314
|
+
const out = readRelationshipOutcomes(projectDir);
|
|
315
|
+
if (out) {
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(` acted-on: ${out.actedOnRate}% of nudges landed or were engaged ${dim(`(${out.landed} landed · ${out.overrode} overrode · ${out.pushedBack} pushed-back · ${out.ignored} ignored)`)}`);
|
|
318
|
+
if (out.actedOnRate < 50 && out.total >= 4) {
|
|
319
|
+
console.log(` ${dim('⚠ a low acted-on rate is the real over-fire smell — the conscience is talking past you.')}`);
|
|
320
|
+
console.log(` ${dim('This beats a hard cap: tune the loops that get ignored, don\'t silence the ones that land.')}`);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(` ${dim('the nudges are landing — measured from .boss/brain/relationship.md, not a guess.')}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
console.log('');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const dim = (s) => (process.stdout.isTTY ? `\x1b[90m${s}\x1b[0m` : s);
|
|
329
|
+
|
|
330
|
+
export function statusConscience(projectDir = process.cwd()) {
|
|
331
|
+
const loops = loadLoops(projectDir);
|
|
332
|
+
if (loops.length === 0) {
|
|
333
|
+
console.log('\n No loops in this project — `docs/loops/` is empty or absent.');
|
|
334
|
+
console.log(' Quickstart loops install via `boss new` or `boss sync --apply`.\n');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const cohort = readCohort(projectDir);
|
|
339
|
+
const pause = readPauseState(projectDir);
|
|
340
|
+
const overrides = readOverrides(projectDir);
|
|
341
|
+
|
|
342
|
+
console.log(`\n conscience state`);
|
|
343
|
+
// Pause state surfaces FIRST and LOUDLY when active (IDEA-011 v0.23.0+) — a
|
|
344
|
+
// founder who paused the conscience needs to remember they did, otherwise the
|
|
345
|
+
// pause silently lingers and the override discipline gets less honest.
|
|
346
|
+
if (pause && pause.mode === 'paused') {
|
|
347
|
+
const expired = pause.expires && new Date(pause.expires) <= new Date();
|
|
348
|
+
if (expired) {
|
|
349
|
+
console.log(` ⏸ PAUSED (EXPIRED — will auto-resume on next prompt)`);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(` ⏸ PAUSED`);
|
|
352
|
+
if (pause.since) console.log(` since: ${pause.since}`);
|
|
353
|
+
if (pause.expires) {
|
|
354
|
+
console.log(` auto-resumes: ${pause.expires}`);
|
|
355
|
+
} else {
|
|
356
|
+
console.log(` no expiry — \`boss conscience resume\` to end`);
|
|
357
|
+
}
|
|
358
|
+
if (pause.reason) console.log(` reason: ${pause.reason}`);
|
|
359
|
+
}
|
|
360
|
+
console.log('');
|
|
361
|
+
}
|
|
362
|
+
// Per-moment mutes (v0.72.0) — surfaced like pause so a forgotten mute doesn't
|
|
363
|
+
// silently swallow a moment forever. Only live (unexpired) mutes are shown.
|
|
364
|
+
const mutes = readMuteState(projectDir);
|
|
365
|
+
const liveMutes = Object.entries(mutes).filter(([, m]) => !m.until || new Date(m.until) > new Date());
|
|
366
|
+
if (liveMutes.length) {
|
|
367
|
+
console.log(` 🔇 muted moments:`);
|
|
368
|
+
for (const [moment, m] of liveMutes) {
|
|
369
|
+
const when = m.until ? `until ${m.until.slice(0, 10)}` : 'until unmuted';
|
|
370
|
+
console.log(` ${moment.padEnd(12)} ${when}${m.reason ? ` — ${m.reason}` : ''}`);
|
|
371
|
+
}
|
|
372
|
+
console.log(` ${dim('`boss conscience unmute <moment>` to bring one back')}`);
|
|
373
|
+
console.log('');
|
|
374
|
+
}
|
|
375
|
+
console.log(` cohort: ${cohort ? cohort : '(unspecified — set via `/boss` or edit .boss/config.json)'}`);
|
|
376
|
+
|
|
377
|
+
// Frequency ledger one-liner (v0.34) — surfaces over-firing at a glance.
|
|
378
|
+
const activity = readActivity(projectDir);
|
|
379
|
+
if (activity.length) {
|
|
380
|
+
const last24h = firesWithin(activity, 24);
|
|
381
|
+
const top = Object.entries(last24h).sort((a, b) => b[1] - a[1])[0];
|
|
382
|
+
const recent = Object.values(last24h).reduce((a, b) => a + b, 0);
|
|
383
|
+
let line = ` fires: ${activity.length} logged`;
|
|
384
|
+
if (recent) line += `; ${recent} in last 24h${top ? ` (most: ${top[0]} ×${top[1]})` : ''}`;
|
|
385
|
+
const smell = top && top[1] >= 8;
|
|
386
|
+
console.log(smell ? `${line} ⚠ over-fire smell — see \`boss conscience activity\`` : `${line} ${dim('(`boss conscience activity` for detail)')}`);
|
|
387
|
+
}
|
|
388
|
+
console.log('');
|
|
389
|
+
|
|
390
|
+
// Sort: open first (they need attention), then closed, then unopenable.
|
|
391
|
+
const classified = loops.map((l) => ({ loop: l, ...classifyLoop(l, projectDir) }));
|
|
392
|
+
const order = { open: 0, closed: 1, unopenable: 2 };
|
|
393
|
+
classified.sort((a, b) => order[a.state] - order[b.state]);
|
|
394
|
+
|
|
395
|
+
for (const { loop, state, entry, exit } of classified) {
|
|
396
|
+
const mark = state === 'closed' ? '✓' : state === 'open' ? '⚠' : '·';
|
|
397
|
+
const padded = loop.id.padEnd(22);
|
|
398
|
+
let line2;
|
|
399
|
+
if (state === 'closed') {
|
|
400
|
+
line2 = ' closed — exit artifact present.';
|
|
401
|
+
} else if (state === 'open') {
|
|
402
|
+
const unmetEntry = (entry.results || []).filter((r) => !r.ok);
|
|
403
|
+
const unmetExit = (exit.results || []).filter((r) => !r.ok);
|
|
404
|
+
const summaries = unmetExit.map(exitSummary).join('; ') || 'exit artifact missing';
|
|
405
|
+
line2 = ` open — would close when: ${summaries}`;
|
|
406
|
+
if (unmetEntry.length === 0 && loop.drift_moment) {
|
|
407
|
+
line2 += `\n drift moment: ${loop.drift_moment}`;
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
line2 = ' unopenable — entry artifact not yet present (upstream dependency).';
|
|
411
|
+
}
|
|
412
|
+
console.log(` ${mark} ${padded} ${state}`);
|
|
413
|
+
console.log(line2);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log('');
|
|
417
|
+
if (overrides.length === 0) {
|
|
418
|
+
console.log(' No recorded overrides in docs/devlog.md.');
|
|
419
|
+
} else {
|
|
420
|
+
console.log(` Recent overrides (${overrides.length}):`);
|
|
421
|
+
for (const o of overrides.slice(-5)) {
|
|
422
|
+
console.log(` • ${o.action} \`${o.loop}\` — ${o.rationale}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
console.log('');
|
|
426
|
+
}
|
package/src/insights.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { listProjects } from './registry.js';
|
|
4
|
+
import { bossVersion, STAGE_ORDER } from './paths.js';
|
|
5
|
+
|
|
6
|
+
// `boss insights` — the honest-trace lens (IDEA-021).
|
|
7
|
+
//
|
|
8
|
+
// Reads the trace your own work already leaves — your registered projects, on THIS machine —
|
|
9
|
+
// and reports where each venture's loop stands: idea → canvas → build → graduation. It measures
|
|
10
|
+
// *graduation and loop-closure*, never activity/engagement (that's the vanity metric BOSS refuses
|
|
11
|
+
// to expose). Nothing is sent anywhere; cross-user learning is opt-in only (shareUp). This is the
|
|
12
|
+
// humane half of "learn how it's used": read the trace, don't instrument the human.
|
|
13
|
+
|
|
14
|
+
const DAY = 86400000;
|
|
15
|
+
|
|
16
|
+
// Pull the `created:` date (YYYY-MM-DD) from a doc's frontmatter, or null. Used for
|
|
17
|
+
// the honest time-to-graduation metric (IDEA-034 Track C): real recorded dates only,
|
|
18
|
+
// never a guess from mtime.
|
|
19
|
+
function createdDate(text) {
|
|
20
|
+
const m = text.match(/^created:\s*(\d{4}-\d{2}-\d{2})/m);
|
|
21
|
+
return m ? m[1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Count IDEA-*.md docs and how many have been pressure-tested (carry a canvas), plus any FEAT-*.md
|
|
25
|
+
// (a feature in build = graduation past the canvas gate). Also reads the earliest IDEA and FEAT
|
|
26
|
+
// `created:` dates so insights can report idea→build cycle time. Reads files; never writes.
|
|
27
|
+
function readProjectTrace(dir) {
|
|
28
|
+
const ideasDir = join(dir, 'docs', 'ideas');
|
|
29
|
+
let ideas = 0, canvassed = 0, features = 0, newest = 0;
|
|
30
|
+
let firstIdea = null, firstFeat = null; // earliest created: dates (lexical ISO compare)
|
|
31
|
+
const seenCanvas = existsSync(join(dir, 'docs', 'ideas', 'CANVAS.md'));
|
|
32
|
+
if (existsSync(ideasDir)) {
|
|
33
|
+
for (const f of readdirSync(ideasDir)) {
|
|
34
|
+
if (!f.endsWith('.md')) continue;
|
|
35
|
+
const full = join(ideasDir, f);
|
|
36
|
+
let mtime = 0;
|
|
37
|
+
try { mtime = statSync(full).mtimeMs; } catch { /* skip */ }
|
|
38
|
+
if (mtime > newest) newest = mtime;
|
|
39
|
+
if (/^IDEA-\d+/.test(f)) {
|
|
40
|
+
ideas++;
|
|
41
|
+
try {
|
|
42
|
+
const txt = readFileSync(full, 'utf8');
|
|
43
|
+
if (/canvas/i.test(txt)) canvassed++;
|
|
44
|
+
const d = createdDate(txt);
|
|
45
|
+
if (d && (!firstIdea || d < firstIdea)) firstIdea = d;
|
|
46
|
+
} catch { /* unreadable — don't guess */ }
|
|
47
|
+
} else if (/^FEAT-\d+/.test(f)) {
|
|
48
|
+
features++;
|
|
49
|
+
try {
|
|
50
|
+
const d = createdDate(readFileSync(full, 'utf8'));
|
|
51
|
+
if (d && (!firstFeat || d < firstFeat)) firstFeat = d;
|
|
52
|
+
} catch { /* unreadable — don't guess */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (seenCanvas && canvassed === 0) canvassed = 1;
|
|
57
|
+
return { ideas, canvassed, features, newest, firstIdea, firstFeat };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// One honest read on where a project's loop stands. Returns null if the project is gone from disk.
|
|
61
|
+
function assess(p, nowMs) {
|
|
62
|
+
if (!p.path || !existsSync(p.path)) return { ...p, missing: true };
|
|
63
|
+
const stampFile = join(p.path, '.boss', 'manifest.json');
|
|
64
|
+
let stamp = null;
|
|
65
|
+
if (existsSync(stampFile)) {
|
|
66
|
+
try { stamp = JSON.parse(readFileSync(stampFile, 'utf8')); } catch { /* tolerate */ }
|
|
67
|
+
}
|
|
68
|
+
const t = readProjectTrace(p.path);
|
|
69
|
+
const depth = (stamp?.installedLayers || []).length || 1;
|
|
70
|
+
const lastTouch = t.newest || (stamp?.createdAt ? Date.parse(stamp.createdAt) : 0);
|
|
71
|
+
const ageDays = lastTouch ? Math.floor((nowMs - lastTouch) / DAY) : null;
|
|
72
|
+
|
|
73
|
+
// Time-to-graduation (IDEA-034 Track C): days from the first captured idea to the
|
|
74
|
+
// first FEAT in build. The honest loop-closure cycle time — derived only from
|
|
75
|
+
// recorded `created:` dates, omitted (never guessed) when they're absent. NOT
|
|
76
|
+
// throughput/velocity (the vanity metric BOSS refuses to expose).
|
|
77
|
+
const toBuildDays = (t.firstIdea && t.firstFeat && t.firstFeat >= t.firstIdea)
|
|
78
|
+
? Math.round((Date.parse(t.firstFeat) - Date.parse(t.firstIdea)) / DAY)
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
// Loop-closure signal — NOT activity. Where did the venture get stuck, if anywhere?
|
|
82
|
+
let signal = 'flowing', note = '';
|
|
83
|
+
if (t.ideas === 0 && t.features === 0) {
|
|
84
|
+
signal = 'empty'; note = 'nothing captured yet — point /boss or /import at your idea';
|
|
85
|
+
} else if (t.canvassed === 0 && t.features === 0) {
|
|
86
|
+
signal = 'untested';
|
|
87
|
+
note = `captured, never pressure-tested${ageDays != null ? ` (${ageDays}d)` : ''} — try /canvas`;
|
|
88
|
+
} else if (ageDays != null && ageDays >= 14) {
|
|
89
|
+
signal = 'stale'; note = `no movement in ${ageDays}d`;
|
|
90
|
+
} else {
|
|
91
|
+
signal = 'flowing'; note = '';
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...p, missing: false,
|
|
95
|
+
mode: stamp?.mode || p.mode || p.stage || '?',
|
|
96
|
+
pin: stamp?.bossVersion || p.bossVersion || '?',
|
|
97
|
+
depth, ideas: t.ideas, canvassed: t.canvassed, features: t.features, ageDays, signal, note,
|
|
98
|
+
toBuildDays,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const MARK = { flowing: '✓', untested: '⚠', empty: '⚠', stale: '·', missing: '·' };
|
|
103
|
+
|
|
104
|
+
export function insights(cwd) {
|
|
105
|
+
const nowMs = Date.now();
|
|
106
|
+
const projects = listProjects();
|
|
107
|
+
if (!projects.length) {
|
|
108
|
+
console.log('\n No projects registered yet. Run `boss new <name>` to start one.\n');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rows = projects.map((p) => assess(p, nowMs)).filter((r) => !r.missing);
|
|
113
|
+
const gone = projects.length - rows.length;
|
|
114
|
+
const current = bossVersion();
|
|
115
|
+
|
|
116
|
+
console.log(`\n insights · your BOSS portfolio (local · nothing sent)\n`);
|
|
117
|
+
console.log(` ${rows.length} project(s) on this machine${gone ? ` (+${gone} registered but not on disk)` : ''}`);
|
|
118
|
+
|
|
119
|
+
// Graduation distribution across the mode ladder — the real "how far have ventures gotten".
|
|
120
|
+
const byMode = {};
|
|
121
|
+
for (const r of rows) byMode[r.mode] = (byMode[r.mode] || 0) + 1;
|
|
122
|
+
const ladder = STAGE_ORDER.map((s) => s.replace(/^L\d+-/, '')).map((label) => {
|
|
123
|
+
const k = Object.keys(byMode).find((m) => m.toLowerCase().startsWith(label.slice(0, 4).toLowerCase()));
|
|
124
|
+
return `${label} ${k ? byMode[k] : 0}`;
|
|
125
|
+
}).join(' · ');
|
|
126
|
+
const behind = rows.filter((r) => r.pin !== current).length;
|
|
127
|
+
console.log(` graduation: ${ladder}`);
|
|
128
|
+
console.log(` pins: ${rows.length - behind} current · ${behind} behind${behind ? ' (run /boss-sync there)' : ''}`);
|
|
129
|
+
|
|
130
|
+
// Time-to-graduation across the portfolio — idea→build cycle time, never throughput.
|
|
131
|
+
const cycles = rows.map((r) => r.toBuildDays).filter((d) => d != null).sort((a, b) => a - b);
|
|
132
|
+
if (cycles.length) {
|
|
133
|
+
const median = cycles[Math.floor((cycles.length - 1) / 2)];
|
|
134
|
+
console.log(` flow: idea→build median ${median}d (across ${cycles.length} graduated · cycle time, not throughput)`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`\n where each loop stands — idea → canvas → build`);
|
|
138
|
+
for (const r of rows) {
|
|
139
|
+
const here = cwd && r.path === cwd ? ' (here)' : '';
|
|
140
|
+
const stat = `${r.ideas} idea${r.ideas === 1 ? '' : 's'} · ${r.canvassed} canvassed${r.features ? ` · ${r.features} building` : ''}${r.toBuildDays != null ? ` · built in ${r.toBuildDays}d` : ''}`;
|
|
141
|
+
console.log(` ${MARK[r.signal] || ' '} ${String(r.name + here).padEnd(20)} ${String(r.mode).padEnd(11)} ${stat}`);
|
|
142
|
+
if (r.note) console.log(` ${''.padEnd(22)}${r.note}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`\n Measures graduation, not activity (idea→canvas→build→ship).`);
|
|
146
|
+
console.log(` Local-only. Cross-user learning is opt-in (shareUp); send something deliberately with /feedback.\n`);
|
|
147
|
+
}
|
package/src/learn.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync, readFileSync, writeFileSync, existsSync, mkdirSync, statSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { join, basename, resolve } from 'node:path';
|
|
5
|
+
import { BOSS_ROOT } from './paths.js';
|
|
6
|
+
import { listProjects } from './registry.js';
|
|
7
|
+
|
|
8
|
+
// The library/ subfolders a pattern can be routed UP into.
|
|
9
|
+
export const LIBRARY_CATEGORIES = ['agents', 'skills', 'hooks', 'practices', 'memory-seed'];
|
|
10
|
+
|
|
11
|
+
// `boss learn` writes into the BOSS SOURCE repo (mutable git checkout), not the
|
|
12
|
+
// installed package. When `boss` runs from a global symlink, BOSS_ROOT is the
|
|
13
|
+
// read-only npm copy — so we locate the dev checkout instead, in order:
|
|
14
|
+
// 1. $BOSS_SRC (explicit override)
|
|
15
|
+
// 2. the self-hosted project in the registry (BOSS dogfoods itself)
|
|
16
|
+
// 3. BOSS_ROOT, if we're running straight from a source checkout (.git + library/)
|
|
17
|
+
function looksLikeSource(dir) {
|
|
18
|
+
return !!dir && existsSync(join(dir, 'VERSION')) && existsSync(join(dir, 'library'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function bossSourceRoot() {
|
|
22
|
+
if (process.env.BOSS_SRC && looksLikeSource(process.env.BOSS_SRC)) {
|
|
23
|
+
return process.env.BOSS_SRC;
|
|
24
|
+
}
|
|
25
|
+
const self = listProjects().find((p) => p.selfHosted || /^(boss|bossbuild|blueprintos)$/i.test(p.name || ''));
|
|
26
|
+
if (self && looksLikeSource(self.path)) return self.path;
|
|
27
|
+
if (existsSync(join(BOSS_ROOT, '.git')) && looksLikeSource(BOSS_ROOT)) return BOSS_ROOT;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bump(version, kind) {
|
|
32
|
+
const [x, y, z] = version.trim().split('.').map((n) => parseInt(n, 10));
|
|
33
|
+
if (kind === 'major') return `${x + 1}.0.0`;
|
|
34
|
+
if (kind === 'patch') return `${x}.${y}.${z + 1}`;
|
|
35
|
+
return `${x}.${y + 1}.0`; // minor (default)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function prependChangelog(file, version, date, lines) {
|
|
39
|
+
const body = readFileSync(file, 'utf8');
|
|
40
|
+
const entry = `## ${version} — ${date}\n\n${lines.map((l) => `- ${l}`).join('\n')}\n\n`;
|
|
41
|
+
const at = body.indexOf('\n## ');
|
|
42
|
+
if (at < 0) return writeFileSync(file, body.trimEnd() + '\n\n' + entry);
|
|
43
|
+
// Insert just before the first existing version heading.
|
|
44
|
+
writeFileSync(file, body.slice(0, at + 1) + entry + body.slice(at + 1));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Route a proven pattern UP into the BOSS library + record the version bump.
|
|
48
|
+
// Returns a result object; throws Error (with a usage-friendly message) on misuse.
|
|
49
|
+
export function learn({ srcPath, category, note, versionKind = 'minor', explicitVersion }) {
|
|
50
|
+
if (!srcPath) throw new Error('usage: boss learn <path> --as <category> [--note "..."]');
|
|
51
|
+
if (!LIBRARY_CATEGORIES.includes(category)) {
|
|
52
|
+
throw new Error(`--as must be one of: ${LIBRARY_CATEGORIES.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
const abs = resolve(process.cwd(), srcPath);
|
|
55
|
+
if (!existsSync(abs)) throw new Error(`source not found: ${srcPath}`);
|
|
56
|
+
|
|
57
|
+
const root = bossSourceRoot();
|
|
58
|
+
if (!root) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'cannot locate the BOSS source repo. Set BOSS_SRC=/path/to/bossbuild, or run from the checkout.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Place it in library/<category>/<basename> (file or directory).
|
|
65
|
+
const destDir = join(root, 'library', category);
|
|
66
|
+
mkdirSync(destDir, { recursive: true });
|
|
67
|
+
const name = basename(abs);
|
|
68
|
+
const dest = join(destDir, name);
|
|
69
|
+
cpSync(abs, dest, { recursive: statSync(abs).isDirectory() });
|
|
70
|
+
|
|
71
|
+
// Bump VERSION + keep package.json in sync.
|
|
72
|
+
const versionFile = join(root, 'VERSION');
|
|
73
|
+
const prev = readFileSync(versionFile, 'utf8').trim();
|
|
74
|
+
const next = explicitVersion || bump(prev, versionKind);
|
|
75
|
+
writeFileSync(versionFile, next + '\n');
|
|
76
|
+
|
|
77
|
+
const pkgFile = join(root, 'package.json');
|
|
78
|
+
if (existsSync(pkgFile)) {
|
|
79
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
|
|
80
|
+
pkg.version = next;
|
|
81
|
+
writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Record it in the CHANGELOG (what /boss-sync reads to tell projects what's new).
|
|
85
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
86
|
+
const relDest = join('library', category, name);
|
|
87
|
+
const lines = [`Learned \`${name}\` into \`${relDest}\`.${note ? ' ' + note : ''}`];
|
|
88
|
+
const changelog = join(root, 'registry', 'CHANGELOG.md');
|
|
89
|
+
if (existsSync(changelog)) prependChangelog(changelog, next, date, lines);
|
|
90
|
+
|
|
91
|
+
return { root, dest: relDest, prev, next, category, name };
|
|
92
|
+
}
|