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.
Files changed (115) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +9 -1
  3. package/README.md +57 -61
  4. package/agents/a11y-architect.md +2 -0
  5. package/agents/briefing-officer.md +172 -0
  6. package/agents/business-model.md +14 -12
  7. package/agents/code-architect.md +6 -1
  8. package/agents/code-reviewer.md +3 -2
  9. package/agents/code-simplifier.md +12 -4
  10. package/agents/design-brand-guardian.md +19 -0
  11. package/agents/design-critic.md +16 -11
  12. package/agents/design-inclusive-visuals-specialist.md +2 -0
  13. package/agents/design-ui-designer.md +17 -0
  14. package/agents/design-ux-architect.md +15 -0
  15. package/agents/design-ux-researcher.md +102 -7
  16. package/agents/engineering-ai-engineer.md +2 -0
  17. package/agents/engineering-backend-architect.md +2 -0
  18. package/agents/engineering-data-engineer.md +2 -0
  19. package/agents/engineering-devops-automator.md +2 -0
  20. package/agents/engineering-frontend-developer.md +13 -0
  21. package/agents/engineering-mobile-app-builder.md +2 -0
  22. package/agents/engineering-rapid-prototyper.md +15 -2
  23. package/agents/engineering-security-engineer.md +2 -0
  24. package/agents/engineering-senior-developer.md +13 -0
  25. package/agents/engineering-sre.md +2 -0
  26. package/agents/engineering-technical-writer.md +2 -0
  27. package/agents/feature-intel.md +8 -7
  28. package/agents/ios-app-review-guardian.md +2 -0
  29. package/agents/ios-foundation-models-specialist.md +2 -0
  30. package/agents/ios-product-reality-auditor.md +292 -0
  31. package/agents/ios-storekit-specialist.md +2 -0
  32. package/agents/ios-swift-architect.md +1 -0
  33. package/agents/ios-swift-search.md +1 -0
  34. package/agents/ios-swift-ui-design.md +7 -4
  35. package/agents/marketing-app-store-optimizer.md +2 -0
  36. package/agents/planner.md +6 -1
  37. package/agents/pr-test-analyzer.md +3 -2
  38. package/agents/product-feedback-synthesizer.md +62 -0
  39. package/agents/product-owner.md +163 -0
  40. package/agents/product-reality-auditor.md +216 -0
  41. package/agents/product-spec-writer.md +176 -0
  42. package/agents/refactor-cleaner.md +9 -1
  43. package/agents/security-reviewer.md +2 -1
  44. package/agents/silent-failure-hunter.md +2 -1
  45. package/agents/swift-build-resolver.md +2 -0
  46. package/agents/swift-reviewer.md +2 -1
  47. package/agents/tech-feasibility.md +5 -3
  48. package/agents/testing-api-tester.md +2 -0
  49. package/agents/testing-evidence-collector.md +24 -0
  50. package/agents/testing-performance-benchmarker.md +2 -0
  51. package/agents/testing-reality-checker.md +2 -1
  52. package/agents/visual-research.md +7 -5
  53. package/bin/adapters/scribe-tool.ts +4 -2
  54. package/bin/adapters/write-lease-tool.ts +1 -1
  55. package/bin/buildanything-runtime.ts +20 -107
  56. package/bin/graph-index.js +24 -0
  57. package/bin/graph-index.ts +340 -0
  58. package/bin/mcp-servers/graph-mcp.js +26 -0
  59. package/bin/mcp-servers/graph-mcp.ts +481 -0
  60. package/bin/mcp-servers/orchestrator-mcp.js +26 -0
  61. package/bin/mcp-servers/orchestrator-mcp.ts +361 -0
  62. package/bin/setup.js +272 -111
  63. package/commands/build.md +371 -158
  64. package/commands/idea-sweep.md +2 -2
  65. package/commands/setup.md +15 -4
  66. package/commands/ux-review.md +3 -3
  67. package/commands/verify.md +3 -0
  68. package/docs/migration/phase-graph.yaml +573 -157
  69. package/hooks/design-md-lint +4 -0
  70. package/hooks/design-md-lint.ts +295 -0
  71. package/hooks/pre-tool-use.ts +37 -6
  72. package/hooks/record-mode-transitions.ts +63 -6
  73. package/hooks/subagent-start.ts +3 -2
  74. package/package.json +3 -1
  75. package/protocols/agent-prompt-authoring.md +165 -0
  76. package/protocols/architecture-schema.md +10 -3
  77. package/protocols/cleanup.md +4 -0
  78. package/protocols/decision-log.md +8 -4
  79. package/protocols/design-md-authoring.md +520 -0
  80. package/protocols/design-md-spec.md +362 -0
  81. package/protocols/fake-data-detector.md +1 -1
  82. package/protocols/ios-fake-data-detector.md +65 -0
  83. package/protocols/ios-phase-branches.md +112 -27
  84. package/protocols/launch-readiness.md +9 -5
  85. package/protocols/metric-loop.md +1 -1
  86. package/protocols/page-spec-schema.md +234 -0
  87. package/protocols/product-spec-schema.md +354 -0
  88. package/protocols/sprint-tasks-schema.md +53 -0
  89. package/protocols/state-schema.json +38 -3
  90. package/protocols/state-schema.md +32 -2
  91. package/protocols/verify.md +29 -1
  92. package/protocols/web-phase-branches.md +234 -64
  93. package/skills/ios/ios-bootstrap/SKILL.md +1 -1
  94. package/src/graph/ids.ts +86 -0
  95. package/src/graph/index.ts +32 -0
  96. package/src/graph/parser/architecture.ts +603 -0
  97. package/src/graph/parser/component-manifest.ts +268 -0
  98. package/src/graph/parser/decisions-jsonl.ts +407 -0
  99. package/src/graph/parser/design-md-pass2.ts +253 -0
  100. package/src/graph/parser/design-md.ts +477 -0
  101. package/src/graph/parser/page-spec.ts +496 -0
  102. package/src/graph/parser/product-spec.ts +930 -0
  103. package/src/graph/parser/screenshot.ts +342 -0
  104. package/src/graph/parser/sprint-tasks.ts +317 -0
  105. package/src/graph/storage/index.ts +1154 -0
  106. package/src/graph/types.ts +432 -0
  107. package/src/graph/util/dhash.ts +84 -0
  108. package/src/lrr/aggregator.ts +105 -10
  109. package/src/orchestrator/hooks/context-header.ts +34 -10
  110. package/src/orchestrator/hooks/token-accounting.ts +25 -14
  111. package/src/orchestrator/mcp/cycle-counter.ts +2 -1
  112. package/src/orchestrator/mcp/scribe.ts +27 -16
  113. package/src/orchestrator/mcp/write-lease.ts +30 -13
  114. package/src/orchestrator/phase4-shared-context.ts +20 -4
  115. package/protocols/visual-dna.md +0 -185
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ cd "$(dirname "$0")/.."
4
+ npx tsx hooks/design-md-lint.ts "$@"
@@ -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
+ }
@@ -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. Exact wins if both are present.
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)) return a;
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 null;
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
- process.exit(main());
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(flag: FlagName): string {
62
- // BUILDANYTHING_SDK defaults to "on" when unset (v2 opt-out contract).
63
- // All other flags default to "false" when unset.
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] !== curr[flag]) changed.push(flag);
150
+ if (!flagValuesEquivalent(flag, prev[flag], curr[flag], prev)) changed.push(flag);
94
151
  }
95
152
  return changed;
96
153
  }
@@ -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 = "docs/plans/visual-dna.md";
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.0.0",
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"