Skip to content

Commit 79417f6

Browse files
committed
feat: add composable monorepo workforce workflows
1 parent d9479e9 commit 79417f6

13 files changed

Lines changed: 566 additions & 22 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Demonstrate Codex Deployment Engineering capability end-to-end:
4646

4747
- `pnpm scaffold:app --name <domain>` for single-app domain growth
4848
- `pnpm scaffold:workforce --web <app> --api <service>` for monorepo workforce bootstrap
49+
- `pnpm scaffold:next-app --web <app>` for Next-only bootstrap
50+
- `pnpm scaffold:fastapi-service --api <service>` for FastAPI-only bootstrap
51+
- `pnpm dev:all` for running all JS apps in `app/` and `apps/*`
4952

5053
## Agent Operating Rules
5154

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ For monorepo starters (Next.js + FastAPI):
4242
pnpm scaffold:workforce --web studio --api core
4343
```
4444

45+
Or use focused scaffolders:
46+
47+
```bash
48+
pnpm scaffold:next-app --web studio
49+
pnpm scaffold:fastapi-service --api core
50+
```
51+
4552
## What We Review For
4653

4754
- Documentation integrity (`pnpm lint:docs`)

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ pnpm check:all
5050
- `artifacts/ui-evidence.json`
5151
- `artifacts/obs-evidence.json`
5252

53-
## Monorepo Workforce Bootstrap (Next.js + FastAPI)
53+
## Monorepo Workforce Bootstrap (Composable)
5454

55-
If you want the repo to become a multi-app + multi-service workforce starter immediately:
55+
You can scaffold only what you need:
5656

5757
```bash
5858
pnpm scaffold:workforce --web studio --api core
@@ -65,16 +65,30 @@ Generated:
6565
- `services/core`: FastAPI service skeleton with health endpoint and tests
6666
- `docs/product-specs/studio-app.md` and `docs/product-specs/core-service.md`
6767

68+
Use alternative modes when you want less opinionated starts:
69+
70+
```bash
71+
# Next.js only
72+
pnpm scaffold:next-app --web studio
73+
74+
# FastAPI service only
75+
pnpm scaffold:fastapi-service --api core
76+
```
77+
6878
## Command Interface
6979

7080
| Command | Purpose |
7181
| --- | --- |
7282
| `pnpm bootstrap` | Install deps, install git hooks, refresh quality score |
7383
| `pnpm dev` | Start app on deterministic worktree-specific port |
84+
| `pnpm dev:all` | Start all JS app workspaces with deterministic ports |
7485
| `pnpm scaffold:workforce --web <app> --api <service>` | Scaffold monorepo starter (Next.js app + optional FastAPI service) |
86+
| `pnpm scaffold:next-app --web <app>` | Scaffold only a Next.js workspace app |
87+
| `pnpm scaffold:fastapi-service --api <service>` | Scaffold only a FastAPI service |
7588
| `pnpm scaffold:app --name <domain>` | Fast path: scaffold + wire + validate |
7689
| `pnpm scaffold:domain --name <domain> [--wire]` | Scaffold domain only (optional wiring) |
7790
| `pnpm check:all` | Run docs lint, architecture lint, tests, UI verify, OBS verify |
91+
| `pnpm check:workspace` | Run `lint/typecheck/test` across JS workspace packages when available |
7892
| `pnpm lint:docs` | Enforce docs metadata, freshness, and indexing |
7993
| `pnpm lint:architecture` | Enforce layer and import boundaries |
8094
| `pnpm verify` | Run both evidence verifiers (`verify:ui`, `verify:obs`) |
@@ -180,6 +194,13 @@ The repo is designed for parallel agent work:
180194
4. Add one custom guardrail script and wire it into CI.
181195
5. Keep docs/quality current with `pnpm doc:garden` and `pnpm quality:update`.
182196

197+
## Design Principle
198+
199+
This starter is intentionally balanced:
200+
201+
- Opinionated where reliability matters (guardrails, evidence, docs contracts).
202+
- Flexible where product shape differs (choose Next only, API only, or both; add any stack under `apps/*`, `services/*`, `packages/*`).
203+
183204
## Open Source
184205

185206
- License: [MIT](./LICENSE)

docs/BUILD_RECIPES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ source .venv/bin/activate
6767
pip install -e ".[dev]"
6868
uvicorn app.main:app --reload --port 8000
6969
```
70+
71+
## Recipe 6: Start Every JS App in the Workspace
72+
73+
```bash
74+
pnpm dev:all
75+
```
76+
77+
For planning only:
78+
79+
```bash
80+
pnpm dev:all -- --dry-run
81+
```

docs/EXTENDING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ pip install -e ".[dev]"
4646
uvicorn app.main:app --reload --port 8000
4747
```
4848

49+
## Run Multiple App Workspaces Together
50+
51+
If you have several JS apps under `apps/*`, run them together:
52+
53+
```bash
54+
pnpm dev:all
55+
```
56+
57+
The command prints and uses deterministic local ports so each app is reachable without manual coordination.
58+
4959
## Add New Guardrails
5060

5161
1. Implement script under `scripts/`.

