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
|
@@ -5,7 +5,7 @@ export interface ContextHeaderInput {
|
|
|
5
5
|
projectType: 'web' | 'ios';
|
|
6
6
|
phase: number;
|
|
7
7
|
iosFeatures?: Record<string, boolean>;
|
|
8
|
-
visualDnaPath?: string; // path to
|
|
8
|
+
visualDnaPath?: string; // path to DESIGN.md (repo root) — DNA lives in `## Overview > ### Brand DNA` block
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface RenderedHeader {
|
|
@@ -13,8 +13,9 @@ export interface RenderedHeader {
|
|
|
13
13
|
hash: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/** Cache: rendered header keyed by phase + input hash for automatic invalidation */
|
|
16
|
+
/** Cache: rendered header keyed by buildId + phase + input hash for automatic invalidation */
|
|
17
17
|
let cachedHeader: RenderedHeader | null = null;
|
|
18
|
+
let cachedBuildId: string | null = null;
|
|
18
19
|
let cachedPhase: number | null = null;
|
|
19
20
|
let cachedInputHash: string | null = null;
|
|
20
21
|
|
|
@@ -22,6 +23,19 @@ let cachedInputHash: string | null = null;
|
|
|
22
23
|
* Compute a hash of the variable inputs that affect the rendered header.
|
|
23
24
|
* Used to detect mid-phase mutations (e.g., ios_features changing within a phase).
|
|
24
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* Extract the 7 DNA axis values from DESIGN.md `## Overview > ### Brand DNA` block.
|
|
28
|
+
* The block is a bullet list of 7 axes; we capture the bullets (one per axis)
|
|
29
|
+
* and join them with newlines. Falls back to first 5 non-frontmatter lines if
|
|
30
|
+
* the block is missing (legacy/partial files).
|
|
31
|
+
*/
|
|
32
|
+
function extractBrandDnaBlock(content: string): string {
|
|
33
|
+
const dnaMatch = content.match(/###\s+Brand DNA\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/);
|
|
34
|
+
if (dnaMatch) return dnaMatch[1].trim().split('\n').slice(0, 7).join('\n');
|
|
35
|
+
const stripped = content.replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
36
|
+
return stripped.split('\n').slice(0, 5).join('\n').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
function inputHash(input: ContextHeaderInput): string {
|
|
26
40
|
const key = JSON.stringify({
|
|
27
41
|
projectType: input.projectType,
|
|
@@ -38,20 +52,27 @@ function inputHash(input: ContextHeaderInput): string {
|
|
|
38
52
|
* Automatically invalidates if inputs change within the same phase
|
|
39
53
|
* (e.g., ios_features mutating mid-build per spec pass criteria).
|
|
40
54
|
*/
|
|
41
|
-
export function renderContextHeader(input: ContextHeaderInput): RenderedHeader {
|
|
55
|
+
export function renderContextHeader(input: ContextHeaderInput, buildId: string): RenderedHeader {
|
|
42
56
|
const currentInputHash = inputHash(input);
|
|
43
|
-
// Return cached if same phase AND same inputs
|
|
44
|
-
if (
|
|
57
|
+
// Return cached if same build, same phase, AND same inputs
|
|
58
|
+
if (
|
|
59
|
+
cachedBuildId === buildId &&
|
|
60
|
+
cachedPhase === input.phase &&
|
|
61
|
+
cachedInputHash === currentInputHash &&
|
|
62
|
+
cachedHeader
|
|
63
|
+
) return cachedHeader;
|
|
45
64
|
|
|
46
65
|
const lines: string[] = ['CONTEXT:'];
|
|
47
66
|
lines.push(` project_type: ${input.projectType}`);
|
|
48
67
|
lines.push(` phase: ${input.phase}`);
|
|
49
68
|
|
|
50
|
-
// DNA: only for web, phase >= 4
|
|
69
|
+
// DNA: only for web, phase >= 4. Extract the 7 axis values from
|
|
70
|
+
// DESIGN.md `## Overview > ### Brand DNA` h3 block — NOT the full file.
|
|
71
|
+
// Falls back to first 5 lines if the block is absent (legacy/partial files).
|
|
51
72
|
if (input.projectType === 'web' && input.phase >= 4 && input.visualDnaPath) {
|
|
52
73
|
if (existsSync(input.visualDnaPath)) {
|
|
53
|
-
const
|
|
54
|
-
const summary =
|
|
74
|
+
const content = readFileSync(input.visualDnaPath, 'utf-8');
|
|
75
|
+
const summary = extractBrandDnaBlock(content);
|
|
55
76
|
lines.push(` dna: ${summary}`);
|
|
56
77
|
}
|
|
57
78
|
}
|
|
@@ -72,6 +93,7 @@ export function renderContextHeader(input: ContextHeaderInput): RenderedHeader {
|
|
|
72
93
|
const content = lines.join('\n');
|
|
73
94
|
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
74
95
|
|
|
96
|
+
cachedBuildId = buildId;
|
|
75
97
|
cachedHeader = { content, hash };
|
|
76
98
|
cachedPhase = input.phase;
|
|
77
99
|
cachedInputHash = currentInputHash;
|
|
@@ -83,13 +105,15 @@ export function renderContextHeader(input: ContextHeaderInput): RenderedHeader {
|
|
|
83
105
|
*/
|
|
84
106
|
export function invalidateCache(): void {
|
|
85
107
|
cachedHeader = null;
|
|
108
|
+
cachedBuildId = null;
|
|
86
109
|
cachedPhase = null;
|
|
87
110
|
cachedInputHash = null;
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
/**
|
|
91
|
-
* Check if the cache is valid for a given phase.
|
|
114
|
+
* Check if the cache is valid for a given build + phase.
|
|
92
115
|
*/
|
|
93
|
-
export function isCacheValid(phase: number): boolean {
|
|
116
|
+
export function isCacheValid(phase: number, buildId?: string): boolean {
|
|
117
|
+
if (buildId !== undefined && cachedBuildId !== buildId) return false;
|
|
94
118
|
return cachedPhase === phase && cachedHeader !== null;
|
|
95
119
|
}
|
|
@@ -14,6 +14,7 @@ export interface AccountingEntry {
|
|
|
14
14
|
step: string;
|
|
15
15
|
task?: string;
|
|
16
16
|
subagent_type?: string;
|
|
17
|
+
model?: string;
|
|
17
18
|
usage: TokenUsage;
|
|
18
19
|
cost_usd: number;
|
|
19
20
|
cumulative_usd: number;
|
|
@@ -22,34 +23,41 @@ export interface AccountingEntry {
|
|
|
22
23
|
/** Running cumulative cost across the build */
|
|
23
24
|
let cumulativeCost = 0;
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
27
|
-
input: 3.0,
|
|
28
|
-
output:
|
|
29
|
-
|
|
30
|
-
cache_create: 3.75,
|
|
26
|
+
/** Per-model pricing per million tokens */
|
|
27
|
+
export const PRICING_TABLE: Record<string, { input: number; output: number; cache_read: number; cache_create: number }> = {
|
|
28
|
+
'claude-sonnet-4-5': { input: 3.0, output: 15.0, cache_read: 0.30, cache_create: 3.75 },
|
|
29
|
+
'claude-haiku-3-5': { input: 0.80, output: 4.0, cache_read: 0.08, cache_create: 1.00 },
|
|
30
|
+
'claude-opus-4-5': { input: 15.0, output: 75.0, cache_read: 1.50, cache_create: 18.75 },
|
|
31
31
|
};
|
|
32
|
+
const DEFAULT_PRICING = PRICING_TABLE['claude-sonnet-4-5'];
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Calculate cost from token usage.
|
|
36
|
+
* When `model` is omitted or unrecognised, Sonnet pricing is used.
|
|
35
37
|
*/
|
|
36
|
-
export function calculateCost(usage: TokenUsage): number {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
38
|
+
export function calculateCost(usage: TokenUsage, model?: string): number {
|
|
39
|
+
const pricing = (model ? PRICING_TABLE[model] : undefined) ?? DEFAULT_PRICING;
|
|
40
|
+
const inputCost = (usage.input_tokens / 1_000_000) * pricing.input;
|
|
41
|
+
const outputCost = (usage.output_tokens / 1_000_000) * pricing.output;
|
|
42
|
+
const cacheReadCost = ((usage.cache_read ?? 0) / 1_000_000) * pricing.cache_read;
|
|
43
|
+
const cacheCreateCost = ((usage.cache_create ?? 0) / 1_000_000) * pricing.cache_create;
|
|
41
44
|
return Math.round((inputCost + outputCost + cacheReadCost + cacheCreateCost) * 10000) / 10000;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Record a token usage entry and append to build-log.md.
|
|
49
|
+
*
|
|
50
|
+
* @param startingCost — optional seed value so callers can restore
|
|
51
|
+
* cumulative cost from a persisted build-log at build start.
|
|
52
|
+
* Defaults to the current in-memory cumulative cost.
|
|
46
53
|
*/
|
|
47
54
|
export function recordUsage(
|
|
48
55
|
buildLogPath: string,
|
|
49
56
|
entry: Omit<AccountingEntry, 'cost_usd' | 'cumulative_usd' | 'timestamp'>,
|
|
57
|
+
startingCost = cumulativeCost,
|
|
50
58
|
): AccountingEntry {
|
|
51
|
-
const cost = calculateCost(entry.usage);
|
|
52
|
-
cumulativeCost
|
|
59
|
+
const cost = calculateCost(entry.usage, entry.model);
|
|
60
|
+
cumulativeCost = startingCost + cost;
|
|
53
61
|
|
|
54
62
|
const record: AccountingEntry = {
|
|
55
63
|
timestamp: new Date().toISOString(),
|
|
@@ -73,6 +81,7 @@ export function formatLogLine(entry: AccountingEntry): string {
|
|
|
73
81
|
const meta = [`phase=${entry.phase}`, `step=${entry.step}`];
|
|
74
82
|
if (entry.task) meta.push(`task=${entry.task}`);
|
|
75
83
|
if (entry.subagent_type) meta.push(`subagent_type=${entry.subagent_type}`);
|
|
84
|
+
if (entry.model) meta.push(`model=${entry.model}`);
|
|
76
85
|
|
|
77
86
|
const tokens = [
|
|
78
87
|
`input_tokens=${entry.usage.input_tokens}`,
|
|
@@ -94,7 +103,9 @@ export function getCumulativeCost(): number {
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
/**
|
|
97
|
-
* Reset cumulative cost
|
|
106
|
+
* Reset cumulative cost to zero.
|
|
107
|
+
* Call at build initialisation to prevent cross-build drift
|
|
108
|
+
* when the process is long-lived.
|
|
98
109
|
*/
|
|
99
110
|
export function reset(): void {
|
|
100
111
|
cumulativeCost = 0;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { InFlightBackwardEdge, BackwardRoutingCounters } from '../schemas/backward-edge';
|
|
11
|
+
import { STALE_EDGE_THRESHOLD_MS } from '../schemas/backward-edge';
|
|
11
12
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Types
|
|
@@ -93,7 +94,7 @@ export function clearInFlightEdge(state: CounterState): void {
|
|
|
93
94
|
* If edge is older than threshold, decrement both counters and clear the edge.
|
|
94
95
|
* Returns true if a stale edge was cleaned up.
|
|
95
96
|
*/
|
|
96
|
-
export function handleStaleEdge(state: CounterState, thresholdMs: number =
|
|
97
|
+
export function handleStaleEdge(state: CounterState, thresholdMs: number = STALE_EDGE_THRESHOLD_MS): boolean {
|
|
97
98
|
const edge = state.in_flight_backward_edge;
|
|
98
99
|
if (!edge) return false;
|
|
99
100
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Migration ref: MIGRATION-PLAN-FINAL.md §4 Stage 1 (tasks 1.2.1–1.2.3)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -37,13 +37,12 @@ export interface DecisionRow {
|
|
|
37
37
|
|
|
38
38
|
export interface ScribeInput {
|
|
39
39
|
phase: string;
|
|
40
|
-
category: string;
|
|
41
40
|
summary: string;
|
|
42
41
|
decided_by: string;
|
|
43
42
|
impact_level: 'low' | 'medium' | 'high' | 'critical';
|
|
44
43
|
chosen_approach: string;
|
|
45
44
|
rejected_alternatives?: RejectedAlternative[];
|
|
46
|
-
ref
|
|
45
|
+
ref: string;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
// ---------------------------------------------------------------------------
|
|
@@ -63,8 +62,15 @@ const MAX_REVISIT_LEN = 240;
|
|
|
63
62
|
/** Ref field pattern from decisions.schema.json */
|
|
64
63
|
const REF_PATTERN = /^[a-zA-Z0-9_\-./]+\.(md|json|jsonl|yaml|yml)(#[a-zA-Z0-9_\-/.]+)?$/;
|
|
65
64
|
|
|
66
|
-
/** Phase string pattern from decisions.schema.json */
|
|
67
|
-
const PHASE_PATTERN =
|
|
65
|
+
/** Phase string pattern from decisions.schema.json. Leading minus allowed for Phase -1 (iOS bootstrap). */
|
|
66
|
+
const PHASE_PATTERN = /^-?[0-9]+(\.[0-9]+)?$/;
|
|
67
|
+
|
|
68
|
+
/** Encode a phase token for embedding in a decision_id. Negative phases get an "N" prefix
|
|
69
|
+
* instead of "-" to avoid colliding with the "-" delimiter in the ID format. So phase "-1"
|
|
70
|
+
* becomes "N1" in the ID slot, yielding "D-N1-01". Round-trippable via decodePhaseFromId. */
|
|
71
|
+
function encodePhaseForId(phase: string): string {
|
|
72
|
+
return phase.startsWith('-') ? `N${phase.slice(1)}` : phase;
|
|
73
|
+
}
|
|
68
74
|
|
|
69
75
|
// ---------------------------------------------------------------------------
|
|
70
76
|
// Per-phase sequence counters (task 1.2.3 — ID allocation)
|
|
@@ -96,10 +102,11 @@ export function loadCounters(filePath: string): void {
|
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
/** Allocate the next decision ID for a phase. Format: D-{
|
|
105
|
+
/** Allocate the next decision ID for a phase. Format: D-{encodedPhase}-{seq} */
|
|
100
106
|
function allocateId(phase: string): string {
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const encoded = encodePhaseForId(phase);
|
|
108
|
+
counters[encoded] = (counters[encoded] ?? 0) + 1;
|
|
109
|
+
return `D-${encoded}-${String(counters[encoded]).padStart(2, '0')}`;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
// ---------------------------------------------------------------------------
|
|
@@ -179,14 +186,11 @@ export function validate(input: ScribeInput): void {
|
|
|
179
186
|
if (!input.decided_by || input.decided_by.length === 0) {
|
|
180
187
|
throw new Error('decided_by is required');
|
|
181
188
|
}
|
|
182
|
-
if (!input.category) {
|
|
183
|
-
throw new Error('category is required');
|
|
184
|
-
}
|
|
185
189
|
if (!VALID_IMPACTS.includes(input.impact_level as typeof VALID_IMPACTS[number])) {
|
|
186
190
|
throw new Error(`Invalid impact_level: ${input.impact_level}. Must be one of: ${VALID_IMPACTS.join(', ')}`);
|
|
187
191
|
}
|
|
188
|
-
if (input.ref
|
|
189
|
-
throw new Error(`Invalid ref: "${input.ref}". Must match pattern ${REF_PATTERN.source}`);
|
|
192
|
+
if (!input.ref || input.ref.length === 0 || !REF_PATTERN.test(input.ref)) {
|
|
193
|
+
throw new Error(`Invalid ref: "${input.ref ?? ''}". Must match pattern ${REF_PATTERN.source}`);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const alts = input.rejected_alternatives ?? [];
|
|
@@ -221,6 +225,9 @@ export function scribeDecision(input: ScribeInput, filePath: string): DecisionRo
|
|
|
221
225
|
try {
|
|
222
226
|
validate(input);
|
|
223
227
|
|
|
228
|
+
// Auto-rehydrate counters from disk (idempotent — takes max per phase)
|
|
229
|
+
loadCounters(filePath);
|
|
230
|
+
|
|
224
231
|
// Enforce max 5 rows per phase
|
|
225
232
|
const existing = countPhaseRows(filePath, input.phase);
|
|
226
233
|
if (existing >= MAX_ROWS_PER_PHASE) {
|
|
@@ -238,7 +245,7 @@ export function scribeDecision(input: ScribeInput, filePath: string): DecisionRo
|
|
|
238
245
|
chosen_approach: input.chosen_approach,
|
|
239
246
|
rejected_alternatives: input.rejected_alternatives ?? [],
|
|
240
247
|
decided_by: input.decided_by,
|
|
241
|
-
ref: input.ref
|
|
248
|
+
ref: input.ref,
|
|
242
249
|
status: 'open',
|
|
243
250
|
};
|
|
244
251
|
|
|
@@ -248,8 +255,12 @@ export function scribeDecision(input: ScribeInput, filePath: string): DecisionRo
|
|
|
248
255
|
mkdirSync(dir, { recursive: true });
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
//
|
|
252
|
-
|
|
258
|
+
// ATOMIC: read existing + append new row + write-to-tmp + rename
|
|
259
|
+
const existingContent = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
260
|
+
const newContent = existingContent + JSON.stringify(row) + '\n';
|
|
261
|
+
const tmp = `${filePath}.tmp`;
|
|
262
|
+
writeFileSync(tmp, newContent, 'utf-8');
|
|
263
|
+
renameSync(tmp, filePath); // atomic on POSIX same-filesystem
|
|
253
264
|
|
|
254
265
|
return row;
|
|
255
266
|
} finally {
|
|
@@ -91,25 +91,42 @@ function persistLeases(): void {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Process-level async mutex for acquireWriteLease.
|
|
96
|
+
*
|
|
97
|
+
* Serializes all callers through a promise chain so that even if two async
|
|
98
|
+
* dispatch paths call acquireWriteLease concurrently, the overlap check and
|
|
99
|
+
* push are never interleaved. This prevents a TOCTOU race where both callers
|
|
100
|
+
* read the leases array before either persists, granting conflicting leases.
|
|
101
|
+
*/
|
|
102
|
+
let acquireLock: Promise<void> = Promise.resolve();
|
|
103
|
+
|
|
94
104
|
/**
|
|
95
105
|
* Acquire a write lease for the given file paths.
|
|
96
106
|
* Persists to disk atomically so the PreToolUse hook sees the lease.
|
|
107
|
+
*
|
|
108
|
+
* Serialized via an async mutex (promise chain) to prevent TOCTOU races
|
|
109
|
+
* when called from concurrent async dispatch paths.
|
|
97
110
|
*/
|
|
98
|
-
export function acquireWriteLease(taskId: string, filePaths: string[]): AcquireResult {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
export async function acquireWriteLease(taskId: string, filePaths: string[]): Promise<AcquireResult> {
|
|
112
|
+
const result = acquireLock.then(() => {
|
|
113
|
+
if (!taskId) throw new Error('task_id is required');
|
|
114
|
+
if (!filePaths.length) throw new Error('file_paths must be non-empty');
|
|
115
|
+
|
|
116
|
+
for (const existing of leases) {
|
|
117
|
+
const overlap = filePaths.filter(p => existing.paths.includes(p));
|
|
118
|
+
if (overlap.length > 0) {
|
|
119
|
+
return { granted: false, conflict: { holder: existing.holder, paths: overlap } };
|
|
120
|
+
}
|
|
106
121
|
}
|
|
107
|
-
}
|
|
108
122
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
const lease: Lease = { holder: taskId, paths: [...filePaths], acquired_at: new Date().toISOString() };
|
|
124
|
+
leases.push(lease);
|
|
125
|
+
persistLeases();
|
|
126
|
+
return { granted: true, lease };
|
|
127
|
+
});
|
|
128
|
+
acquireLock = result.then(() => {}, () => {}); // advance chain regardless of success/failure
|
|
129
|
+
return result;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
/**
|
|
@@ -31,11 +31,27 @@ export function renderSprintContext(input: SprintContextInput): SprintContextBlo
|
|
|
31
31
|
return { content, hash };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Hash only the inputs that affect rendered output (excludes buildState).
|
|
36
|
+
* Cheaper than a full renderSprintContext — no section joins or content hashing.
|
|
37
|
+
* Callers should store this hash and pass it to shouldInvalidate.
|
|
38
|
+
*/
|
|
39
|
+
export function inputHash(input: SprintContextInput): string {
|
|
40
|
+
return createHash('sha256')
|
|
41
|
+
.update(JSON.stringify({
|
|
42
|
+
architecture: input.architecture,
|
|
43
|
+
qualityTargets: input.qualityTargets,
|
|
44
|
+
refs: input.refs,
|
|
45
|
+
iosFeatures: input.iosFeatures ?? null,
|
|
46
|
+
}))
|
|
47
|
+
.digest('hex')
|
|
48
|
+
.slice(0, 16);
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/**
|
|
35
52
|
* Check if the sprint context needs re-rendering (hash invalidation).
|
|
36
|
-
*
|
|
53
|
+
* Compares input hashes directly — avoids a full render just to get a hash.
|
|
37
54
|
*/
|
|
38
|
-
export function shouldInvalidate(
|
|
39
|
-
|
|
40
|
-
return newBlock.hash !== currentHash;
|
|
55
|
+
export function shouldInvalidate(currentInputHash: string, newInput: SprintContextInput): boolean {
|
|
56
|
+
return inputHash(newInput) !== currentInputHash;
|
|
41
57
|
}
|
package/protocols/visual-dna.md
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# Visual DNA Protocol
|
|
2
|
-
|
|
3
|
-
Phase 3.0 Brand Guardian locks a 7-axis Visual DNA card that becomes the single source of truth for every downstream Phase 3 design step and every Phase 4 implementer that touches visual output. Visual Research (3.1), Visual Designer (3.2, 3.4), UX Architect (3.3), Inclusive Visuals Specialist (3.5), Frontend Developer generator (3.6), Design Critic (3.6), Accessibility Auditor (3.7), and every Phase 4 implementer via `refs.json` read this card before producing any visual artifact. It is the most load-bearing artifact in the visual pipeline — if the DNA drifts, every downstream surface drifts with it. This protocol exists to make the schema, ownership, and legal-combination rules explicit.
|
|
4
|
-
|
|
5
|
-
## 1. The 7 axes
|
|
6
|
-
|
|
7
|
-
Each axis has a closed value set. Brand Guardian picks one value per axis at Phase 3.0 and never revises it mid-build (a DNA revision is a new build session).
|
|
8
|
-
|
|
9
|
-
### Scope
|
|
10
|
-
|
|
11
|
-
**Values:** Marketing / Product / Dashboard / Internal Tool
|
|
12
|
-
|
|
13
|
-
Scope is the **gating axis**. It decides which libraries get vendored into the Phase 4 scaffold and sets the per-page bundle budget enforced by the Phase 6 SRE chapter. Three.js/WebGL libraries install only for Marketing or Product-with-Expressive-motion; dashboards and internal tools never ship them. Perf budgets: Marketing 500KB, Product 300KB, Dashboard 400KB, Internal Tool 200KB (gzipped, excluding images). Exceeding budget by >25% auto-blocks the LRR SRE chapter.
|
|
14
|
-
|
|
15
|
-
### Density
|
|
16
|
-
|
|
17
|
-
**Values:** Airy / Balanced / Dense
|
|
18
|
-
|
|
19
|
-
Density controls the spacing scale, type size ramp, whitespace rhythm, and information-per-viewport target. Airy is marketing-friendly breathing room; Dense is power-user tool territory (Linear, Superhuman, Datadog consoles). The Visual Designer at 3.4 maps this to concrete Tailwind spacing tokens and line-height ramps.
|
|
20
|
-
|
|
21
|
-
### Character
|
|
22
|
-
|
|
23
|
-
**Values:** Minimal / Editorial / Maximalist / Brutalist / Playful
|
|
24
|
-
|
|
25
|
-
Character is the overall visual personality. It drives typographic choice, color saturation, decoration rules, and the general "what kind of product is this" read within 200ms of page load. Minimal is Stripe-adjacent. Editorial is magazine-like with strong type hierarchy. Maximalist is decoration-heavy (aceternity/ui territory). Brutalist is raw and aggressive. Playful is rounded, animated, approachable.
|
|
26
|
-
|
|
27
|
-
### Material
|
|
28
|
-
|
|
29
|
-
**Values:** Flat / Glassy / Physical / Neumorphic
|
|
30
|
-
|
|
31
|
-
Material is the surface treatment across every card, modal, button, and panel. It determines blur radii, border styles, elevation system, and shadow character. Flat is modern shadcn default. Glassy is backdrop-filter blur + subtle borders (Apple / Vercel aesthetic). Physical is realistic drop shadows and depth. Neumorphic is soft inset/outset shadows (fragile — breaks contrast easily).
|
|
32
|
-
|
|
33
|
-
### Motion
|
|
34
|
-
|
|
35
|
-
**Values:** Still / Subtle / Expressive / Cinematic
|
|
36
|
-
|
|
37
|
-
Motion drives easing curves, animation durations, scroll choreography, hover feedback, and page transitions. Still is no motion. Subtle is shadcn-default hover transitions (150-200ms ease-out). Expressive is framer-motion choreography with spring physics. Cinematic is GSAP + ScrollTrigger with 500-800ms eases — the Neuform / aura.build kind of motion that requires heavy libraries and is bundle-expensive.
|
|
38
|
-
|
|
39
|
-
### Type
|
|
40
|
-
|
|
41
|
-
**Values:** Neutral Sans / Humanist Sans / Serif-forward / Display-forward / Mono-accented
|
|
42
|
-
|
|
43
|
-
Type is the font-pairing strategy. It controls primary/secondary font choice, tracking rules, and optical sizing decisions. Neutral Sans is Inter / Geist (safe default). Humanist Sans is Söhne / Söhne Breit (warmer). Serif-forward is Tiempos / GT Super (editorial feel). Display-forward is a bold display face paired with a neutral body. Mono-accented uses JetBrains Mono / IBM Plex Mono for labels and callouts inside an otherwise-sans design. Specific font pairings live in `docs/library-refs/component-library-catalog.md`, not here.
|
|
44
|
-
|
|
45
|
-
### Copy
|
|
46
|
-
|
|
47
|
-
**Values:** Functional / Narrative / Punchy / Technical
|
|
48
|
-
|
|
49
|
-
Copy is the language register across headlines, CTAs, labels, and microcopy. It controls vocabulary density, sentence rhythm, and the emotional distance between product and user. Functional is labels-only, no sales language — content-first (Notion, Linear dashboard). Narrative uses scene-setting and emotional pull — headlines paint a moment (Stripe, Loom). Punchy enforces 3-5 word headlines, one idea per sentence, newspaper economy — cut half, then cut again (Arc, Raycast). Technical uses precise terminology, spec-like voice, avoids marketing softeners — values exactness over warmth (Vercel, Railway).
|
|
50
|
-
|
|
51
|
-
## 2. Incompatibility matrix
|
|
52
|
-
|
|
53
|
-
<HARD-GATE>
|
|
54
|
-
Brand Guardian is forbidden from locking any of the combinations below. If the user's references or design doc push toward an illegal combo, Brand Guardian picks the closest legal alternative and emits a decision-log row explaining the rejection.
|
|
55
|
-
</HARD-GATE>
|
|
56
|
-
|
|
57
|
-
| # | Illegal combination | Why |
|
|
58
|
-
|---|---|---|
|
|
59
|
-
| 1 | Dashboard + Cinematic motion | Dashboards need snappy feedback (100-200ms), not 650ms cinematic eases. Users lose their place. |
|
|
60
|
-
| 2 | Internal Tool + Maximalist character | Internal tools exist for fast parse; decoration-heavy styling buries the data users came for. |
|
|
61
|
-
| 3 | Internal Tool + Expressive or Cinematic motion | Internal tools ship at <200KB budget; framer-motion choreography + GSAP break that budget. |
|
|
62
|
-
| 4 | Marketing + Dense density | Marketing pages need breathing room to sell; dense kills scroll rhythm and conversion. |
|
|
63
|
-
| 5 | Dashboard + Glassy material + Dense density | Glass blur on dense data surfaces renders unreadable — the backdrop-filter eats legibility. |
|
|
64
|
-
| 6 | Dashboard + Serif-forward type | Dashboards need high-readability UI faces at small sizes; serifs lose clarity at 12-14px. |
|
|
65
|
-
| 7 | Product + Neumorphic material (with WCAG AA target) | Neumorphic shadows depend on low-contrast surfaces; AA contrast math fails by construction. |
|
|
66
|
-
| 8 | Brutalist character + Glassy material | Brutalism is raw, unapologetic, unadorned; glass is the opposite ethos. Visual contradiction. |
|
|
67
|
-
| 9 | Playful character + Still motion | Playful without motion reads as stiff and off-brand. Playful implies at least Subtle motion. |
|
|
68
|
-
| 10 | Marketing + Still motion | Marketing pages rely on scroll reveal and choreography to guide attention; Still kills that. |
|
|
69
|
-
| 11 | Internal Tool + Display-forward type | Display faces are for hero moments, not tool chrome. Clashes with fast-parse requirement. |
|
|
70
|
-
| 12 | Dashboard + Physical material | Heavy drop shadows on data grids add visual noise that competes with the chart ink. |
|
|
71
|
-
| 13 | Playful character + Technical copy | Playful implies approachable warmth; technical copy creates cognitive dissonance — the visual feel promises friendliness, the words deliver distance. |
|
|
72
|
-
| 14 | Brutalist character + Narrative copy | Brutalism is raw, direct, unapologetic; narrative copy is storytelling and emotional pull — the aesthetic actively rejects the storytelling register. |
|
|
73
|
-
|
|
74
|
-
Anything not on this list is legal. When two legal combinations both fit the user's references, Brand Guardian reads `quality-targets.json` and the architecture stack to resolve ties (e.g., if the stack is Next.js + shadcn default and quality-targets say "fast MVP," prefer Flat over Glassy).
|
|
75
|
-
|
|
76
|
-
## 3. Schema
|
|
77
|
-
|
|
78
|
-
The DNA card lives at `docs/plans/visual-dna.md` — one file per build, locked at Phase 3.0. Shape:
|
|
79
|
-
|
|
80
|
-
```markdown
|
|
81
|
-
---
|
|
82
|
-
locked_at: 2026-04-14T01:23:45Z
|
|
83
|
-
locked_by: Brand Guardian
|
|
84
|
-
build_session: <session_id>
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
# Visual DNA
|
|
88
|
-
|
|
89
|
-
## Axes
|
|
90
|
-
|
|
91
|
-
- Scope: Marketing
|
|
92
|
-
- Density: Airy
|
|
93
|
-
- Character: Editorial
|
|
94
|
-
- Material: Glassy
|
|
95
|
-
- Motion: Expressive
|
|
96
|
-
- Type: Display-forward
|
|
97
|
-
- Copy: Punchy
|
|
98
|
-
|
|
99
|
-
## Rationale
|
|
100
|
-
|
|
101
|
-
<why these axes, with explicit references to design-doc.md sections and findings-digest signals
|
|
102
|
-
that pushed each axis to the chosen value; 4-8 sentences total, no padding>
|
|
103
|
-
|
|
104
|
-
## References that exemplify this DNA
|
|
105
|
-
|
|
106
|
-
- <url or path> — exemplifies Character + Motion
|
|
107
|
-
- <url or path> — exemplifies Material + Type
|
|
108
|
-
- <url or path> — exemplifies Scope + Density
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
<HARD-GATE>
|
|
112
|
-
SCHEMA CONTRACT:
|
|
113
|
-
|
|
114
|
-
- All seven axis fields MUST be present and MUST be one of the allowed values from Section 1.
|
|
115
|
-
- The combination across all six axes MUST NOT appear in the Section 2 incompatibility matrix.
|
|
116
|
-
- `locked_at` is set exactly once, at Phase 3.0 Brand Guardian completion, and is never rewritten.
|
|
117
|
-
- `References that exemplify this DNA` MUST contain at least two entries, each tied to specific axis pairs. "Looks good" without axis attribution is not permitted.
|
|
118
|
-
- Only `docs/plans/visual-dna.md` is the canonical location. No other path is read.
|
|
119
|
-
</HARD-GATE>
|
|
120
|
-
|
|
121
|
-
## 4. Ownership
|
|
122
|
-
|
|
123
|
-
**Writer:** Brand Guardian at Phase 3.0, exactly once per build. No other agent writes `visual-dna.md`. If the user requests a DNA revision mid-build, that is a new Phase 3.0 invocation, not an edit.
|
|
124
|
-
|
|
125
|
-
**Readers:**
|
|
126
|
-
|
|
127
|
-
| Phase / Step | Agent | Why it reads DNA |
|
|
128
|
-
|---|---|---|
|
|
129
|
-
| 3.1 | Visual Research | Frames reference-site lookup queries around the locked axes instead of casting wide. |
|
|
130
|
-
| 3.2 | Visual Designer (Component Library Mapping) | Picks component variants from `component-library-catalog.md` matching the DNA axis combination. |
|
|
131
|
-
| 3.3 | UX Architect | Information architecture and flow choices respect Density and Character. |
|
|
132
|
-
| 3.4 | Visual Designer (Visual Design Spec) | Material system, motion system, and typographic tuning all derive from Material/Motion/Type axes. |
|
|
133
|
-
| 3.5 | Inclusive Visuals Specialist | Checks DNA for a11y risks (Neumorphic contrast, Dense density tap targets, Cinematic motion reduced-motion fallback). |
|
|
134
|
-
| 3.6 | Frontend Developer generator | Every generated surface honors DNA; deviations flagged by Design Critic. |
|
|
135
|
-
| 3.6 | Design Critic | Scores rendered output against DNA on all seven axes + craft dimensions in the metric loop. |
|
|
136
|
-
| 3.7 | Accessibility Auditor | Re-verifies DNA-level a11y claims from 3.5 against real rendered output. |
|
|
137
|
-
| 4 | Phase 4 implementers | Read via `refs.json` primary anchor; every component they write must match DNA or compose from the DNA-matching library variants. |
|
|
138
|
-
| 5 | Drift check | Verifies visual output still reads as the locked DNA. |
|
|
139
|
-
| 6 | Brand Guardian chapter | Final verdict pass against DNA as ground truth. |
|
|
140
|
-
|
|
141
|
-
## 5. Legal-combo examples
|
|
142
|
-
|
|
143
|
-
**Premium marketing landing page — Stripe / Linear / Neuform aesthetic:**
|
|
144
|
-
|
|
145
|
-
- Scope: Marketing
|
|
146
|
-
- Density: Airy
|
|
147
|
-
- Character: Editorial
|
|
148
|
-
- Material: Glassy
|
|
149
|
-
- Motion: Expressive
|
|
150
|
-
- Type: Display-forward
|
|
151
|
-
- Copy: Punchy
|
|
152
|
-
|
|
153
|
-
Airy density + Editorial character gives the breathing room and strong type hierarchy. Glassy material + Display-forward type hits the premium feel. Expressive motion is the bundle budget ceiling Marketing scope can afford without tipping into Cinematic territory. Punchy copy matches the premium marketing register — short, declarative headlines that let the type and motion carry the emotional weight. All seven axes reinforce the same read.
|
|
154
|
-
|
|
155
|
-
**Internal analytics dashboard — Datadog / Grafana aesthetic:**
|
|
156
|
-
|
|
157
|
-
- Scope: Dashboard
|
|
158
|
-
- Density: Balanced
|
|
159
|
-
- Character: Minimal
|
|
160
|
-
- Material: Flat
|
|
161
|
-
- Motion: Subtle
|
|
162
|
-
- Type: Humanist Sans
|
|
163
|
-
- Copy: Functional
|
|
164
|
-
|
|
165
|
-
Minimal character + Flat material + Humanist Sans is the "tool chrome that disappears" combination. Subtle motion keeps interactions snappy without distracting from the data. Balanced density fits dashboard chart density without triggering the Glassy + Dense incompatibility. Functional copy keeps labels terse and data-first — no marketing softeners competing with the charts for attention. Ships well under the 400KB Dashboard budget.
|
|
166
|
-
|
|
167
|
-
**Consumer productivity app — Notion / Superhuman aesthetic:**
|
|
168
|
-
|
|
169
|
-
- Scope: Product
|
|
170
|
-
- Density: Balanced
|
|
171
|
-
- Character: Minimal
|
|
172
|
-
- Material: Physical
|
|
173
|
-
- Motion: Expressive
|
|
174
|
-
- Type: Neutral Sans
|
|
175
|
-
- Copy: Functional
|
|
176
|
-
|
|
177
|
-
Minimal + Neutral Sans is the "content is the hero" base. Physical material adds just enough depth to make interactive surfaces feel tactile. Expressive motion is where Superhuman-style transitions live. Functional copy keeps the interface out of the way of the user's content — the app labels actions, it doesn't narrate them. Product scope allows the motion library cost, since it's inside the 300KB budget when managed carefully.
|
|
178
|
-
|
|
179
|
-
## 6. Illegal-combo example
|
|
180
|
-
|
|
181
|
-
**Dashboard + Maximalist + Cinematic — rejected.**
|
|
182
|
-
|
|
183
|
-
Three conflicts at once. Dashboards need parse-speed, which Maximalist actively fights by burying data under decoration. Cinematic motion (650ms+ eases) is too slow for the interaction rhythm dashboards require — a user filtering a table cannot wait 800ms for every state change. And the combined library cost of aceternity-heavy maximalist surfaces + GSAP cinematic motion blows the 400KB Dashboard budget on its own before any app code is added.
|
|
184
|
-
|
|
185
|
-
When the user's references push toward this combination (e.g., "I want it to feel like [Neuform marketing page] but it's actually a dashboard"), Brand Guardian rejects the combination, writes a decision-log row naming the incompatibility, and asks the user to pick a direction: keep the scope and drop the Maximalist/Cinematic, or change the product scope to Marketing (which will change the whole app's shape, not just its look). There is no middle ground — the axes are load-bearing, not decorative.
|