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
package/src/map.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// boss map — the live cheatsheet (IDEA-018). Where you are on the ladder, what
|
|
2
|
+
// you can run right now (grouped by the rung that unlocked it), and what's one
|
|
3
|
+
// unlock away. Like `boss board`, it's a pure render of state the project
|
|
4
|
+
// already holds — the .boss stamp + the installed SKILL.md files — so there is
|
|
5
|
+
// nothing to maintain and nothing to drift.
|
|
6
|
+
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { STAGE_ORDER } from './paths.js';
|
|
10
|
+
import { loadModes, packageSkillMd, skillGloss, modeWord } from './modes.js';
|
|
11
|
+
|
|
12
|
+
function projectSkillMd(projectDir, name) {
|
|
13
|
+
return join(projectDir, '.claude', 'skills', name, 'SKILL.md');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Gloss for an installed skill: prefer the project's OWN copy (truthful about
|
|
17
|
+
// local edits), fall back to the package stage that defines it.
|
|
18
|
+
function installedGloss(projectDir, name, definedIn) {
|
|
19
|
+
const own = projectSkillMd(projectDir, name);
|
|
20
|
+
if (existsSync(own)) return skillGloss(own);
|
|
21
|
+
if (definedIn) return skillGloss(packageSkillMd(definedIn, name));
|
|
22
|
+
return { gloss: '', usage: '' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderMap(projectDir, stamp) {
|
|
26
|
+
const modes = loadModes();
|
|
27
|
+
const byId = Object.fromEntries(modes.map((m) => [m.id, m]));
|
|
28
|
+
// skill name -> the stage id that first introduces it (ladder order).
|
|
29
|
+
const skillStage = {};
|
|
30
|
+
for (const m of modes) for (const s of m.skills || []) if (!(s in skillStage)) skillStage[s] = m.id;
|
|
31
|
+
|
|
32
|
+
const installed = stamp.installedLayers || [stamp.stage];
|
|
33
|
+
const deepest = installed[installed.length - 1];
|
|
34
|
+
|
|
35
|
+
// Scannable, single-line glosses. Substitute the project name into any
|
|
36
|
+
// not-yet-installed (package) gloss so the "one unlock away" preview reads as
|
|
37
|
+
// what the founder would actually get; cap width so a long sentence can't
|
|
38
|
+
// dominate the terminal.
|
|
39
|
+
const fit = (g) => {
|
|
40
|
+
let t = (g || '').replace(/\{\{PROJECT_NAME\}\}/g, stamp.name).replace(/\{\{[^}]+\}\}/g, '');
|
|
41
|
+
if (t.length > 64) t = t.slice(0, 63).trimEnd() + '…';
|
|
42
|
+
return t;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(` ${stamp.name} · map`);
|
|
48
|
+
lines.push(` ▸ You are here: ${stamp.mode || stamp.stage} (${installed.join(' → ')})`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
|
|
51
|
+
// Available now — grouped by the rung that unlocked each skill, ladder order.
|
|
52
|
+
lines.push(' Available now');
|
|
53
|
+
for (const layerId of STAGE_ORDER) {
|
|
54
|
+
if (!installed.includes(layerId)) continue;
|
|
55
|
+
const mode = byId[layerId];
|
|
56
|
+
const skillsHere = (stamp.skills || []).filter((s) => skillStage[s] === layerId).sort();
|
|
57
|
+
if (!skillsHere.length) continue;
|
|
58
|
+
lines.push(` ${mode.name}`);
|
|
59
|
+
for (const s of skillsHere) {
|
|
60
|
+
const { gloss } = installedGloss(projectDir, s, layerId);
|
|
61
|
+
lines.push(` /${s.padEnd(18)} ${fit(gloss)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
|
|
66
|
+
// One unlock away — read the next rung's skills from the package (not yet
|
|
67
|
+
// installed here), so the founder sees what they'd gain before committing.
|
|
68
|
+
const idx = STAGE_ORDER.indexOf(deepest);
|
|
69
|
+
const nextId = idx >= 0 ? STAGE_ORDER[idx + 1] : null;
|
|
70
|
+
if (nextId) {
|
|
71
|
+
const next = byId[nextId];
|
|
72
|
+
if (next && next.authored) {
|
|
73
|
+
lines.push(` One unlock away: ${next.name} → boss unlock ${modeWord(nextId)}`);
|
|
74
|
+
for (const s of next.skills || []) {
|
|
75
|
+
const { gloss } = skillGloss(packageSkillMd(nextId, s));
|
|
76
|
+
lines.push(` /${s.padEnd(18)} ${fit(gloss)}`);
|
|
77
|
+
}
|
|
78
|
+
if (next.graduationHint) lines.push(` ${next.graduationHint}`);
|
|
79
|
+
} else if (next) {
|
|
80
|
+
lines.push(` One unlock away: ${next.name} — not authored yet.`);
|
|
81
|
+
}
|
|
82
|
+
lines.push('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Standing controls — always available, mode-independent (the git-cheatsheet core).
|
|
86
|
+
lines.push(' Anytime');
|
|
87
|
+
lines.push(' boss board [--html] what\'s in flight (captured → shipped); --html = visual kanban');
|
|
88
|
+
lines.push(' boss brain the conscience\'s read on this venture');
|
|
89
|
+
lines.push(' boss insights how far your ventures have gotten (local)');
|
|
90
|
+
lines.push(' boss team [add @user] who\'s on the venture — add a cofounder (solo by default)');
|
|
91
|
+
lines.push(' boss status --conscience loop states + cohort + recent overrides');
|
|
92
|
+
lines.push(' boss conscience pause --for 8h silence the whole conscience for a sprint');
|
|
93
|
+
lines.push(' boss conscience mute <moment> turn down just one nudge (drift|caution|…)');
|
|
94
|
+
lines.push(' /boss-sync pull the latest BOSS practices into this project');
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push(' The map is a read of your install. To change it, climb a rung: boss unlock <mode>.');
|
|
97
|
+
lines.push('');
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function map(projectDir, stamp) {
|
|
102
|
+
console.log(renderMap(projectDir, stamp));
|
|
103
|
+
}
|
package/src/modes.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Shared mode + skill metadata — the SINGLE source both `boss map` (live, in a
|
|
2
|
+
// founder's project) and scripts/gen-docs.js (static, in the BOSS repo) read,
|
|
3
|
+
// so the live map and the generated cheatsheet can never disagree about what a
|
|
4
|
+
// mode adds. This is the de-rot mechanism (IDEA-018): the per-mode lists are
|
|
5
|
+
// derived from the manifests + SKILL.md frontmatter, never hand-typed.
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { STAGES_DIR, STAGE_ORDER } from './paths.js';
|
|
10
|
+
import { readStageManifest } from './scaffold.js';
|
|
11
|
+
|
|
12
|
+
// Display name for a rung even when it isn't authored yet (no manifest.json).
|
|
13
|
+
const STAGE_NAMES = {
|
|
14
|
+
'L0-quickstart': 'Quickstart',
|
|
15
|
+
'L1-mvp': 'MVP',
|
|
16
|
+
'L2-v1': 'V1',
|
|
17
|
+
'L3-scale': 'Scale',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// The mode word a user types into `boss unlock` (strips the L#- level prefix).
|
|
21
|
+
export function modeWord(stageId) {
|
|
22
|
+
return stageId.replace(/^l\d+-/i, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The ordered ladder. Unauthored stages (no manifest — e.g. Scale today) come
|
|
26
|
+
// back as { authored: false } so callers can show the rung without faking
|
|
27
|
+
// content for it.
|
|
28
|
+
export function loadModes() {
|
|
29
|
+
return STAGE_ORDER.map((id) => {
|
|
30
|
+
try {
|
|
31
|
+
const m = readStageManifest(id);
|
|
32
|
+
return {
|
|
33
|
+
authored: true,
|
|
34
|
+
id,
|
|
35
|
+
name: m.name || STAGE_NAMES[id] || id,
|
|
36
|
+
summary: m.summary || '',
|
|
37
|
+
agents: m.agents || [],
|
|
38
|
+
skills: m.skills || [],
|
|
39
|
+
loops: m.loops || [],
|
|
40
|
+
hooks: m.hooks || [],
|
|
41
|
+
requires: m.requires || null,
|
|
42
|
+
unlocksNext: m.unlocksNext || null,
|
|
43
|
+
graduationHint: m.graduationHint || '',
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return { authored: false, id, name: STAGE_NAMES[id] || id, agents: [], skills: [], loops: [] };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The SKILL.md for a skill inside the PACKAGE (a given stage's template).
|
|
52
|
+
export function packageSkillMd(stageId, name) {
|
|
53
|
+
return join(STAGES_DIR, stageId, 'template', '.claude', 'skills', name, 'SKILL.md');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function frontmatterDescription(text) {
|
|
57
|
+
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
58
|
+
if (!m) return '';
|
|
59
|
+
for (const line of m[1].split('\n')) {
|
|
60
|
+
const i = line.indexOf(':');
|
|
61
|
+
if (i === -1) continue;
|
|
62
|
+
if (line.slice(0, i).trim() === 'description') return line.slice(i + 1).trim();
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Split a SKILL.md description into a one-line gloss + a usage hint. Descriptions
|
|
68
|
+
// follow the house format "<gloss sentence>. … Usage - /name <args>". Returns
|
|
69
|
+
// { gloss, usage } — empty strings when the file is missing or has no description.
|
|
70
|
+
export function skillGloss(skillMdPath) {
|
|
71
|
+
if (!existsSync(skillMdPath)) return { gloss: '', usage: '' };
|
|
72
|
+
const desc = frontmatterDescription(readFileSync(skillMdPath, 'utf8'));
|
|
73
|
+
if (!desc) return { gloss: '', usage: '' };
|
|
74
|
+
const u = desc.search(/\bUsage\s*[-:]/i);
|
|
75
|
+
const body = (u === -1 ? desc : desc.slice(0, u)).trim();
|
|
76
|
+
const usage = u === -1 ? '' : desc.slice(u).replace(/^Usage\s*[-:]\s*/i, '').trim();
|
|
77
|
+
// First sentence of the body is the gloss.
|
|
78
|
+
const dot = body.indexOf('. ');
|
|
79
|
+
let gloss = dot === -1 ? body : body.slice(0, dot + 1);
|
|
80
|
+
gloss = gloss.replace(/\.$/, '').trim();
|
|
81
|
+
return { gloss, usage };
|
|
82
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, resolve, join } from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
// BOSS install root — resolves correctly even when `boss` is globally linked,
|
|
7
|
+
// because import.meta.url points at the real file in src/. This is the PACKAGE
|
|
8
|
+
// (immutable, what gets published). Never write into it at runtime.
|
|
9
|
+
export const BOSS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
|
|
11
|
+
export const STAGES_DIR = join(BOSS_ROOT, 'stages');
|
|
12
|
+
|
|
13
|
+
// Mutable, machine-local state lives in the user's home — NOT in the package.
|
|
14
|
+
// This keeps the published package immutable and keeps a user's project list
|
|
15
|
+
// (with absolute paths) out of the repo.
|
|
16
|
+
export const BOSS_HOME = join(homedir(), '.boss');
|
|
17
|
+
export const REGISTRY_FILE = join(BOSS_HOME, 'registry.json');
|
|
18
|
+
|
|
19
|
+
export function bossVersion() {
|
|
20
|
+
return readFileSync(join(BOSS_ROOT, 'VERSION'), 'utf8').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Stage order — index = maturity level. Used to validate `unlock` jumps.
|
|
24
|
+
// Each stage is a "mode" in the user's vocabulary: Quickstart → MVP → V1 → Scale.
|
|
25
|
+
export const STAGE_ORDER = ['L0-quickstart', 'L1-mvp', 'L2-v1', 'L3-scale'];
|
|
26
|
+
|
|
27
|
+
// Resolve a user-typed layer to a canonical stage id.
|
|
28
|
+
// Accepts the full id ('L1-mvp'), the level ('L1'), or the mode name ('mvp').
|
|
29
|
+
export function resolveStageId(input) {
|
|
30
|
+
if (!input) return undefined;
|
|
31
|
+
const q = input.toLowerCase();
|
|
32
|
+
return STAGE_ORDER.find((s) => {
|
|
33
|
+
const sl = s.toLowerCase();
|
|
34
|
+
return sl === q || sl.startsWith(q + '-') || sl.replace(/^l\d+-/, '') === q;
|
|
35
|
+
});
|
|
36
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { REGISTRY_FILE } from './paths.js';
|
|
4
|
+
|
|
5
|
+
function load() {
|
|
6
|
+
if (!existsSync(REGISTRY_FILE)) return { projects: [] };
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
|
|
9
|
+
} catch {
|
|
10
|
+
return { projects: [] };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function save(data) {
|
|
15
|
+
mkdirSync(dirname(REGISTRY_FILE), { recursive: true }); // ensure ~/.boss exists
|
|
16
|
+
writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2) + '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function listProjects() {
|
|
20
|
+
return load().projects;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Upsert by absolute path — a project is identified by where it lives on disk.
|
|
24
|
+
export function registerProject(entry) {
|
|
25
|
+
const data = load();
|
|
26
|
+
const idx = data.projects.findIndex((p) => p.path === entry.path);
|
|
27
|
+
if (idx >= 0) data.projects[idx] = { ...data.projects[idx], ...entry };
|
|
28
|
+
else data.projects.push(entry);
|
|
29
|
+
save(data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findByPath(absPath) {
|
|
33
|
+
return load().projects.find((p) => p.path === absPath);
|
|
34
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync, readdirSync, statSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { STAGES_DIR } from './paths.js';
|
|
6
|
+
|
|
7
|
+
// A stage template may carry this file. Instead of being copied verbatim, its
|
|
8
|
+
// (substituted) contents are APPENDED to the project's CLAUDE.md under an
|
|
9
|
+
// idempotent marker — so unlocking a mode adds its working rules without ever
|
|
10
|
+
// clobbering rules the project (or earlier modes) already wrote.
|
|
11
|
+
const CLAUDE_APPEND = 'claude-append.md';
|
|
12
|
+
|
|
13
|
+
const TEXT_EXT = new Set([
|
|
14
|
+
'.md', '.json', '.js', '.ts', '.tsx', '.txt', '.yaml', '.yml',
|
|
15
|
+
'.sh', '.toml', '.gitignore', '.css', '.html',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function isTextFile(name) {
|
|
19
|
+
if (name.startsWith('.')) return true; // dotfiles like .gitignore
|
|
20
|
+
const dot = name.lastIndexOf('.');
|
|
21
|
+
return dot >= 0 && TEXT_EXT.has(name.slice(dot));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readStageManifest(stageId) {
|
|
25
|
+
const file = join(STAGES_DIR, stageId, 'manifest.json');
|
|
26
|
+
if (!existsSync(file)) {
|
|
27
|
+
throw new Error(`Stage ${stageId} has no manifest.json (not authored yet).`);
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function substituteInTree(dir, vars) {
|
|
33
|
+
for (const name of readdirSync(dir)) {
|
|
34
|
+
const full = join(dir, name);
|
|
35
|
+
if (statSync(full).isDirectory()) {
|
|
36
|
+
substituteInTree(full, vars);
|
|
37
|
+
} else if (isTextFile(name)) {
|
|
38
|
+
let body = readFileSync(full, 'utf8');
|
|
39
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
40
|
+
body = body.replaceAll(`{{${k}}}`, v);
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(full, body);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Append a marked block to a file, once. Idempotent: keyed by a marker id, so
|
|
48
|
+
// re-applying is a no-op. Creates the file from the block if absent. The marker
|
|
49
|
+
// is an HTML comment (stripped from Claude's context, kept in the file).
|
|
50
|
+
export function appendMarkedBlock(filePath, markerId, body) {
|
|
51
|
+
const startMark = `<!-- boss:${markerId} start -->`;
|
|
52
|
+
const endMark = `<!-- boss:${markerId} end -->`;
|
|
53
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
54
|
+
if (existing.includes(startMark)) return false; // already applied
|
|
55
|
+
const block = `${startMark}\n${body.trim()}\n${endMark}\n`;
|
|
56
|
+
const sep = existing && !existing.endsWith('\n\n')
|
|
57
|
+
? (existing.endsWith('\n') ? '\n' : '\n\n')
|
|
58
|
+
: '';
|
|
59
|
+
writeFileSync(filePath, existing + sep + block);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Append a stage's claude-append.md block to the project's CLAUDE.md, once.
|
|
64
|
+
export function appendClaudeBlock(stageId, targetDir, body) {
|
|
65
|
+
return appendMarkedBlock(join(targetDir, 'CLAUDE.md'), stageId, body);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Recursive copy-if-absent: copy every template file that doesn't already exist
|
|
69
|
+
// in the target, skipping (never clobbering) any the founder already has. The
|
|
70
|
+
// non-destructive half of `boss adopt`. Records copied + skipped paths.
|
|
71
|
+
function cpSafeTree(srcDir, destDir, copied, skipped) {
|
|
72
|
+
mkdirSync(destDir, { recursive: true });
|
|
73
|
+
for (const name of readdirSync(srcDir)) {
|
|
74
|
+
const s = join(srcDir, name);
|
|
75
|
+
const d = join(destDir, name);
|
|
76
|
+
if (statSync(s).isDirectory()) {
|
|
77
|
+
cpSafeTree(s, d, copied, skipped);
|
|
78
|
+
} else if (existsSync(d)) {
|
|
79
|
+
skipped.push(d);
|
|
80
|
+
} else {
|
|
81
|
+
cpSync(s, d);
|
|
82
|
+
copied.push(d);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Adopt a stage into an EXISTING repo non-destructively: copy only files that
|
|
88
|
+
// don't collide, substitute placeholders in just those (never touch the
|
|
89
|
+
// founder's own files), and fold any claude-append.md block into CLAUDE.md.
|
|
90
|
+
// Returns { copied, skipped, claudePreexisted, appendedClaude } for reporting.
|
|
91
|
+
export function applyStageSafe(stageId, targetDir, vars) {
|
|
92
|
+
const templateDir = join(STAGES_DIR, stageId, 'template');
|
|
93
|
+
if (!existsSync(templateDir)) {
|
|
94
|
+
throw new Error(`Stage ${stageId} has no template/ dir (not authored yet).`);
|
|
95
|
+
}
|
|
96
|
+
const claudePreexisted = existsSync(join(targetDir, 'CLAUDE.md'));
|
|
97
|
+
const copied = [];
|
|
98
|
+
const skipped = [];
|
|
99
|
+
cpSafeTree(templateDir, targetDir, copied, skipped);
|
|
100
|
+
|
|
101
|
+
// Substitute placeholders only in the files we actually wrote.
|
|
102
|
+
for (const f of copied) {
|
|
103
|
+
if (!isTextFile(f.slice(f.lastIndexOf('/') + 1))) continue;
|
|
104
|
+
let body = readFileSync(f, 'utf8');
|
|
105
|
+
for (const [k, v] of Object.entries(vars)) body = body.replaceAll(`{{${k}}}`, v);
|
|
106
|
+
writeFileSync(f, body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fold a stray claude-append.md (L1/L2 carry one) into CLAUDE.md, then remove it.
|
|
110
|
+
let appendedClaude = false;
|
|
111
|
+
const stray = join(targetDir, CLAUDE_APPEND);
|
|
112
|
+
if (existsSync(stray)) {
|
|
113
|
+
appendedClaude = appendClaudeBlock(stageId, targetDir, readFileSync(stray, 'utf8'));
|
|
114
|
+
rmSync(stray);
|
|
115
|
+
}
|
|
116
|
+
return { copied, skipped, claudePreexisted, appendedClaude };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Copy a stage's template/ tree into targetDir and fill placeholders.
|
|
120
|
+
// Returns { appendedClaude } so callers can report what changed.
|
|
121
|
+
export function applyStage(stageId, targetDir, vars) {
|
|
122
|
+
const templateDir = join(STAGES_DIR, stageId, 'template');
|
|
123
|
+
if (!existsSync(templateDir)) {
|
|
124
|
+
throw new Error(`Stage ${stageId} has no template/ dir (not authored yet).`);
|
|
125
|
+
}
|
|
126
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
127
|
+
substituteInTree(targetDir, vars);
|
|
128
|
+
|
|
129
|
+
// Handle the additive CLAUDE.md block: the file was copied into the project
|
|
130
|
+
// by cpSync; lift it out and fold it into CLAUDE.md instead of leaving it.
|
|
131
|
+
let appendedClaude = false;
|
|
132
|
+
const stray = join(targetDir, CLAUDE_APPEND);
|
|
133
|
+
if (existsSync(stray)) {
|
|
134
|
+
appendedClaude = appendClaudeBlock(stageId, targetDir, readFileSync(stray, 'utf8'));
|
|
135
|
+
rmSync(stray);
|
|
136
|
+
}
|
|
137
|
+
return { appendedClaude };
|
|
138
|
+
}
|
package/src/sync.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
STAGES_DIR, bossVersion, resolveStageId,
|
|
7
|
+
} from './paths.js';
|
|
8
|
+
import { readStageManifest } from './scaffold.js';
|
|
9
|
+
|
|
10
|
+
// Resolve a possibly-stale layer id (e.g. an old "L0-sketch" pin) to the
|
|
11
|
+
// canonical current stage id by its level prefix. Returns undefined if it
|
|
12
|
+
// can't be mapped at all.
|
|
13
|
+
export function canonicalLayer(layerId) {
|
|
14
|
+
return (
|
|
15
|
+
resolveStageId(layerId) ||
|
|
16
|
+
resolveStageId((String(layerId).match(/^l\d+/i) || [])[0]) ||
|
|
17
|
+
undefined
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The files BOSS manages for a stage: one .md per agent, one SKILL.md per skill.
|
|
22
|
+
// Each entry maps a source template file → its path inside the project.
|
|
23
|
+
function managedFiles(stageId, manifest) {
|
|
24
|
+
const stageRoot = join(STAGES_DIR, stageId, 'template');
|
|
25
|
+
const base = join(stageRoot, '.claude');
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const a of manifest.agents || []) {
|
|
28
|
+
out.push({
|
|
29
|
+
kind: 'agent',
|
|
30
|
+
name: a,
|
|
31
|
+
src: join(base, 'agents', `${a}.md`),
|
|
32
|
+
rel: join('.claude', 'agents', `${a}.md`),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
for (const s of manifest.skills || []) {
|
|
36
|
+
out.push({
|
|
37
|
+
kind: 'skill',
|
|
38
|
+
name: s,
|
|
39
|
+
src: join(base, 'skills', s, 'SKILL.md'),
|
|
40
|
+
rel: join('.claude', 'skills', s, 'SKILL.md'),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
for (const h of manifest.hooks || []) {
|
|
44
|
+
// Hook scripts may be .js (v0.18.0+ Node-based) or .sh (legacy). Prefer .js
|
|
45
|
+
// when present; fall back to .sh for backwards-compatibility with legacy stages.
|
|
46
|
+
const jsSrc = join(base, 'hooks', `${h}.js`);
|
|
47
|
+
const shSrc = join(base, 'hooks', `${h}.sh`);
|
|
48
|
+
const ext = existsSync(jsSrc) ? 'js' : 'sh';
|
|
49
|
+
out.push({
|
|
50
|
+
kind: 'hook',
|
|
51
|
+
name: h,
|
|
52
|
+
src: ext === 'js' ? jsSrc : shSrc,
|
|
53
|
+
rel: join('.claude', 'hooks', `${h}.${ext}`),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Hook library files (helpers like loop-runtime, yaml parser) — non-manifest;
|
|
57
|
+
// discovered by scanning the template's hooks/lib/ dir if present.
|
|
58
|
+
const libDir = join(base, 'hooks', 'lib');
|
|
59
|
+
if (existsSync(libDir)) {
|
|
60
|
+
for (const f of readdirSync(libDir)) {
|
|
61
|
+
if (!f.endsWith('.js')) continue;
|
|
62
|
+
out.push({
|
|
63
|
+
kind: 'hook-lib',
|
|
64
|
+
name: f,
|
|
65
|
+
src: join(libDir, f),
|
|
66
|
+
rel: join('.claude', 'hooks', 'lib', f),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Loop specs (IDEA-008, v0.18.0+) live in docs/loops/. Each is a managed
|
|
71
|
+
// markdown file with YAML frontmatter that the runtime parses.
|
|
72
|
+
for (const l of manifest.loops || []) {
|
|
73
|
+
out.push({
|
|
74
|
+
kind: 'loop',
|
|
75
|
+
name: l,
|
|
76
|
+
src: join(stageRoot, 'docs', 'loops', `${l}.md`),
|
|
77
|
+
rel: join('docs', 'loops', `${l}.md`),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Hook *scripts* sync like any managed file (above). Their *registration* lives in
|
|
84
|
+
// settings.json — a user-editable file — so we merge it in additively instead of
|
|
85
|
+
// overwriting: BOSS owns the hook entries it ships, the user owns everything else
|
|
86
|
+
// (permissions, their own hooks). Matched by command, so re-syncing is idempotent.
|
|
87
|
+
function templateHooks(stageId) {
|
|
88
|
+
const f = join(STAGES_DIR, stageId, 'template', '.claude', 'settings.json');
|
|
89
|
+
if (!existsSync(f)) return {};
|
|
90
|
+
try { return JSON.parse(readFileSync(f, 'utf8')).hooks || {}; } catch { return {}; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function eventCommands(entries) {
|
|
94
|
+
const cmds = new Set();
|
|
95
|
+
for (const entry of entries || []) {
|
|
96
|
+
for (const h of entry.hooks || []) if (h.command) cmds.add(h.command);
|
|
97
|
+
}
|
|
98
|
+
return cmds;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One-time hook-command migrations: when BOSS refactors a hook (e.g. bash → node
|
|
102
|
+
// in v0.18.0), existing projects need their old command entries replaced, not
|
|
103
|
+
// merely supplemented (otherwise both old + new fire, and the old points at a
|
|
104
|
+
// file that's been removed). Each migration matches the old command and either
|
|
105
|
+
// rewrites or removes that entry. Keep the list short and dated — these are
|
|
106
|
+
// load-bearing for in-the-wild projects.
|
|
107
|
+
const HOOK_MIGRATIONS = [
|
|
108
|
+
{
|
|
109
|
+
// v0.18.0 — conscience hook moved from bash to node. The shipped file
|
|
110
|
+
// (conscience.sh) is no longer in the template; leaving its registration
|
|
111
|
+
// would mean Claude Code invokes a missing script. Drop the stale entry;
|
|
112
|
+
// the additive merge then registers the new node command.
|
|
113
|
+
matches: (cmd) => /conscience\.sh/i.test(cmd),
|
|
114
|
+
action: 'drop',
|
|
115
|
+
note: 'v0.18.0 migration: conscience hook moved from bash to node',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
function applyHookMigrations(merged) {
|
|
120
|
+
if (!merged.hooks) return false;
|
|
121
|
+
let changed = false;
|
|
122
|
+
for (const event of Object.keys(merged.hooks)) {
|
|
123
|
+
const entries = merged.hooks[event] || [];
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const before = entry.hooks ? entry.hooks.length : 0;
|
|
126
|
+
entry.hooks = (entry.hooks || []).filter((h) => {
|
|
127
|
+
for (const m of HOOK_MIGRATIONS) {
|
|
128
|
+
if (m.matches(h.command || '')) return m.action !== 'drop';
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
132
|
+
if ((entry.hooks?.length || 0) !== before) changed = true;
|
|
133
|
+
}
|
|
134
|
+
// Remove empty entry containers (an entry with no hooks left).
|
|
135
|
+
merged.hooks[event] = entries.filter((e) => (e.hooks || []).length > 0);
|
|
136
|
+
}
|
|
137
|
+
return changed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Merge BOSS-owned hook registrations from the installed layers into the project's
|
|
141
|
+
// settings.json. Returns { changed, merged, rel } — caller writes `merged` on apply.
|
|
142
|
+
export function computeSettingsMerge(projectDir, layers) {
|
|
143
|
+
const rel = join('.claude', 'settings.json');
|
|
144
|
+
const dest = join(projectDir, rel);
|
|
145
|
+
let merged = {};
|
|
146
|
+
if (existsSync(dest)) {
|
|
147
|
+
try { merged = JSON.parse(readFileSync(dest, 'utf8')); } catch { merged = {}; }
|
|
148
|
+
}
|
|
149
|
+
let changed = false;
|
|
150
|
+
// Apply hook-command migrations first (e.g. v0.18.0 bash→node) so stale entries
|
|
151
|
+
// don't masquerade as already-present and block the new entry from being added.
|
|
152
|
+
if (applyHookMigrations(merged)) changed = true;
|
|
153
|
+
for (const stageId of layers) {
|
|
154
|
+
for (const [event, tEntries] of Object.entries(templateHooks(stageId))) {
|
|
155
|
+
merged.hooks ||= {};
|
|
156
|
+
merged.hooks[event] ||= [];
|
|
157
|
+
const present = eventCommands(merged.hooks[event]);
|
|
158
|
+
for (const entry of tEntries) {
|
|
159
|
+
const cmds = (entry.hooks || []).map((h) => h.command).filter(Boolean);
|
|
160
|
+
if (cmds.length && cmds.every((c) => present.has(c))) continue; // already registered
|
|
161
|
+
merged.hooks[event].push(JSON.parse(JSON.stringify(entry)));
|
|
162
|
+
cmds.forEach((c) => present.add(c));
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { changed, merged, rel };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function substitute(body, vars) {
|
|
171
|
+
for (const [k, v] of Object.entries(vars)) body = body.replaceAll(`{{${k}}}`, v);
|
|
172
|
+
return body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A cheap, dependency-free change signal: how many lines differ between two
|
|
176
|
+
// versions (positional compare + length delta). Enough to flag a file as worth
|
|
177
|
+
// reviewing; the /boss-sync skill does the real side-by-side read.
|
|
178
|
+
function lineDelta(oldText, newText) {
|
|
179
|
+
const a = oldText.split('\n');
|
|
180
|
+
const b = newText.split('\n');
|
|
181
|
+
let diff = Math.abs(a.length - b.length);
|
|
182
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
183
|
+
if (a[i] !== b[i]) diff++;
|
|
184
|
+
}
|
|
185
|
+
return diff;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Compute what a sync would do for a project, without writing anything.
|
|
189
|
+
// Returns { entries, layers, pin, current, drift }.
|
|
190
|
+
export function planSync(projectDir, stamp) {
|
|
191
|
+
const current = bossVersion();
|
|
192
|
+
const vars = {
|
|
193
|
+
PROJECT_NAME: stamp.name,
|
|
194
|
+
DATE: new Date().toISOString().slice(0, 10),
|
|
195
|
+
BOSS_VERSION: current,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Canonicalize + dedupe the installed layers, preserving order.
|
|
199
|
+
const layers = [];
|
|
200
|
+
for (const raw of stamp.installedLayers || [stamp.stage]) {
|
|
201
|
+
const c = canonicalLayer(raw);
|
|
202
|
+
if (c && !layers.includes(c)) layers.push(c);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const entries = [];
|
|
206
|
+
for (const stageId of layers) {
|
|
207
|
+
let manifest;
|
|
208
|
+
try {
|
|
209
|
+
manifest = readStageManifest(stageId);
|
|
210
|
+
} catch {
|
|
211
|
+
continue; // stage not authored in this BOSS version — skip
|
|
212
|
+
}
|
|
213
|
+
for (const f of managedFiles(stageId, manifest)) {
|
|
214
|
+
if (!existsSync(f.src)) continue; // manifest lists it but template lacks it
|
|
215
|
+
const next = substitute(readFileSync(f.src, 'utf8'), {
|
|
216
|
+
...vars, STAGE: stageId, MODE: manifest.name,
|
|
217
|
+
});
|
|
218
|
+
const dest = join(projectDir, f.rel);
|
|
219
|
+
const exists = existsSync(dest);
|
|
220
|
+
const cur = exists ? readFileSync(dest, 'utf8') : '';
|
|
221
|
+
let status = 'ok';
|
|
222
|
+
if (!exists) status = 'new';
|
|
223
|
+
else if (cur !== next) status = 'changed';
|
|
224
|
+
entries.push({ ...f, stageId, status, next, delta: exists ? lineDelta(cur, next) : 0 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
entries,
|
|
230
|
+
layers,
|
|
231
|
+
pin: stamp.bossVersion,
|
|
232
|
+
current,
|
|
233
|
+
drift: stamp.bossVersion !== current,
|
|
234
|
+
settings: computeSettingsMerge(projectDir, layers),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Apply a plan: write new/changed files and return the canonicalized stamp
|
|
239
|
+
// fields the caller should persist (it owns writeStamp + registry).
|
|
240
|
+
export function applySync(projectDir, plan, stamp) {
|
|
241
|
+
const written = [];
|
|
242
|
+
for (const e of plan.entries) {
|
|
243
|
+
if (e.status === 'ok') continue;
|
|
244
|
+
const dest = join(projectDir, e.rel);
|
|
245
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
246
|
+
writeFileSync(dest, e.next);
|
|
247
|
+
written.push(e);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Merge BOSS-owned hook registrations into settings.json (additive — preserves
|
|
251
|
+
// the user's permissions and their own hooks).
|
|
252
|
+
if (plan.settings && plan.settings.changed) {
|
|
253
|
+
const dest = join(projectDir, plan.settings.rel);
|
|
254
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
255
|
+
writeFileSync(dest, JSON.stringify(plan.settings.merged, null, 2) + '\n');
|
|
256
|
+
written.push({ kind: 'settings', name: 'settings.json', rel: plan.settings.rel });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Reconcile the stamp to current canonical layers + the union of their
|
|
260
|
+
// agents/skills/hooks/loops, and bump the pin. Mode/stage track the most-mature layer.
|
|
261
|
+
const agents = new Set();
|
|
262
|
+
const skills = new Set();
|
|
263
|
+
const hooks = new Set();
|
|
264
|
+
const loops = new Set();
|
|
265
|
+
for (const stageId of plan.layers) {
|
|
266
|
+
try {
|
|
267
|
+
const m = readStageManifest(stageId);
|
|
268
|
+
(m.agents || []).forEach((a) => agents.add(a));
|
|
269
|
+
(m.skills || []).forEach((s) => skills.add(s));
|
|
270
|
+
(m.hooks || []).forEach((h) => hooks.add(h));
|
|
271
|
+
(m.loops || []).forEach((l) => loops.add(l));
|
|
272
|
+
} catch { /* skip unauthored */ }
|
|
273
|
+
}
|
|
274
|
+
const top = plan.layers[plan.layers.length - 1];
|
|
275
|
+
let topMode = stamp.mode;
|
|
276
|
+
try { topMode = readStageManifest(top).name; } catch { /* keep */ }
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
written,
|
|
280
|
+
stamp: {
|
|
281
|
+
...stamp,
|
|
282
|
+
stage: top,
|
|
283
|
+
mode: topMode,
|
|
284
|
+
installedLayers: plan.layers,
|
|
285
|
+
agents: [...agents],
|
|
286
|
+
skills: [...skills],
|
|
287
|
+
hooks: [...hooks],
|
|
288
|
+
loops: [...loops],
|
|
289
|
+
bossVersion: plan.current,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|