docs/GETTING_STARTED.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ pnpm install
4242
pnpm --dir apps/studio dev
4343
```
4444

45+
Alternative starts:
46+
47+
```bash
48+
# Next.js only
49+
pnpm scaffold:next-app --web studio
50+
51+
# FastAPI only
52+
pnpm scaffold:fastapi-service --api core
53+
```
54+
4555
## Troubleshooting
4656

4757
- If `verify:ui` fails, inspect `artifacts/ui-evidence.json`.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@
2929
"quality:update": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/quality-score.ts update",
3030
"doc:garden": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/doc-gardener.ts",
3131
"worktree:dev": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/worktree-dev.ts",
32+
"dev:all": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/dev-all.ts",
3233
"scaffold:domain": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/scaffold-domain.ts",
3334
"scaffold:workforce": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/scaffold-workforce.ts",
35+
"scaffold:next-app": "pnpm scaffold:workforce --no-fastapi",
36+
"scaffold:fastapi-service": "pnpm scaffold:workforce --no-next",
3437
"scaffold:app": "pnpm quickstart",
3538
"quickstart": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/quickstart.ts",
3639
"review:check": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/check-review-loop.ts",
40+
"check:workspace": "TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm scripts/check-workspace.ts",
3741
"test": "vitest run",
3842
"test:watch": "vitest",
3943
"check:all": "pnpm lint:docs && pnpm lint:architecture && pnpm test && pnpm verify:ui && pnpm verify:obs"

scripts/check-workspace.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { spawnSync } from "node:child_process";
4+
import { fileURLToPath } from "node:url";
5+
6+
export interface WorkspacePackage {
7+
name: string;
8+
dir: string;
9+
scripts: string[];
10+
}
11+
12+
interface CheckWorkspaceArgs {
13+
includeBuild: boolean;
14+
}
15+
16+
function readPackageJson(packageJsonPath: string): { scripts?: Record<string, string> } | null {
17+
try {
18+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { scripts?: Record<string, string> };
19+
} catch {
20+
return null;
21+
}
22+
}
23+
24+
function collectWorkspacePackageDirs(repoRoot: string): string[] {
25+
const dirs: string[] = [];
26+
27+
const rootApp = path.join(repoRoot, "app");
28+
if (fs.existsSync(path.join(rootApp, "package.json"))) {
29+
dirs.push(rootApp);
30+
}
31+
32+
for (const section of ["apps", "packages"]) {
33+
const sectionDir = path.join(repoRoot, section);
34+
if (!fs.existsSync(sectionDir)) {
35+
continue;
36+
}
37+
38+
const entries = fs.readdirSync(sectionDir, { withFileTypes: true });
39+
for (const entry of entries) {
40+
if (!entry.isDirectory()) {
41+
continue;
42+
}
43+
44+
const packageDir = path.join(sectionDir, entry.name);
45+
if (fs.existsSync(path.join(packageDir, "package.json"))) {
46+
dirs.push(packageDir);
47+
}
48+
}
49+
}
50+
51+
return dirs.sort();
52+
}
53+
54+
export function discoverWorkspacePackages(repoRoot: string): WorkspacePackage[] {
55+
const dirs = collectWorkspacePackageDirs(repoRoot);
56+
57+
return dirs
58+
.map((dir) => {
59+
const pkg = readPackageJson(path.join(dir, "package.json"));
60+
if (!pkg) {
61+
return null;
62+
}
63+
64+
const scripts = Object.keys(pkg.scripts ?? {});
65+
return {
66+
name: path.relative(repoRoot, dir),
67+
dir,
68+
scripts
69+
};
70+
})
71+
.filter((pkg): pkg is WorkspacePackage => pkg !== null);
72+
}
73+
74+
export function parseCheckWorkspaceArgs(argv: string[]): CheckWorkspaceArgs {
75+
return {
76+
includeBuild: argv.includes("--include-build")
77+
};
78+
}
79+
80+
function runScript(pkg: WorkspacePackage, script: string): number {
81+
const result = spawnSync("pnpm", ["--dir", pkg.dir, "run", script], {
82+
stdio: "inherit"
83+
});
84+
return result.status ?? 1;
85+
}
86+
87+
export function runCheckWorkspace(repoRoot: string, args: CheckWorkspaceArgs): number {
88+
const packages = discoverWorkspacePackages(repoRoot);
89+
if (packages.length === 0) {
90+
console.log("check:workspace: no JS workspace packages found under app/, apps/*, or packages/*");
91+
return 0;
92+
}
93+
94+
const scriptOrder = ["lint", "typecheck", "test", ...(args.includeBuild ? ["build"] : [])];
95+
let failures = 0;
96+
97+
for (const pkg of packages) {
98+
console.log(`check:workspace: ${pkg.name}`);
99+
for (const script of scriptOrder) {
100+
if (!pkg.scripts.includes(script)) {
101+
continue;
102+
}
103+
104+
const code = runScript(pkg, script);
105+
if (code !== 0) {
106+
console.error(`check:workspace: FAIL ${pkg.name} script '${script}' exited with ${code}`);
107+
failures += 1;
108+
break;
109+
}
110+
}
111+
}
112+
113+
if (failures > 0) {
114+
return 1;
115+
}
116+
117+
console.log("check:workspace: PASS");
118+
return 0;
119+
}
120+
121+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
122+
123+
if (isMain) {
124+
const args = parseCheckWorkspaceArgs(process.argv.slice(2));
125+
const code = runCheckWorkspace(process.cwd(), args);
126+
process.exit(code);
127+
}
128+

0 commit comments

Comments
 (0)