Files
stack/docs/tasks/WP1-forge-package.md
Jarvis 774b76447d
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
fix: rename all packages from @mosaic/* to @mosaicstack/*
- Updated all package.json name fields and dependency references
- Updated all TypeScript/JavaScript imports
- Updated .woodpecker/publish.yml filters and registry paths
- Updated tools/install.sh scope default
- Updated .npmrc registry paths (worktree + host)
- Enhanced update-checker.ts with checkForAllUpdates() multi-package support
- Updated CLI update command to show table of all packages
- Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand
- Marked checkForUpdate() with @deprecated JSDoc

Closes #391
2026-04-04 21:43:23 -05:00

8.2 KiB

WP1: packages/forge — Forge Pipeline Package

Context

Port the Forge progressive refinement pipeline from Python (~/src/mosaic-stack/forge/) to TypeScript as packages/forge in this monorepo. The pipeline markdown assets (stages, agents, personas, rails, gates, templates) are already copied to packages/forge/pipeline/. This task is the TypeScript implementation layer.

Board decisions that constrain this work:

  • Abstract TaskExecutor interface — packages/forge must NOT hard-import packages/coord. Define an abstract interface; coord satisfies it.
  • Clean index.ts exports, no internal path leakage, no hardcoded paths
  • 85% test coverage on TS implementation files (markdown assets excluded)
  • Test strategy for non-deterministic AI orchestration: fixture-based integration tests
  • OpenBrain is OUT OF SCOPE
  • ESM only, zero Python

Dependencies available:

  • @mosaicstack/macp (packages/macp) is built and provides: GateEntry, GateResult, Task types, credential resolution, gate running, event emission

Source Files (Python → TypeScript)

1. types.ts

Define all Forge-specific types:

// Stage specification
interface StageSpec {
  number: string;
  title: string;
  dispatch: 'exec' | 'yolo' | 'pi';
  type: 'research' | 'review' | 'coding' | 'deploy';
  gate: string;
  promptFile: string;
  qualityGates: (string | GateEntry)[];
}

// Brief classification
type BriefClass = 'strategic' | 'technical' | 'hotfix';
type ClassSource = 'cli' | 'frontmatter' | 'auto';

// Run manifest (persisted to disk)
interface RunManifest {
  runId: string;
  brief: string;
  codebase: string;
  briefClass: BriefClass;
  classSource: ClassSource;
  forceBoard: boolean;
  createdAt: string;
  updatedAt: string;
  currentStage: string;
  status: 'in_progress' | 'completed' | 'failed' | 'interrupted' | 'rejected';
  stages: Record<string, StageStatus>;
}

// Abstract task executor (decouples from packages/coord)
interface TaskExecutor {
  submitTask(task: ForgeTask): Promise<void>;
  waitForCompletion(taskId: string, timeoutMs: number): Promise<TaskResult>;
}

// Persona override config
interface ForgeConfig {
  board?: {
    additionalMembers?: string[];
    skipMembers?: string[];
  };
  specialists?: {
    alwaysInclude?: string[];
  };
}

2. constants.ts

Source: Top of ~/src/mosaic-stack/forge/lib (ALL_STAGES, LABELS, STAGE_SPECS equivalent) + ~/src/mosaic-stack/forge/pipeline/orchestrator/stage_adapter.py (STAGE_TIMEOUTS)

export const STAGE_SEQUENCE = [
  '00-intake',
  '00b-discovery',
  '01-board',
  '01b-brief-analyzer',
  '02-planning-1',
  '03-planning-2',
  '04-planning-3',
  '05-coding',
  '06-review',
  '07-remediate',
  '08-test',
  '09-deploy',
];

export const STAGE_TIMEOUTS: Record<string, number> = {
  '00-intake': 120,
  '00b-discovery': 300,
  '01-board': 120,
  '02-planning-1': 600,
  // ... etc
};

export const STAGE_LABELS: Record<string, string> = {
  '00-intake': 'INTAKE',
  // ... etc
};

Also: STRATEGIC_KEYWORDS, TECHNICAL_KEYWORDS for brief classification.

3. brief-classifier.ts

Source: classify_brief(), parse_brief_frontmatter(), stages_for_class() from ~/src/mosaic-stack/forge/lib

  • Auto-classify brief by keyword analysis (strategic vs technical)
  • Parse YAML frontmatter for explicit class: field
  • CLI flag override
  • Return stage list based on classification (strategic = full pipeline, technical = skip board, hotfix = skip board + brief analyzer)

4. stage-adapter.ts

Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/stage_adapter.py

  • mapStageToTask(): Convert a Forge stage into a task compatible with TaskExecutor
  • Stage briefs written to {runDir}/{stageName}/brief.md
  • Result paths at {runDir}/{stageName}/result.json
  • Previous results read from disk at runtime (not baked into brief)
  • Per-stage timeouts from STAGE_TIMEOUTS
  • depends_on chain built from stage sequence

5. board-tasks.ts

Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/board_tasks.py

  • loadBoardPersonas(): Read all .md files from pipeline/agents/board/
  • generateBoardTasks(): One task per persona + synthesis task
  • Synthesis depends on all persona tasks with depends_on_policy: 'all_terminal'
  • Persona briefs include role description + brief under review
  • Synthesis script merges independent reviews into board memo

6. pipeline-runner.ts

Source: ~/src/mosaic-stack/forge/pipeline/orchestrator/pipeline_runner.py + ~/src/mosaic-stack/forge/lib (cmd_run, cmd_resume, cmd_status)

  • runPipeline(briefPath, projectRoot, options): Full pipeline execution
  • Creates run directory at {projectRoot}/.forge/runs/{runId}/
  • Generates tasks for all stages, submits to TaskExecutor
  • Tracks manifest.json with stage statuses
  • resumePipeline(runDir): Pick up from last incomplete stage
  • getPipelineStatus(runDir): Read manifest and report

Key difference from Python: Run output goes to PROJECT-scoped .forge/runs/, not inside the Forge package.

7. Persona Override System (NEW — not in Python)

  • Base personas read from packages/forge/pipeline/agents/
  • Project overrides read from {projectRoot}/.forge/personas/{role}.md
  • Merge strategy: project persona content APPENDED to base persona (not replaced)
  • Board composition configurable via {projectRoot}/.forge/config.yaml
  • If no project config exists, use defaults (all base personas, no overrides)

Package Structure

packages/forge/
├── src/
│   ├── index.ts
│   ├── types.ts
│   ├── constants.ts
│   ├── brief-classifier.ts
│   ├── stage-adapter.ts
│   ├── board-tasks.ts
│   ├── pipeline-runner.ts
│   └── persona-loader.ts
├── pipeline/              # Already copied (WP4) — markdown assets
│   ├── stages/
│   ├── agents/
│   ├── rails/
│   ├── gates/
│   └── templates/
├── __tests__/
│   ├── brief-classifier.test.ts
│   ├── stage-adapter.test.ts
│   ├── board-tasks.test.ts
│   ├── pipeline-runner.test.ts
│   └── persona-loader.test.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts

Package.json

{
  "name": "@mosaicstack/forge",
  "version": "0.0.1",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "dependencies": {
    "@mosaicstack/macp": "workspace:*"
  },
  "devDependencies": {
    "vitest": "workspace:*",
    "typescript": "workspace:*"
  }
}

Only dependency: @mosaicstack/macp (for gate types, event emission).

Test Strategy (Board requirement)

Deterministic code (brief-classifier, stage-adapter, board-tasks, persona-loader, constants):

  • Standard unit tests with known inputs/outputs
  • 100% of classification logic, stage mapping, persona loading covered

Non-deterministic code (pipeline-runner):

  • Fixture-based integration tests using a mock TaskExecutor
  • Mock executor returns pre-recorded results for each stage
  • Tests verify: manifest progression, stage ordering, dependency enforcement, resume behavior, error handling
  • NO real AI calls in tests

Markdown assets: Excluded from coverage measurement (configure vitest to exclude pipeline/ directory).

ESM Requirements

  • "type": "module" in package.json
  • NodeNext module resolution in tsconfig
  • .js extensions in all imports
  • No CommonJS

Key Design: Abstract TaskExecutor

// In packages/forge/src/types.ts
export interface TaskExecutor {
  submitTask(task: ForgeTask): Promise<void>;
  waitForCompletion(taskId: string, timeoutMs: number): Promise<TaskResult>;
  getTaskStatus(taskId: string): Promise<TaskStatus>;
}

// In packages/coord (or wherever the concrete impl lives)
export class CoordTaskExecutor implements TaskExecutor {
  // ... uses packages/coord runner
}

This means packages/forge can be tested with a mock executor and deployed with any backend.

Asset Resolution

Pipeline markdown assets (stages, personas, rails) must be resolved relative to the package installation, NOT hardcoded paths:

// Use import.meta.url to find package root
const PACKAGE_ROOT = new URL('..', import.meta.url).pathname;
const PIPELINE_DIR = path.join(PACKAGE_ROOT, 'pipeline');

Project-level overrides resolved relative to projectRoot parameter.