buildanything 2.0.0 → 2.1.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -1
- package/README.md +57 -61
- package/agents/a11y-architect.md +2 -0
- package/agents/briefing-officer.md +172 -0
- package/agents/business-model.md +14 -12
- package/agents/code-architect.md +6 -1
- package/agents/code-reviewer.md +3 -2
- package/agents/code-simplifier.md +12 -4
- package/agents/design-brand-guardian.md +19 -0
- package/agents/design-critic.md +16 -11
- package/agents/design-inclusive-visuals-specialist.md +2 -0
- package/agents/design-ui-designer.md +17 -0
- package/agents/design-ux-architect.md +15 -0
- package/agents/design-ux-researcher.md +102 -7
- package/agents/engineering-ai-engineer.md +2 -0
- package/agents/engineering-backend-architect.md +2 -0
- package/agents/engineering-data-engineer.md +2 -0
- package/agents/engineering-devops-automator.md +2 -0
- package/agents/engineering-frontend-developer.md +13 -0
- package/agents/engineering-mobile-app-builder.md +2 -0
- package/agents/engineering-rapid-prototyper.md +15 -2
- package/agents/engineering-security-engineer.md +2 -0
- package/agents/engineering-senior-developer.md +13 -0
- package/agents/engineering-sre.md +2 -0
- package/agents/engineering-technical-writer.md +2 -0
- package/agents/feature-intel.md +8 -7
- package/agents/ios-app-review-guardian.md +2 -0
- package/agents/ios-foundation-models-specialist.md +2 -0
- package/agents/ios-product-reality-auditor.md +292 -0
- package/agents/ios-storekit-specialist.md +2 -0
- package/agents/ios-swift-architect.md +1 -0
- package/agents/ios-swift-search.md +1 -0
- package/agents/ios-swift-ui-design.md +7 -4
- package/agents/marketing-app-store-optimizer.md +2 -0
- package/agents/planner.md +6 -1
- package/agents/pr-test-analyzer.md +3 -2
- package/agents/product-feedback-synthesizer.md +62 -0
- package/agents/product-owner.md +163 -0
- package/agents/product-reality-auditor.md +216 -0
- package/agents/product-spec-writer.md +176 -0
- package/agents/refactor-cleaner.md +9 -1
- package/agents/security-reviewer.md +2 -1
- package/agents/silent-failure-hunter.md +2 -1
- package/agents/swift-build-resolver.md +2 -0
- package/agents/swift-reviewer.md +2 -1
- package/agents/tech-feasibility.md +5 -3
- package/agents/testing-api-tester.md +2 -0
- package/agents/testing-evidence-collector.md +24 -0
- package/agents/testing-performance-benchmarker.md +2 -0
- package/agents/testing-reality-checker.md +2 -1
- package/agents/visual-research.md +7 -5
- package/bin/adapters/scribe-tool.ts +4 -2
- package/bin/adapters/write-lease-tool.ts +1 -1
- package/bin/buildanything-runtime.ts +20 -107
- package/bin/graph-index.js +24 -0
- package/bin/graph-index.ts +340 -0
- package/bin/mcp-servers/graph-mcp.js +26 -0
- package/bin/mcp-servers/graph-mcp.ts +481 -0
- package/bin/mcp-servers/orchestrator-mcp.js +26 -0
- package/bin/mcp-servers/orchestrator-mcp.ts +361 -0
- package/bin/setup.js +272 -111
- package/commands/build.md +371 -158
- package/commands/idea-sweep.md +2 -2
- package/commands/setup.md +15 -4
- package/commands/ux-review.md +3 -3
- package/commands/verify.md +3 -0
- package/docs/migration/phase-graph.yaml +573 -157
- package/hooks/design-md-lint +4 -0
- package/hooks/design-md-lint.ts +295 -0
- package/hooks/pre-tool-use.ts +37 -6
- package/hooks/record-mode-transitions.ts +63 -6
- package/hooks/subagent-start.ts +3 -2
- package/package.json +3 -1
- package/protocols/agent-prompt-authoring.md +165 -0
- package/protocols/architecture-schema.md +10 -3
- package/protocols/cleanup.md +4 -0
- package/protocols/decision-log.md +8 -4
- package/protocols/design-md-authoring.md +520 -0
- package/protocols/design-md-spec.md +362 -0
- package/protocols/fake-data-detector.md +1 -1
- package/protocols/ios-fake-data-detector.md +65 -0
- package/protocols/ios-phase-branches.md +112 -27
- package/protocols/launch-readiness.md +9 -5
- package/protocols/metric-loop.md +1 -1
- package/protocols/page-spec-schema.md +234 -0
- package/protocols/product-spec-schema.md +354 -0
- package/protocols/sprint-tasks-schema.md +53 -0
- package/protocols/state-schema.json +38 -3
- package/protocols/state-schema.md +32 -2
- package/protocols/verify.md +29 -1
- package/protocols/web-phase-branches.md +234 -64
- package/skills/ios/ios-bootstrap/SKILL.md +1 -1
- package/src/graph/ids.ts +86 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/parser/architecture.ts +603 -0
- package/src/graph/parser/component-manifest.ts +268 -0
- package/src/graph/parser/decisions-jsonl.ts +407 -0
- package/src/graph/parser/design-md-pass2.ts +253 -0
- package/src/graph/parser/design-md.ts +477 -0
- package/src/graph/parser/page-spec.ts +496 -0
- package/src/graph/parser/product-spec.ts +930 -0
- package/src/graph/parser/screenshot.ts +342 -0
- package/src/graph/parser/sprint-tasks.ts +317 -0
- package/src/graph/storage/index.ts +1154 -0
- package/src/graph/types.ts +432 -0
- package/src/graph/util/dhash.ts +84 -0
- package/src/lrr/aggregator.ts +105 -10
- package/src/orchestrator/hooks/context-header.ts +34 -10
- package/src/orchestrator/hooks/token-accounting.ts +25 -14
- package/src/orchestrator/mcp/cycle-counter.ts +2 -1
- package/src/orchestrator/mcp/scribe.ts +27 -16
- package/src/orchestrator/mcp/write-lease.ts +30 -13
- package/src/orchestrator/phase4-shared-context.ts +20 -4
- package/protocols/visual-dna.md +0 -185
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/*
|
|
3
|
+
* buildanything: DESIGN.md lint runner (Phase 3 Step 3.8 gate).
|
|
4
|
+
*
|
|
5
|
+
* Runs the pinned @google/design.md linter (devDependency in package.json) via
|
|
6
|
+
* `npx --no-install` against the DESIGN.md at the current working directory.
|
|
7
|
+
* Auto-installs the pinned version on first use if the package is listed in
|
|
8
|
+
* package.json devDependencies but not yet in node_modules — runs plain
|
|
9
|
+
* `npm install` (no args) so package.json is not mutated. Classifies findings
|
|
10
|
+
* (broken-ref => error, everything else => warning per
|
|
11
|
+
* protocols/design-md-authoring.md §8),
|
|
12
|
+
* writes a JSON summary to .buildanything/graph/lint-status.json (consumed by
|
|
13
|
+
* src/graph/storage/index.ts queryDna lint_status field), and appends a
|
|
14
|
+
* one-line summary to docs/plans/build-log.md under
|
|
15
|
+
* `## Phase 3 Step 3.8 — DESIGN.md Lint`.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — pass (broken-refs == 0; warnings allowed)
|
|
19
|
+
* 2 — fail (broken-refs > 0; orchestrator routes back to Step 3.4)
|
|
20
|
+
* 3 — DESIGN.md missing
|
|
21
|
+
*
|
|
22
|
+
* Invocation:
|
|
23
|
+
* npx tsx hooks/design-md-lint.ts # run inside project root
|
|
24
|
+
* hooks/design-md-lint # via bash wrapper
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, renameSync } from "node:fs";
|
|
30
|
+
import { dirname, resolve } from "node:path";
|
|
31
|
+
import process from "node:process";
|
|
32
|
+
import YAML from "yaml";
|
|
33
|
+
|
|
34
|
+
interface LintFinding {
|
|
35
|
+
rule: string;
|
|
36
|
+
severity: "error" | "warning" | "info";
|
|
37
|
+
message: string;
|
|
38
|
+
line?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface LintSummary {
|
|
42
|
+
file_hash: string;
|
|
43
|
+
broken_refs: number;
|
|
44
|
+
warnings: LintFinding[];
|
|
45
|
+
errors: LintFinding[];
|
|
46
|
+
ran_at: string;
|
|
47
|
+
exit_code: number;
|
|
48
|
+
raw_stdout: string;
|
|
49
|
+
raw_stderr: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BROKEN_REF_RULES = new Set(["broken-ref"]);
|
|
53
|
+
|
|
54
|
+
function sha256(content: string): string {
|
|
55
|
+
return createHash("sha256").update(content).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureDir(filePath: string): void {
|
|
59
|
+
const dir = dirname(filePath);
|
|
60
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Atomic write — tmp + rename so a crash mid-write never leaves a partial
|
|
64
|
+
// JSON that graph/storage queryDna would fail to parse.
|
|
65
|
+
function atomicWrite(path: string, content: string): void {
|
|
66
|
+
ensureDir(path);
|
|
67
|
+
const tmp = `${path}.tmp`;
|
|
68
|
+
writeFileSync(tmp, content);
|
|
69
|
+
renameSync(tmp, path);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseFindings(stdout: string): LintFinding[] {
|
|
73
|
+
const findings: LintFinding[] = [];
|
|
74
|
+
for (const raw of stdout.split(/\r?\n/)) {
|
|
75
|
+
const line = raw.trim();
|
|
76
|
+
if (!line) continue;
|
|
77
|
+
const m = line.match(/^DESIGN\.md(?::(\d+))?\s+(error|warn|warning|info)\s+([\w-]+)\s+(.+)$/i);
|
|
78
|
+
if (!m) continue;
|
|
79
|
+
const sev = m[2].toLowerCase();
|
|
80
|
+
const severity: LintFinding["severity"] =
|
|
81
|
+
sev === "error" ? "error" : sev === "info" ? "info" : "warning";
|
|
82
|
+
findings.push({
|
|
83
|
+
rule: m[3],
|
|
84
|
+
severity,
|
|
85
|
+
message: m[4],
|
|
86
|
+
line: m[1] ? Number(m[1]) : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface PackageJson {
|
|
93
|
+
devDependencies?: Record<string, string>;
|
|
94
|
+
dependencies?: Record<string, string>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if @google/design.md is pinned in package.json devDependencies
|
|
99
|
+
* (or dependencies) but is NOT installed under node_modules. This is the
|
|
100
|
+
* narrow window where auto-install is safe — the package is "ours" to install.
|
|
101
|
+
* Returns false in any other state (missing package.json, missing pin,
|
|
102
|
+
* already installed, parse error) so the caller falls through to existing
|
|
103
|
+
* behavior.
|
|
104
|
+
*/
|
|
105
|
+
function shouldAutoInstall(cwd: string): boolean {
|
|
106
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
107
|
+
if (!existsSync(pkgPath)) return false;
|
|
108
|
+
let pkg: PackageJson;
|
|
109
|
+
try {
|
|
110
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as PackageJson;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const pinned =
|
|
115
|
+
pkg.devDependencies?.["@google/design.md"] ??
|
|
116
|
+
pkg.dependencies?.["@google/design.md"];
|
|
117
|
+
if (!pinned) return false;
|
|
118
|
+
// Installed if the package directory exists. We don't validate the
|
|
119
|
+
// version range here — npm install will reconcile against package-lock.
|
|
120
|
+
const installedPath = resolve(cwd, "node_modules", "@google", "design.md");
|
|
121
|
+
return !existsSync(installedPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main(): number {
|
|
125
|
+
const cwd = process.cwd();
|
|
126
|
+
const designMd = resolve(cwd, "DESIGN.md");
|
|
127
|
+
if (!existsSync(designMd)) {
|
|
128
|
+
process.stderr.write(`design-md-lint: DESIGN.md not found at ${designMd}\n`);
|
|
129
|
+
return 3;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fileContent = readFileSync(designMd, "utf8");
|
|
133
|
+
const fileHash = sha256(fileContent);
|
|
134
|
+
|
|
135
|
+
// First attempt: --no-install ensures we use the pinned version from
|
|
136
|
+
// package.json (devDependency). If the package isn't installed locally,
|
|
137
|
+
// we auto-install once below rather than silently fetching latest from
|
|
138
|
+
// npm — that's the whole point of pinning.
|
|
139
|
+
const runLint = () =>
|
|
140
|
+
spawnSync("npx", ["--no-install", "@google/design.md", "lint", "DESIGN.md"], {
|
|
141
|
+
cwd,
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
let lint = runLint();
|
|
146
|
+
|
|
147
|
+
// Auto-install fallback: only fires when the package is pinned in
|
|
148
|
+
// package.json but missing from node_modules. We run plain `npm install`
|
|
149
|
+
// (no args, no --save-dev) — that installs from the existing devDeps
|
|
150
|
+
// block and does NOT mutate package.json or package-lock.json beyond
|
|
151
|
+
// what npm already does for normal install resolution.
|
|
152
|
+
const linterNotInstalled = lint.error || (lint.status !== null && lint.status !== 0 && /could not determine|could not find/i.test(lint.stderr ?? ""));
|
|
153
|
+
if (linterNotInstalled && shouldAutoInstall(cwd)) {
|
|
154
|
+
process.stdout.write("design-md-lint: pinned linter not installed; running 'npm install' once (~5-15s)...\n");
|
|
155
|
+
const install = spawnSync("npm", ["install", "--silent"], {
|
|
156
|
+
cwd,
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
});
|
|
159
|
+
if (install.status === 0) {
|
|
160
|
+
lint = runLint();
|
|
161
|
+
} else {
|
|
162
|
+
process.stderr.write(`design-md-lint: auto-install failed (exit ${install.status}). Run 'npm install' from the plugin root manually.\n`);
|
|
163
|
+
if (install.stderr) process.stderr.write(install.stderr);
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const stdout = lint.stdout ?? "";
|
|
169
|
+
const stderr = lint.stderr ?? "";
|
|
170
|
+
|
|
171
|
+
if (lint.error) {
|
|
172
|
+
process.stderr.write(`design-md-lint: linter spawn failed: ${lint.error.message}\n`);
|
|
173
|
+
process.stderr.write(`design-md-lint: install pinned version with: npm install --save-dev @google/design.md\n`);
|
|
174
|
+
process.stderr.write(stderr);
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const findings = parseFindings(stdout);
|
|
179
|
+
const errors = findings.filter((f) => f.severity === "error" && BROKEN_REF_RULES.has(f.rule));
|
|
180
|
+
const warnings = findings.filter((f) => f.severity === "warning" || (f.severity === "error" && !BROKEN_REF_RULES.has(f.rule)));
|
|
181
|
+
const infos: LintFinding[] = [];
|
|
182
|
+
|
|
183
|
+
// §9.5 iOS-specific post-process checks (gated on project_type=ios).
|
|
184
|
+
const buildStatePath = resolve(cwd, "docs/plans/.build-state.json");
|
|
185
|
+
if (existsSync(buildStatePath)) {
|
|
186
|
+
try {
|
|
187
|
+
const bs = JSON.parse(readFileSync(buildStatePath, "utf8")) as Record<string, unknown>;
|
|
188
|
+
if (bs.project_type === "ios") {
|
|
189
|
+
// Parse YAML frontmatter for token inspection.
|
|
190
|
+
const fmMatch = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
191
|
+
const fmYaml = fmMatch ? fmMatch[1] : "";
|
|
192
|
+
let fm: Record<string, unknown> = {};
|
|
193
|
+
try { fm = YAML.parse(fmYaml) ?? {}; } catch { /* skip if unparseable */ }
|
|
194
|
+
|
|
195
|
+
const colors = (typeof fm.colors === "object" && fm.colors !== null) ? fm.colors as Record<string, unknown> : {};
|
|
196
|
+
const typography = (typeof fm.typography === "object" && fm.typography !== null) ? fm.typography as Record<string, unknown> : {};
|
|
197
|
+
const components = (typeof fm.components === "object" && fm.components !== null) ? fm.components as Record<string, unknown> : {};
|
|
198
|
+
|
|
199
|
+
// 1. Dark-pair rule: every color token needs a -dark counterpart.
|
|
200
|
+
const colorKeys = Object.keys(colors);
|
|
201
|
+
for (const ck of colorKeys) {
|
|
202
|
+
if (ck.endsWith("-dark")) continue;
|
|
203
|
+
if (!colorKeys.includes(`${ck}-dark`)) {
|
|
204
|
+
warnings.push({ rule: "ios-dark-pair", severity: "warning", message: `colors.${ck} has no -dark pair for dark mode` });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 2. Dynamic Type role check: typography tokens should match iOS roles.
|
|
209
|
+
const DYNAMIC_TYPE_ROLES = new Set(["largeTitle", "title", "title2", "title3", "headline", "body", "callout", "subheadline", "footnote", "caption", "caption2"]);
|
|
210
|
+
for (const tk of Object.keys(typography)) {
|
|
211
|
+
if (!DYNAMIC_TYPE_ROLES.has(tk)) {
|
|
212
|
+
warnings.push({ rule: "ios-dynamic-type", severity: "warning", message: `typography.${tk} is not a Dynamic Type role — fixed-size Font, breaks accessibility scaling` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 3. iOS 26 gating: Glassy material + iOS 26 → require card-glass/button-tinted.
|
|
217
|
+
const dnaMatch = fileContent.match(/###\s*Brand DNA[\s\S]*?(?=###|\n##\s|$)/);
|
|
218
|
+
const dnaBlock = dnaMatch ? dnaMatch[0] : "";
|
|
219
|
+
const materialMatch = dnaBlock.match(/Material\s*:\s*(.+)/i);
|
|
220
|
+
const material = materialMatch ? materialMatch[1].trim() : "";
|
|
221
|
+
const iosFeatures = Array.isArray(bs.ios_features) ? bs.ios_features as string[] : [];
|
|
222
|
+
const targetsIos26 = iosFeatures.some((f) => /ios\s*26|26\s*sdk/i.test(String(f)));
|
|
223
|
+
const isGlassy = /glassy/i.test(material);
|
|
224
|
+
const compKeys = Object.keys(components);
|
|
225
|
+
|
|
226
|
+
if (isGlassy && targetsIos26) {
|
|
227
|
+
if (!compKeys.includes("card-glass")) infos.push({ rule: "ios26-glassy-gate", severity: "info", message: "Material=Glassy + iOS 26 target but components.card-glass is missing" });
|
|
228
|
+
if (!compKeys.includes("button-tinted")) infos.push({ rule: "ios26-glassy-gate", severity: "info", message: "Material=Glassy + iOS 26 target but components.button-tinted is missing" });
|
|
229
|
+
} else if (!isGlassy) {
|
|
230
|
+
if (compKeys.includes("card-glass")) infos.push({ rule: "ios26-glassy-gate", severity: "info", message: "Material is not Glassy but components.card-glass is present — remove it" });
|
|
231
|
+
if (compKeys.includes("button-tinted")) infos.push({ rule: "ios26-glassy-gate", severity: "info", message: "Material is not Glassy but components.button-tinted is present — remove it" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch { /* build-state unreadable — skip iOS checks */ }
|
|
235
|
+
}
|
|
236
|
+
const brokenRefs = errors.length;
|
|
237
|
+
const exitCode = brokenRefs > 0 ? 2 : 0;
|
|
238
|
+
|
|
239
|
+
const ranAt = new Date().toISOString();
|
|
240
|
+
const status: "pass" | "warn" | "fail" =
|
|
241
|
+
brokenRefs > 0 ? "fail" : (warnings.length > 0 || infos.length > 0) ? "warn" : "pass";
|
|
242
|
+
|
|
243
|
+
// Storage layer (src/graph/storage/index.ts queryDna) reads
|
|
244
|
+
// .buildanything/graph/lint-status.json and only consumes the `status`
|
|
245
|
+
// field. Extra diagnostic fields (broken_refs, warnings, errors, raw_*)
|
|
246
|
+
// are preserved here for human/tool inspection and are ignored by storage.
|
|
247
|
+
const summary: LintSummary & { status: "pass" | "warn" | "fail"; at: string; source: string } = {
|
|
248
|
+
status,
|
|
249
|
+
at: ranAt,
|
|
250
|
+
source: "DESIGN.md",
|
|
251
|
+
file_hash: fileHash,
|
|
252
|
+
broken_refs: brokenRefs,
|
|
253
|
+
warnings,
|
|
254
|
+
errors,
|
|
255
|
+
ran_at: ranAt,
|
|
256
|
+
exit_code: exitCode,
|
|
257
|
+
raw_stdout: stdout.slice(0, 8000),
|
|
258
|
+
raw_stderr: stderr.slice(0, 2000),
|
|
259
|
+
...(infos.length > 0 ? { infos } : {}),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const summaryPath = resolve(cwd, ".buildanything/graph/lint-status.json");
|
|
263
|
+
atomicWrite(summaryPath, JSON.stringify(summary, null, 2));
|
|
264
|
+
|
|
265
|
+
const buildLogPath = resolve(cwd, "docs/plans/build-log.md");
|
|
266
|
+
if (existsSync(buildLogPath)) {
|
|
267
|
+
const shortHash = fileHash.slice(0, 12);
|
|
268
|
+
const oneLine = `${summary.ran_at} | broken-refs: ${brokenRefs} | warnings: ${warnings.length} | hash: ${shortHash}\n`;
|
|
269
|
+
let existing = readFileSync(buildLogPath, "utf8");
|
|
270
|
+
if (!/## Phase 3 Step 3\.8 — DESIGN\.md Lint/.test(existing)) {
|
|
271
|
+
existing += `\n## Phase 3 Step 3.8 — DESIGN.md Lint\n\n`;
|
|
272
|
+
writeFileSync(buildLogPath, existing);
|
|
273
|
+
}
|
|
274
|
+
appendFileSync(buildLogPath, oneLine);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (brokenRefs > 0) {
|
|
278
|
+
process.stderr.write(`design-md-lint: ${brokenRefs} broken-ref error(s) — Phase 3.8 routes back to Step 3.4\n`);
|
|
279
|
+
for (const e of errors) {
|
|
280
|
+
process.stderr.write(` ${e.rule}${e.line ? ` (line ${e.line})` : ""}: ${e.message}\n`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (warnings.length > 0) {
|
|
284
|
+
process.stdout.write(`design-md-lint: ${warnings.length} warning(s) (logged, non-blocking)\n`);
|
|
285
|
+
}
|
|
286
|
+
if (infos.length > 0) {
|
|
287
|
+
process.stdout.write(`design-md-lint: ${infos.length} info(s) (logged, non-blocking)\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return exitCode;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (process.argv[1]?.endsWith("design-md-lint.ts") || process.argv[1]?.endsWith("design-md-lint")) {
|
|
294
|
+
process.exit(main());
|
|
295
|
+
}
|
package/hooks/pre-tool-use.ts
CHANGED
|
@@ -127,7 +127,7 @@ interface NormalizedLease {
|
|
|
127
127
|
file_paths: string[];
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
interface ArtifactEntry {
|
|
130
|
+
export interface ArtifactEntry {
|
|
131
131
|
path: string;
|
|
132
132
|
writer?: string;
|
|
133
133
|
writers?: string[];
|
|
@@ -257,10 +257,16 @@ function globToRegex(pattern: string): RegExp {
|
|
|
257
257
|
return new RegExp(`^${out}$`);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
function findArtifact(filePath: string, artifacts: ArtifactEntry[]): ArtifactEntry | null {
|
|
261
|
-
// Exact match first, then glob match.
|
|
260
|
+
export function findArtifact(filePath: string, artifacts: ArtifactEntry[]): ArtifactEntry | null {
|
|
261
|
+
// Exact match first, then most-specific glob match. A glob's specificity is
|
|
262
|
+
// the length of its literal prefix before the first wildcard (`*` or `[`).
|
|
263
|
+
// Longer prefix = more specific. This prevents broad catch-all globs from
|
|
264
|
+
// shadowing narrow ones regardless of declaration order in phase-graph.yaml —
|
|
265
|
+
// e.g. `evidence/lrr/*.json` wins over `evidence/**/*.json` for LRR paths.
|
|
262
266
|
const exact = artifacts.find((a) => a.path === filePath);
|
|
263
267
|
if (exact) return exact;
|
|
268
|
+
let best: ArtifactEntry | null = null;
|
|
269
|
+
let bestSpecificity = -1;
|
|
264
270
|
for (const a of artifacts) {
|
|
265
271
|
const isGlob = a.is_glob ?? (a.path.includes("*") || a.path.includes("["));
|
|
266
272
|
if (!isGlob) continue;
|
|
@@ -272,9 +278,15 @@ function findArtifact(filePath: string, artifacts: ArtifactEntry[]): ArtifactEnt
|
|
|
272
278
|
const normalized = a.path.replace(/\[[^\]]+\]/g, "*");
|
|
273
279
|
re = globToRegex(normalized);
|
|
274
280
|
}
|
|
275
|
-
if (re.test(filePath))
|
|
281
|
+
if (!re.test(filePath)) continue;
|
|
282
|
+
const firstWildcard = a.path.search(/[*[]/);
|
|
283
|
+
const specificity = firstWildcard === -1 ? a.path.length : firstWildcard;
|
|
284
|
+
if (specificity > bestSpecificity) {
|
|
285
|
+
best = a;
|
|
286
|
+
bestSpecificity = specificity;
|
|
287
|
+
}
|
|
276
288
|
}
|
|
277
|
-
return
|
|
289
|
+
return best;
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
function normalizePhase(raw: unknown): string | null {
|
|
@@ -701,6 +713,17 @@ function main(): number {
|
|
|
701
713
|
);
|
|
702
714
|
}
|
|
703
715
|
|
|
716
|
+
// Maintainer-mode exemption: when no build phase is active, writes to
|
|
717
|
+
// protected prefixes are plugin-source edits by the user (or an agent
|
|
718
|
+
// acting on their behalf), not phase-agent runaway. Phase agents during
|
|
719
|
+
// a real build still default-deny — currentPhase is set whenever
|
|
720
|
+
// .build-state.json reflects an in-flight build.
|
|
721
|
+
if (!currentPhase) {
|
|
722
|
+
return applyLeaseDecision(
|
|
723
|
+
evaluateLease(taskId, leases, toolName, filePath, relCandidates),
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
704
727
|
const denyPath = [...relCandidates].find((c) => isProtectedPath(c)) ?? filePath;
|
|
705
728
|
const msg = `buildanything: writer-owner hook denied ${toolName} on ${denyPath} — path not in writer-owner table. Please add an entry to docs/migration/phase-graph.yaml or route the write through the scribe_decision MCP.`;
|
|
706
729
|
|
|
@@ -773,4 +796,12 @@ function main(): number {
|
|
|
773
796
|
return 2;
|
|
774
797
|
}
|
|
775
798
|
|
|
776
|
-
|
|
799
|
+
// Guard CLI-only execution so the module is safe to import in tests.
|
|
800
|
+
function isCliEntry(): boolean {
|
|
801
|
+
const entry = process.argv[1] ?? "";
|
|
802
|
+
return entry.endsWith("pre-tool-use.ts") || entry.endsWith("pre-tool-use");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (isCliEntry()) {
|
|
806
|
+
process.exit(main());
|
|
807
|
+
}
|
|
@@ -35,12 +35,21 @@ import process from "node:process";
|
|
|
35
35
|
|
|
36
36
|
const TRACKED_FLAGS = [
|
|
37
37
|
"BUILDANYTHING_SDK",
|
|
38
|
-
"BUILDANYTHING_ENFORCE_WRITER_OWNER",
|
|
39
|
-
"BUILDANYTHING_ENFORCE_WRITE_LEASE",
|
|
40
38
|
"BUILDANYTHING_SDK_SPRINT_CONTEXT",
|
|
41
39
|
"BUILDANYTHING_SDK_SPRINT_CONTEXT_IOS",
|
|
40
|
+
"BUILDANYTHING_ENFORCE_WRITER_OWNER",
|
|
41
|
+
"BUILDANYTHING_ENFORCE_WRITE_LEASE",
|
|
42
|
+
"BUILDANYTHING_SCRIBE_SINGLE_WRITER",
|
|
43
|
+
"BUILDANYTHING_ALLOW_RAW_STATE_WRITES",
|
|
44
|
+
"BUILDANYTHING_STRICT_TASK_ID",
|
|
42
45
|
] as const;
|
|
43
46
|
|
|
47
|
+
// Distinct unset sentinel — angle brackets make it un-shell-settable without
|
|
48
|
+
// quoting, six chars, no sane operator uses this literal. Replaces the prior
|
|
49
|
+
// colliding default of "false" which silently swallowed real operator opt-outs
|
|
50
|
+
// like BUILDANYTHING_ENFORCE_WRITER_OWNER=false (warn-mode rollback).
|
|
51
|
+
const UNSET_SENTINEL = "<unset>";
|
|
52
|
+
|
|
44
53
|
type FlagName = (typeof TRACKED_FLAGS)[number];
|
|
45
54
|
type FlagSnapshot = Record<FlagName, string>;
|
|
46
55
|
|
|
@@ -58,12 +67,60 @@ interface BuildState {
|
|
|
58
67
|
[key: string]: unknown;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
function defaultFor(
|
|
62
|
-
//
|
|
63
|
-
//
|
|
70
|
+
function defaultFor(_flag: FlagName): string {
|
|
71
|
+
// Unified sentinel for all tracked flags — collision-free with any plausible
|
|
72
|
+
// operator value. See UNSET_SENTINEL comment above.
|
|
73
|
+
return UNSET_SENTINEL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Legacy default the pre-fix recorder wrote to post_flags snapshots when a
|
|
77
|
+
// tracked env var was unset. Only consulted during prev-vs-curr equivalence so
|
|
78
|
+
// the first post-fix run does not spuriously record transitions against old
|
|
79
|
+
// state files. Matches the old defaultFor() behavior exactly.
|
|
80
|
+
function legacyDefaultFor(flag: FlagName): string {
|
|
64
81
|
return flag === "BUILDANYTHING_SDK" ? "on" : "false";
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
// True when the `prev` snapshot was written by the pre-fix recorder. Signal:
|
|
85
|
+
// every tracked flag in the snapshot equals its legacy default ("on" for SDK,
|
|
86
|
+
// "false" for everything else) and no `<unset>` appears anywhere. Once even a
|
|
87
|
+
// single flag has been recorded as `<unset>` (or as any operator-set literal),
|
|
88
|
+
// the snapshot is post-fix and legacy-compat does NOT apply — keeps real
|
|
89
|
+
// "false" → `<unset>` transitions visible.
|
|
90
|
+
function isLegacyDefaultOnlySnapshot(snap: FlagSnapshot): boolean {
|
|
91
|
+
for (const flag of TRACKED_FLAGS) {
|
|
92
|
+
if (snap[flag] !== legacyDefaultFor(flag)) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// True when `prev` (from disk) and `curr` (from env this run) both represent
|
|
98
|
+
// the "unset / default" state, so no real transition occurred. Handles:
|
|
99
|
+
// - exact match: prev == curr (any literal operator value) → equivalent
|
|
100
|
+
// - new shape: prev=<unset>, curr=<unset> → equivalent
|
|
101
|
+
// - legacy whole-snapshot: prev is pristine legacy defaults AND curr=<unset>
|
|
102
|
+
// for this flag → equivalent (suppresses spurious diff on first post-fix
|
|
103
|
+
// run against a state file written by the pre-fix recorder)
|
|
104
|
+
// Any other mismatch — including the critical "false" → `<unset>` un-set after
|
|
105
|
+
// a real operator flip — falls through to non-equivalent and records a diff.
|
|
106
|
+
function flagValuesEquivalent(
|
|
107
|
+
flag: FlagName,
|
|
108
|
+
prev: string,
|
|
109
|
+
curr: string,
|
|
110
|
+
prevSnapshot: FlagSnapshot,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (prev === curr) return true;
|
|
113
|
+
if (prev === UNSET_SENTINEL && curr === UNSET_SENTINEL) return true;
|
|
114
|
+
if (
|
|
115
|
+
curr === UNSET_SENTINEL &&
|
|
116
|
+
prev === legacyDefaultFor(flag) &&
|
|
117
|
+
isLegacyDefaultOnlySnapshot(prevSnapshot)
|
|
118
|
+
) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
67
124
|
function readCurrentFlags(): FlagSnapshot {
|
|
68
125
|
const out = {} as FlagSnapshot;
|
|
69
126
|
for (const flag of TRACKED_FLAGS) {
|
|
@@ -90,7 +147,7 @@ function lastRecordedFlags(state: BuildState): FlagSnapshot | null {
|
|
|
90
147
|
function diffFlags(prev: FlagSnapshot, curr: FlagSnapshot): FlagName[] {
|
|
91
148
|
const changed: FlagName[] = [];
|
|
92
149
|
for (const flag of TRACKED_FLAGS) {
|
|
93
|
-
if (prev[flag]
|
|
150
|
+
if (!flagValuesEquivalent(flag, prev[flag], curr[flag], prev)) changed.push(flag);
|
|
94
151
|
}
|
|
95
152
|
return changed;
|
|
96
153
|
}
|
package/hooks/subagent-start.ts
CHANGED
|
@@ -45,7 +45,7 @@ import process from "node:process";
|
|
|
45
45
|
|
|
46
46
|
const CACHE_DIR_REL = ".buildanything/subagent-start-cache";
|
|
47
47
|
const STATE_PATH_REL = "docs/plans/.build-state.json";
|
|
48
|
-
const VISUAL_DNA_PATH_REL = "
|
|
48
|
+
const VISUAL_DNA_PATH_REL = "DESIGN.md";
|
|
49
49
|
const REFS_PATH_REL = "docs/plans/refs.json";
|
|
50
50
|
const ARCHITECTURE_PATH_REL = "docs/plans/architecture.md";
|
|
51
51
|
const SPRINT_CONTEXT_MARKER = "\n\n--- SPRINT CONTEXT ---\n\n";
|
|
@@ -274,12 +274,13 @@ async function renderHeader(
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
try {
|
|
277
|
+
const buildId = pickString(state.session_id) ?? '';
|
|
277
278
|
return mod.renderContextHeader({
|
|
278
279
|
projectType,
|
|
279
280
|
phase,
|
|
280
281
|
iosFeatures: pickIosFeatures(state.ios_features),
|
|
281
282
|
visualDnaPath: resolve(projectDir, VISUAL_DNA_PATH_REL),
|
|
282
|
-
});
|
|
283
|
+
}, buildId);
|
|
283
284
|
} catch (err) {
|
|
284
285
|
const msg = err instanceof Error ? err.message : String(err);
|
|
285
286
|
process.stderr.write(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buildanything",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "One command to build an entire product. 44 specialist agents orchestrated into a full engineering pipeline for Claude Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"buildanything": "./bin/setup.js",
|
|
@@ -50,12 +50,14 @@
|
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@anthropic-ai/claude-agent-sdk": "0.2.114",
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
54
|
"semver": "^7.7.4",
|
|
54
55
|
"tsx": "^4.21.0",
|
|
55
56
|
"yaml": "^2.8.0",
|
|
56
57
|
"zod": "^4.0.0"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
60
|
+
"@google/design.md": "^0.1.1",
|
|
59
61
|
"@types/node": "^22",
|
|
60
62
|
"@types/semver": "^7.7.1",
|
|
61
63
|
"typescript": "^6.0.3"
|