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,507 @@
|
|
|
1
|
+
// BOSS loop runtime (IDEA-008 promoted to FEAT in v0.18.0).
|
|
2
|
+
//
|
|
3
|
+
// Reads docs/loops/*.md from the project, parses their YAML frontmatter, and
|
|
4
|
+
// evaluates entry/exit predicates against the live project state. Returns a
|
|
5
|
+
// list of *signals* — one per loop whose state warrants attention (drifting,
|
|
6
|
+
// stalled, just-graduated, etc.). The conscience hook composes these signals
|
|
7
|
+
// into structured output for Claude.
|
|
8
|
+
//
|
|
9
|
+
// Predicate vocabulary (closed set; extend deliberately):
|
|
10
|
+
// - exists: { path } — a file/dir exists at the project-relative path
|
|
11
|
+
// - count_at_least: { path_glob, pattern, min, exclude_files_matching?, not_path_glob? }
|
|
12
|
+
// — N+ regex matches across globbed files
|
|
13
|
+
// - any_file_matches: { path_glob, pattern, related_idea_not_matching? }
|
|
14
|
+
// — at least one globbed file matches the regex;
|
|
15
|
+
// optional related-idea filter for canvas → idea
|
|
16
|
+
// cross-file checks
|
|
17
|
+
//
|
|
18
|
+
// Loop spec frontmatter:
|
|
19
|
+
// id: <slug>
|
|
20
|
+
// type: loop
|
|
21
|
+
// stage: <L0-quickstart | L1-mvp | ...>
|
|
22
|
+
// runner_type: hook | skill | manual | external
|
|
23
|
+
// entry: [<predicate>, ...]
|
|
24
|
+
// exit: [<predicate>, ...]
|
|
25
|
+
// drift_moment: caution | done | capture | restraint | <other>
|
|
26
|
+
// attributed_to: [<practitioner>, ...]
|
|
27
|
+
//
|
|
28
|
+
// Drift derivation (auto, no per-loop encoding needed):
|
|
29
|
+
// - All entry predicates satisfied AND any exit predicate not satisfied
|
|
30
|
+
// → loop is OPEN; emit a signal with the loop's drift_moment.
|
|
31
|
+
// - All entry + exit predicates satisfied → loop is CLOSED (no signal —
|
|
32
|
+
// unless the closure JUST happened; future work: session-state to detect
|
|
33
|
+
// just-closed transitions and emit "done" signals).
|
|
34
|
+
// - Entry predicates not satisfied → loop is UNOPENABLE; no signal.
|
|
35
|
+
|
|
36
|
+
import { readFileSync, writeFileSync, appendFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
37
|
+
import { join, dirname, basename } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { parseFrontmatter } from './yaml.js';
|
|
40
|
+
|
|
41
|
+
// Moments whose voiced instruction induces a model BOUNDED READ in the live turn
|
|
42
|
+
// (judgment past the predicate gate) — drift (v0.31), caution (v0.33). The rest
|
|
43
|
+
// are predicate-only (they point at a skill; no induced read). Used by the
|
|
44
|
+
// frequency ledger (v0.34) to flag which fires carry induced-judgment overhead.
|
|
45
|
+
export const JUDGE_MOMENTS = new Set(['drift', 'caution', 'capture', 'focus', 'coordination']);
|
|
46
|
+
|
|
47
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Glob expansion (single-`*`, single-level — no `**`).
|
|
51
|
+
// Examples: `docs/ideas/IDEA-*.md`, `docs/loops/*.md`.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function expandGlob(pattern, projectDir) {
|
|
55
|
+
const fullPattern = join(projectDir, pattern);
|
|
56
|
+
const dir = dirname(fullPattern);
|
|
57
|
+
const fileGlob = basename(fullPattern);
|
|
58
|
+
if (!existsSync(dir)) return [];
|
|
59
|
+
if (!fileGlob.includes('*')) {
|
|
60
|
+
return existsSync(fullPattern) ? [fullPattern] : [];
|
|
61
|
+
}
|
|
62
|
+
const regex = new RegExp(`^${fileGlob.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`);
|
|
63
|
+
return readdirSync(dir)
|
|
64
|
+
.filter((name) => regex.test(name))
|
|
65
|
+
.map((name) => join(dir, name));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matchesGlob(filePath, pattern, projectDir) {
|
|
69
|
+
return expandGlob(pattern, projectDir).includes(filePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Predicate evaluators.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const PREDICATES = {
|
|
77
|
+
exists({ path }, projectDir) {
|
|
78
|
+
return existsSync(join(projectDir, path));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
count_at_least({ path_glob, pattern, min, exclude_files_matching, not_path_glob }, projectDir) {
|
|
82
|
+
let files = expandGlob(path_glob, projectDir);
|
|
83
|
+
if (not_path_glob) {
|
|
84
|
+
files = files.filter((f) => !matchesGlob(f, not_path_glob, projectDir));
|
|
85
|
+
}
|
|
86
|
+
if (exclude_files_matching) {
|
|
87
|
+
const exclRe = new RegExp(exclude_files_matching, 'm');
|
|
88
|
+
files = files.filter((f) => {
|
|
89
|
+
try { return !exclRe.test(readFileSync(f, 'utf8')); } catch { return true; }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const re = new RegExp(pattern, 'gm');
|
|
93
|
+
let count = 0;
|
|
94
|
+
for (const f of files) {
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(f, 'utf8');
|
|
97
|
+
count += (content.match(re) || []).length;
|
|
98
|
+
} catch { /* ignore unreadable */ }
|
|
99
|
+
}
|
|
100
|
+
return { ok: count >= min, evidence: { count, min, files: files.length } };
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
any_file_matches({ path_glob, pattern, related_idea_not_matching }, projectDir) {
|
|
104
|
+
const files = expandGlob(path_glob, projectDir);
|
|
105
|
+
const re = new RegExp(pattern, 'm');
|
|
106
|
+
let matchedCount = 0;
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
// Optional cross-file filter: a canvas's related idea (strip `-canvas.md`,
|
|
109
|
+
// append `.md`) must NOT match a given pattern.
|
|
110
|
+
if (related_idea_not_matching) {
|
|
111
|
+
const idea = f.replace(/-canvas\.md$/, '.md');
|
|
112
|
+
if (existsSync(idea)) {
|
|
113
|
+
try {
|
|
114
|
+
const idText = readFileSync(idea, 'utf8');
|
|
115
|
+
if (new RegExp(related_idea_not_matching, 'm').test(idText)) continue;
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
if (re.test(readFileSync(f, 'utf8'))) matchedCount++;
|
|
121
|
+
} catch { /* ignore */ }
|
|
122
|
+
}
|
|
123
|
+
return { ok: matchedCount >= 1, evidence: { matched_files: matchedCount, total_files: files.length } };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Evaluate a single predicate. Always returns { ok, evidence }.
|
|
128
|
+
function evalPredicate(pred, projectDir) {
|
|
129
|
+
const type = Object.keys(pred).find((k) => PREDICATES[k]);
|
|
130
|
+
if (!type) return { ok: false, evidence: { error: `unknown predicate: ${JSON.stringify(pred)}` } };
|
|
131
|
+
try {
|
|
132
|
+
const res = PREDICATES[type](pred[type] || pred, projectDir);
|
|
133
|
+
// exists returns a bare boolean; normalize to { ok, evidence }.
|
|
134
|
+
if (typeof res === 'boolean') return { ok: res, evidence: { type, path: pred[type]?.path || pred.path } };
|
|
135
|
+
return res;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { ok: false, evidence: { error: e.message } };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Evaluate a list of predicates. Returns { all_ok, results }.
|
|
142
|
+
function evalList(preds, projectDir) {
|
|
143
|
+
const results = (preds || []).map((p) => evalPredicate(p, projectDir));
|
|
144
|
+
return { all_ok: results.every((r) => r.ok), results };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Loop loading + state classification.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function loadLoops(projectDir) {
|
|
152
|
+
const loopsDir = join(projectDir, 'docs', 'loops');
|
|
153
|
+
if (!existsSync(loopsDir)) return [];
|
|
154
|
+
return readdirSync(loopsDir)
|
|
155
|
+
.filter((n) => n.endsWith('.md'))
|
|
156
|
+
.map((n) => {
|
|
157
|
+
const path = join(loopsDir, n);
|
|
158
|
+
try {
|
|
159
|
+
const text = readFileSync(path, 'utf8');
|
|
160
|
+
const fm = parseFrontmatter(text);
|
|
161
|
+
if (!fm || fm.type !== 'loop') return null;
|
|
162
|
+
return { ...fm, _file: path };
|
|
163
|
+
} catch { return null; }
|
|
164
|
+
})
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function classifyLoop(loop, projectDir) {
|
|
169
|
+
const entry = evalList(loop.entry, projectDir);
|
|
170
|
+
const exit = evalList(loop.exit, projectDir);
|
|
171
|
+
let state;
|
|
172
|
+
if (!entry.all_ok) state = 'unopenable';
|
|
173
|
+
else if (entry.all_ok && exit.all_ok) state = 'closed';
|
|
174
|
+
else state = 'open';
|
|
175
|
+
return { state, entry, exit };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Signal composition.
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export function detectSignals(projectDir) {
|
|
183
|
+
const loops = loadLoops(projectDir);
|
|
184
|
+
const signals = [];
|
|
185
|
+
for (const loop of loops) {
|
|
186
|
+
// Only `hook`-runner loops emit signals automatically; skill/manual/external
|
|
187
|
+
// are tested by their own runners.
|
|
188
|
+
if (loop.runner_type && loop.runner_type !== 'hook') continue;
|
|
189
|
+
|
|
190
|
+
// Loops without a `drift_moment` are structural — they express dependencies
|
|
191
|
+
// downstream loops check, but don't themselves emit signals when open. (E.g.
|
|
192
|
+
// capture-loop: its job is to be the upstream of canvas-loop; it doesn't
|
|
193
|
+
// drift just because a fresh project has no captures yet — that's the
|
|
194
|
+
// over-fires-on-fresh-project failure mode the moment-1 evals catch.)
|
|
195
|
+
if (!loop.drift_moment) continue;
|
|
196
|
+
|
|
197
|
+
const { state, entry, exit } = classifyLoop(loop, projectDir);
|
|
198
|
+
if (state !== 'open') continue;
|
|
199
|
+
|
|
200
|
+
const confidence = computeConfidence(loop, entry, exit);
|
|
201
|
+
signals.push({
|
|
202
|
+
loop_id: loop.id,
|
|
203
|
+
type: 'stalled',
|
|
204
|
+
moment: loop.drift_moment || 'caution',
|
|
205
|
+
confidence,
|
|
206
|
+
evidence: {
|
|
207
|
+
entry: entry.results.map((r) => r.evidence),
|
|
208
|
+
exit: exit.results.map((r) => r.evidence),
|
|
209
|
+
},
|
|
210
|
+
suppress_if: [],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return signals;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Confidence: scales with how much "drift overshoot" exists past the entry
|
|
217
|
+
// threshold. Captured here as a heuristic — refines via eval feedback.
|
|
218
|
+
function computeConfidence(loop, entry) {
|
|
219
|
+
// Find a count-style entry predicate and read its count vs min.
|
|
220
|
+
for (const r of entry.results || []) {
|
|
221
|
+
if (r.evidence && typeof r.evidence.count === 'number' && typeof r.evidence.min === 'number') {
|
|
222
|
+
const ratio = r.evidence.count / r.evidence.min;
|
|
223
|
+
if (ratio >= 2) return 'high';
|
|
224
|
+
if (ratio >= 1.33) return 'medium';
|
|
225
|
+
return 'low';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return 'medium';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Read the optional founder-cohort declaration from .boss/config.json (v0.20.0+).
|
|
232
|
+
// Returns null if no config or no cohort field — Claude composes the voice
|
|
233
|
+
// generically when cohort is null.
|
|
234
|
+
export function readCohort(projectDir) {
|
|
235
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
236
|
+
if (!existsSync(f)) return null;
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(readFileSync(f, 'utf8')).cohort || null;
|
|
239
|
+
} catch { return null; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Read a BOUNDED slice of the venture brain (.boss/brain/read.md) so the
|
|
243
|
+
// conscience can voice WITH continuity (IDEA-022 Track 4): the standing summary
|
|
244
|
+
// (preamble) + the single most recent dated read. Bounded on purpose — continuity,
|
|
245
|
+
// not the whole history (structured-output discipline on the input side, same as
|
|
246
|
+
// drift-loop's bounded read). Returns null when there's no brain yet, so the
|
|
247
|
+
// conscience speaks generically and the output is byte-identical to before.
|
|
248
|
+
export function readBrainContext(projectDir) {
|
|
249
|
+
try {
|
|
250
|
+
const f = join(projectDir, '.boss', 'brain', 'read.md');
|
|
251
|
+
if (!existsSync(f)) return null;
|
|
252
|
+
const text = readFileSync(f, 'utf8');
|
|
253
|
+
if (!text.trim()) return null;
|
|
254
|
+
// Line-based split (robust): preamble = everything before the first dated
|
|
255
|
+
// `## YYYY-MM-DD` header; keep only the LAST dated block.
|
|
256
|
+
const dateRe = /^##\s+\d{4}-\d{2}-\d{2}\b/;
|
|
257
|
+
const preambleLines = [];
|
|
258
|
+
const blocks = [];
|
|
259
|
+
let cur = null;
|
|
260
|
+
for (const l of text.split('\n')) {
|
|
261
|
+
if (dateRe.test(l)) { if (cur) blocks.push(cur); cur = [l]; }
|
|
262
|
+
else if (cur) cur.push(l);
|
|
263
|
+
else preambleLines.push(l);
|
|
264
|
+
}
|
|
265
|
+
if (cur) blocks.push(cur);
|
|
266
|
+
const preamble = preambleLines.join('\n').trim();
|
|
267
|
+
const lastBlock = blocks.length ? blocks[blocks.length - 1].join('\n').trim() : '';
|
|
268
|
+
let out = [preamble, lastBlock].filter(Boolean).join('\n\n');
|
|
269
|
+
const CAP = 1400; // bounded; the brain is continuity, not the whole file
|
|
270
|
+
if (out.length > CAP) out = out.slice(0, CAP).trimEnd() + ' …';
|
|
271
|
+
return out || null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Read a BOUNDED slice of the relationship log (.boss/brain/relationship.md) — the
|
|
278
|
+
// most recent session of what the conscience SAID and what the founder DID with it.
|
|
279
|
+
// This is what lets the conscience LEARN: "I've raised this before and you moved
|
|
280
|
+
// past it" / "last time I nudged drift you ran a test — good." Returns null when
|
|
281
|
+
// there's no log yet (byte-identical output, evals unaffected).
|
|
282
|
+
export function readRelationshipContext(projectDir) {
|
|
283
|
+
try {
|
|
284
|
+
const f = join(projectDir, '.boss', 'brain', 'relationship.md');
|
|
285
|
+
if (!existsSync(f)) return null;
|
|
286
|
+
const text = readFileSync(f, 'utf8');
|
|
287
|
+
if (!text.trim()) return null;
|
|
288
|
+
const dateRe = /^##\s+\d{4}-\d{2}-\d{2}\b/;
|
|
289
|
+
const blocks = [];
|
|
290
|
+
let cur = null;
|
|
291
|
+
let preamble = [];
|
|
292
|
+
for (const l of text.split('\n')) {
|
|
293
|
+
if (dateRe.test(l)) { if (cur) blocks.push(cur); cur = [l]; }
|
|
294
|
+
else if (cur) cur.push(l);
|
|
295
|
+
else preamble.push(l);
|
|
296
|
+
}
|
|
297
|
+
if (cur) blocks.push(cur);
|
|
298
|
+
// The most recent 1-2 logged sessions — recent outcomes, not the whole history.
|
|
299
|
+
const recent = blocks.slice(-2).map((b) => b.join('\n').trim()).join('\n\n');
|
|
300
|
+
let out = recent || preamble.join('\n').trim();
|
|
301
|
+
const CAP = 900;
|
|
302
|
+
if (out.length > CAP) out = out.slice(0, CAP).trimEnd() + ' …';
|
|
303
|
+
return out || null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Read the conscience pause state from .boss/config.json (v0.23.0+, IDEA-011).
|
|
310
|
+
// Returns { mode, since, expires, reason } or null. Mode is 'paused' or 'active'
|
|
311
|
+
// (or null when never set). When paused, the hook exits silent if not expired.
|
|
312
|
+
export function readPauseState(projectDir) {
|
|
313
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
314
|
+
if (!existsSync(f)) return null;
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(readFileSync(f, 'utf8')).conscience || null;
|
|
317
|
+
} catch { return null; }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Clear the conscience pause state (set mode: 'active'). Called by the hook when
|
|
321
|
+
// it detects an expired pause — the auto-resume IS the kindness. The founder
|
|
322
|
+
// learns the pause ended because the conscience starts speaking again on the
|
|
323
|
+
// next prompt; we don't emit a special "your pause expired" signal (that would
|
|
324
|
+
// be performative noise; IDEA-011 explicitly chose silent auto-resume).
|
|
325
|
+
export function clearPauseState(projectDir) {
|
|
326
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
327
|
+
if (!existsSync(f)) return;
|
|
328
|
+
try {
|
|
329
|
+
const cfg = JSON.parse(readFileSync(f, 'utf8'));
|
|
330
|
+
cfg.conscience = { mode: 'active' };
|
|
331
|
+
writeFileSync(f, JSON.stringify(cfg, null, 2) + '\n');
|
|
332
|
+
} catch { /* fail silent — hook must never block */ }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Per-moment mute (v0.72.0) — the surgical companion to pause. `pause` silences
|
|
336
|
+
// the WHOLE conscience for a bounded session; a mute silences ONE moment (drift,
|
|
337
|
+
// caution, capture, …) until it expires or is unmuted. This is the hook-enforced
|
|
338
|
+
// "don't voice it if I don't want it" — consent at the granularity of the moment,
|
|
339
|
+
// not all-or-nothing.
|
|
340
|
+
//
|
|
341
|
+
// Stored under its OWN top-level key (`conscienceMutes`), deliberately NOT inside
|
|
342
|
+
// `cfg.conscience`: pause/resume overwrite `cfg.conscience` wholesale, so nesting
|
|
343
|
+
// mutes there would let a `resume` silently wipe them. The two controls are
|
|
344
|
+
// orthogonal by construction. Shape:
|
|
345
|
+
// cfg.conscienceMutes = { <moment>: { until: ISO|null, since: ISO, reason } }
|
|
346
|
+
export function readMuteState(projectDir) {
|
|
347
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
348
|
+
if (!existsSync(f)) return {};
|
|
349
|
+
try {
|
|
350
|
+
return JSON.parse(readFileSync(f, 'utf8')).conscienceMutes || {};
|
|
351
|
+
} catch { return {}; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Is this moment muted right now (and not expired)? Pure read; expiry pruning is
|
|
355
|
+
// clearExpiredMutes's job. Used by the hook to filter signals and by the CLI to
|
|
356
|
+
// show only live mutes.
|
|
357
|
+
export function isMomentMuted(mutes, moment, now = new Date()) {
|
|
358
|
+
const m = mutes[moment];
|
|
359
|
+
if (!m) return false;
|
|
360
|
+
if (m.until && new Date(m.until) <= now) return false; // expired → speaks again
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Prune any mutes whose `until` has passed — the per-moment twin of pause's silent
|
|
365
|
+
// auto-resume. The founder learns a mute lapsed because the moment starts speaking
|
|
366
|
+
// again, not via a "your mute expired" announcement (that would be the performative
|
|
367
|
+
// noise IDEA-011 rejected). Returns true if it wrote. Swallows errors — like every
|
|
368
|
+
// hook-path write, it must never block the prompt.
|
|
369
|
+
export function clearExpiredMutes(projectDir) {
|
|
370
|
+
const f = join(projectDir, '.boss', 'config.json');
|
|
371
|
+
if (!existsSync(f)) return false;
|
|
372
|
+
try {
|
|
373
|
+
const cfg = JSON.parse(readFileSync(f, 'utf8'));
|
|
374
|
+
const mutes = cfg.conscienceMutes || {};
|
|
375
|
+
const now = new Date();
|
|
376
|
+
let changed = false;
|
|
377
|
+
for (const [moment, m] of Object.entries(mutes)) {
|
|
378
|
+
if (m && m.until && new Date(m.until) <= now) { delete mutes[moment]; changed = true; }
|
|
379
|
+
}
|
|
380
|
+
if (changed) {
|
|
381
|
+
if (Object.keys(mutes).length === 0) delete cfg.conscienceMutes;
|
|
382
|
+
else cfg.conscienceMutes = mutes;
|
|
383
|
+
writeFileSync(f, JSON.stringify(cfg, null, 2) + '\n');
|
|
384
|
+
}
|
|
385
|
+
return changed;
|
|
386
|
+
} catch { return false; }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Append one line to .boss/conscience-log.jsonl — a FREQUENCY ledger (v0.34.0).
|
|
390
|
+
//
|
|
391
|
+
// BOSS eating its own /ai-cost dogfood — HONESTLY. The hook never calls a model,
|
|
392
|
+
// so a token/dollar estimate would be lying with numbers: the dominant cost
|
|
393
|
+
// (the induced bounded reads judge-moments trigger in the main turn) is
|
|
394
|
+
// invisible here. So we log FACTS, not estimates — which moments fired, whether
|
|
395
|
+
// any induces a model read (judge-moment), and the injected-context CHAR count.
|
|
396
|
+
// The real way a conscience becomes costly/annoying is OVER-FIRING; that's what
|
|
397
|
+
// this measures. Measure-only — it never throttles (a throttle would gag the
|
|
398
|
+
// conscience exactly when a drifting founder needs it most: humane before viable).
|
|
399
|
+
//
|
|
400
|
+
// CORRECTNESS-INVISIBLE — the hook's first fire-path side effect. Runs only when
|
|
401
|
+
// something fired (after the silent early-exit), append-only, single write, in
|
|
402
|
+
// its own swallowing try/catch. Delete it entirely and the conscience behaves
|
|
403
|
+
// identically. Telemetry must never affect the conscience.
|
|
404
|
+
export function logActivity(projectDir, signals, additionalContext, cohort) {
|
|
405
|
+
try {
|
|
406
|
+
if (!signals || signals.length === 0) return;
|
|
407
|
+
const entry = {
|
|
408
|
+
ts: new Date().toISOString(),
|
|
409
|
+
moments: signals.map((s) => ({ moment: s.moment, confidence: s.confidence })),
|
|
410
|
+
judge: signals.some((s) => JUDGE_MOMENTS.has(s.moment)),
|
|
411
|
+
injected_chars: (additionalContext || '').length,
|
|
412
|
+
cohort: cohort || null,
|
|
413
|
+
};
|
|
414
|
+
appendFileSync(join(projectDir, '.boss', 'conscience-log.jsonl'), JSON.stringify(entry) + '\n');
|
|
415
|
+
} catch { /* fail silent — the ledger is overhead, never a gate */ }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Per-cohort framing directives (v0.20.0+). Added to additionalContext so the
|
|
419
|
+
// model composes the conscience voice appropriately for the founder's cohort.
|
|
420
|
+
// Personas in v0.19 surfaced that one-sized voice fails first-product and
|
|
421
|
+
// returning-founder differently — first-product needs teaching; returning-founder
|
|
422
|
+
// wants a harder question. The signal stays the same; the *voice* varies.
|
|
423
|
+
const COHORT_FRAMING = {
|
|
424
|
+
'vibe-coder-newbie':
|
|
425
|
+
'This founder is a vibe-coding newbie (no eng/startup background, ~6 months into AI tools, learns by doing). Avoid jargon. Show, don\'t lecture. Specifics over categories.',
|
|
426
|
+
'eng-builder':
|
|
427
|
+
'This founder is an experienced engineer turned first-time founder. Be terse and inspectable. They want transparency, not encouragement; respect their tooling fluency. The founder skills are new; the eng skills are not.',
|
|
428
|
+
'non-tech-founder':
|
|
429
|
+
'This founder has deep domain expertise but no coding background; AI is their bridge. Use plain language, not framework jargon. They respect mentor framing (they\'ve had real mentors); they have no patience for tech-bro phrasing.',
|
|
430
|
+
'first-product':
|
|
431
|
+
'This founder is an ABSOLUTE BEGINNER — first product ever; may not know what an MVP is. *Teach, don\'t grade.* Define terms inline. Invite, never assess. Their face when they read the nudge IS the design signal — if they\'d feel stupid, the nudge is wrong.',
|
|
432
|
+
'vibe-virtuoso':
|
|
433
|
+
'This founder ships a lot but doesn\'t sustain. Don\'t coach the discipline they\'ve already read books about and won\'t do. Ask SHARPER questions; lean into the architecture they respect (the override pattern, the structured signal). The voice they hear most is praise — give them friction instead.',
|
|
434
|
+
'indie-hacker':
|
|
435
|
+
'This founder is in the right-sized / calm-company lineage (Walling/Fried/Jarvis). Anti-VC by choice. Plain Fitzpatrick-style language lands; framework jargon does not. Use understatement; "this is fine" is high praise.',
|
|
436
|
+
'returning-founder':
|
|
437
|
+
'This founder has shipped before. *Skip the 101.* Ask the HARDER cohort-aware version: "is your conviction here at the level it needs to be for 12 months" not "what does this prove." Respect experience; don\'t teach the obvious.',
|
|
438
|
+
'domain-expert':
|
|
439
|
+
'This founder has 10+ years in a high-stakes domain (medical/legal/financial). Real stakes; real regulatory context. Caveat appropriately. Ask about who specifically could be harmed; lean into the humane lens. Avoid generic startup advice that won\'t fit the domain.',
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// Compose `additionalContext` for hosts that consume the flat field. For one
|
|
443
|
+
// signal, a single nudge; for multiple, a brief enumeration. Voice stays with
|
|
444
|
+
// the model — this hands signal + ask + cohort frame, not canned voice.
|
|
445
|
+
export function composeContext(signals, opts = {}) {
|
|
446
|
+
if (!signals.length) return null;
|
|
447
|
+
const cohort = opts.cohort || null;
|
|
448
|
+
const cohortLine = cohort && COHORT_FRAMING[cohort]
|
|
449
|
+
? `\n\nCohort framing — ${cohort}: ${COHORT_FRAMING[cohort]}`
|
|
450
|
+
: '';
|
|
451
|
+
// Continuity (IDEA-022 Track 4): when a venture brain exists, hand the model its
|
|
452
|
+
// standing read so the nudge is voiced WITH what the conscience already understands
|
|
453
|
+
// — the "how did it know that" specificity that earns trust. Added only when a brain
|
|
454
|
+
// is present, so output is byte-identical when there's none.
|
|
455
|
+
const brainLine = opts.brain
|
|
456
|
+
? `\n\nContinuity — your standing read on this venture (the conscience's own POV over time, from .boss/brain/). Voice the nudge *with* this: make it specific to what you already understand instead of generic, and ground it in the read. Don't read it back as fact or restate it — let it sharpen the one line you say. If it conflicts with what you see now, trust what you see (the founder can correct the brain):\n${opts.brain}`
|
|
457
|
+
: '';
|
|
458
|
+
// Learning (IDEA-022 — the relationship half): what you said recently and what the
|
|
459
|
+
// founder DID with it. Use it to adjust: if you've raised this before and they moved
|
|
460
|
+
// past it with a good reason, say it lighter or drop it; if a past nudge landed, you can
|
|
461
|
+
// build on it. Don't nag a point they've already answered.
|
|
462
|
+
const relationshipLine = opts.relationship
|
|
463
|
+
? `\n\nWhat happened last time (from the relationship log — what you said and what they did): use this to *calibrate*, not repeat. If you've already raised this and they moved past it for a stated reason, don't say it again the same way (lighten it, or stay silent). If a past nudge landed, you can build on it rather than restart:\n${opts.relationship}`
|
|
464
|
+
: '';
|
|
465
|
+
if (signals.length === 1) {
|
|
466
|
+
return signalAsContext(signals[0]) + cohortLine + brainLine + relationshipLine;
|
|
467
|
+
}
|
|
468
|
+
const parts = signals.map((s, i) => `(${i + 1}) ${signalAsContext(s)}`);
|
|
469
|
+
return `[BOSS conscience — ${signals.length} signals]\n` + parts.join('\n') + cohortLine + brainLine + relationshipLine;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function signalAsContext(s) {
|
|
473
|
+
const moment = s.moment || 'attention';
|
|
474
|
+
const loopId = s.loop_id || 'loop';
|
|
475
|
+
// Per-moment phrasing — gives the model a starting frame; it composes the voice.
|
|
476
|
+
// Voice lineage (v0.20.0+): leaning Fitzpatrick (talk-to-someone, plain language)
|
|
477
|
+
// consistently. Indie-hacker persona caught the prior Fitzpatrick/Maurya mix; this
|
|
478
|
+
// chooses the cohort-portable version.
|
|
479
|
+
if (moment === 'caution') {
|
|
480
|
+
return `[BOSS conscience — ${loopId} stalled · ${s.confidence} confidence] The ${loopId} is open: ≥3 ideas/captures exist but no canvas names a real riskiest assumption yet. Before voicing, do the judgment the predicate can't (v0.33): silently read the active idea's capture log. If the captures are ONE idea getting sharper — each entry refining the same bet, narrowing the user, finding the real pain, or wrestling the same hard question — that's DEPTH, not avoidance, and convergence toward a canvas. Stay silent; firing here punishes exactly the thinking caution should encourage. Fire only if the captures are scattered or accumulating without converging on a bet: idea-hopping (each capture a different product), feature-piling (scope growing, no customer or risk named), or market/competitor-watching with no bet of their own forming — the capturing-lots / validating-nothing drift. If it does fit: name the *specific* pattern you read in one spare line (not a generic "you should validate"), ask *what they'd want to learn* before going further (or *who they'd ask first* — Fitzpatrick-style), point at \`/canvas\`, and hand the decision back. Say it at most once; if you've already raised it this session or the user is clearly mid-other-work, stay silent. It's a nudge, never a gate.`;
|
|
481
|
+
}
|
|
482
|
+
if (moment === 'restraint') {
|
|
483
|
+
return `[BOSS conscience — ${loopId} premature · ${s.confidence} confidence] The founder is reaching for ${loopId} but an upstream artifact is missing. If it fits the moment, surface BOSS's restraint nudge in your own voice: name what's missing in one line, offer to back up, hand the decision back. Never block.`;
|
|
484
|
+
}
|
|
485
|
+
if (moment === 'cost') {
|
|
486
|
+
return `[BOSS conscience — ${loopId} unbudgeted · ${s.confidence} confidence] The code calls an LLM but no AI cost budget has been declared (or the cost-logger isn't wired). If it fits the moment, surface BOSS's nudge in your own voice: name that the bill exists in one line (the cohort decides the framing — first-product wants a number, vibe-virtuoso wants the inspect affordance, domain-expert wants the privacy posture), point at \`/ai-cost\`, hand the decision back. Never block.`;
|
|
487
|
+
}
|
|
488
|
+
if (moment === 'failure-mode') {
|
|
489
|
+
return `[BOSS conscience — ${loopId} undesigned · ${s.confidence} confidence] The code calls an LLM but no failure-states design exists (no \`docs/ai-failure-states.md\` or no fallback handlers wired). The five failure modes always exist (garbage / refusal / hallucination / timeout / cost-spike); they just aren't designed yet. If it fits the moment, surface BOSS's nudge in your own voice: name that the failures are unmet in one line (cohort decides framing — first-product wants patterns named, eng-builder wants the unhandled-path lint angle, domain-expert wants the human-in-the-loop framing for high-stakes domains), point at \`/ai-failure-states\`, hand the decision back. Never block.`;
|
|
490
|
+
}
|
|
491
|
+
if (moment === 'capture') {
|
|
492
|
+
return `[BOSS conscience — ${loopId} extractable · ${s.confidence} confidence] PRINCIPLE #1's own moment: the founder has accumulated work (devlog ≥3 dated entries) and hasn't recorded an extraction decision yet. But ≥3 entries is only the gate — before voicing, do the judgment the predicate can't (v0.39): silently read the ~5 most recent devlog entries. Fire ONLY if there's a real extraction candidate in that work — a pattern built twice (a reusable practice → UP into BOSS's library), a fix or guard hand-applied in several places (hardening → DOWN into the app's core), or a manual ritual repeated enough to deserve a skill/loop. If the recent work is one-off (distinct features that don't repeat), deep focus on a single still-in-progress thing, or early throwaway spikes, then nothing has generalized yet — stay silent. Nudging \`/extract\` with nothing to extract earns a NOT-YET every time and trains the founder to tune the conscience out; that's the premature ceremony PRINCIPLE #2 warns against. If it DOES fit: name the *specific* repeated pattern you read in one spare line (not a generic "you should extract patterns"). Cohort decides framing — returning-founder wants the seasoned "what did you do twice?" prompt, first-product wants the gentler "here's what the pause is for," indie-hacker wants the calm-company frame. **Don't sound like a productivity-reward.** The principle is the discipline, not the dopamine. Point at \`/extract\`, hand the decision back. Say it at most once this session; never block.`;
|
|
493
|
+
}
|
|
494
|
+
if (moment === 'focus') {
|
|
495
|
+
return `[BOSS conscience — ${loopId} piling up · ${s.confidence} confidence] The board shows ≥4 FEATs in Building and nothing Shipped: a pile started, nothing finished. But the count is only the gate — before voicing, do the judgment the predicate can't (IDEA-034): silently read the board (\`boss board\`, or the in-flight FEATs' status + \`building_since\`). Fire ONLY if this is real focus-drift — work scattered across many half-built FEATs, the oldest aging in build, each started then left for the next thing (the "stop starting, start finishing" smell). If it's honest parallel work — a few genuinely-concurrent tracks a small team is carrying, or things blocked-on-review rather than abandoned — stay silent; firing there punishes legitimate parallelism. If it DOES fit: name the *specific* pile in one spare line (which FEATs, how long the oldest has been open — not a generic "limit your WIP"), and ask which *one* they'd finish first. Cohort decides framing — returning-founder wants the blunt "five started, none shipped — which is real?"; first-product wants "finishing one beats starting three, here's why" taught plainly; indie-hacker wants the calm "small and done beats big and open." **Don't sound like a productivity scold.** Point at finishing one (\`/close\` when it ships) or \`/revalidate\` for the stalest. Say it at most once this session; never block — it's a nudge, and shipping anything silences it.`;
|
|
496
|
+
}
|
|
497
|
+
if (moment === 'cost-stale') {
|
|
498
|
+
return `[BOSS conscience — ${loopId} unread · ${s.confidence} confidence] The founder declared an AI cost budget (\`docs/ai-cost-budget.md\` exists) but hasn't recorded a cost review yet. Declaring is half the discipline; reading the ledger is the other half. If it fits the moment, surface BOSS's nudge in your own voice: name the unread-ledger gap in one line (cohort decides framing — indie-hacker wants the calm-company "%-of-revenue" frame, returning-founder wants unit-economics, eng-builder wants the inspectable numbers, domain-expert wants the privacy-first confirmation first). **Don't sound like a productivity-reward.** Point at \`/cost-review\`, hand the decision back. Never block.`;
|
|
499
|
+
}
|
|
500
|
+
if (moment === 'drift') {
|
|
501
|
+
return `[BOSS conscience — ${loopId} adrift · ${s.confidence} confidence] The founder named a riskiest assumption on the canvas but hasn't recorded a validation plan for it (no real "Experiment this week" line), and work has been accumulating (≥3 devlog entries). This is the moment to check the work *against the named bet* — the comparison predicates can't make and you can. If — and ONLY if — it fits this moment: silently read the riskiest-assumption line on the canvas (\`docs/ideas/*-canvas.md\`), then the most recent ~5 entries of \`docs/devlog.md\`, plus the open FEAT/spec if there is one. Read only that — not the whole project. Then judge: is that recent work actually *testing* the named risk, or building *around* it? If it has drifted, name the specific gap in one spare line — "you said X is the bet that could sink this; the last sessions built Y and Z; neither tests X" — and ask what the smallest experiment on the risk would be (point at \`/canvas\` to write it, or \`/pretotype\` to run it; if they want the full whole-project audit rather than this bounded read, \`/drift-deep\`). If the work IS engaging the risk, stay silent — silence is the correct output when they're on-aim. This is not a "you've been productive!" reward and not a generic "you should validate" line; the value is the specific stated-vs-actual comparison. Cohort decides framing — returning-founder wants the harder "is your conviction here where it needs to be for 12 months" cut, first-product wants "here's what 'test your riskiest bet' means" taught plainly, domain-expert wants the who-could-be-harmed lens on the named risk. Say it at most once this session; never block.`;
|
|
502
|
+
}
|
|
503
|
+
if (moment === 'coordination') {
|
|
504
|
+
return `[BOSS conscience — ${loopId} open · ${s.confidence} confidence] A founding-team seam signal (IDEA-037 slice 5b): this is a *team* (a cofounder is on the roster), real work has happened (devlog ≥3), and **not one decision has been recorded together** (\`docs/decisions/\` is empty). The evidence behind this is that AI erodes the human-to-human seam *invisibly* — a pair can feel productive while building in parallel, each in their own AI session, never deciding together. But ≥0 DECs is only the gate, and it's WEAK-transfer evidence — before voicing, do the judgment the predicate can't: silently read the bounded slice — \`docs/decisions/\` (empty), \`boss board\` (who's building what), \`boss team\` (who's on the venture). Fire ONLY if it reads like a real seam: work flowing through one founder's agent while the shared log sits untouched by the other, divergence with nothing decided jointly. If the deciding is plausibly happening *off-repo* (a distributed pair who talk on calls and just haven't written a \`DEC\`), **stay silent** — a quiet log is NOT proof of a problem, and firing there punishes a healthy team. If it DOES fit: name the *specific* structural observation in one spare line (building a while, nothing decided together — not a generic "communicate more"), and ask the *coordination* question — are you two actually deciding this jointly, or in parallel? Point at \`/decide\` (record one together) and \`mentor-cofounder\` (the deeper coaching). **Serve the partnership as the unit; NEVER take a side** — surface the seam, never say whose fault it is. Cohort decides framing — non-tech-founder wants plain "are you and your cofounder on the same page on the big calls?", eng-builder wants the terse structural read, returning-founder wants the blunt "you've shipped for weeks and not one joint decision — is one of you actually driving alone?". Say it at most once this session; never block — recording a single decision together silences it.`;
|
|
505
|
+
}
|
|
506
|
+
return `[BOSS conscience — ${loopId} (${moment}) · ${s.confidence} confidence] signal warrants attention.`;
|
|
507
|
+
}
|