xtruct 0.1.0__py3-none-any.whl
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.
- xtruct/__init__.py +3 -0
- xtruct/__main__.py +5 -0
- xtruct/_cli/bin/xtruct-foundry.js +3 -0
- xtruct/_cli/bin/xtruct.js +541 -0
- xtruct/_cli/global/claude-md-block.md +5 -0
- xtruct/_cli/global/skills/foundry/SKILL.md +46 -0
- xtruct/_cli/lib/dashboard.html +326 -0
- xtruct/_cli/lib/dashboard.js +212 -0
- xtruct/_cli/lib/paths.js +17 -0
- xtruct/_cli/lib/setup.js +136 -0
- xtruct/_cli/lib/tracker.js +155 -0
- xtruct/_cli/lib/ui.js +95 -0
- xtruct/_cli/package.json +44 -0
- xtruct/_cli/template/.claude/agents/architect.md +35 -0
- xtruct/_cli/template/.claude/agents/integrator.md +16 -0
- xtruct/_cli/template/.claude/agents/junior-dev.md +31 -0
- xtruct/_cli/template/.claude/agents/qa-lead.md +17 -0
- xtruct/_cli/template/.claude/agents/reviewer.md +23 -0
- xtruct/_cli/template/.claude/agents/senior-lead.md +29 -0
- xtruct/_cli/template/.claude/hooks/guard-bash.sh +38 -0
- xtruct/_cli/template/.claude/hooks/guard-files.sh +24 -0
- xtruct/_cli/template/.claude/settings.json +39 -0
- xtruct/_cli/template/.claude/skills/run-epics/SKILL.md +45 -0
- xtruct/_cli/template/.claude/workflows/plan-product.js +136 -0
- xtruct/_cli/template/.claude/workflows/run-epic.js +248 -0
- xtruct/_cli/template/.github/pull_request_template.md +24 -0
- xtruct/_cli/template/CLAUDE.md +65 -0
- xtruct/_cli/template/Makefile +8 -0
- xtruct/_cli/template/_gitignore +13 -0
- xtruct/_cli/template/contracts/README.md +4 -0
- xtruct/_cli/template/docs/CONVENTIONS.md +3 -0
- xtruct/_cli/template/docs/templates/ARCHITECTURE.md +30 -0
- xtruct/_cli/template/docs/templates/PRD.md +25 -0
- xtruct/_cli/template/docs/templates/ci.yml +22 -0
- xtruct/_cli/template/foundry.config.json +25 -0
- xtruct/_cli/template/scripts/bootstrap-github.sh +48 -0
- xtruct/_cli/template/scripts/capacity.mjs +53 -0
- xtruct/_cli/template/scripts/merge-pr.sh +97 -0
- xtruct/launcher.py +56 -0
- xtruct-0.1.0.dist-info/METADATA +41 -0
- xtruct-0.1.0.dist-info/RECORD +44 -0
- xtruct-0.1.0.dist-info/WHEEL +4 -0
- xtruct-0.1.0.dist-info/entry_points.txt +2 -0
- xtruct-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse guard for Edit/Write: keeps agents out of founder-only paths and secret files.
|
|
3
|
+
# Exit 2 = block, stderr is shown to the agent. Founder override: FOUNDRY_ALLOW_PROTECTED=1.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
[ "${FOUNDRY_ALLOW_PROTECTED:-0}" = 1 ] && exit 0
|
|
7
|
+
|
|
8
|
+
path=$(jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')
|
|
9
|
+
[ -z "$path" ] && exit 0
|
|
10
|
+
|
|
11
|
+
block() { echo "Blocked by company guardrail: $1 ($path)" >&2; exit 2; }
|
|
12
|
+
|
|
13
|
+
case "$path" in
|
|
14
|
+
*.env.example) ;;
|
|
15
|
+
*.env|*.env.*|*/.env|*/.env.*) block "secret files are off limits; document variables in .env.example" ;;
|
|
16
|
+
*.pem|*.key|*/id_rsa*|*/id_ed25519*) block "private keys are off limits" ;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
grep -Eq '/\.claude/(settings[^/]*\.json|hooks/|agents/|workflows/|skills/)' <<<"$path" \
|
|
20
|
+
&& block "the company's agent setup is founder-only"
|
|
21
|
+
grep -Eq '(^|/)(CLAUDE\.md|scripts/merge-pr\.sh|scripts/bootstrap-github\.sh|scripts/capacity\.mjs)$' <<<"$path" \
|
|
22
|
+
&& block "this file is founder-only"
|
|
23
|
+
|
|
24
|
+
exit 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git:*)",
|
|
5
|
+
"Bash(gh:*)",
|
|
6
|
+
"Bash(make:*)",
|
|
7
|
+
"Bash(scripts/merge-pr.sh:*)",
|
|
8
|
+
"Bash(jq:*)",
|
|
9
|
+
"Bash(pnpm:*)",
|
|
10
|
+
"Bash(npm:*)",
|
|
11
|
+
"Bash(npx:*)",
|
|
12
|
+
"Bash(node:*)",
|
|
13
|
+
"Bash(uv:*)",
|
|
14
|
+
"Bash(python3:*)",
|
|
15
|
+
"Bash(pytest:*)",
|
|
16
|
+
"Bash(docker compose:*)",
|
|
17
|
+
"mcp__claude_ai_ClickUp"
|
|
18
|
+
],
|
|
19
|
+
"deny": [
|
|
20
|
+
"Read(./.env)",
|
|
21
|
+
"Read(./.env.local)",
|
|
22
|
+
"Read(./.env.production)",
|
|
23
|
+
"Bash(gh repo delete:*)",
|
|
24
|
+
"Bash(gh pr merge:*)"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"hooks": {
|
|
28
|
+
"PreToolUse": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-bash.sh" }]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
|
35
|
+
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-files.sh" }]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-epics
|
|
3
|
+
description: Run one or more epics with the agent team, sized to this machine's capacity. Use when the founder says to run, build or start an epic or epics, names epic ids (E2, E3…), or says "build everything that's ready".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run epics at machine capacity
|
|
7
|
+
|
|
8
|
+
Each epic runs as its own `run-epic` workflow. Claude Code caps a single workflow at min(16, cores − 2) agents, so to use the whole machine, run several epics side by side, each capped at its share.
|
|
9
|
+
|
|
10
|
+
## 1. Measure the machine
|
|
11
|
+
|
|
12
|
+
Run `node scripts/capacity.mjs`. Use `total_agents` (the machine's budget) and `per_workflow_ceiling`.
|
|
13
|
+
|
|
14
|
+
## 2. Pick the epics
|
|
15
|
+
|
|
16
|
+
Read `foundry.config.json` for `clickup.epics_list_id`, then list the epics in ClickUp with their status and the dependencies in their descriptions.
|
|
17
|
+
|
|
18
|
+
- An epic is **ready** when every epic it depends on is `in review` or `done`.
|
|
19
|
+
- If the founder named epics, use those. If one isn't ready, say which dependency is missing and leave it out.
|
|
20
|
+
- If the founder said "everything that's ready", take all ready epics in key order.
|
|
21
|
+
- **Foundation (E1) always runs alone.** If it isn't done yet, run only E1.
|
|
22
|
+
|
|
23
|
+
## 3. Split the budget and launch
|
|
24
|
+
|
|
25
|
+
- `n` = number of epics to start now = min(ready epics, max(1, floor(total_agents / 8))). With one ready epic, n = 1.
|
|
26
|
+
- Each epic's `maxAgents` = min(per_workflow_ceiling, floor(total_agents / n)).
|
|
27
|
+
- Start all `n` workflows **in the same message**, one Workflow call each: `name: "run-epic"`, `args: {"epic": "<ClickUp id>", "maxAgents": <share>}`.
|
|
28
|
+
- Tell the founder in two or three lines: which epics started, agents per epic, total versus the machine's budget, and which ready epics are queued.
|
|
29
|
+
|
|
30
|
+
## 4. Refill as epics finish
|
|
31
|
+
|
|
32
|
+
When a run-epic workflow completes:
|
|
33
|
+
|
|
34
|
+
1. Report its result to the founder in a few lines: merged / rejected / failed / blocked counts, whether QA passed, and the ClickUp epic link.
|
|
35
|
+
2. Rerun `node scripts/capacity.mjs`, since load may have changed.
|
|
36
|
+
3. Recheck readiness: an epic that just reached `in review` may unblock others.
|
|
37
|
+
4. Start queued or newly ready epics, keeping the sum of `maxAgents` across running workflows at or below `total_agents`.
|
|
38
|
+
|
|
39
|
+
Stop when nothing is ready or queued. Never start an epic whose dependency finished with failed or rejected tasks; report it as blocked and let the founder decide.
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
- Never exceed `total_agents` in total, even if the founder names more epics than fit. Queue the rest.
|
|
44
|
+
- If `capacity.mjs` shows `limited_by: "ram"` and the machine feels slow, lower the shares rather than raising them.
|
|
45
|
+
- The founder can tune capacity in `foundry.config.json` → `limits` (`agents_per_core`, `ram_per_agent_gb`, `reserve_ram_gb`, `max_agents`).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'plan-product',
|
|
3
|
+
description: 'Architect turns the founder idea into PRD, architecture, contracts and epics; two critics challenge it; epics land in ClickUp',
|
|
4
|
+
whenToUse: 'Start of a new product. args: {idea: "<what to build, for whom, any constraints>"}',
|
|
5
|
+
phases: [
|
|
6
|
+
{ title: 'Spec', detail: 'architect drafts PRD, architecture, stack, contracts, epics' },
|
|
7
|
+
{ title: 'Critique', detail: 'product and engineering critics challenge the draft' },
|
|
8
|
+
{ title: 'Revise', detail: 'architect applies or rejects each critique point' },
|
|
9
|
+
{ title: 'Board', detail: 'ClickUp folder + Epics list with one task per epic' },
|
|
10
|
+
],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const IDEA = typeof args === 'string' ? args : args && args.idea
|
|
14
|
+
if (!IDEA) throw new Error('plan-product needs args: {idea: "<what to build>"}')
|
|
15
|
+
|
|
16
|
+
const EPICS_SCHEMA = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
product_name: { type: 'string' },
|
|
20
|
+
stack_summary: { type: 'string', description: 'one line, e.g. "Next.js + tRPC + Postgres (Drizzle), Playwright"' },
|
|
21
|
+
epics: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
key: { type: 'string', description: 'E1, E2, ...; E1 is Foundation' },
|
|
27
|
+
name: { type: 'string' },
|
|
28
|
+
goal: { type: 'string' },
|
|
29
|
+
acceptance: { type: 'array', items: { type: 'string' } },
|
|
30
|
+
depends_on: { type: 'array', items: { type: 'string' } },
|
|
31
|
+
},
|
|
32
|
+
required: ['key', 'name', 'goal', 'acceptance', 'depends_on'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
open_questions: { type: 'array', items: { type: 'string' }, description: 'decisions only the founder can make' },
|
|
36
|
+
},
|
|
37
|
+
required: ['product_name', 'stack_summary', 'epics', 'open_questions'],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CRITIQUE_SCHEMA = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
issues: {
|
|
44
|
+
type: 'array',
|
|
45
|
+
items: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
severity: { type: 'string', enum: ['must-fix', 'should-fix', 'consider'] },
|
|
49
|
+
where: { type: 'string' },
|
|
50
|
+
issue: { type: 'string' },
|
|
51
|
+
suggestion: { type: 'string' },
|
|
52
|
+
},
|
|
53
|
+
required: ['severity', 'where', 'issue', 'suggestion'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['issues'],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BOARD_SCHEMA = {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
folder_id: { type: 'string' },
|
|
64
|
+
epics_list_id: { type: 'string' },
|
|
65
|
+
note: { type: 'string', description: 'anything the founder must act on, e.g. which ClickUp space to use' },
|
|
66
|
+
epics: {
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: { key: { type: 'string' }, name: { type: 'string' }, clickup_id: { type: 'string' }, url: { type: 'string' } },
|
|
71
|
+
required: ['key', 'name', 'clickup_id'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ['folder_id', 'epics_list_id', 'epics'],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
phase('Spec')
|
|
79
|
+
const draft = await agent(
|
|
80
|
+
`Mode: SPEC. The founder's idea:\n\n${IDEA}\n\nWrite docs/PRD.md, docs/ARCHITECTURE.md, docs/CONVENTIONS.md and contracts/, commit them, and return the epic plan. Where the idea is ambiguous, choose the simplest reasonable option, note it in the PRD, and list it in open_questions.`,
|
|
81
|
+
{ agentType: 'architect', phase: 'Spec', label: 'architect:spec', schema: EPICS_SCHEMA },
|
|
82
|
+
)
|
|
83
|
+
if (!draft) throw new Error('architect did not return a spec')
|
|
84
|
+
|
|
85
|
+
phase('Critique')
|
|
86
|
+
const LENSES = [
|
|
87
|
+
{
|
|
88
|
+
key: 'product',
|
|
89
|
+
prompt: 'You are a skeptical product manager. Does the PRD actually solve the founder\'s idea? Is the MVP the smallest thing worth shipping? Missing user flows, missing unhappy paths, acceptance criteria that are not testable, scope creep.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'engineering',
|
|
93
|
+
prompt: 'You are a principal engineer. Is the stack a good fit and boring enough? Are contracts complete enough that juniors building opposite sides never need to talk? Security gaps (auth, tenancy, input validation)? Can the feature epics really run in parallel after Foundation, or are there hidden dependencies?',
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
const critiques = await parallel(LENSES.map(l => () => agent(
|
|
97
|
+
`${l.prompt}\n\nThe founder's idea:\n${IDEA}\n\nReview docs/PRD.md, docs/ARCHITECTURE.md, docs/CONVENTIONS.md and contracts/ as committed, and this epic plan:\n${JSON.stringify(draft.epics, null, 2)}\n\nDo not edit files. Return concrete issues only; no praise.`,
|
|
98
|
+
{ phase: 'Critique', label: `critic:${l.key}`, schema: CRITIQUE_SCHEMA },
|
|
99
|
+
)))
|
|
100
|
+
const issues = critiques.filter(Boolean).flatMap((c, i) => c.issues.map(x => ({ lens: LENSES[i].key, ...x })))
|
|
101
|
+
log(`${issues.length} critique points (${issues.filter(x => x.severity === 'must-fix').length} must-fix)`)
|
|
102
|
+
|
|
103
|
+
phase('Revise')
|
|
104
|
+
const final = issues.length
|
|
105
|
+
? await agent(
|
|
106
|
+
`Mode: REVISE. Apply this critique to the docs, contracts and epic plan. Fix every must-fix point unless it is wrong, in which case record why under Decisions. Commit, then return the revised epic plan.\n\n${JSON.stringify(issues, null, 2)}`,
|
|
107
|
+
{ agentType: 'architect', phase: 'Revise', label: 'architect:revise', schema: EPICS_SCHEMA },
|
|
108
|
+
)
|
|
109
|
+
: draft
|
|
110
|
+
if (!final) throw new Error('architect did not return a revised plan')
|
|
111
|
+
|
|
112
|
+
phase('Board')
|
|
113
|
+
const board = await agent(
|
|
114
|
+
`Set up the ClickUp board for product "${final.product_name}". Read the clickup section of foundry.config.json.
|
|
115
|
+
0. If space_id is empty, look up the workspace hierarchy with the ClickUp tools and pick the space named space_name, or the only space if there is exactly one. If that's still ambiguous, create nothing; return empty ids, no epics, and a note listing the available spaces so the founder can set clickup.space_id.
|
|
116
|
+
1. Create a folder named "${final.product_name}" in that space, and inside it a list named "Epics".
|
|
117
|
+
2. Create one task per epic below, in order, status "to do". Description: goal, acceptance criteria as a checklist, dependencies (by epic name), and a link to docs/PRD.md. Prefix names with the key, e.g. "E1 · Foundation".
|
|
118
|
+
3. Write workspace_id, space_id, folder_id and epics_list_id into foundry.config.json and commit ("chore: link ClickUp board"). Use jq or a small script; do not hand-edit other keys.
|
|
119
|
+
Return the ids.
|
|
120
|
+
|
|
121
|
+
Epics:\n${JSON.stringify(final.epics, null, 2)}`,
|
|
122
|
+
{ phase: 'Board', label: 'lead:board', schema: BOARD_SCHEMA },
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
product: final.product_name,
|
|
127
|
+
stack: final.stack_summary,
|
|
128
|
+
epics: final.epics.map(e => {
|
|
129
|
+
const b = board && board.epics.find(x => x.key === e.key)
|
|
130
|
+
return { key: e.key, name: e.name, depends_on: e.depends_on, clickup_id: b ? b.clickup_id : null, url: b ? b.url : null }
|
|
131
|
+
}),
|
|
132
|
+
open_questions: final.open_questions,
|
|
133
|
+
critique_points: issues.length,
|
|
134
|
+
board_ready: Boolean(board && board.epics_list_id),
|
|
135
|
+
board_note: board ? board.note || '' : 'board agent did not complete',
|
|
136
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'run-epic',
|
|
3
|
+
description: 'Senior lead splits a ClickUp epic into tasks; juniors build them in parallel worktrees; reviewer + lead gate each PR; integrator merges through the gate; QA verifies main',
|
|
4
|
+
whenToUse: 'After the spec is approved and scripts/bootstrap-github.sh has run. Usually started by the run-epics skill. args: {epic: "<ClickUp epic task id>", maxAgents?: <share from scripts/capacity.mjs>, fixRounds?: 2}',
|
|
5
|
+
phases: [
|
|
6
|
+
{ title: 'Plan', detail: 'senior lead splits the epic into tasks in ClickUp' },
|
|
7
|
+
{ title: 'Build', detail: 'one junior per task in its own worktree, opens a PR' },
|
|
8
|
+
{ title: 'Review', detail: 'reviewer and lead check each PR in parallel; fix rounds on rejection' },
|
|
9
|
+
{ title: 'Merge', detail: 'integrator merges through scripts/merge-pr.sh' },
|
|
10
|
+
{ title: 'Verify', detail: 'QA runs the full suite and acceptance walk-through on main' },
|
|
11
|
+
],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EPIC = typeof args === 'string' ? args : args && args.epic
|
|
15
|
+
if (!EPIC) throw new Error('run-epic needs args: {epic: "<ClickUp epic task id>"}')
|
|
16
|
+
const FIX_ROUNDS = args && Number.isInteger(args.fixRounds) ? args.fixRounds : 2
|
|
17
|
+
// This epic's share of the machine (from scripts/capacity.mjs). Several epics run side by side,
|
|
18
|
+
// each capped at its share, so together they never exceed what the machine can take.
|
|
19
|
+
const MAX_AGENTS = args && Number.isInteger(args.maxAgents) && args.maxAgents > 0 ? args.maxAgents : 16
|
|
20
|
+
|
|
21
|
+
// Counting semaphore around agent(). A finishing agent hands its slot straight to the next waiter,
|
|
22
|
+
// so a new caller can never slip in between and push the count over MAX_AGENTS.
|
|
23
|
+
let active = 0
|
|
24
|
+
const waiting = []
|
|
25
|
+
async function run(prompt, opts) {
|
|
26
|
+
if (active >= MAX_AGENTS) await new Promise(resolve => waiting.push(resolve))
|
|
27
|
+
else active++
|
|
28
|
+
try {
|
|
29
|
+
return await agent(prompt, opts)
|
|
30
|
+
} finally {
|
|
31
|
+
const next = waiting.shift()
|
|
32
|
+
if (next) next()
|
|
33
|
+
else active--
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TASKS_SCHEMA = {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
epic_title: { type: 'string' },
|
|
41
|
+
tasks: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
key: { type: 'string', description: 'T1, T2, ...' },
|
|
47
|
+
clickup_id: { type: 'string' },
|
|
48
|
+
title: { type: 'string' },
|
|
49
|
+
spec: { type: 'string', description: 'what to build plus acceptance criteria as testable statements' },
|
|
50
|
+
scope: { type: 'array', items: { type: 'string' }, description: 'globs this task may change' },
|
|
51
|
+
depends_on: { type: 'array', items: { type: 'string' } },
|
|
52
|
+
},
|
|
53
|
+
required: ['key', 'clickup_id', 'title', 'spec', 'scope', 'depends_on'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['epic_title', 'tasks'],
|
|
58
|
+
}
|
|
59
|
+
const PR_SCHEMA = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
pr_number: { type: 'integer', description: '0 if no PR could be opened' },
|
|
63
|
+
branch: { type: 'string' },
|
|
64
|
+
checks_passed: { type: 'boolean', description: 'make lint typecheck test passed locally' },
|
|
65
|
+
notes: { type: 'string' },
|
|
66
|
+
},
|
|
67
|
+
required: ['pr_number', 'branch', 'checks_passed', 'notes'],
|
|
68
|
+
}
|
|
69
|
+
const VERDICT_SCHEMA = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
approved: { type: 'boolean' },
|
|
73
|
+
blocking_issues: { type: 'array', items: { type: 'string' } },
|
|
74
|
+
notes: { type: 'string' },
|
|
75
|
+
},
|
|
76
|
+
required: ['approved', 'blocking_issues'],
|
|
77
|
+
}
|
|
78
|
+
const MERGE_SCHEMA = {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
merged: { type: 'boolean' },
|
|
82
|
+
merge_sha: { type: 'string' },
|
|
83
|
+
needs_rebase: { type: 'boolean' },
|
|
84
|
+
reason: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
required: ['merged'],
|
|
87
|
+
}
|
|
88
|
+
const QA_SCHEMA = {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
passed: { type: 'boolean' },
|
|
92
|
+
summary: { type: 'string' },
|
|
93
|
+
bugs: {
|
|
94
|
+
type: 'array',
|
|
95
|
+
items: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
title: { type: 'string' },
|
|
99
|
+
severity: { type: 'string', enum: ['blocker', 'major', 'minor'] },
|
|
100
|
+
clickup_id: { type: 'string' },
|
|
101
|
+
},
|
|
102
|
+
required: ['title', 'severity'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ['passed', 'summary', 'bugs'],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Plan ----
|
|
110
|
+
phase('Plan')
|
|
111
|
+
const plan = await run(
|
|
112
|
+
`Mode: PLAN. Epic: ${EPIC}. Split it into tasks, create each as a ClickUp subtask of the epic, set the epic to in progress, and return the tasks.`,
|
|
113
|
+
{ agentType: 'senior-lead', phase: 'Plan', label: 'lead:plan', schema: TASKS_SCHEMA },
|
|
114
|
+
)
|
|
115
|
+
if (!plan || !plan.tasks.length) return { epic: EPIC, status: 'no tasks planned' }
|
|
116
|
+
|
|
117
|
+
const byKey = new Map(plan.tasks.map(t => [t.key, t]))
|
|
118
|
+
for (const t of plan.tasks) {
|
|
119
|
+
const unknown = t.depends_on.filter(k => !byKey.has(k))
|
|
120
|
+
if (unknown.length) {
|
|
121
|
+
log(`${t.key}: ignoring unknown dependencies ${unknown.join(', ')}`)
|
|
122
|
+
t.depends_on = t.depends_on.filter(k => byKey.has(k))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const visiting = new Set(), visited = new Set()
|
|
126
|
+
const visit = (k, path) => {
|
|
127
|
+
if (visited.has(k)) return
|
|
128
|
+
if (visiting.has(k)) throw new Error(`dependency cycle in plan: ${[...path, k].join(' -> ')}`)
|
|
129
|
+
visiting.add(k)
|
|
130
|
+
byKey.get(k).depends_on.forEach(d => visit(d, [...path, k]))
|
|
131
|
+
visiting.delete(k)
|
|
132
|
+
visited.add(k)
|
|
133
|
+
}
|
|
134
|
+
plan.tasks.forEach(t => visit(t.key, []))
|
|
135
|
+
const roots = plan.tasks.filter(t => !t.depends_on.length).length
|
|
136
|
+
log(`"${plan.epic_title}": ${plan.tasks.length} tasks, ${roots} can start immediately, up to ${MAX_AGENTS} agents at once`)
|
|
137
|
+
|
|
138
|
+
// ---- Build → Review → Merge, per task, started as soon as its dependencies merge ----
|
|
139
|
+
const settled = new Map(plan.tasks.map(t => {
|
|
140
|
+
let resolve
|
|
141
|
+
const promise = new Promise(r => { resolve = r })
|
|
142
|
+
return [t.key, { promise, resolve }]
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
const brief = t => `Task ${t.key}: ${t.title}
|
|
146
|
+
ClickUp task: ${t.clickup_id}
|
|
147
|
+
Epic: ${EPIC}
|
|
148
|
+
Scope (globs you may change): ${t.scope.join(', ')}
|
|
149
|
+
|
|
150
|
+
${t.spec}`
|
|
151
|
+
|
|
152
|
+
async function build(t, pr, issues, label) {
|
|
153
|
+
const prompt = pr
|
|
154
|
+
? `Mode: FIX. PR #${pr.pr_number} on branch ${pr.branch}.\n${brief(t)}\n\nBlocking issues:\n${issues.map((s, i) => `${i + 1}. ${s}`).join('\n')}`
|
|
155
|
+
: `Mode: BUILD.\n${brief(t)}`
|
|
156
|
+
return run(prompt, { agentType: 'junior-dev', isolation: 'worktree', phase: 'Build', label, schema: PR_SCHEMA })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function review(t, pr, round) {
|
|
160
|
+
const suffix = round ? `#${round}` : ''
|
|
161
|
+
const [rev, lead] = await parallel([
|
|
162
|
+
() => run(`Mode: REVIEW. PR #${pr.pr_number}.\n${brief(t)}`,
|
|
163
|
+
{ agentType: 'reviewer', phase: 'Review', label: `review:${t.key}${suffix}`, schema: VERDICT_SCHEMA }),
|
|
164
|
+
() => run(`Mode: APPROVE. PR #${pr.pr_number}.\n${brief(t)}`,
|
|
165
|
+
{ agentType: 'senior-lead', phase: 'Review', label: `lead:${t.key}${suffix}`, schema: VERDICT_SCHEMA }),
|
|
166
|
+
])
|
|
167
|
+
const issues = [
|
|
168
|
+
...(rev ? rev.blocking_issues.map(s => `[reviewer] ${s}`) : ['[reviewer] review did not complete; re-request it']),
|
|
169
|
+
...(lead ? lead.blocking_issues.map(s => `[lead] ${s}`) : ['[lead] approval did not complete; re-request it']),
|
|
170
|
+
]
|
|
171
|
+
return { approved: Boolean(rev && rev.approved && lead && lead.approved), issues }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const merge = (t, pr, attempt) => run(
|
|
175
|
+
`Merge PR #${pr.pr_number} (branch ${pr.branch}) for task ${t.key}, ClickUp ${t.clickup_id}.`,
|
|
176
|
+
{ agentType: 'integrator', isolation: 'worktree', phase: 'Merge', label: `merge:${t.key}${attempt ? '#2' : ''}`, schema: MERGE_SCHEMA },
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async function runTask(t) {
|
|
180
|
+
const base = { key: t.key, title: t.title, clickup_id: t.clickup_id }
|
|
181
|
+
const deps = await Promise.all(t.depends_on.map(k => settled.get(k).promise))
|
|
182
|
+
const failedDeps = t.depends_on.filter((k, i) => deps[i].status !== 'merged')
|
|
183
|
+
if (failedDeps.length) return { ...base, status: 'blocked', reason: `dependencies not merged: ${failedDeps.join(', ')}` }
|
|
184
|
+
|
|
185
|
+
let pr = await build(t, null, null, `junior:${t.key}`)
|
|
186
|
+
if (!pr || !pr.pr_number) return { ...base, status: 'failed', reason: pr ? pr.notes : 'junior agent died' }
|
|
187
|
+
|
|
188
|
+
let verdict
|
|
189
|
+
for (let round = 0; ; round++) {
|
|
190
|
+
verdict = await review(t, pr, round)
|
|
191
|
+
if (verdict.approved || round >= FIX_ROUNDS) break
|
|
192
|
+
const fixed = await build(t, pr, verdict.issues, `fix:${t.key}#${round + 1}`)
|
|
193
|
+
if (fixed && fixed.pr_number) pr = fixed
|
|
194
|
+
}
|
|
195
|
+
if (!verdict.approved) {
|
|
196
|
+
return { ...base, status: 'rejected', pr: pr.pr_number, reason: `still blocked after ${FIX_ROUNDS} fix rounds: ${verdict.issues.join(' | ')}` }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let m = await merge(t, pr, 0)
|
|
200
|
+
if (m && !m.merged && m.needs_rebase) {
|
|
201
|
+
const fixed = await build(t, pr, [`Rebase onto origin/main and resolve conflicts: ${m.reason || 'see integrator output'}`], `rebase:${t.key}`)
|
|
202
|
+
if (fixed && fixed.pr_number) {
|
|
203
|
+
const recheck = await review(t, fixed, FIX_ROUNDS + 1)
|
|
204
|
+
if (recheck.approved) m = await merge(t, fixed, 1)
|
|
205
|
+
else m = { merged: false, reason: `re-review after rebase failed: ${recheck.issues.join(' | ')}` }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!m || !m.merged) return { ...base, status: 'failed', pr: pr.pr_number, reason: m ? m.reason : 'integrator died' }
|
|
209
|
+
return { ...base, status: 'merged', pr: pr.pr_number, sha: m.merge_sha }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const results = await parallel(plan.tasks.map(t => async () => {
|
|
213
|
+
let r
|
|
214
|
+
try {
|
|
215
|
+
r = await runTask(t)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
r = { key: t.key, title: t.title, clickup_id: t.clickup_id, status: 'failed', reason: String(e) }
|
|
218
|
+
}
|
|
219
|
+
settled.get(t.key).resolve(r)
|
|
220
|
+
log(`${t.key} ${r.status}${r.pr ? ` (PR #${r.pr})` : ''}`)
|
|
221
|
+
return r
|
|
222
|
+
}))
|
|
223
|
+
|
|
224
|
+
// ---- Verify ----
|
|
225
|
+
phase('Verify')
|
|
226
|
+
const count = s => results.filter(r => r && r.status === s).length
|
|
227
|
+
log(`merged ${count('merged')}, rejected ${count('rejected')}, failed ${count('failed')}, blocked ${count('blocked')}`)
|
|
228
|
+
|
|
229
|
+
const qa = count('merged')
|
|
230
|
+
? await run(
|
|
231
|
+
`Mode: VERIFY. Epic ${EPIC} ("${plan.epic_title}"). Merged tasks:\n${results.filter(r => r.status === 'merged').map(r => `- ${r.key} ${r.title} (PR #${r.pr})`).join('\n')}`,
|
|
232
|
+
{ agentType: 'qa-lead', isolation: 'worktree', phase: 'Verify', label: 'qa:verify', schema: QA_SCHEMA },
|
|
233
|
+
)
|
|
234
|
+
: null
|
|
235
|
+
|
|
236
|
+
const report = await run(
|
|
237
|
+
`Mode: REPORT. Epic ${EPIC} ("${plan.epic_title}").\n${JSON.stringify({ tasks: results, qa }, null, 2)}`,
|
|
238
|
+
{ agentType: 'senior-lead', phase: 'Verify', label: 'lead:report' },
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
epic: EPIC,
|
|
243
|
+
title: plan.epic_title,
|
|
244
|
+
done: count('merged') === plan.tasks.length && Boolean(qa && qa.passed),
|
|
245
|
+
tasks: results,
|
|
246
|
+
qa,
|
|
247
|
+
report,
|
|
248
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Task
|
|
2
|
+
ClickUp: <link> · Epic: <link>
|
|
3
|
+
|
|
4
|
+
## What changed
|
|
5
|
+
-
|
|
6
|
+
|
|
7
|
+
## Acceptance criteria → tests
|
|
8
|
+
| Criterion | Test |
|
|
9
|
+
|---|---|
|
|
10
|
+
| | |
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
<!-- scope
|
|
14
|
+
src/example/**
|
|
15
|
+
tests/example/**
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
## Checks run locally
|
|
19
|
+
- [ ] make lint
|
|
20
|
+
- [ ] make typecheck
|
|
21
|
+
- [ ] make test
|
|
22
|
+
- [ ] make e2e (if a user flow changed)
|
|
23
|
+
|
|
24
|
+
## Notes for reviewers
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Company handbook
|
|
2
|
+
|
|
3
|
+
This product is built by a team of AI agents. The founder (the human) sets direction and has final say. Every agent follows this handbook.
|
|
4
|
+
|
|
5
|
+
@docs/CONVENTIONS.md
|
|
6
|
+
|
|
7
|
+
## Roles
|
|
8
|
+
|
|
9
|
+
| Role | Agent | Owns |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Founder | human | Approving the spec and epics, final sign-off, founder-only paths |
|
|
12
|
+
| Architect | `architect` | PRD, architecture, stack choice, `contracts/` |
|
|
13
|
+
| Senior lead | `senior-lead` | One epic: splits it into tasks, assigns each task's scope, approves PRs for spec fit |
|
|
14
|
+
| Junior dev | `junior-dev` | One task: code + tests on its own branch, opens the PR |
|
|
15
|
+
| Reviewer | `reviewer` | Code review: correctness, security, test quality |
|
|
16
|
+
| Integrator | `integrator` | Merging, only through `scripts/merge-pr.sh` |
|
|
17
|
+
| QA lead | `qa-lead` | E2E tests from the spec, verifying `main` after each epic |
|
|
18
|
+
|
|
19
|
+
## Sources of truth
|
|
20
|
+
|
|
21
|
+
- Product: `docs/PRD.md`. Architecture and stack: `docs/ARCHITECTURE.md`. Stack-specific coding rules: `docs/CONVENTIONS.md`.
|
|
22
|
+
- Contracts (DB schema, API spec, shared types, route map): `contracts/`. They are frozen once the Foundation epic merges. If a task needs a contract change, stop and report it; do not change it.
|
|
23
|
+
- Work tracking: ClickUp. Workspace, space, folder and list IDs are in `foundry.config.json`.
|
|
24
|
+
|
|
25
|
+
## Make targets (stack-agnostic contract)
|
|
26
|
+
|
|
27
|
+
Every product implements `make setup`, `lint`, `typecheck`, `test`, `e2e`, `build`, `dev` and `ci`. CI runs `make ci`. Run at least `make lint typecheck test` before opening a PR.
|
|
28
|
+
|
|
29
|
+
## Git
|
|
30
|
+
|
|
31
|
+
- Branch from fresh `origin/main`: `git fetch origin && git switch -c t/<clickup-id>-<slug> origin/main`.
|
|
32
|
+
- Conventional commits ending with the ClickUp id, e.g. `feat: login form (CU-86abc12)`.
|
|
33
|
+
- Never push to `main`. Never force-push (`--force-with-lease` on your own task branch only). Never `--no-verify`.
|
|
34
|
+
- Never merge yourself. Only the integrator merges, and only through `scripts/merge-pr.sh`.
|
|
35
|
+
- One task = one PR, under ~800 changed lines excluding lockfiles.
|
|
36
|
+
|
|
37
|
+
## Pull requests
|
|
38
|
+
|
|
39
|
+
- Use `.github/pull_request_template.md`. The `<!-- scope ... -->` block lists the task's scope globs exactly as the lead assigned them; the merge gate rejects files outside it.
|
|
40
|
+
- Labels: `review:passed` (reviewer), `lead:approved` (lead), `large-ok` (lead, oversized PR), `founder-approved` (founder only, never add it yourself).
|
|
41
|
+
- All agents share one GitHub account, so `gh pr review --approve` does not work. Approval is a PR comment plus the label.
|
|
42
|
+
|
|
43
|
+
## ClickUp
|
|
44
|
+
|
|
45
|
+
- Hierarchy: product folder → list `Epics` → task = epic → subtask = task → checklist items = subtasks.
|
|
46
|
+
- Status flow: `to do` → `in progress` → `in review` → `changes requested` → `merged` → `done`; `blocked` at any point. If the list lacks a status, use the closest one and add a tag with the intended status.
|
|
47
|
+
- Comment on the ClickUp task at each step: PR link when opened, review verdict, merge SHA.
|
|
48
|
+
|
|
49
|
+
## Diagrams (Xtruct)
|
|
50
|
+
|
|
51
|
+
When the `xtruct` skill is available and `foundry.config.json` → `integrations.xtruct` is not `"off"`, the architect and senior leads produce interactive Xtruct diagrams as their roles describe. Diagrams are optional output: if the skill is missing or its validation fails twice, carry on without one and say so in one line. Never block a task on a diagram.
|
|
52
|
+
|
|
53
|
+
## Definition of done (task)
|
|
54
|
+
|
|
55
|
+
- Every acceptance criterion is met and covered by at least one test that would fail without the change.
|
|
56
|
+
- `make lint typecheck test` pass locally and CI is green.
|
|
57
|
+
- No skipped or disabled tests, no TODOs without a ClickUp task, no secrets, no debug output.
|
|
58
|
+
- Reviewer and lead approved, integrator merged, ClickUp updated.
|
|
59
|
+
|
|
60
|
+
## Guardrails
|
|
61
|
+
|
|
62
|
+
- Founder-only paths: `.claude/`, `CLAUDE.md`, `scripts/merge-pr.sh`, `scripts/bootstrap-github.sh`, `scripts/capacity.mjs`. Hooks block agent edits there.
|
|
63
|
+
- Never read or write `.env` files other than `.env.example`. Document every config variable in `.env.example`.
|
|
64
|
+
- If a guardrail blocks you, do not work around it. Report what you needed and why.
|
|
65
|
+
- After two honest attempts at something that still fails, stop and report `blocked` with what you tried. An honest "blocked" beats a fake "done".
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Stack-agnostic contract. CI and every agent call these targets, never the stack's tools directly.
|
|
2
|
+
# The Foundation epic replaces each recipe with the real command for the chosen stack.
|
|
3
|
+
.PHONY: setup dev lint typecheck test e2e build ci
|
|
4
|
+
|
|
5
|
+
setup dev lint typecheck test e2e build:
|
|
6
|
+
@echo "make $@: not configured yet (the Foundation epic sets this up)"; exit 1
|
|
7
|
+
|
|
8
|
+
ci: lint typecheck test build e2e
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# <Product> — Architecture
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
| Layer | Choice | Why |
|
|
5
|
+
|---|---|---|
|
|
6
|
+
|
|
7
|
+
### Alternatives considered
|
|
8
|
+
What was rejected and why.
|
|
9
|
+
|
|
10
|
+
## Components
|
|
11
|
+
Each service/app/package: responsibility, owner epic, main dependencies.
|
|
12
|
+
|
|
13
|
+
## Data model
|
|
14
|
+
Summary; source of truth is `contracts/`.
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
Style (REST/GraphQL/RPC) and conventions; source of truth is `contracts/`.
|
|
18
|
+
|
|
19
|
+
## Auth and permissions
|
|
20
|
+
|
|
21
|
+
## Deployment and environments
|
|
22
|
+
Local (`make dev`), preview, production.
|
|
23
|
+
|
|
24
|
+
## Observability
|
|
25
|
+
Logging, errors, metrics.
|
|
26
|
+
|
|
27
|
+
## Security considerations
|
|
28
|
+
|
|
29
|
+
## Decisions
|
|
30
|
+
Dated log of significant decisions and rejected critique.
|