chapterhouse 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/api/auth.js +4 -3
- package/dist/api/auth.test.js +27 -39
- package/dist/api/server.js +3 -0
- package/dist/api/team.js +4 -2
- package/dist/cli.js +8 -2
- package/dist/copilot/episode-writer.js +4 -2
- package/dist/copilot/orchestrator.js +300 -355
- package/dist/copilot/orchestrator.test.js +39 -0
- package/dist/copilot/session-manager.js +307 -0
- package/dist/copilot/session-manager.test.js +292 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/system-message.test.js +8 -0
- package/dist/daemon.js +2 -1
- package/dist/integrations/teams-notify.js +3 -1
- package/dist/squad/index.js +1 -0
- package/dist/squad/init-cli.js +109 -0
- package/dist/squad/init.js +395 -0
- package/dist/squad/init.test.js +351 -0
- package/dist/squad/mirror.js +4 -2
- package/dist/squad/mirror.scheduler.js +9 -7
- package/dist/version.js +7 -0
- package/dist/wiki/team-sync.js +3 -1
- package/package.json +2 -3
- package/web/dist/assets/index-Dpt-MCe8.css +10 -0
- package/web/dist/assets/index-NmxVWGY1.js +205 -0
- package/web/dist/assets/index-NmxVWGY1.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CxT9905O.css +0 -10
- package/web/dist/assets/index-DI3rnGm-.js +0 -142
- package/web/dist/assets/index-DI3rnGm-.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -17,6 +17,7 @@ import { StandupScheduler } from "./copilot/standup.js";
|
|
|
17
17
|
import { DecisionsSyncScheduler } from "./squad/mirror.scheduler.js";
|
|
18
18
|
import { registerShutdownSignals } from "./shutdown-signals.js";
|
|
19
19
|
import { logger } from "./util/logger.js";
|
|
20
|
+
import { CHAPTERHOUSE_VERSION } from "./version.js";
|
|
20
21
|
const log = logger.child({ module: "daemon" });
|
|
21
22
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
23
|
/**
|
|
@@ -78,7 +79,7 @@ function truncate(text, max = 200) {
|
|
|
78
79
|
return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
|
|
79
80
|
}
|
|
80
81
|
async function main() {
|
|
81
|
-
log.info("Starting Chapterhouse daemon");
|
|
82
|
+
log.info({ version: CHAPTERHOUSE_VERSION }, "Starting Chapterhouse daemon");
|
|
82
83
|
if (config.selfEditEnabled) {
|
|
83
84
|
log.warn("Self-edit mode enabled — Chapterhouse can modify his own source code");
|
|
84
85
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
+
import { childLogger } from "../util/logger.js";
|
|
3
|
+
const log = childLogger("teams-notify");
|
|
2
4
|
export const TEAMS_MILESTONE_THRESHOLDS = [25, 50, 75, 100];
|
|
3
5
|
const DEFAULT_COLOR = "0076D7";
|
|
4
6
|
export class TeamsNotifier {
|
|
@@ -10,7 +12,7 @@ export class TeamsNotifier {
|
|
|
10
12
|
this.webhookUrl = (options.webhookUrl ?? config.teamsWebhookUrl).trim();
|
|
11
13
|
this.enabled = options.enabled ?? config.teamsNotificationsEnabled;
|
|
12
14
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
13
|
-
this.warn = options.warn ?? ((message) =>
|
|
15
|
+
this.warn = options.warn ?? ((message) => log.warn(message));
|
|
14
16
|
}
|
|
15
17
|
async sendMessage(title, body, color = DEFAULT_COLOR) {
|
|
16
18
|
return await this.postCard({
|
package/dist/squad/index.js
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/squad/init-cli.ts
|
|
3
|
+
*
|
|
4
|
+
* Interactive CLI for `chapterhouse squad init`.
|
|
5
|
+
* Asks the user 5 questions, proposes a roster, then scaffolds `.squad/`.
|
|
6
|
+
*
|
|
7
|
+
* Flags:
|
|
8
|
+
* --force Re-scaffold even if .squad/ already exists (clobbers existing)
|
|
9
|
+
* --yes Skip confirmation prompt (non-interactive / CI mode)
|
|
10
|
+
* --universe <name> Override the naming universe (default: Dune)
|
|
11
|
+
*/
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { scaffoldSquad, isSquadInitialized, allocateCast, resolveRoles, UNIVERSE_CHARACTERS, DEFAULT_UNIVERSE, } from './init.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Entry point
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export async function runInitCli(argv) {
|
|
20
|
+
const force = argv.includes('--force');
|
|
21
|
+
const yes = argv.includes('--yes');
|
|
22
|
+
// --universe <name>
|
|
23
|
+
const univIdx = argv.indexOf('--universe');
|
|
24
|
+
const univArg = univIdx !== -1 ? argv[univIdx + 1] : undefined;
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
if (!force && isSquadInitialized(projectRoot)) {
|
|
27
|
+
console.error('⚠️ Squad is already initialized in this directory.\n' +
|
|
28
|
+
' Run with --force to re-scaffold (existing files will be overwritten).');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log('\n🚀 chapterhouse squad init — guided setup\n');
|
|
32
|
+
// Try to pre-fill the human name from git
|
|
33
|
+
let defaultHuman = 'the team';
|
|
34
|
+
try {
|
|
35
|
+
defaultHuman = execSync('git config user.name', { encoding: 'utf-8' }).trim() || defaultHuman;
|
|
36
|
+
}
|
|
37
|
+
catch { /* not a git repo or git not available */ }
|
|
38
|
+
const availableUniverses = Object.keys(UNIVERSE_CHARACTERS);
|
|
39
|
+
const universeList = availableUniverses.join(', ');
|
|
40
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
41
|
+
const ask = (prompt) => new Promise(resolve => rl.question(prompt, answer => resolve(answer.trim())));
|
|
42
|
+
try {
|
|
43
|
+
// ── Q1: project name ───────────────────────────────────────────────────
|
|
44
|
+
const projectName = (await ask('Project name: ')) || 'My Project';
|
|
45
|
+
// ── Q2: language / stack ───────────────────────────────────────────────
|
|
46
|
+
const stack = (await ask('Language / stack (e.g. TypeScript, React, Node): ')) || 'TypeScript';
|
|
47
|
+
// ── Q3: primary goal ──────────────────────────────────────────────────
|
|
48
|
+
const goal = (await ask('Primary goal — what does it do?: ')) || 'A software project';
|
|
49
|
+
// ── Q4: team size ─────────────────────────────────────────────────────
|
|
50
|
+
const teamSizeRaw = await ask('Team size (3–5, default 4): ');
|
|
51
|
+
const teamSize = Math.min(Math.max(parseInt(teamSizeRaw || '4', 10) || 4, 3), 5);
|
|
52
|
+
// ── Q5: universe ──────────────────────────────────────────────────────
|
|
53
|
+
const defaultUniverse = univArg && availableUniverses.includes(univArg) ? univArg : DEFAULT_UNIVERSE;
|
|
54
|
+
const universeInput = await ask(`Naming universe [${universeList}]\n (default: ${defaultUniverse}): `);
|
|
55
|
+
const universe = availableUniverses.find(u => u.toLowerCase() === universeInput.toLowerCase()) ?? defaultUniverse;
|
|
56
|
+
rl.close();
|
|
57
|
+
// ── Propose roster ────────────────────────────────────────────────────
|
|
58
|
+
const roles = resolveRoles(teamSize);
|
|
59
|
+
const castMap = allocateCast(roles, universe);
|
|
60
|
+
console.log(`\n📋 Proposed team (${universe} universe):\n`);
|
|
61
|
+
for (const role of roles) {
|
|
62
|
+
const castName = castMap[role.slug];
|
|
63
|
+
console.log(` ${role.icon} ${castName.padEnd(12)} — ${role.role.padEnd(14)} ${role.description}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(' 📋 Scribe — Scribe Memory, decisions, session logs');
|
|
66
|
+
console.log(' 🔄 Ralph — Monitor Work queue, backlog, keep-alive\n');
|
|
67
|
+
// ── Confirm ───────────────────────────────────────────────────────────
|
|
68
|
+
if (!yes) {
|
|
69
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
const confirm = await new Promise(resolve => rl2.question('Hire this team? [Y/n]: ', ans => { rl2.close(); resolve(ans.trim()); }));
|
|
71
|
+
if (confirm && confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
|
72
|
+
console.log('Aborted. No files written.');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── Scaffold ──────────────────────────────────────────────────────────
|
|
77
|
+
const result = scaffoldSquad(projectRoot, { projectName, stack, goal, teamSize, universe, humanName: defaultHuman }, { force });
|
|
78
|
+
if (!result) {
|
|
79
|
+
// Should not happen (we checked above) — safety fallback
|
|
80
|
+
console.error('⚠️ Squad already initialized. Use --force to overwrite.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
console.log(`\n✅ Squad initialized at ${join(projectRoot, '.squad')}/\n`);
|
|
84
|
+
console.log('Files written:');
|
|
85
|
+
console.log(' .squad/team.md');
|
|
86
|
+
console.log(' .squad/routing.md');
|
|
87
|
+
console.log(' .squad/ceremonies.md');
|
|
88
|
+
console.log(' .squad/decisions.md');
|
|
89
|
+
for (const agent of result.agents) {
|
|
90
|
+
console.log(` .squad/agents/${agent.slug}/charter.md`);
|
|
91
|
+
console.log(` .squad/agents/${agent.slug}/history.md`);
|
|
92
|
+
}
|
|
93
|
+
console.log(' .squad/agents/scribe/charter.md');
|
|
94
|
+
console.log(' .squad/agents/ralph/charter.md');
|
|
95
|
+
console.log(' .squad/casting/policy.json');
|
|
96
|
+
console.log(' .squad/casting/registry.json');
|
|
97
|
+
console.log(' .squad/casting/history.json');
|
|
98
|
+
console.log(' .gitattributes (updated with union merge rules)\n');
|
|
99
|
+
const firstAgent = result.agents[0];
|
|
100
|
+
if (firstAgent) {
|
|
101
|
+
console.log(`💡 Try: "@${firstAgent.slug}, set up the project structure"\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
rl.close();
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=init-cli.js.map
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/squad/init.ts
|
|
3
|
+
*
|
|
4
|
+
* Core scaffolding logic for `chapterhouse squad init`.
|
|
5
|
+
* Creates a complete `.squad/` directory structure with casting state,
|
|
6
|
+
* agent charters, and team coordination files.
|
|
7
|
+
*
|
|
8
|
+
* This is the mechanical implementation of the Squad coordinator's
|
|
9
|
+
* Init Mode Phase 2 flow (see `.squad/squad.agent.md`).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Universe character pools
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Dune character pool — function/pressure/consequence-flavored names.
|
|
19
|
+
* Coordinator = "Squad", Scribe = "Scribe", Ralph = "Ralph" are exempted.
|
|
20
|
+
* Source: Issue #23 + `.squad/skills/dune-release-names/SKILL.md`
|
|
21
|
+
*/
|
|
22
|
+
export const DUNE_CHARACTERS = [
|
|
23
|
+
'Leto',
|
|
24
|
+
'Jessica',
|
|
25
|
+
'Stilgar',
|
|
26
|
+
'Chani',
|
|
27
|
+
'Gurney',
|
|
28
|
+
'Duncan',
|
|
29
|
+
'Thufir',
|
|
30
|
+
'Piter',
|
|
31
|
+
'Feyd',
|
|
32
|
+
'Hawat',
|
|
33
|
+
'Mapes',
|
|
34
|
+
'Liet',
|
|
35
|
+
'Halleck',
|
|
36
|
+
'Idaho',
|
|
37
|
+
'Shaddam',
|
|
38
|
+
'Irulan',
|
|
39
|
+
];
|
|
40
|
+
/** All available universe pools. Add new universes here. */
|
|
41
|
+
export const UNIVERSE_CHARACTERS = {
|
|
42
|
+
Dune: DUNE_CHARACTERS,
|
|
43
|
+
Firefly: ['Mal', 'Zoe', 'Wash', 'Inara', 'Jayne', 'Kaylee', 'Simon', 'River', 'Book', 'Shepherd'],
|
|
44
|
+
'Star Wars': ['Luke', 'Leia', 'Han', 'Chewie', 'Obi-Wan', 'Yoda', 'Vader', 'Wedge', 'Lando', 'R2-D2', 'C-3PO', 'Poe'],
|
|
45
|
+
'The Matrix': ['Neo', 'Trinity', 'Morpheus', 'Oracle', 'Tank', 'Apoc', 'Switch', 'Dozer', 'Cypher', 'Agent-Smith'],
|
|
46
|
+
'Breaking Bad': ['Walt', 'Jesse', 'Skyler', 'Hank', 'Saul', 'Mike', 'Gus', 'Jane', 'Lydia', 'Todd', 'Andrea', 'Huell'],
|
|
47
|
+
};
|
|
48
|
+
export const DEFAULT_UNIVERSE = 'Dune';
|
|
49
|
+
/** Roles cast when teamSize ≤ 4 (Lead + 3 specialists + Scribe). */
|
|
50
|
+
const ROLE_PRESETS = {
|
|
51
|
+
3: [
|
|
52
|
+
{ slug: 'lead', role: 'Lead', description: 'Scope, decisions, code review', icon: '🏗️' },
|
|
53
|
+
{ slug: 'dev', role: 'Developer', description: 'Implementation, features', icon: '⚙️' },
|
|
54
|
+
{ slug: 'tester', role: 'Tester', description: 'Tests, quality, edge cases', icon: '🧪' },
|
|
55
|
+
],
|
|
56
|
+
4: [
|
|
57
|
+
{ slug: 'lead', role: 'Lead', description: 'Scope, decisions, code review', icon: '🏗️' },
|
|
58
|
+
{ slug: 'frontend', role: 'Frontend Dev', description: 'UI, components, styling', icon: '⚛️' },
|
|
59
|
+
{ slug: 'backend', role: 'Backend Dev', description: 'APIs, database, services', icon: '🔧' },
|
|
60
|
+
{ slug: 'tester', role: 'Tester', description: 'Tests, quality, edge cases', icon: '🧪' },
|
|
61
|
+
],
|
|
62
|
+
5: [
|
|
63
|
+
{ slug: 'lead', role: 'Lead', description: 'Scope, decisions, architecture', icon: '🏗️' },
|
|
64
|
+
{ slug: 'frontend', role: 'Frontend Dev', description: 'UI, components, styling', icon: '⚛️' },
|
|
65
|
+
{ slug: 'backend', role: 'Backend Dev', description: 'APIs, database, services', icon: '🔧' },
|
|
66
|
+
{ slug: 'tester', role: 'Tester', description: 'Tests, quality, edge cases', icon: '🧪' },
|
|
67
|
+
{ slug: 'devops', role: 'DevOps', description: 'CI/CD, infra, deployment', icon: '🚀' },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
/** Always-present fixed-name agents (exempt from casting). */
|
|
71
|
+
const FIXED_AGENTS = [
|
|
72
|
+
{ slug: 'scribe', role: 'Scribe', description: 'Memory, decisions, session logs', icon: '📋', castName: 'Scribe' },
|
|
73
|
+
{ slug: 'ralph', role: 'Monitor', description: 'Work queue, backlog, keep-alive', icon: '🔄', castName: 'Ralph' },
|
|
74
|
+
];
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Public API
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/** Returns true if `.squad/team.md` with a populated `## Members` section exists. */
|
|
79
|
+
export function isSquadInitialized(projectRoot) {
|
|
80
|
+
const teamMd = join(projectRoot, '.squad', 'team.md');
|
|
81
|
+
if (!existsSync(teamMd))
|
|
82
|
+
return false;
|
|
83
|
+
const content = readFileSync(teamMd, 'utf-8');
|
|
84
|
+
return /^## Members/m.test(content) && content.includes('|');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Allocate cast names from the chosen universe for the given role slots.
|
|
88
|
+
* Returns a map of role slug → cast name.
|
|
89
|
+
*/
|
|
90
|
+
export function allocateCast(roles, universe) {
|
|
91
|
+
const pool = UNIVERSE_CHARACTERS[universe] ?? DUNE_CHARACTERS;
|
|
92
|
+
const result = {};
|
|
93
|
+
pool.slice(0, roles.length).forEach((name, i) => {
|
|
94
|
+
result[roles[i].slug] = name;
|
|
95
|
+
});
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the AgentRole[] for a given team size.
|
|
100
|
+
* Clamps to valid presets (3–5). Extras default to the 5-slot preset.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveRoles(teamSize) {
|
|
103
|
+
const clamped = Math.min(Math.max(teamSize, 3), 5);
|
|
104
|
+
return ROLE_PRESETS[clamped] ?? ROLE_PRESETS[4];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Scaffold a complete `.squad/` directory for `projectRoot`.
|
|
108
|
+
*
|
|
109
|
+
* Safe to call on an already-initialized project — if `.squad/team.md`
|
|
110
|
+
* already has roster entries, the call returns `null` without touching disk.
|
|
111
|
+
* Pass `{ force: true }` to override.
|
|
112
|
+
*/
|
|
113
|
+
export function scaffoldSquad(projectRoot, config, opts = {}) {
|
|
114
|
+
if (!opts.force && isSquadInitialized(projectRoot)) {
|
|
115
|
+
return null; // idempotent guard
|
|
116
|
+
}
|
|
117
|
+
const { projectName, stack, goal, teamSize = 4, universe = DEFAULT_UNIVERSE, humanName = 'the team', } = config;
|
|
118
|
+
const squadDir = join(projectRoot, '.squad');
|
|
119
|
+
const roles = resolveRoles(teamSize);
|
|
120
|
+
const castMap = allocateCast(roles, universe);
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
const assignmentId = randomUUID();
|
|
123
|
+
// Build flat agent list (cast agents + fixed agents)
|
|
124
|
+
const castAgents = roles.map(r => ({
|
|
125
|
+
slug: castMap[r.slug].toLowerCase(),
|
|
126
|
+
castName: castMap[r.slug],
|
|
127
|
+
role: r.role,
|
|
128
|
+
description: r.description,
|
|
129
|
+
icon: r.icon,
|
|
130
|
+
originalSlug: r.slug,
|
|
131
|
+
}));
|
|
132
|
+
// Directories to create
|
|
133
|
+
const dirs = [
|
|
134
|
+
squadDir,
|
|
135
|
+
join(squadDir, 'agents'),
|
|
136
|
+
join(squadDir, 'casting'),
|
|
137
|
+
join(squadDir, 'decisions'),
|
|
138
|
+
join(squadDir, 'decisions', 'inbox'),
|
|
139
|
+
join(squadDir, 'orchestration-log'),
|
|
140
|
+
join(squadDir, 'log'),
|
|
141
|
+
join(squadDir, 'skills'),
|
|
142
|
+
join(squadDir, 'identity'),
|
|
143
|
+
...castAgents.map(a => join(squadDir, 'agents', a.slug)),
|
|
144
|
+
...FIXED_AGENTS.map(a => join(squadDir, 'agents', a.slug)),
|
|
145
|
+
];
|
|
146
|
+
dirs.forEach(d => mkdirSync(d, { recursive: true }));
|
|
147
|
+
// ── team.md ──────────────────────────────────────────────────────────────
|
|
148
|
+
const rosterRows = [
|
|
149
|
+
...castAgents.map(a => `| ${a.icon} ${a.castName} | @${a.slug} | ${a.role} | ${a.description} |`),
|
|
150
|
+
`| 📋 Scribe | @scribe | Scribe | Memory, decisions, session logs |`,
|
|
151
|
+
`| 🔄 Ralph | @ralph | Monitor | Work queue, backlog, keep-alive |`,
|
|
152
|
+
].join('\n');
|
|
153
|
+
writeFile(join(squadDir, 'team.md'), `# ${projectName} — Squad Team
|
|
154
|
+
|
|
155
|
+
## Project Context
|
|
156
|
+
|
|
157
|
+
- **Project:** ${projectName}
|
|
158
|
+
- **Stack:** ${stack}
|
|
159
|
+
- **Goal:** ${goal}
|
|
160
|
+
- **Human:** ${humanName}
|
|
161
|
+
- **Universe:** ${universe}
|
|
162
|
+
- **Initialized:** ${now}
|
|
163
|
+
|
|
164
|
+
## Members
|
|
165
|
+
|
|
166
|
+
| Icon | Name | Role | Description |
|
|
167
|
+
|------|------|------|-------------|
|
|
168
|
+
${rosterRows}
|
|
169
|
+
`);
|
|
170
|
+
// ── routing.md ───────────────────────────────────────────────────────────
|
|
171
|
+
const routingRules = castAgents.map(a => `- **${a.role}** → @${a.slug}`).join('\n');
|
|
172
|
+
writeFile(join(squadDir, 'routing.md'), `# Routing Rules
|
|
173
|
+
|
|
174
|
+
Route tasks to the right agent based on their role.
|
|
175
|
+
|
|
176
|
+
${routingRules}
|
|
177
|
+
- **Memory / Decisions** → @scribe
|
|
178
|
+
- **Backlog / Queue** → @ralph
|
|
179
|
+
|
|
180
|
+
## Default
|
|
181
|
+
|
|
182
|
+
When unclear, route to the Lead.
|
|
183
|
+
`);
|
|
184
|
+
// ── ceremonies.md ────────────────────────────────────────────────────────
|
|
185
|
+
writeFile(join(squadDir, 'ceremonies.md'), `# Ceremonies
|
|
186
|
+
|
|
187
|
+
## Sprint Rhythm
|
|
188
|
+
|
|
189
|
+
- **Sprint Planning** — Start of each sprint: define goals, assign issues
|
|
190
|
+
- **Daily Standup** — Each session: what's in progress, blockers
|
|
191
|
+
- **Sprint Review** — End of sprint: demo, retro, decisions
|
|
192
|
+
|
|
193
|
+
## PR Review Gate
|
|
194
|
+
|
|
195
|
+
All PRs require approval from the Lead before merging.
|
|
196
|
+
|
|
197
|
+
## Decision Log
|
|
198
|
+
|
|
199
|
+
Architectural decisions land in \`.squad/decisions/inbox/\` via each agent's decision-drop.
|
|
200
|
+
Scribe merges inbox items into \`.squad/decisions.md\`.
|
|
201
|
+
`);
|
|
202
|
+
// ── decisions.md ─────────────────────────────────────────────────────────
|
|
203
|
+
writeFile(join(squadDir, 'decisions.md'), `# Decisions
|
|
204
|
+
|
|
205
|
+
This file is maintained by Scribe. Append-only — do not edit existing entries.
|
|
206
|
+
|
|
207
|
+
<!-- git attribute: .squad/decisions.md merge=union -->
|
|
208
|
+
`);
|
|
209
|
+
// ── agent charters ───────────────────────────────────────────────────────
|
|
210
|
+
for (const agent of castAgents) {
|
|
211
|
+
writeFile(join(squadDir, 'agents', agent.slug, 'charter.md'), buildCharter({
|
|
212
|
+
castName: agent.castName,
|
|
213
|
+
role: agent.role,
|
|
214
|
+
description: agent.description,
|
|
215
|
+
projectName,
|
|
216
|
+
stack,
|
|
217
|
+
goal,
|
|
218
|
+
humanName,
|
|
219
|
+
universe,
|
|
220
|
+
}));
|
|
221
|
+
writeFile(join(squadDir, 'agents', agent.slug, 'history.md'), buildHistory({
|
|
222
|
+
castName: agent.castName,
|
|
223
|
+
role: agent.role,
|
|
224
|
+
projectName,
|
|
225
|
+
stack,
|
|
226
|
+
goal,
|
|
227
|
+
humanName,
|
|
228
|
+
now,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
// Scribe charter
|
|
232
|
+
writeFile(join(squadDir, 'agents', 'scribe', 'charter.md'), `# Scribe — Memory Keeper
|
|
233
|
+
|
|
234
|
+
> Keeps the record. Remembers everything so everyone else can focus.
|
|
235
|
+
|
|
236
|
+
## Identity
|
|
237
|
+
|
|
238
|
+
- **Name:** Scribe
|
|
239
|
+
- **Role:** Scribe / Memory Keeper
|
|
240
|
+
- **Project:** ${projectName}
|
|
241
|
+
|
|
242
|
+
## What I Own
|
|
243
|
+
|
|
244
|
+
- \`.squad/decisions.md\` — merge inbox items, maintain canonical list
|
|
245
|
+
- \`.squad/orchestration-log/\` — session summaries
|
|
246
|
+
- Cross-agent context sharing
|
|
247
|
+
|
|
248
|
+
## Constraints
|
|
249
|
+
|
|
250
|
+
- Read-only on all non-\`.squad/\` files
|
|
251
|
+
- Never authors code or domain artifacts
|
|
252
|
+
`);
|
|
253
|
+
writeFile(join(squadDir, 'agents', 'scribe', 'history.md'), `# Scribe History\n\n## Init — ${now}\n\nInitialized for **${projectName}** by ${humanName}.\nStack: ${stack}\nGoal: ${goal}\n`);
|
|
254
|
+
// Ralph charter
|
|
255
|
+
writeFile(join(squadDir, 'agents', 'ralph', 'charter.md'), `# Ralph — Monitor
|
|
256
|
+
|
|
257
|
+
> Keeps the queue moving. Never lets work pile up unnoticed.
|
|
258
|
+
|
|
259
|
+
## Identity
|
|
260
|
+
|
|
261
|
+
- **Name:** Ralph
|
|
262
|
+
- **Role:** Monitor / Work Queue
|
|
263
|
+
- **Project:** ${projectName}
|
|
264
|
+
|
|
265
|
+
## What I Own
|
|
266
|
+
|
|
267
|
+
- GitHub issue triage and backlog health
|
|
268
|
+
- Keep-alive heartbeat checks
|
|
269
|
+
- Sprint queue management
|
|
270
|
+
`);
|
|
271
|
+
writeFile(join(squadDir, 'agents', 'ralph', 'history.md'), `# Ralph History\n\n## Init — ${now}\n\nInitialized for **${projectName}** by ${humanName}.\n`);
|
|
272
|
+
// ── casting state ─────────────────────────────────────────────────────────
|
|
273
|
+
const policyJson = {
|
|
274
|
+
casting_policy_version: '1.1',
|
|
275
|
+
allowlist_universes: [universe],
|
|
276
|
+
universe_capacity: {
|
|
277
|
+
[universe]: (UNIVERSE_CHARACTERS[universe] ?? DUNE_CHARACTERS).length,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const registryAgents = {};
|
|
281
|
+
for (const a of castAgents) {
|
|
282
|
+
registryAgents[a.originalSlug] = {
|
|
283
|
+
persistent_name: a.castName,
|
|
284
|
+
universe,
|
|
285
|
+
created_at: now,
|
|
286
|
+
legacy_named: false,
|
|
287
|
+
status: 'active',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
for (const a of FIXED_AGENTS) {
|
|
291
|
+
registryAgents[a.slug] = {
|
|
292
|
+
persistent_name: a.castName,
|
|
293
|
+
universe: 'exempt',
|
|
294
|
+
created_at: now,
|
|
295
|
+
legacy_named: false,
|
|
296
|
+
status: 'active',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const registryJson = { agents: registryAgents };
|
|
300
|
+
const agentSnapshot = {};
|
|
301
|
+
for (const a of castAgents)
|
|
302
|
+
agentSnapshot[a.originalSlug] = a.castName;
|
|
303
|
+
agentSnapshot['scribe'] = 'Scribe';
|
|
304
|
+
agentSnapshot['ralph'] = 'Ralph';
|
|
305
|
+
const historyJson = {
|
|
306
|
+
universe_usage_history: [{ universe, assignment_id: assignmentId, used_at: now }],
|
|
307
|
+
assignment_cast_snapshots: {
|
|
308
|
+
[assignmentId]: { universe, agents: agentSnapshot, created_at: now },
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
writeFile(join(squadDir, 'casting', 'policy.json'), JSON.stringify(policyJson, null, 2));
|
|
312
|
+
writeFile(join(squadDir, 'casting', 'registry.json'), JSON.stringify(registryJson, null, 2));
|
|
313
|
+
writeFile(join(squadDir, 'casting', 'history.json'), JSON.stringify(historyJson, null, 2));
|
|
314
|
+
// ── .gitattributes merge driver ───────────────────────────────────────────
|
|
315
|
+
ensureGitattributes(projectRoot);
|
|
316
|
+
return {
|
|
317
|
+
squadDir,
|
|
318
|
+
agents: castAgents.map(a => ({ slug: a.slug, castName: a.castName, role: a.role })),
|
|
319
|
+
universe,
|
|
320
|
+
assignmentId,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Private helpers
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
function writeFile(path, content) {
|
|
327
|
+
writeFileSync(path, content, 'utf-8');
|
|
328
|
+
}
|
|
329
|
+
function buildCharter(opts) {
|
|
330
|
+
return `# ${opts.castName} — ${opts.role}
|
|
331
|
+
|
|
332
|
+
> ${opts.description}
|
|
333
|
+
|
|
334
|
+
## Identity
|
|
335
|
+
|
|
336
|
+
- **Name:** ${opts.castName}
|
|
337
|
+
- **Role:** ${opts.role}
|
|
338
|
+
- **Universe:** ${opts.universe}
|
|
339
|
+
- **Project:** ${opts.projectName}
|
|
340
|
+
|
|
341
|
+
## Project Context
|
|
342
|
+
|
|
343
|
+
- **Stack:** ${opts.stack}
|
|
344
|
+
- **Goal:** ${opts.goal}
|
|
345
|
+
- **Human:** ${opts.humanName}
|
|
346
|
+
|
|
347
|
+
## What I Own
|
|
348
|
+
|
|
349
|
+
_Defined by role: ${opts.role}. Update this section with project-specific scope._
|
|
350
|
+
|
|
351
|
+
## Constraints
|
|
352
|
+
|
|
353
|
+
- Follow the team's conventions in \`.squad/decisions.md\`
|
|
354
|
+
- All commits must follow Conventional Commits
|
|
355
|
+
- All PRs must reference the issue they close
|
|
356
|
+
|
|
357
|
+
## Voice
|
|
358
|
+
|
|
359
|
+
Direct. Focused on delivering quality work for ${opts.projectName}.
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
function buildHistory(opts) {
|
|
363
|
+
return `# ${opts.castName} History
|
|
364
|
+
|
|
365
|
+
<!-- git attribute: .squad/agents/${opts.castName.toLowerCase()}/history.md merge=union -->
|
|
366
|
+
|
|
367
|
+
## Init — ${opts.now}
|
|
368
|
+
|
|
369
|
+
Joined **${opts.projectName}** as ${opts.role}, working with ${opts.humanName}.
|
|
370
|
+
|
|
371
|
+
**Stack:** ${opts.stack}
|
|
372
|
+
**Goal:** ${opts.goal}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
const GITATTRIBUTES_BLOCK = `
|
|
376
|
+
# Squad — append-only conflict-free merge
|
|
377
|
+
.squad/decisions.md merge=union
|
|
378
|
+
.squad/agents/*/history.md merge=union
|
|
379
|
+
.squad/log/** merge=union
|
|
380
|
+
.squad/orchestration-log/** merge=union
|
|
381
|
+
`;
|
|
382
|
+
function ensureGitattributes(projectRoot) {
|
|
383
|
+
const gaPath = join(projectRoot, '.gitattributes');
|
|
384
|
+
const marker = '.squad/decisions.md merge=union';
|
|
385
|
+
if (existsSync(gaPath)) {
|
|
386
|
+
const existing = readFileSync(gaPath, 'utf-8');
|
|
387
|
+
if (!existing.includes(marker)) {
|
|
388
|
+
appendFileSync(gaPath, GITATTRIBUTES_BLOCK);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
writeFileSync(gaPath, GITATTRIBUTES_BLOCK.trimStart());
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=init.js.map
|