baldart 4.64.0 → 4.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/bin/baldart.js +15 -0
- package/framework/.claude/agents/code-reviewer.md +25 -11
- package/framework/.claude/agents/codebase-architect.md +13 -5
- package/framework/.claude/agents/doc-reviewer.md +26 -12
- package/framework/.claude/agents/ui-expert.md +18 -10
- package/framework/.claude/skills/context-primer/SKILL.md +7 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +164 -156
- package/framework/.claude/skills/design-system-init/scripts/component-spec.template.md +26 -3
- package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +151 -0
- package/framework/.claude/skills/new/references/final-review.md +12 -2
- package/framework/.claude/skills/new/references/team-mode.md +1 -1
- package/framework/.claude/workflows/new-card-review.js +67 -13
- package/framework/.claude/workflows/new-final-review.js +63 -7
- package/framework/agents/component-manifest-schema.md +145 -0
- package/framework/agents/design-system-protocol.md +63 -23
- package/framework/agents/index.md +2 -0
- package/framework/docs/COMPONENT-MANIFEST-LAYER.md +100 -0
- package/framework/routines/ds-drift.routine.yml +19 -12
- package/framework/templates/baldart.config.template.yml +22 -0
- package/package.json +1 -1
- package/src/commands/configure.js +54 -0
- package/src/commands/doctor.js +85 -0
- package/src/commands/tokens.js +80 -0
- package/src/commands/update.js +7 -0
- package/src/utils/token-emitters/README.md +36 -0
- package/src/utils/token-emitters/css.js +18 -0
- package/src/utils/token-emitters/index.js +47 -0
- package/src/utils/token-emitters/ts.js +32 -0
- package/src/utils/tokens-generator.js +165 -0
|
@@ -61,6 +61,47 @@ async function parallelCapped(thunks, cap) {
|
|
|
61
61
|
return results
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// ---- Transient-aware spawn (SSOT parity with new2.js `agentSafe`, v4.66.0) ---
|
|
65
|
+
// A reviewer killed by a SERVER-SIDE API error (529 Overloaded, 429/5xx, rate-limit —
|
|
66
|
+
// the org-level limit is SHARED across every terminal/session) must NOT vanish from the
|
|
67
|
+
// wave review. `agent()` THROWS a transient error → retried in-workflow up to `cap`; it
|
|
68
|
+
// RETURNS null (terminal API death after the tool's own retries, or a skip) → recorded as
|
|
69
|
+
// degraded. No reliable sleep in the runtime ⇒ NOT timed backoff (absorbs brief blips only);
|
|
70
|
+
// a sustained outage exhausts the cap and surfaces as DEGRADED COVERAGE (and, for the
|
|
71
|
+
// qa-sentinel merge gate, a synthetic FAIL), NEVER a silent drop. Mirrors the transient-vs-
|
|
72
|
+
// genuine discipline in references/team-mode.md § empty-result classification.
|
|
73
|
+
const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
|
|
74
|
+
const isTransient = (e) => TRANSIENT.test(String((e && e.message) || e || ''))
|
|
75
|
+
async function agentSafe(prompt, opts, maxAttempts) {
|
|
76
|
+
const cap = maxAttempts || 3
|
|
77
|
+
let lastErr = null
|
|
78
|
+
for (let i = 0; i < cap; i++) {
|
|
79
|
+
try {
|
|
80
|
+
const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
|
|
81
|
+
return await agent(prompt, o)
|
|
82
|
+
} catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
|
|
83
|
+
}
|
|
84
|
+
const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
|
|
85
|
+
err.transientExhausted = true
|
|
86
|
+
throw err
|
|
87
|
+
}
|
|
88
|
+
const degradedReviewers = []
|
|
89
|
+
function reviewSafe(kind, card, prompt, opts) {
|
|
90
|
+
return agentSafe(prompt, opts)
|
|
91
|
+
.then((r) => {
|
|
92
|
+
if (r == null) {
|
|
93
|
+
degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: 'null-return' })
|
|
94
|
+
log(`Discovery: ${(opts && opts.label) || kind} returned null (terminal API error after tool retries / skip) — coverage degraded, NOT silently dropped.`)
|
|
95
|
+
}
|
|
96
|
+
return { kind, card, r }
|
|
97
|
+
})
|
|
98
|
+
.catch((e) => {
|
|
99
|
+
degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: e && e.transientExhausted ? 'transient-exhausted' : 'error' })
|
|
100
|
+
log(`Discovery: ${(opts && opts.label) || kind} FAILED after retries (${e && e.transientExhausted ? 'transient/529 exhausted' : 'terminal'}) — coverage degraded, NOT silently dropped.`)
|
|
101
|
+
return { kind, card, r: null }
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
// Curated toolchain (since v4.41.0): when features.has_toolchain is on, the
|
|
65
106
|
// consumer records LITERAL gate commands in toolchain.commands.* — agents run
|
|
66
107
|
// THOSE instead of guessing (e.g. `npx biome check .` not `npm run lint`). Empty
|
|
@@ -336,10 +377,10 @@ function securityPrompt(c) {
|
|
|
336
377
|
const findThunks = []
|
|
337
378
|
for (const c of cards) {
|
|
338
379
|
if (c.runSimplify !== false) {
|
|
339
|
-
findThunks.push(() =>
|
|
380
|
+
findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', schema: FINDINGS_SCHEMA }))
|
|
340
381
|
}
|
|
341
382
|
if (c.hasSecurityFiles === true) {
|
|
342
|
-
findThunks.push(() =>
|
|
383
|
+
findThunks.push(() => reviewSafe('security', c, securityPrompt(c), { label: `security:${c.cardId}`, phase: 'Discovery', agentType: 'security-reviewer', schema: FINDINGS_SCHEMA }))
|
|
343
384
|
}
|
|
344
385
|
}
|
|
345
386
|
if (codexResolved) {
|
|
@@ -348,10 +389,10 @@ if (codexResolved) {
|
|
|
348
389
|
// can't depend on the tier. The relay now returns RAW command outputs and the JS fan-in owns the
|
|
349
390
|
// success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a
|
|
350
391
|
// finding. We still REQUEST sonnet because when capacity allows it follows steps more reliably.
|
|
351
|
-
findThunks.push(() =>
|
|
392
|
+
findThunks.push(() => reviewSafe('codex', null, codexPrompt, { label: 'codex', phase: 'Discovery', model: 'sonnet', schema: CODEX_SCHEMA }))
|
|
352
393
|
}
|
|
353
394
|
if (maxQaTier === 'full') {
|
|
354
|
-
findThunks.push(() =>
|
|
395
|
+
findThunks.push(() => reviewSafe('qa', null, qaPrompt, { label: 'qa-sentinel', phase: 'Discovery', agentType: 'qa-sentinel', schema: GATES_SCHEMA }))
|
|
355
396
|
} else {
|
|
356
397
|
log('Discovery: qa-sentinel SKIPPED (wave max tier ≤ light — full suite deferred to the Final FULL gate).')
|
|
357
398
|
}
|
|
@@ -390,7 +431,16 @@ for (const item of findResults) {
|
|
|
390
431
|
log(`Discovery: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} — falling back to code-reviewer.`)
|
|
391
432
|
}
|
|
392
433
|
} else if (item.kind === 'qa') {
|
|
393
|
-
|
|
434
|
+
if (item.r && Array.isArray(item.r.gates)) {
|
|
435
|
+
gateTable = item.r.gates
|
|
436
|
+
} else {
|
|
437
|
+
// qa-sentinel DIED (null after retries — the 529 case). The full-tier mechanical gate is
|
|
438
|
+
// UNKNOWN. NEVER pass blind: inject a synthetic FAIL so summary.failingGates + the skill's
|
|
439
|
+
// gateTable-OR gate treat it as non-PASS (blocks the wave boundary). An empty [] would
|
|
440
|
+
// silently read as "no gates ran ⇒ nothing to block" — the bug.
|
|
441
|
+
gateTable = [{ gate: 'mechanical-gates (qa-sentinel)', status: 'FAIL', detail: 'qa-sentinel agent died (transient/529 exhausted or terminal API error) — full-tier mechanical gates UNKNOWN, treated as non-PASS. Re-run the review (or re-run lint/tsc/test/build manually) before merging the wave.' }]
|
|
442
|
+
log('Discovery: qa-sentinel DIED — injected a synthetic FAIL mechanical gate (wave-blocking, non-silent).')
|
|
443
|
+
}
|
|
394
444
|
} else if (item.r && Array.isArray(item.r.findings)) {
|
|
395
445
|
// simplify / security own their lane and FP-check their OWN findings → already validated.
|
|
396
446
|
raw.push(...item.r.findings.map((f) => ({ ...f, source: item.kind, preValidated: true })))
|
|
@@ -583,12 +633,15 @@ const fixPhaseTouchedCode = appliedIds.size > 0
|
|
|
583
633
|
if (maxQaTier !== 'full' && fixPhaseTouchedCode) {
|
|
584
634
|
const scopedQaPrompt = qaPrompt +
|
|
585
635
|
`\n\nDIFF-SCOPED RUN: do NOT run the whole test suite. Scope tests to the files that import the changed files above (the post-fix blast radius) — run only those, plus lint/type-check on the changed files. If the test runner cannot select a diff-scoped subset, SKIP the test gate gracefully (return status SKIP with a one-line reason) rather than running the full suite.`
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
636
|
+
let scopedQa = null
|
|
637
|
+
try { scopedQa = await agentSafe(scopedQaPrompt, { label: 'qa-sentinel (light-tier post-fix)', phase: 'Fix', agentType: 'qa-sentinel', schema: GATES_SCHEMA }) } catch (e) { scopedQa = null }
|
|
638
|
+
const scopedGates = (scopedQa && Array.isArray(scopedQa.gates)) ? scopedQa.gates
|
|
639
|
+
// qa-sentinel DIED after a code fix actually touched code → the post-fix regression gate is
|
|
640
|
+
// UNKNOWN. NEVER pass blind: a synthetic FAIL so the skill's gateTable-OR catches it.
|
|
641
|
+
: [{ gate: 'mechanical-gates (qa-sentinel, light-tier post-fix)', status: 'FAIL', detail: 'diff-scoped qa-sentinel died (transient/529 exhausted or terminal API error) after code fixes were applied — post-fix regression UNKNOWN, treated as non-PASS.' }]
|
|
642
|
+
gateTable = [...gateTable, ...scopedGates]
|
|
643
|
+
if (!(scopedQa && Array.isArray(scopedQa.gates))) degradedReviewers.push({ reviewer: 'qa-sentinel (light-tier post-fix)', reason: 'died-synthetic-fail' })
|
|
644
|
+
log(`Fix: light-tier diff-scoped qa-sentinel ${scopedQa && Array.isArray(scopedQa.gates) ? 'ran' : 'DIED (synthetic FAIL injected)'} after applying code fixes — ${scopedGates.filter((g) => g.status === 'FAIL').length} failing gate(s) merged into gateTable.`)
|
|
592
645
|
}
|
|
593
646
|
|
|
594
647
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -681,8 +734,9 @@ const summary = makeSummary({
|
|
|
681
734
|
failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate), // qa-sentinel FAIL gates (test/build/audit/markdownlint)
|
|
682
735
|
blockers: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
|
|
683
736
|
highs: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
|
|
737
|
+
degradedReviewers, // reviewers that died after retries (transient/529 or terminal) — coverage was incomplete; NEVER a silent drop.
|
|
684
738
|
})
|
|
685
|
-
log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.docApplied} doc-applied, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
|
|
739
|
+
log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.docApplied} doc-applied, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${degradedReviewers.length ? `, ${degradedReviewers.length} DEGRADED reviewer(s): ${degradedReviewers.map((d) => d.reviewer).join(', ')}` : ''}${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
|
|
686
740
|
|
|
687
741
|
return { codexEngine, perCard, gateTable, summary }
|
|
688
742
|
|
|
@@ -690,7 +744,7 @@ return { codexEngine, perCard, gateTable, summary }
|
|
|
690
744
|
function asArr(x) { return Array.isArray(x) ? x.filter(Boolean) : [] }
|
|
691
745
|
function dedupe(xs) { return Array.from(new Set(asArr(xs))) }
|
|
692
746
|
function makeSummary(o) {
|
|
693
|
-
return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
|
|
747
|
+
return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0, degradedReviewers: [] }, o || {})
|
|
694
748
|
}
|
|
695
749
|
function slimFinding(f) {
|
|
696
750
|
return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification, card: f.card }
|
|
@@ -72,6 +72,51 @@ async function parallelCapped(thunks, cap) {
|
|
|
72
72
|
return results
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// ---- Transient-aware spawn (SSOT parity with new2.js `agentSafe`, v4.66.0) ---
|
|
76
|
+
// A reviewer killed by a SERVER-SIDE API error (529 Overloaded, 429/5xx, rate-limit —
|
|
77
|
+
// the org-level limit is SHARED across every terminal/session) must NOT vanish from the
|
|
78
|
+
// batch review. Two failure shapes exist and BOTH are handled: (1) `agent()` THROWS a
|
|
79
|
+
// transient error → retried in-workflow up to `cap`; (2) `agent()` RETURNS null (terminal
|
|
80
|
+
// API death after the tool's own retries, or a user skip) → recorded as degraded. There is
|
|
81
|
+
// NO reliable sleep/jitter in the workflow runtime, so this is NOT timed backoff — it
|
|
82
|
+
// absorbs brief blips; a sustained outage exhausts the cap and surfaces as DEGRADED COVERAGE
|
|
83
|
+
// (and, for the qa-sentinel merge gate, a synthetic FAIL), NEVER a silent drop. Mirrors the
|
|
84
|
+
// transient-vs-genuine discipline in references/team-mode.md § empty-result classification.
|
|
85
|
+
const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
|
|
86
|
+
const isTransient = (e) => TRANSIENT.test(String((e && e.message) || e || ''))
|
|
87
|
+
async function agentSafe(prompt, opts, maxAttempts) {
|
|
88
|
+
const cap = maxAttempts || 3
|
|
89
|
+
let lastErr = null
|
|
90
|
+
for (let i = 0; i < cap; i++) {
|
|
91
|
+
try {
|
|
92
|
+
const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
|
|
93
|
+
return await agent(prompt, o)
|
|
94
|
+
} catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
|
|
95
|
+
}
|
|
96
|
+
const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
|
|
97
|
+
err.transientExhausted = true
|
|
98
|
+
throw err
|
|
99
|
+
}
|
|
100
|
+
// Degraded-coverage ledger — a reviewer that died (transient-exhausted OR null terminal
|
|
101
|
+
// return) is RECORDED here and surfaced in the return summary, so the skill's F.5 knows the
|
|
102
|
+
// batch review ran with incomplete coverage instead of mistaking it for "clean".
|
|
103
|
+
const degradedReviewers = []
|
|
104
|
+
function reviewSafe(kind, prompt, opts) {
|
|
105
|
+
return agentSafe(prompt, opts)
|
|
106
|
+
.then((r) => {
|
|
107
|
+
if (r == null) {
|
|
108
|
+
degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: 'null-return' })
|
|
109
|
+
log(`Review: ${(opts && opts.label) || kind} returned null (terminal API error after tool retries / skip) — coverage degraded, NOT silently dropped.`)
|
|
110
|
+
}
|
|
111
|
+
return { kind, r }
|
|
112
|
+
})
|
|
113
|
+
.catch((e) => {
|
|
114
|
+
degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: e && e.transientExhausted ? 'transient-exhausted' : 'error' })
|
|
115
|
+
log(`Review: ${(opts && opts.label) || kind} FAILED after retries (${e && e.transientExhausted ? 'transient/529 exhausted' : 'terminal'}) — coverage degraded, NOT silently dropped.`)
|
|
116
|
+
return { kind, r: null }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
75
120
|
if (!scope.length) {
|
|
76
121
|
log('new-final-review: empty review scope — nothing to review.')
|
|
77
122
|
return { codexEngine: 'none', findings: [], gateTable: [], summary: emptySummary() }
|
|
@@ -325,10 +370,10 @@ const slimDoc = a.slimDoc !== undefined ? a.slimDoc === true : a.singleCard ===
|
|
|
325
370
|
const slimApi = a.slimApi !== undefined ? a.slimApi === true : a.singleCard === true
|
|
326
371
|
// qa-sentinel always runs — the merge integrity gate reads its PASS/FAIL table.
|
|
327
372
|
const reviewThunks = [
|
|
328
|
-
() =>
|
|
373
|
+
() => reviewSafe('qa', qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }),
|
|
329
374
|
]
|
|
330
375
|
if (!slimDoc) {
|
|
331
|
-
reviewThunks.unshift(() =>
|
|
376
|
+
reviewThunks.unshift(() => reviewSafe('doc', docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }))
|
|
332
377
|
} else {
|
|
333
378
|
log('Review: single-card batch — final doc-reviewer skipped (already ran per-card); kept cross-model Codex + qa gates.')
|
|
334
379
|
}
|
|
@@ -338,11 +383,11 @@ if (codexResolved) {
|
|
|
338
383
|
// so correctness can't depend on the tier. The relay returns RAW outputs and the JS fan-in owns the
|
|
339
384
|
// success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a finding.
|
|
340
385
|
// We still REQUEST sonnet: this is the last cross-model gate before merge and a dropped find is final.
|
|
341
|
-
reviewThunks.unshift(() =>
|
|
386
|
+
reviewThunks.unshift(() => reviewSafe('codex', codexPrompt, { label: 'codex', phase: 'Review', model: 'sonnet', schema: CODEX_SCHEMA }))
|
|
342
387
|
}
|
|
343
388
|
// api-perf-cost-auditor: skipped when no API/data files OR when it already ran per-card (slimApi).
|
|
344
389
|
if (!slimApi && a.hasApiDataFiles !== false) {
|
|
345
|
-
reviewThunks.push(() =>
|
|
390
|
+
reviewThunks.push(() => reviewSafe('api', apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }))
|
|
346
391
|
} else {
|
|
347
392
|
log(slimApi
|
|
348
393
|
? 'Review: single-card batch — final api-perf-cost-auditor skipped (already ran per-card).'
|
|
@@ -383,7 +428,17 @@ for (const item of reviewResults) {
|
|
|
383
428
|
log(`Review: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} — falling back to code-reviewer.`)
|
|
384
429
|
}
|
|
385
430
|
} else if (item.kind === 'qa') {
|
|
386
|
-
|
|
431
|
+
if (item.r && Array.isArray(item.r.gates)) {
|
|
432
|
+
gateTable = item.r.gates
|
|
433
|
+
} else {
|
|
434
|
+
// qa-sentinel DIED (null after retries — the screenshotted 529 case). The mechanical
|
|
435
|
+
// merge gate (lint/tsc/test/build/audit/i18n) is UNKNOWN. NEVER pass blind: inject a
|
|
436
|
+
// synthetic FAIL so summary.failingGates + the skill's F.5 gate treat it as non-PASS
|
|
437
|
+
// (merge-blocking), and the O5(b) build-claim demotion below is suppressed (buildGateFail).
|
|
438
|
+
// An empty [] here would silently read as "no gates ran ⇒ nothing to block" — the bug.
|
|
439
|
+
gateTable = [{ gate: 'mechanical-gates (qa-sentinel)', status: 'FAIL', detail: 'qa-sentinel agent died (transient/529 exhausted or terminal API error) — mechanical gates UNKNOWN, treated as non-PASS to block the merge. Re-run the final review (or re-run lint/tsc/test/build manually) before merging.' }]
|
|
440
|
+
log('Final review: qa-sentinel DIED — injected a synthetic FAIL mechanical gate (merge-blocking, non-silent).')
|
|
441
|
+
}
|
|
387
442
|
} else if (item.r && Array.isArray(item.r.findings)) {
|
|
388
443
|
// doc-reviewer / api-perf-cost-auditor own their domain and FP-check their OWN findings in
|
|
389
444
|
// the finding pass (their prompts mandate it) → already validated, no cross-domain re-judge.
|
|
@@ -526,8 +581,9 @@ const summary = {
|
|
|
526
581
|
blockers: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
|
|
527
582
|
highs: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
|
|
528
583
|
failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate),
|
|
584
|
+
degradedReviewers, // reviewers that died after retries (transient/529 or terminal) — coverage was incomplete; NEVER a silent drop.
|
|
529
585
|
}
|
|
530
|
-
log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
|
|
586
|
+
log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${degradedReviewers.length ? `, ${degradedReviewers.length} DEGRADED reviewer(s): ${degradedReviewers.map((d) => d.reviewer).join(', ')}` : ''}. Engine: ${codexEngine}.`)
|
|
531
587
|
|
|
532
588
|
// ───────────────────────────────────────────────────────────────────────────
|
|
533
589
|
// Phase Fix (P0-A, v4.59.0) — OPT-IN. Mirrors new-card-review.js's Fix+Doc phases EXACTLY:
|
|
@@ -670,7 +726,7 @@ const base = { codexEngine, findings, noActionFindings, gateTable, summary }
|
|
|
670
726
|
return applyFixes ? { ...base, fixesApplied, docFixesApplied, residual } : base
|
|
671
727
|
|
|
672
728
|
function emptySummary() {
|
|
673
|
-
return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
|
|
729
|
+
return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [], degradedReviewers: [] }
|
|
674
730
|
}
|
|
675
731
|
function slimFinding(f) {
|
|
676
732
|
return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification }
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Component Manifest Schema
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Define the **machine-readable HEAD** every per-component spec
|
|
6
|
+
(`${paths.design_system}/components/<Name>.md`) carries so agents discover and
|
|
7
|
+
reuse primitives by reading a small, structured frontmatter **on demand** — not
|
|
8
|
+
by parsing a monolithic `INDEX.md` or the component source. This module is the
|
|
9
|
+
**single source of truth** for *which fields a component spec needs* and *in what
|
|
10
|
+
state*; every writer and every consumer cites it.
|
|
11
|
+
|
|
12
|
+
It is the design-system analogue of `agents/card-schema.md`: a schema that ≥6
|
|
13
|
+
surfaces produce or consume (`design-system-init`, `doc-reviewer`, `ui-expert`,
|
|
14
|
+
`code-reviewer`, `codebase-architect`, `context-primer`, `ds-drift`) and must
|
|
15
|
+
therefore live in one place that none of them re-describe.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
**In**: the frontmatter field table, the deterministic-vs-agentic split, the
|
|
20
|
+
regeneration contract, the on-demand read contract, the thin-INDEX router shape,
|
|
21
|
+
and the transition-leniency rule.
|
|
22
|
+
|
|
23
|
+
**Out**: the *enforcement* (when a missing/stale spec blocks) and the drift codes
|
|
24
|
+
(`DS_INDEX_DRIFT` / `DS_COMPONENT_STALE` / `DS_TOKENS_DRIFT`) — those stay in
|
|
25
|
+
`agents/design-system-protocol.md` (§ Post-Intervention Coherence Check). This
|
|
26
|
+
module defines the *fields those codes are checked against*; it **never copies**
|
|
27
|
+
the codes or the gate (that would re-introduce the drift the SSOT split prevents).
|
|
28
|
+
Token *values* and the DTCG source live in the token layer
|
|
29
|
+
(`${paths.design_tokens}` → `.tokens.json`); this module references token **IDs**,
|
|
30
|
+
never values.
|
|
31
|
+
|
|
32
|
+
## Gating
|
|
33
|
+
|
|
34
|
+
Same as the design-system protocol: meaningful only when
|
|
35
|
+
`features.has_design_system: true`. There is **no separate feature flag** — the
|
|
36
|
+
manifest rides on the design-system layer. When the flag is `false`, component
|
|
37
|
+
specs (and this schema) do not apply.
|
|
38
|
+
|
|
39
|
+
## The frontmatter HEAD
|
|
40
|
+
|
|
41
|
+
Additive: a YAML frontmatter block is prepended to the EXISTING prose spec. The
|
|
42
|
+
prose body (Purpose, Props table, Accessibility, Anti-patterns, …) is preserved
|
|
43
|
+
verbatim — the HEAD makes the *mechanical* half machine-addressable; the prose
|
|
44
|
+
stays the human-readable long form.
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
---
|
|
48
|
+
# === DETERMINISTIC — extracted from source (TS types) + git; regenerable, never hand-curated ===
|
|
49
|
+
name: Button # exported component name
|
|
50
|
+
source: components/ui/Button.tsx # path relative to repo root
|
|
51
|
+
source_sha: a1b2c3d # git blob sha of source at last generation (drift anchor)
|
|
52
|
+
status: stable # stable | experimental | deprecated (default stable; `deprecated` is a hand override)
|
|
53
|
+
category: primitive # primitive | composite | feature (heuristic on import fan-in; hand-overridable)
|
|
54
|
+
props: # from the TS prop type / interface
|
|
55
|
+
variant: { type: "'primary'|'ghost'|'destructive'", required: false, default: "'primary'" }
|
|
56
|
+
size: { type: "'sm'|'md'|'lg'", required: false, default: "'md'" }
|
|
57
|
+
disabled: { type: boolean, required: false, default: false }
|
|
58
|
+
variants: [primary, ghost, destructive] # enumerated literal union of the `variant` prop
|
|
59
|
+
composes: [Slot, Spinner] # in-registry primitives imported by this component
|
|
60
|
+
# === AGENTIC — curated by doc-reviewer; survives regeneration, never clobbered when non-empty ===
|
|
61
|
+
purpose: "Primary action trigger; one primary per surface."
|
|
62
|
+
token_bindings: [color.action.primary, radius.md, space.2, motion.fast] # DTCG token IDs (not values)
|
|
63
|
+
a11y: "Icon-only requires aria-label; ≥44px target; respects prefers-reduced-motion."
|
|
64
|
+
related: [IconButton, Link] # sibling primitives (not composition)
|
|
65
|
+
must_rules: ["MUST consume color.action.primary", "MUST set aria-label when icon-only"]
|
|
66
|
+
last_verified: 2026-06-23 # set by doc-reviewer / ds-drift on semantic re-verify
|
|
67
|
+
owner: doc-reviewer # curation owner (constant; documents the SSOT)
|
|
68
|
+
---
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Field origin
|
|
72
|
+
|
|
73
|
+
| Origin | Fields | Rule |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
|
|
76
|
+
| **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `last_verified`; plus `status: deprecated` as a judgment | **Never clobbered** when non-empty. Empty agentic fields = "not yet enriched", filled later by `doc-reviewer` / `ds-drift`, never auto-invented. |
|
|
77
|
+
|
|
78
|
+
## Read contract (the context win)
|
|
79
|
+
|
|
80
|
+
1. An agent doing reuse-discovery reads the thin `INDEX.md` (router) → then **only
|
|
81
|
+
the frontmatter** of the N specs in scope (front-load the first `---…---`
|
|
82
|
+
block), NOT the prose body and NOT the source.
|
|
83
|
+
2. The prose body is read only when the agent is about to **modify** that
|
|
84
|
+
component.
|
|
85
|
+
3. `token_bindings` are IDs into `.tokens.json`, so one already-read tokens file
|
|
86
|
+
answers every component's bindings — no per-component token re-read, and the
|
|
87
|
+
binding stays stable when a token *value* changes.
|
|
88
|
+
|
|
89
|
+
This is the whole point: replace "read a 33KB monolith + fall to source" with
|
|
90
|
+
"read a small router + N small frontmatters".
|
|
91
|
+
|
|
92
|
+
## Thin INDEX router
|
|
93
|
+
|
|
94
|
+
`${paths.design_system}/INDEX.md` is **generated** and holds only routing data —
|
|
95
|
+
one row per primitive: `name | source | one-line purpose | spec link`. It carries
|
|
96
|
+
NO human-curated judgment (the Authority Matrix moved to
|
|
97
|
+
`design-system-protocol.md`; per-component detail lives in each spec). Because it
|
|
98
|
+
is generated, it is always safe to regenerate from the specs — never hand-edit it.
|
|
99
|
+
|
|
100
|
+
## Regeneration contract
|
|
101
|
+
|
|
102
|
+
- **Anchor**: `source_sha`. A regeneration run skips a component whose source is
|
|
103
|
+
unchanged AND whose spec already has a HEAD (no-op); regenerates only
|
|
104
|
+
stale/missing ones.
|
|
105
|
+
- **Deterministic fields**: overwritten from current source every run.
|
|
106
|
+
- **Agentic fields**: preserved unless empty; an empty field is a candidate for
|
|
107
|
+
enrichment, never a reason to clobber a filled one.
|
|
108
|
+
- **Idempotent + resumable**: a kill mid-run leaves a mix of HEAD-bearing and
|
|
109
|
+
prose-only specs — all valid (see leniency below). No half-written state.
|
|
110
|
+
|
|
111
|
+
## Transition leniency (do not raise the floor)
|
|
112
|
+
|
|
113
|
+
The HEAD is an **upgrade target, not a new floor**. During and after adoption:
|
|
114
|
+
|
|
115
|
+
- A **prose-only spec** (no HEAD) is still a valid spec — it does NOT trip
|
|
116
|
+
`DS_COMPONENT_STALE` merely for lacking a HEAD.
|
|
117
|
+
- **Empty deterministic fields** (e.g. a JS/Vue/Svelte consumer with no TS types,
|
|
118
|
+
or an unresolved prop) are **advisory** — "not yet enriched", never a blocking
|
|
119
|
+
finding.
|
|
120
|
+
- A **missing** spec for a new primitive remains exactly the `DS_INDEX_DRIFT` it
|
|
121
|
+
is today — the manifest adds no new failure mode, it only makes the existing
|
|
122
|
+
spec richer where the data is available.
|
|
123
|
+
|
|
124
|
+
This keeps consumers that have not yet upgraded (and non-TS stacks) from becoming
|
|
125
|
+
*more* broken than before the manifest existed.
|
|
126
|
+
|
|
127
|
+
## Producers & consumers
|
|
128
|
+
|
|
129
|
+
- **Bootstrap / upgrade**: `/design-system-init` (deterministic extract + agentic
|
|
130
|
+
enrich; greenfield and upgrade modes).
|
|
131
|
+
- **Per-task write**: `ui-expert` / `ui-design` / `frontend-design` in the
|
|
132
|
+
Post-Intervention Coherence Check — a new/modified primitive regenerates its
|
|
133
|
+
HEAD in the same change.
|
|
134
|
+
- **Curation owner**: `doc-reviewer` (agentic fields).
|
|
135
|
+
- **Per-merge gate**: `code-reviewer` (reads HEAD-first; enforces the drift codes
|
|
136
|
+
per the protocol).
|
|
137
|
+
- **Read-first discovery**: `codebase-architect`, `context-primer`.
|
|
138
|
+
- **Weekly net**: `ds-drift` (compares `source_sha`/HEAD against source).
|
|
139
|
+
|
|
140
|
+
## See also
|
|
141
|
+
|
|
142
|
+
- `agents/design-system-protocol.md` — the enforcement gate + drift codes + read
|
|
143
|
+
cascade this schema feeds.
|
|
144
|
+
- `agents/card-schema.md` — the sibling schema module this one mirrors.
|
|
145
|
+
- `agents/project-context.md` — `baldart.config.yml` + overlay resolution.
|
|
@@ -47,13 +47,21 @@ To bootstrap a registry on a project that does not yet have one, invoke the
|
|
|
47
47
|
|
|
48
48
|
## Canonical sources (resolve via `baldart.config.yml`)
|
|
49
49
|
|
|
50
|
-
- `${paths.design_system}/INDEX.md` —
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
- `${paths.design_system}/INDEX.md` — **generated** thin router: one row per
|
|
51
|
+
primitive (`name | source | one-line purpose | spec link`). Entry point for
|
|
52
|
+
every UI task. Human-curated judgement does NOT live here (it moved to the
|
|
53
|
+
Authority Matrix below + each spec's frontmatter) — never hand-edit the INDEX.
|
|
54
|
+
- `${paths.design_tokens}` → `.tokens.json` — **the token SSOT**, W3C DTCG
|
|
55
|
+
format (`$type`/`$value`). The stack-native artifact (`tokens.ts` / CSS custom
|
|
56
|
+
properties) is **generated** from it via `baldart tokens build`; editing that
|
|
57
|
+
generated output by hand is drift (see Token Contract).
|
|
58
|
+
- `${paths.design_system}/tokens-reference.md` — **generated/narrative** human
|
|
59
|
+
view of the tokens. NOT a third source of values: either generated from
|
|
60
|
+
`.tokens.json` or pure narrative ("how to use tokens"), never hand-authored
|
|
61
|
+
values that duplicate the DTCG source.
|
|
62
|
+
- `${paths.design_system}/components/<Name>.md` — per-component spec. Carries a
|
|
63
|
+
machine-readable frontmatter HEAD (see `agents/component-manifest-schema.md`)
|
|
64
|
+
+ prose body (props, variants, accessibility, anti-patterns). One per primitive.
|
|
57
65
|
- `${paths.design_system}/patterns/*.md` — opt-in pattern docs (theming,
|
|
58
66
|
overlays, motion choreography, multi-tenant pairing rules).
|
|
59
67
|
- `${paths.ui_guidelines}` — brand voice, typography, philosophy. Always read.
|
|
@@ -64,9 +72,12 @@ To bootstrap a registry on a project that does not yet have one, invoke the
|
|
|
64
72
|
|
|
65
73
|
Before producing any new UI code, mockup, or design review:
|
|
66
74
|
|
|
67
|
-
1. **BLOCKING** — read `${paths.design_system}/INDEX.md
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
1. **BLOCKING** — read `${paths.design_system}/INDEX.md` (the thin router) to
|
|
76
|
+
locate the primitives in scope, then read **only the frontmatter HEAD** of
|
|
77
|
+
each in-scope `components/<Name>.md` (per `agents/component-manifest-schema.md`)
|
|
78
|
+
— not the prose body, not the source. Read the prose body only when you are
|
|
79
|
+
about to modify that primitive. Also read the Authority Matrix (below) + any
|
|
80
|
+
pattern docs the router points to.
|
|
70
81
|
2. **BLOCKING** — read `${paths.ui_guidelines}` for visual language, typography,
|
|
71
82
|
accessibility, and brand voice. The `identity.design_philosophy` (when set)
|
|
72
83
|
is the lens through which these guidelines apply.
|
|
@@ -120,10 +131,26 @@ any code or mockup produced under this protocol:
|
|
|
120
131
|
the `motion-design` skill).
|
|
121
132
|
|
|
122
133
|
The exact token consumption mechanism (CSS custom properties, Tailwind theme
|
|
123
|
-
extend, theme provider) is project-specific — defer to
|
|
124
|
-
`${paths.design_system}/tokens-reference.md` and the project's chosen
|
|
134
|
+
extend, theme provider) is project-specific — defer to the project's chosen
|
|
125
135
|
mechanism.
|
|
126
136
|
|
|
137
|
+
**Token SSOT inversion (when `paths.design_tokens` is set).** The canonical
|
|
138
|
+
token source is the DTCG `.tokens.json` at `${paths.design_tokens}`. The
|
|
139
|
+
stack-native artifact agents/components consume (`tokens.ts`, a CSS custom-property
|
|
140
|
+
sheet, …) is **generated** from it by `baldart tokens build` and carries a
|
|
141
|
+
`// baldart-generated from .tokens.json — do not edit` marker. Consequences:
|
|
142
|
+
|
|
143
|
+
- To add/change a token, edit `.tokens.json` and regenerate — never hand-edit the
|
|
144
|
+
generated output.
|
|
145
|
+
- `DS_TOKENS_DRIFT` is now mechanically checkable: the generated output must equal
|
|
146
|
+
`baldart tokens build` from the current `.tokens.json`. A hand-edit to the
|
|
147
|
+
generated file (output ≠ rebuild) is the violation — port it to `.tokens.json`.
|
|
148
|
+
- A `token_bindings` ID in a component HEAD must resolve to a real node in
|
|
149
|
+
`.tokens.json`; a dangling ID is `DS_TOKENS_DRIFT`.
|
|
150
|
+
|
|
151
|
+
When `paths.design_tokens` is unset, the project has not adopted the DTCG SSOT —
|
|
152
|
+
fall back to the project's existing token file as the source (no inversion).
|
|
153
|
+
|
|
127
154
|
## Reference Tables (embed)
|
|
128
155
|
|
|
129
156
|
These tables are the SSOT for the *numeric ranges* every agent/skill should
|
|
@@ -306,13 +333,16 @@ visually uneven steps because HSL lightness is not perceptual.
|
|
|
306
333
|
|
|
307
334
|
## Authority Matrix
|
|
308
335
|
|
|
309
|
-
|
|
310
|
-
|
|
336
|
+
This matrix is **canonical here** (protocol-level), not in `INDEX.md` — the INDEX
|
|
337
|
+
is a generated router and cannot hold curated judgement. For each primitive, it
|
|
338
|
+
declares who is authoritative over which dimension:
|
|
311
339
|
|
|
312
|
-
- **Visual authority** — colors, spacing, shadows, radii: the
|
|
313
|
-
tokens
|
|
340
|
+
- **Visual authority** — colors, spacing, shadows, radii, motion: the DTCG token
|
|
341
|
+
SSOT (`${paths.design_tokens}` → `.tokens.json`, surfaced via the generated
|
|
342
|
+
token reference). Agents/skills may not deviate.
|
|
314
343
|
- **Behavioral authority** — props, variants, accessibility hooks: the
|
|
315
|
-
per-component spec
|
|
344
|
+
per-component spec (its frontmatter HEAD for the machine-readable contract, its
|
|
345
|
+
prose body for rationale).
|
|
316
346
|
- **Composition authority** — how primitives are combined into patterns: the
|
|
317
347
|
pattern docs.
|
|
318
348
|
|
|
@@ -441,12 +471,19 @@ For each item in the diff that touched a visual surface, run these 4 checks:
|
|
|
441
471
|
in the same change. Missing spec = `DS_INDEX_DRIFT` finding, task blocked.
|
|
442
472
|
2. **Modified primitive ⇒ updated spec**: did the change alter a primitive's
|
|
443
473
|
props / variants / accessibility behavior? If yes, the corresponding
|
|
444
|
-
`components/<Name>.md` MUST be updated to match
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
474
|
+
`components/<Name>.md` MUST be updated to match — including its **frontmatter
|
|
475
|
+
HEAD** (deterministic fields `props`/`variants`/`composes`/`source_sha`
|
|
476
|
+
regenerated from source; agentic fields re-verified). Stale spec =
|
|
477
|
+
`DS_COMPONENT_STALE` finding, task blocked. (A prose-only spec lacking a HEAD
|
|
478
|
+
is NOT stale merely for that — see `agents/component-manifest-schema.md`
|
|
479
|
+
§ Transition leniency.)
|
|
480
|
+
3. **New / changed token ⇒ updated SSOT**: did the change introduce or modify a
|
|
481
|
+
token (color, spacing, shadow, radius, motion)? If `paths.design_tokens` is
|
|
482
|
+
set, the change MUST be made in `.tokens.json` and the stack-native output
|
|
483
|
+
regenerated via `baldart tokens build` (a hand-edit to the generated output is
|
|
484
|
+
the drift). Otherwise update the project's token source + reference. Either
|
|
485
|
+
way, a dangling `token_bindings` ID is drift. Stale = `DS_TOKENS_DRIFT`,
|
|
486
|
+
task blocked.
|
|
450
487
|
4. **Surfaced silent primitive ⇒ added to INDEX**: during the work, did you
|
|
451
488
|
reuse a primitive that exists in the source tree but is **not** in
|
|
452
489
|
`INDEX.md`? If yes, add it to the INDEX in the same change (or open a
|
|
@@ -529,6 +566,9 @@ When changing the BLOCKING cascade, change it here — not in the consumers.
|
|
|
529
566
|
|
|
530
567
|
## See also
|
|
531
568
|
|
|
569
|
+
- `framework/agents/component-manifest-schema.md` — the machine-readable
|
|
570
|
+
frontmatter HEAD on each `components/<Name>.md`, the on-demand read contract,
|
|
571
|
+
and the regeneration + transition-leniency rules this gate is checked against.
|
|
532
572
|
- `framework/agents/project-context.md` — always-ask / never-assume contract
|
|
533
573
|
and the `baldart.config.yml` resolution flow.
|
|
534
574
|
- `framework/agents/design-review.md` — design-review workflow and report
|
|
@@ -20,6 +20,7 @@ Route agents to the right module with minimal reading.
|
|
|
20
20
|
- If touching database schema or fields -> read `agents/data-model.md` and `${paths.references_dir}/data-model.md`.
|
|
21
21
|
- If touching UI pages/routes or flows -> read `${paths.references_dir}/ui/index.md` (then specific UI module for your domain).
|
|
22
22
|
- If touching ANY visual surface (component, page, mockup, design review) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` and treat the BLOCKING cascade (`${paths.design_system}/INDEX.md` + `tokens-reference.md` + `components/<Name>.md`) as mandatory pre-work. When the flag is `false`, recommend `/design-system-init` to scaffold a registry.
|
|
23
|
+
- If CREATING, UPDATING, or READING a per-component spec (`${paths.design_system}/components/<Name>.md`) -> read `agents/component-manifest-schema.md` for the machine-readable frontmatter HEAD (deterministic-from-TS + agentic fields) and the on-demand read contract (INDEX router → frontmatter-first, prose only when modifying). The drift codes + gate stay in `design-system-protocol.md`.
|
|
23
24
|
- If touching design-review workflows or UI guidelines -> read `agents/design-review.md` and project-specific UI guidelines.
|
|
24
25
|
- If touching architecture, auth, or tech stack -> read `agents/architecture.md`.
|
|
25
26
|
- If touching workflow/process/commits/backlog -> read `agents/workflows.md`.
|
|
@@ -72,6 +73,7 @@ When adding or updating agents, update REGISTRY.md — not this file.
|
|
|
72
73
|
- `agents/i18n-protocol.md` — Internationalization discipline: no hardcoded user-facing strings, context registry (`paths.i18n_registry`) + ICU + context-aware translation; BLOCKING anti-hardcoded lint gate (since v4.52.0, gated on `features.has_i18n`)
|
|
73
74
|
- `agents/design-system-protocol.md` — Registry-first discipline for UI work: BLOCKING cascade on `INDEX.md` + `tokens-reference.md` + `components/<Name>.md` (since v3.11.0, gated on `features.has_design_system`)
|
|
74
75
|
- `agents/card-schema.md` — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field contract every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract (since v4.35.0)
|
|
76
|
+
- `agents/component-manifest-schema.md` — Component Manifest Schema: the machine-readable frontmatter HEAD on each `components/<Name>.md` (deterministic-from-TS + agentic fields), on-demand read contract, regeneration + transition-leniency rules (since v4.65.0, gated on `features.has_design_system`)
|
|
75
77
|
- `agents/return-contract-protocol.md` — Subagent return-message economy: COMPACT (bounded headline + `path:line` findings + disk pointer) vs FULL, persist-then-summarize — the input-side twin of `effort-protocol.md` (since v4.59.0)
|
|
76
78
|
|
|
77
79
|
## Where to Document (Decision Tree)
|