agentic-team 0.0.0-stage → 6.2.0
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.
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/bin/build-template.js +208 -0
- package/bin/index.js +1334 -0
- package/bin/platforms.js +510 -0
- package/package.json +41 -4
- package/template/.claude-plugin/plugin.json +10 -0
- package/template/.orchestra/README.md +391 -0
- package/template/.orchestra/blueprints/README.md +87 -0
- package/template/.orchestra/blueprints/api-only.md +46 -0
- package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
- package/template/.orchestra/blueprints/saas-starter.md +69 -0
- package/template/.orchestra/config.yml +293 -0
- package/template/.orchestra/roles/orchestrator.md +105 -0
- package/template/.orchestra/roles/product-manager.md +439 -0
- package/template/.orchestra/scripts/compact-diff.awk +45 -0
- package/template/.orchestra/scripts/context.mjs +373 -0
- package/template/.orchestra/stacks/README.md +81 -0
- package/template/.orchestra/stacks/_lib/common.sh +3095 -0
- package/template/.orchestra/stacks/_repo/setup.sh +1113 -0
- package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
- package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
- package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
- package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
- package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
- package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
- package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
- package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
- package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
- package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
- package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
- package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
- package/template/.orchestra/trackers/github.sh +467 -0
- package/template/CLAUDE.md +54 -0
- package/template/agents/architect.md +83 -0
- package/template/agents/engineer-high.md +17 -0
- package/template/agents/engineer-low.md +17 -0
- package/template/agents/engineer-max.md +17 -0
- package/template/agents/engineer-medium.md +17 -0
- package/template/agents/engineer-xhigh.md +17 -0
- package/template/agents/lead.md +925 -0
- package/template/agents/milestone-reviewer.md +65 -0
- package/template/agents/orchestrator.md +105 -0
- package/template/agents/product-manager.md +439 -0
- package/template/agents/re-reviewer.md +25 -0
- package/template/agents/reviewer.md +303 -0
- package/template/agents/scaffolder.md +206 -0
- package/template/agents/tracker.md +202 -0
- package/template/commands/blueprint.md +23 -0
- package/template/commands/create-role.md +64 -0
- package/template/commands/export.md +45 -0
- package/template/commands/help.md +56 -0
- package/template/commands/hotfix.md +14 -0
- package/template/commands/orchestrator.md +12 -0
- package/template/commands/pm.md +18 -0
- package/template/commands/rewind.md +63 -0
- package/template/commands/setup.md +15 -0
- package/template/commands/start.md +29 -0
- package/template/commands/status.md +11 -0
- package/template/commands/sync.md +76 -0
- package/template/commands/verifier.md +53 -0
- package/template/hooks/audit-context.sh +166 -0
- package/template/hooks/enforce-role.sh +193 -0
- package/template/hooks/protect-system-files.sh +123 -0
- package/template/hooks/review-rounds.sh +172 -0
- package/template/rules/acceptance-check.orchestra.md +11 -0
- package/template/rules/analytics-kpi.orchestra.md +56 -0
- package/template/rules/code-standards.orchestra.md +35 -0
- package/template/rules/comments.orchestra.md +77 -0
- package/template/rules/commit-format.orchestra.md +20 -0
- package/template/rules/dependency-upgrade.orchestra.md +112 -0
- package/template/rules/design-parity.orchestra.md +181 -0
- package/template/rules/observability.orchestra.md +121 -0
- package/template/rules/performance.orchestra.md +190 -0
- package/template/rules/phase-limits.orchestra.md +3 -0
- package/template/rules/pull-request.orchestra.md +218 -0
- package/template/rules/role-boundaries.orchestra.md +132 -0
- package/template/rules/security.orchestra.md +106 -0
- package/template/rules/stuck-detection.orchestra.md +25 -0
- package/template/rules/testing-standards.orchestra.md +219 -0
- package/template/rules/tracker-authority.orchestra.md +198 -0
- package/template/rules/verification-gate.orchestra.md +56 -0
- package/template/skills/accessibility/SKILL.md +31 -0
- package/template/skills/android-testing/SKILL.md +112 -0
- package/template/skills/architecture/SKILL.md +175 -0
- package/template/skills/auth-setup/SKILL.md +105 -0
- package/template/skills/best-practices/SKILL.md +29 -0
- package/template/skills/code-optimizer/SKILL.md +27 -0
- package/template/skills/core-web-vitals/SKILL.md +32 -0
- package/template/skills/crud-api/SKILL.md +32 -0
- package/template/skills/debug/SKILL.md +28 -0
- package/template/skills/deployment/SKILL.md +104 -0
- package/template/skills/error-handling/SKILL.md +154 -0
- package/template/skills/frontend-design/SKILL.md +30 -0
- package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
- package/template/skills/ios-testing/SKILL.md +100 -0
- package/template/skills/mobile-testing/SKILL.md +102 -0
- package/template/skills/observability/SKILL.md +91 -0
- package/template/skills/project-structure/SKILL.md +114 -0
- package/template/skills/push-notifications/SKILL.md +146 -0
- package/template/skills/react-best-practices/SKILL.md +31 -0
- package/template/skills/realtime/SKILL.md +146 -0
- package/template/skills/review/SKILL.md +82 -0
- package/template/skills/testing/SKILL.md +45 -0
- package/template/skills/web-quality-audit/SKILL.md +47 -0
- package/template/skills/web-testing/SKILL.md +120 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Lead's record of a milestone, in three files with one writer:
|
|
4
|
+
//
|
|
5
|
+
// context.md state: Status, Phases, Codebase Map — read at every phase
|
|
6
|
+
// decisions.md decisions, each open until a later line closes it
|
|
7
|
+
// journal.md what was tried and why it lost — append-only, rarely read
|
|
8
|
+
//
|
|
9
|
+
// The split follows who reads what. One run kept all of it in a single
|
|
10
|
+
// context.md: 72 KB, of which the 43 KB journal was dead weight to anyone
|
|
11
|
+
// reading for the current state. It also had lead edit that file with an
|
|
12
|
+
// ad-hoc script per change — ~555k characters of edit code that stayed in
|
|
13
|
+
// lead's own context — so every write goes through a command here instead.
|
|
14
|
+
//
|
|
15
|
+
// A decision is never edited. It closes when a later line names it, and only
|
|
16
|
+
// open decisions reach a phase prompt (`live`), so a resolved blocker or a
|
|
17
|
+
// superseded choice stops costing anything without being lost.
|
|
18
|
+
//
|
|
19
|
+
// Lead is the only writer, so ids are assigned by read-then-append without a
|
|
20
|
+
// lock. Metrics are not stored: `metrics` computes them from git and the
|
|
21
|
+
// review-round count when asked, because hand-kept numbers went unmaintained.
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
|
|
27
|
+
const TYPES = ["binding", "tech_debt", "blocker", "question"];
|
|
28
|
+
const VERDICTS = ["done", "superseded", "dropped", "answered"];
|
|
29
|
+
const STATUSES = ["pending", "in-progress", "done", "failed", "blocked"];
|
|
30
|
+
// One line a reader can take in at a glance. One run's decision lines reached
|
|
31
|
+
// 728 characters because nothing stopped them; the detail belongs in journal.
|
|
32
|
+
const MAX_DECISION = 200;
|
|
33
|
+
|
|
34
|
+
const DECISION =
|
|
35
|
+
/^- (D\d+) (binding|tech_debt|blocker|question|legacy) ?(\S*) ?(\d{4}-\d{2}-\d{2})?: (.*)$/;
|
|
36
|
+
const CLOSE = /^- (D\d+) closed (\S+) (\d{4}-\d{2}-\d{2}): (.*)$/;
|
|
37
|
+
|
|
38
|
+
const root = process.cwd();
|
|
39
|
+
const dir = (m) => path.join(root, ".orchestra", "milestones", m);
|
|
40
|
+
const file = (m, name) => path.join(dir(m), name);
|
|
41
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
42
|
+
// CRLF is normalised on read: a section matcher that expects "\n" would
|
|
43
|
+
// otherwise miss a heading and append a second copy of the section.
|
|
44
|
+
const read = (p) => (fs.existsSync(p) ? fs.readFileSync(p, "utf8").replace(/\r\n/g, "\n") : "");
|
|
45
|
+
const PHASE_ID = /^[A-Za-z0-9._-]+$/;
|
|
46
|
+
|
|
47
|
+
function die(msg) {
|
|
48
|
+
process.stderr.write(`context: ${msg}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeAtomic(p, text) {
|
|
53
|
+
const tmp = `${p}.${process.pid}`;
|
|
54
|
+
fs.writeFileSync(tmp, text);
|
|
55
|
+
fs.renameSync(tmp, p);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function need(m) {
|
|
59
|
+
if (!m) die("name the milestone directory, e.g. M2-design-system");
|
|
60
|
+
if (!fs.existsSync(dir(m))) die(`no milestone directory .orchestra/milestones/${m}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function oneLine(text, what) {
|
|
64
|
+
const t = String(text || "").trim();
|
|
65
|
+
if (!t) die(`${what} is empty`);
|
|
66
|
+
if (/[\r\n]/.test(t)) die(`${what} must be one line`);
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const STATE_HEADER = (m) =>
|
|
71
|
+
`## Status\nmilestone: ${m}\nstarted: ${today()}\n\n## Phases\n\n## Codebase Map\n`;
|
|
72
|
+
|
|
73
|
+
function init(m) {
|
|
74
|
+
need(m);
|
|
75
|
+
if (!fs.existsSync(file(m, "context.md"))) writeAtomic(file(m, "context.md"), STATE_HEADER(m));
|
|
76
|
+
if (!fs.existsSync(file(m, "decisions.md")))
|
|
77
|
+
writeAtomic(file(m, "decisions.md"), "# Decisions\n\n");
|
|
78
|
+
if (!fs.existsSync(file(m, "journal.md"))) writeAtomic(file(m, "journal.md"), "# Journal\n\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Replace the body of a `## name` section, or add the section at the end. */
|
|
82
|
+
function setSection(text, name, body) {
|
|
83
|
+
const re = new RegExp(`(^|\\n)## ${name}\\n[\\s\\S]*?(?=\\n## |$)`);
|
|
84
|
+
const block = `## ${name}\n${body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
85
|
+
if (re.test(text)) return text.replace(re, (_, lead) => `${lead}${block}`);
|
|
86
|
+
return `${text.replace(/\n*$/, "\n\n")}${block}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function section(text, name) {
|
|
90
|
+
const m = new RegExp(`(?:^|\\n)## ${name}\\n([\\s\\S]*?)(?=\\n## |$)`).exec(text);
|
|
91
|
+
return m ? m[1] : "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function phase(m, id, status, note) {
|
|
95
|
+
need(m);
|
|
96
|
+
init(m);
|
|
97
|
+
if (!id || !PHASE_ID.test(id)) die("name the phase as its file is named, e.g. phase-3");
|
|
98
|
+
if (!STATUSES.includes(status)) die(`status is one of ${STATUSES.join(", ")}`);
|
|
99
|
+
if (status === "done" && !/commits?:\s*\S+/.test(note || "")) {
|
|
100
|
+
die("a done phase carries its commit: pass a note like 'commit: abc123 | files: 4'");
|
|
101
|
+
}
|
|
102
|
+
const p = file(m, "context.md");
|
|
103
|
+
const text = read(p);
|
|
104
|
+
const lines = section(text, "Phases")
|
|
105
|
+
.split("\n")
|
|
106
|
+
.filter((l) => l.trim());
|
|
107
|
+
const entry = `- ${id}: ${status}${note ? ` | ${oneLine(note, "note")}` : ""}`;
|
|
108
|
+
const at = lines.findIndex((l) => l.startsWith(`- ${id}:`));
|
|
109
|
+
if (at === -1) lines.push(entry);
|
|
110
|
+
else lines[at] = entry;
|
|
111
|
+
writeAtomic(p, setSection(text, "Phases", `${lines.join("\n")}\n`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function map(m) {
|
|
115
|
+
need(m);
|
|
116
|
+
init(m);
|
|
117
|
+
const body = fs.readFileSync(0, "utf8").trim();
|
|
118
|
+
// An empty stdin is a forgotten heredoc, not a request to erase the map.
|
|
119
|
+
if (!body) die("pipe the map in on stdin; nothing came, so the map is unchanged");
|
|
120
|
+
if (body.split("\n").length > 30) die("the codebase map is at most 30 lines");
|
|
121
|
+
const p = file(m, "context.md");
|
|
122
|
+
writeAtomic(p, setSection(read(p), "Codebase Map", `${body}\n`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseDecisions(m) {
|
|
126
|
+
const decisions = new Map();
|
|
127
|
+
const closes = new Map();
|
|
128
|
+
for (const line of read(file(m, "decisions.md")).split("\n")) {
|
|
129
|
+
const c = CLOSE.exec(line);
|
|
130
|
+
if (c) {
|
|
131
|
+
closes.set(c[1], { verdict: c[2], date: c[3], text: c[4] });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const d = DECISION.exec(line);
|
|
135
|
+
if (d) decisions.set(d[1], { id: d[1], type: d[2], phase: d[3], line });
|
|
136
|
+
}
|
|
137
|
+
return { decisions, closes };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function nextId(m) {
|
|
141
|
+
let max = 0;
|
|
142
|
+
for (const id of parseDecisions(m).decisions.keys()) max = Math.max(max, Number(id.slice(1)));
|
|
143
|
+
return `D${max + 1}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function decide(m, type, phaseId, text) {
|
|
147
|
+
need(m);
|
|
148
|
+
init(m);
|
|
149
|
+
if (!TYPES.includes(type)) die(`type is one of ${TYPES.join(", ")}`);
|
|
150
|
+
if (!phaseId || !PHASE_ID.test(phaseId)) {
|
|
151
|
+
die("name the phase the decision came from as its file is named, e.g. phase-3, or milestone");
|
|
152
|
+
}
|
|
153
|
+
const t = oneLine(text, "decision");
|
|
154
|
+
if (t.length > MAX_DECISION) {
|
|
155
|
+
die(
|
|
156
|
+
`a decision is at most ${MAX_DECISION} characters (this is ${t.length}); put the detail in the journal`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const id = nextId(m);
|
|
160
|
+
fs.appendFileSync(file(m, "decisions.md"), `- ${id} ${type} ${phaseId} ${today()}: ${t}\n`);
|
|
161
|
+
process.stdout.write(`${id}\n`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function close(m, id, verdict, text) {
|
|
165
|
+
need(m);
|
|
166
|
+
const { decisions, closes } = parseDecisions(m);
|
|
167
|
+
if (!decisions.has(id)) die(`no decision ${id} in ${m}`);
|
|
168
|
+
if (closes.has(id)) die(`${id} is already closed`);
|
|
169
|
+
if (!VERDICTS.includes(verdict)) die(`verdict is one of ${VERDICTS.join(", ")}`);
|
|
170
|
+
const t = oneLine(text, "reason");
|
|
171
|
+
if (t.length > MAX_DECISION) die(`a reason is at most ${MAX_DECISION} characters`);
|
|
172
|
+
fs.appendFileSync(file(m, "decisions.md"), `- ${id} closed ${verdict} ${today()}: ${t}\n`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function journal(m, phaseId, text) {
|
|
176
|
+
need(m);
|
|
177
|
+
init(m);
|
|
178
|
+
if (!phaseId || !PHASE_ID.test(phaseId)) die("name the phase, e.g. phase-3, or milestone");
|
|
179
|
+
fs.appendFileSync(file(m, "journal.md"), `- ${today()} ${phaseId}: ${oneLine(text, "entry")}\n`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `live`: the open binding decisions — what a phase must follow, so what a
|
|
184
|
+
* phase prompt carries. `open`: every open decision, blockers, questions and
|
|
185
|
+
* tech debt included — lead's to act on, not an engineer's to read.
|
|
186
|
+
*/
|
|
187
|
+
function live(m, all) {
|
|
188
|
+
need(m);
|
|
189
|
+
const { decisions, closes } = parseDecisions(m);
|
|
190
|
+
for (const d of decisions.values()) {
|
|
191
|
+
if (closes.has(d.id)) continue;
|
|
192
|
+
if (all || d.type === "binding") process.stdout.write(`${d.line}\n`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function check(m) {
|
|
197
|
+
const found = [];
|
|
198
|
+
const { decisions, closes } = parseDecisions(m);
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
for (const line of read(file(m, "decisions.md")).split("\n")) {
|
|
201
|
+
if (!line.startsWith("- ")) continue;
|
|
202
|
+
if (CLOSE.test(line)) continue;
|
|
203
|
+
const d = DECISION.exec(line);
|
|
204
|
+
if (!d) {
|
|
205
|
+
found.push(`${m}/decisions.md: not a decision or a close: ${line.slice(0, 60)}`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (seen.has(d[1])) found.push(`${m}/decisions.md: ${d[1]} is used twice`);
|
|
209
|
+
seen.add(d[1]);
|
|
210
|
+
const migrated = d[2] === "legacy" || d[3] === "legacy";
|
|
211
|
+
if (!migrated && d[5].length > MAX_DECISION) {
|
|
212
|
+
found.push(`${m}/decisions.md: ${d[1]} is ${d[5].length} characters, over ${MAX_DECISION}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const id of closes.keys()) {
|
|
216
|
+
if (!decisions.has(id))
|
|
217
|
+
found.push(`${m}/decisions.md: a close names ${id}, which does not exist`);
|
|
218
|
+
}
|
|
219
|
+
return found;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function milestones() {
|
|
223
|
+
const base = path.join(root, ".orchestra", "milestones");
|
|
224
|
+
if (!fs.existsSync(base)) return [];
|
|
225
|
+
return fs
|
|
226
|
+
.readdirSync(base, { withFileTypes: true })
|
|
227
|
+
.filter((e) => e.isDirectory())
|
|
228
|
+
.map((e) => e.name);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Split an older single-file context.md. Lossless: Decisions, Tech debt and
|
|
233
|
+
* Blocked bullets become open `legacy` decisions with their text unchanged;
|
|
234
|
+
* Journal and Metrics move to journal.md verbatim; any other section stays.
|
|
235
|
+
*/
|
|
236
|
+
function migrate(m) {
|
|
237
|
+
const p = file(m, "context.md");
|
|
238
|
+
const text = read(p);
|
|
239
|
+
const moves = { Decisions: "legacy", "Tech debt": "legacy", Blocked: "legacy" };
|
|
240
|
+
const toJournal = ["Journal", "Metrics"];
|
|
241
|
+
const present = [...Object.keys(moves), ...toJournal].filter((s) =>
|
|
242
|
+
new RegExp(`(^|\\n)## ${s}\\n`).test(text),
|
|
243
|
+
);
|
|
244
|
+
if (!present.length) return false;
|
|
245
|
+
|
|
246
|
+
let decisionsText = read(file(m, "decisions.md")) || "# Decisions\n\n";
|
|
247
|
+
let n = Number(nextId(m).slice(1));
|
|
248
|
+
for (const s of Object.keys(moves)) {
|
|
249
|
+
// A bullet starts an entry and an indented line continues it. Anything
|
|
250
|
+
// else — prose, a numbered item, a sub-heading — becomes its own entry,
|
|
251
|
+
// so nothing in the section is dropped when the section is removed.
|
|
252
|
+
const entries = [];
|
|
253
|
+
for (const raw of section(text, s).split("\n")) {
|
|
254
|
+
if (!raw.trim()) continue;
|
|
255
|
+
if (raw.startsWith("- ")) entries.push(raw.slice(2).trim());
|
|
256
|
+
else if (/^\s/.test(raw) && entries.length) entries[entries.length - 1] += ` ${raw.trim()}`;
|
|
257
|
+
else entries.push(raw.trim());
|
|
258
|
+
}
|
|
259
|
+
const label = s === "Decisions" ? "" : `[${s}] `;
|
|
260
|
+
for (const e of entries) {
|
|
261
|
+
// An older line that says it is binding still binds the phases left;
|
|
262
|
+
// the rest is history lead can close as it reads them.
|
|
263
|
+
const kind = s === "Decisions" && /^binding\b/i.test(e) ? "binding legacy" : "legacy";
|
|
264
|
+
decisionsText += `- D${n++} ${kind}: ${label}${e}\n`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let journalText = read(file(m, "journal.md")) || "# Journal\n\n";
|
|
269
|
+
for (const s of toJournal) {
|
|
270
|
+
const body = section(text, s).trim();
|
|
271
|
+
if (body) journalText += `${s === "Journal" ? "" : `\n## Migrated from ${s}\n`}${body}\n`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// New files first: if this stops halfway, context.md still holds everything.
|
|
275
|
+
writeAtomic(file(m, "decisions.md"), decisionsText);
|
|
276
|
+
writeAtomic(file(m, "journal.md"), journalText);
|
|
277
|
+
let rest = text;
|
|
278
|
+
for (const s of present)
|
|
279
|
+
rest = rest.replace(new RegExp(`(^|\\n)## ${s}\\n[\\s\\S]*?(?=\\n## |$)`), "");
|
|
280
|
+
writeAtomic(p, `${rest.trim()}\n`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function metrics(m) {
|
|
285
|
+
need(m);
|
|
286
|
+
let rounds = {};
|
|
287
|
+
try {
|
|
288
|
+
rounds = JSON.parse(
|
|
289
|
+
read(path.join(root, ".orchestra", ".sessions", "review-rounds.json")) || "{}",
|
|
290
|
+
);
|
|
291
|
+
} catch {
|
|
292
|
+
// An unreadable count reads as no rounds recorded, which is what it says.
|
|
293
|
+
rounds = {};
|
|
294
|
+
}
|
|
295
|
+
const commitTime = (hash) => {
|
|
296
|
+
try {
|
|
297
|
+
return execFileSync("git", ["show", "-s", "--format=%cI", hash], {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
300
|
+
}).trim();
|
|
301
|
+
} catch {
|
|
302
|
+
return "unknown";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const lines = section(read(file(m, "context.md")), "Phases")
|
|
306
|
+
.split("\n")
|
|
307
|
+
.filter((l) => l.startsWith("- "));
|
|
308
|
+
for (const l of lines) {
|
|
309
|
+
const [, id, status] = /^- (\S+): ([a-z-]+)/.exec(l) || [];
|
|
310
|
+
if (!id) continue;
|
|
311
|
+
const hashes = [];
|
|
312
|
+
for (const field of l.matchAll(/commits?:\s*([^|]+)/g)) {
|
|
313
|
+
for (const h of field[1].matchAll(/\b[0-9a-f]{7,40}\b/g)) hashes.push(h[0]);
|
|
314
|
+
}
|
|
315
|
+
const when = hashes.length ? commitTime(hashes[hashes.length - 1]) : "no commit";
|
|
316
|
+
process.stdout.write(
|
|
317
|
+
`${id}\t${status}\treview rounds: ${rounds[`${m}/${id}`] || 0}\tlast commit: ${when}\n`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// An agent pipes output into `head`; a closed pipe ends the read, not the script.
|
|
323
|
+
process.stdout.on("error", (e) => {
|
|
324
|
+
if (e.code === "EPIPE") process.exit(0);
|
|
325
|
+
throw e;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
329
|
+
switch (cmd) {
|
|
330
|
+
case "init":
|
|
331
|
+
init(args[0]);
|
|
332
|
+
break;
|
|
333
|
+
case "phase":
|
|
334
|
+
phase(args[0], args[1], args[2], args.slice(3).join(" "));
|
|
335
|
+
break;
|
|
336
|
+
case "map":
|
|
337
|
+
map(args[0]);
|
|
338
|
+
break;
|
|
339
|
+
case "decide":
|
|
340
|
+
decide(args[0], args[1], args[2], args.slice(3).join(" "));
|
|
341
|
+
break;
|
|
342
|
+
case "close":
|
|
343
|
+
close(args[0], args[1], args[2], args.slice(3).join(" "));
|
|
344
|
+
break;
|
|
345
|
+
case "journal":
|
|
346
|
+
journal(args[0], args[1], args.slice(2).join(" "));
|
|
347
|
+
break;
|
|
348
|
+
case "live":
|
|
349
|
+
live(args[0], false);
|
|
350
|
+
break;
|
|
351
|
+
case "open":
|
|
352
|
+
live(args[0], true);
|
|
353
|
+
break;
|
|
354
|
+
case "metrics":
|
|
355
|
+
metrics(args[0]);
|
|
356
|
+
break;
|
|
357
|
+
case "check": {
|
|
358
|
+
const found = (args[0] ? [args[0]] : milestones()).flatMap(check);
|
|
359
|
+
for (const f of found) process.stdout.write(`${f}\n`);
|
|
360
|
+
process.exit(found.length ? 1 : 0);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "migrate": {
|
|
364
|
+
const moved = (args[0] ? [args[0]] : milestones()).filter(migrate);
|
|
365
|
+
for (const m of moved)
|
|
366
|
+
process.stdout.write(`split ${m}/context.md into decisions.md and journal.md\n`);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
die(
|
|
371
|
+
"usage: context.mjs init|phase|map|decide|close|journal|live|open|check|migrate|metrics <milestone> …",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Stacks
|
|
2
|
+
|
|
3
|
+
A **stack** is one kind of app: how it is scaffolded, and how it is built.
|
|
4
|
+
`/orchestra:setup` runs the stack's script, then copies its conventions in.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
.orchestra/stacks/
|
|
8
|
+
├── _lib/common.sh # shared helpers, sourced by every setup.sh
|
|
9
|
+
├── _repo/setup.sh # repo-level config — CI, dependabot, PR template,
|
|
10
|
+
│ # commit hook. Runs once, after the apps.
|
|
11
|
+
└── <stack-id>/
|
|
12
|
+
├── setup.sh # scaffolds the app — deterministic, reviewable
|
|
13
|
+
├── stack.md # → apps/<name>/CLAUDE.md (architecture + conventions)
|
|
14
|
+
└── claude/ # → apps/<name>/.claude/
|
|
15
|
+
├── commands/ # workflows, e.g. /web:component
|
|
16
|
+
└── skills/ # depth, loaded on demand
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Why scripts
|
|
20
|
+
|
|
21
|
+
Scaffolding is deterministic work. An agent improvising `npm create` flags
|
|
22
|
+
produces a different repo every run and leaves nothing to review. The script
|
|
23
|
+
is the contract: it runs the same way every time, and it shows up in a diff.
|
|
24
|
+
|
|
25
|
+
The agent asks the questions and runs the script. It does not scaffold by hand.
|
|
26
|
+
|
|
27
|
+
## setup.sh contract
|
|
28
|
+
|
|
29
|
+
- Takes one argument: the target directory
|
|
30
|
+
- Sources `../_lib/common.sh`
|
|
31
|
+
- Resolves every version from the registry — **never hardcodes one**
|
|
32
|
+
- Installs with `--save-exact` and commits the lockfile
|
|
33
|
+
- Ends with `assert_verification_scripts` so the Orchestra gate cannot
|
|
34
|
+
point at commands that do not exist
|
|
35
|
+
- Is idempotent enough to fail loudly rather than half-apply
|
|
36
|
+
|
|
37
|
+
## Namespacing
|
|
38
|
+
|
|
39
|
+
None needed. Claude Code namespaces by directory: a command at
|
|
40
|
+
`apps/web/.claude/commands/build.md` is invoked as `/web:build`, and skills
|
|
41
|
+
under `apps/web/.claude/skills/` are directory-scoped. `apps/web/CLAUDE.md`
|
|
42
|
+
loads automatically when work happens in that folder.
|
|
43
|
+
|
|
44
|
+
Never prefix filenames. The folder is the namespace.
|
|
45
|
+
|
|
46
|
+
## Where a convention goes
|
|
47
|
+
|
|
48
|
+
| Kind | Home |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Always applies in this app | `stack.md` → `apps/<name>/CLAUDE.md` |
|
|
51
|
+
| Detailed how-to, loaded when relevant | `claude/skills/` |
|
|
52
|
+
| A workflow the user invokes | `claude/commands/` |
|
|
53
|
+
| True for *every* app | root `.claude/rules/` — not here |
|
|
54
|
+
|
|
55
|
+
App-level `CLAUDE.md` loads automatically when work happens in that folder.
|
|
56
|
+
That is the reliable always-on mechanism; do not invent another one.
|
|
57
|
+
|
|
58
|
+
## stack.md contract
|
|
59
|
+
|
|
60
|
+
- Describes **how this app is built** — structure, conventions, testing
|
|
61
|
+
rules, and the engineer identity an agent adopts when working here
|
|
62
|
+
- Contains no product or business logic. That lives in milestones.
|
|
63
|
+
- Anything true for *every* app belongs in root `.claude/rules/`, not here
|
|
64
|
+
|
|
65
|
+
## Stack IDs
|
|
66
|
+
|
|
67
|
+
| ID | App |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `web-vite-react` | React SPA on Vite |
|
|
70
|
+
| `web-nextjs` | Next.js App Router |
|
|
71
|
+
| `mobile-expo` | Expo / React Native |
|
|
72
|
+
| `mobile-ios-native` | Native iOS (Swift) |
|
|
73
|
+
| `mobile-android-native` | Native Android (Kotlin) |
|
|
74
|
+
| `backend-hono` | Hono API |
|
|
75
|
+
|
|
76
|
+
Deliberately not authored: desktop and game. Both are too varied to guess
|
|
77
|
+
at (Tauri vs Electron; Godot vs Unity vs Unreal), and a guessed convention
|
|
78
|
+
reads as a decided one. Author the pack when a real project needs it.
|
|
79
|
+
|
|
80
|
+
Packs are authored with the user, not generated. A missing pack means
|
|
81
|
+
`setup` writes a `CLAUDE.md` stub and reports it as unauthored.
|