feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
Work packages completed: - WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier, persona loader with project-level overrides. 89 tests, 95.62% coverage. - WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types. 65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior. - WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start + subagent_spawning hooks for Mosaic contract enforcement). - WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs), skills/ (5 universal skills), forge pipeline assets (48 markdown files). Board deliberation: docs/reviews/consolidation-board-memo.md Brief: briefs/monorepo-consolidation.md Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack. 154 new tests total. Zero Python — all TypeScript/ESM.
This commit is contained in:
102
packages/forge/src/brief-classifier.ts
Normal file
102
packages/forge/src/brief-classifier.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { STAGE_SEQUENCE, STRATEGIC_KEYWORDS, TECHNICAL_KEYWORDS } from './constants.js';
|
||||
import type { BriefClass, ClassSource } from './types.js';
|
||||
|
||||
const VALID_CLASSES: ReadonlySet<string> = new Set<BriefClass>([
|
||||
'strategic',
|
||||
'technical',
|
||||
'hotfix',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Auto-classify a brief based on keyword analysis.
|
||||
* Returns 'strategic' if strategic keywords dominate,
|
||||
* 'technical' if any technical keywords are found,
|
||||
* otherwise defaults to 'strategic' (full pipeline).
|
||||
*/
|
||||
export function classifyBrief(text: string): BriefClass {
|
||||
const lower = text.toLowerCase();
|
||||
let strategicHits = 0;
|
||||
let technicalHits = 0;
|
||||
|
||||
for (const kw of STRATEGIC_KEYWORDS) {
|
||||
if (lower.includes(kw)) strategicHits++;
|
||||
}
|
||||
for (const kw of TECHNICAL_KEYWORDS) {
|
||||
if (lower.includes(kw)) technicalHits++;
|
||||
}
|
||||
|
||||
if (strategicHits > technicalHits) return 'strategic';
|
||||
if (technicalHits > 0) return 'technical';
|
||||
return 'strategic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from a brief.
|
||||
* Supports simple `key: value` pairs via regex (no YAML dependency).
|
||||
*/
|
||||
export function parseBriefFrontmatter(text: string): Record<string, string> {
|
||||
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
if (!match?.[1]) return {};
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of match[1].split('\n')) {
|
||||
const km = line.trim().match(/^(\w[\w-]*)\s*:\s*(.+)$/);
|
||||
if (km?.[1] && km[2]) {
|
||||
result[km[1]] = km[2].trim().replace(/^["']|["']$/g, '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine brief class from all sources with priority:
|
||||
* CLI flag > frontmatter > auto-classify.
|
||||
*/
|
||||
export function determineBriefClass(
|
||||
text: string,
|
||||
cliClass?: string,
|
||||
): { briefClass: BriefClass; classSource: ClassSource } {
|
||||
if (cliClass && VALID_CLASSES.has(cliClass)) {
|
||||
return { briefClass: cliClass as BriefClass, classSource: 'cli' };
|
||||
}
|
||||
|
||||
const fm = parseBriefFrontmatter(text);
|
||||
if (fm['class'] && VALID_CLASSES.has(fm['class'])) {
|
||||
return { briefClass: fm['class'] as BriefClass, classSource: 'frontmatter' };
|
||||
}
|
||||
|
||||
return { briefClass: classifyBrief(text), classSource: 'auto' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the stage list based on brief classification.
|
||||
* - strategic: full pipeline (all stages)
|
||||
* - technical: skip board (01-board)
|
||||
* - hotfix: skip board + brief analyzer
|
||||
*
|
||||
* forceBoard re-adds the board stage regardless of class.
|
||||
*/
|
||||
export function stagesForClass(briefClass: BriefClass, forceBoard = false): string[] {
|
||||
const stages = ['00-intake', '00b-discovery'];
|
||||
|
||||
if (briefClass === 'strategic' || forceBoard) {
|
||||
stages.push('01-board');
|
||||
}
|
||||
if (briefClass === 'strategic' || briefClass === 'technical' || forceBoard) {
|
||||
stages.push('01b-brief-analyzer');
|
||||
}
|
||||
|
||||
stages.push(
|
||||
'02-planning-1',
|
||||
'03-planning-2',
|
||||
'04-planning-3',
|
||||
'05-coding',
|
||||
'06-review',
|
||||
'07-remediate',
|
||||
'08-test',
|
||||
'09-deploy',
|
||||
);
|
||||
|
||||
// Maintain canonical order
|
||||
return stages.filter((s) => STAGE_SEQUENCE.includes(s));
|
||||
}
|
||||
Reference in New Issue
Block a user