feat(org): agenthub org sync — CLAUDE.md is the team-structure source
Add an org service + CLI: parse a fenced ```agenthub-org YAML block from CLAUDE.md (or AGENTS.md), validate it (one root, resolvable parents, unique ids) and write it to .agenthub/agenthub.config.json. So the human-readable doc drives /team: edit the block, run `agenthub org sync`, the chart adapts. Can be wired into a post-commit hook to sync automatically when CLAUDE.md changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0f11714873
commit
c2939966b5
@ -7,6 +7,7 @@ import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js';
|
|||||||
import { decisionCreate, decisionList } from './commands/decision.js';
|
import { decisionCreate, decisionList } from './commands/decision.js';
|
||||||
import { messageSend, inboxList } from './commands/message.js';
|
import { messageSend, inboxList } from './commands/message.js';
|
||||||
import { agentSetup, hookContext } from './commands/agentSetup.js';
|
import { agentSetup, hookContext } from './commands/agentSetup.js';
|
||||||
|
import { syncOrgFromFile } from '../core/services/orgService.js';
|
||||||
import { delegate } from './commands/delegate.js';
|
import { delegate } from './commands/delegate.js';
|
||||||
import { serverStart } from './commands/server.js';
|
import { serverStart } from './commands/server.js';
|
||||||
import { update } from './commands/update.js';
|
import { update } from './commands/update.js';
|
||||||
@ -508,6 +509,23 @@ export function createProgram(cwd: string): Command {
|
|||||||
});
|
});
|
||||||
program.addCommand(agentCmd);
|
program.addCommand(agentCmd);
|
||||||
|
|
||||||
|
// ─── org sync (CLAUDE.md → team structure) ───────────────────────────────
|
||||||
|
const orgCmd = new Command('org').description('Team org-chart structure');
|
||||||
|
orgCmd
|
||||||
|
.command('sync')
|
||||||
|
.description('Sync the team org chart from an `agenthub-org` block in CLAUDE.md / AGENTS.md')
|
||||||
|
.option('--from <file>', 'Source markdown file (default: CLAUDE.md or AGENTS.md at the project root)')
|
||||||
|
.action((options: { from?: string }) => {
|
||||||
|
try {
|
||||||
|
const r = syncOrgFromFile(cwd, options.from);
|
||||||
|
console.log(`AgentHub: org synced — ${r.count} nodes from ${r.file}. /team now reflects it.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`AgentHub: org sync failed — ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
program.addCommand(orgCmd);
|
||||||
|
|
||||||
// Called by the SessionStart hook; prints the work-loop instruction as context.
|
// Called by the SessionStart hook; prints the work-loop instruction as context.
|
||||||
program
|
program
|
||||||
.command('hook-context')
|
.command('hook-context')
|
||||||
|
|||||||
58
src/core/services/orgService.ts
Normal file
58
src/core/services/orgService.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { parse as parseYaml } from 'yaml';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { findProjectRoot } from '../paths.js';
|
||||||
|
import { loadConfig, saveConfig } from '../config.js';
|
||||||
|
import { OrgNodeSchema, type OrgNode } from '../schema.js';
|
||||||
|
|
||||||
|
const OrgArray = z.array(OrgNodeSchema).min(1);
|
||||||
|
|
||||||
|
/** Pull the fenced ```agenthub-org … ``` YAML block out of a markdown file. */
|
||||||
|
export function extractOrgBlock(text: string): string | null {
|
||||||
|
const m = text.match(/```agenthub-org\s*\n([\s\S]*?)\n```/);
|
||||||
|
return m ? m[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse + validate an org spec (YAML text) into a checked node list. */
|
||||||
|
export function parseOrg(yamlText: string): OrgNode[] {
|
||||||
|
const org = OrgArray.parse(parseYaml(yamlText));
|
||||||
|
const ids = new Set(org.map((n) => n.id));
|
||||||
|
if (ids.size !== org.length) throw new Error('duplicate node id in org block');
|
||||||
|
for (const n of org) {
|
||||||
|
if (n.parentId && !ids.has(n.parentId)) throw new Error(`node "${n.id}" → unknown parentId "${n.parentId}"`);
|
||||||
|
}
|
||||||
|
const roots = org.filter((n) => !n.parentId);
|
||||||
|
if (roots.length !== 1) throw new Error(`org must have exactly one root (found ${roots.length})`);
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the source markdown file (explicit, else CLAUDE.md / AGENTS.md at root). */
|
||||||
|
export function resolveSourceFile(cwd: string, from?: string): string | undefined {
|
||||||
|
const root = findProjectRoot(cwd) ?? cwd;
|
||||||
|
if (from) return existsSync(from) ? from : undefined;
|
||||||
|
for (const cand of ['CLAUDE.md', 'AGENTS.md']) {
|
||||||
|
const p = join(root, cand);
|
||||||
|
if (existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync the team org chart from an `agenthub-org` block in a markdown file
|
||||||
|
* (CLAUDE.md by default) into `.agenthub/agenthub.config.json`. This makes the
|
||||||
|
* human-readable doc the single source of truth for the team structure — edit
|
||||||
|
* the block, run `agenthub org sync`, and /team reflects it.
|
||||||
|
*/
|
||||||
|
export function syncOrgFromFile(cwd: string, from?: string): { count: number; file: string } {
|
||||||
|
const root = findProjectRoot(cwd) ?? cwd;
|
||||||
|
const file = resolveSourceFile(cwd, from);
|
||||||
|
if (!file) throw new Error('no source file found (looked for CLAUDE.md / AGENTS.md) — pass --from <file>');
|
||||||
|
const block = extractOrgBlock(readFileSync(file, 'utf-8'));
|
||||||
|
if (!block) throw new Error(`no \`\`\`agenthub-org block found in ${file}`);
|
||||||
|
const org = parseOrg(block);
|
||||||
|
const config = loadConfig(root);
|
||||||
|
config.org = org;
|
||||||
|
saveConfig(root, config);
|
||||||
|
return { count: org.length, file };
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user