@tekyzinc/gsd-t 3.23.11 → 3.25.10

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 (74) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +7 -0
  3. package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
  4. package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
  5. package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
  6. package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
  7. package/bin/cli-preflight-checks/ports-free.cjs +110 -0
  8. package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
  9. package/bin/cli-preflight.cjs +265 -0
  10. package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
  11. package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
  12. package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
  13. package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
  14. package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
  15. package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
  16. package/bin/gsd-t-context-brief.cjs +395 -0
  17. package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
  18. package/bin/gsd-t-ratelimit-probe.cjs +648 -0
  19. package/bin/gsd-t-verify-gate-judge.cjs +224 -0
  20. package/bin/gsd-t-verify-gate.cjs +612 -0
  21. package/bin/gsd-t.js +45 -1
  22. package/bin/live-activity-report.cjs +615 -0
  23. package/bin/m55-substrate-proof.cjs +134 -0
  24. package/bin/parallel-cli-tee.cjs +206 -0
  25. package/bin/parallel-cli.cjs +478 -0
  26. package/commands/gsd-t-execute.md +31 -0
  27. package/commands/gsd-t-help.md +21 -0
  28. package/commands/gsd-t-verify.md +38 -0
  29. package/docs/architecture.md +194 -0
  30. package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
  31. package/docs/diagrams/.gsd-t/context-meter.log +9 -0
  32. package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
  33. package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
  34. package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
  35. package/docs/diagrams/01-top-level-map-d2.png +0 -0
  36. package/docs/diagrams/01-top-level-map.d2 +77 -0
  37. package/docs/diagrams/01-top-level-map.mmd +48 -0
  38. package/docs/diagrams/01-top-level-map.png +0 -0
  39. package/docs/diagrams/01-top-level-map.svg +126 -0
  40. package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
  41. package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
  42. package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
  43. package/docs/diagrams/02-milestone-lifecycle.png +0 -0
  44. package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
  45. package/docs/diagrams/03-wave-mode-d2.png +0 -0
  46. package/docs/diagrams/03-wave-mode.d2 +33 -0
  47. package/docs/diagrams/03-wave-mode.mmd +21 -0
  48. package/docs/diagrams/03-wave-mode.png +0 -0
  49. package/docs/diagrams/03-wave-mode.svg +113 -0
  50. package/docs/diagrams/04-design-to-code-d2.png +0 -0
  51. package/docs/diagrams/04-design-to-code.d2 +35 -0
  52. package/docs/diagrams/04-design-to-code.mmd +29 -0
  53. package/docs/diagrams/04-design-to-code.png +0 -0
  54. package/docs/diagrams/04-design-to-code.svg +115 -0
  55. package/docs/diagrams/05-backlog-d2.png +0 -0
  56. package/docs/diagrams/05-backlog.d2 +40 -0
  57. package/docs/diagrams/05-backlog.mmd +20 -0
  58. package/docs/diagrams/05-backlog.png +0 -0
  59. package/docs/diagrams/05-backlog.svg +113 -0
  60. package/docs/diagrams/06-automation-utilities-d2.png +0 -0
  61. package/docs/diagrams/06-automation-utilities.d2 +48 -0
  62. package/docs/diagrams/06-automation-utilities.mmd +47 -0
  63. package/docs/diagrams/06-automation-utilities.png +0 -0
  64. package/docs/diagrams/06-automation-utilities.svg +110 -0
  65. package/docs/diagrams/_theme.d2 +86 -0
  66. package/docs/requirements.md +48 -0
  67. package/docs/workflow-diagram.md +338 -0
  68. package/package.json +1 -1
  69. package/scripts/gsd-t-dashboard-server.js +190 -0
  70. package/scripts/gsd-t-transcript.html +200 -0
  71. package/templates/CLAUDE-global.md +46 -0
  72. package/templates/prompts/design-verify-subagent.md +3 -0
  73. package/templates/prompts/qa-subagent.md +3 -0
  74. package/templates/prompts/red-team-subagent.md +3 -0
@@ -209,6 +209,32 @@
209
209
  .frame.tool-use > .ts, .frame.thinking > .ts { padding: 8px 0 0 12px; }
210
210
  .frame.tool-result > .ts { padding-top: 8px; }
211
211
  .frame.compact-marker > .ts { padding: 0; min-width: 64px; }
212
+
213
+ /* M54 D2 T1 — LIVE ACTIVITY rail section.
214
+ Additive only. No existing CSS renamed or removed. */
215
+ #rail-live-activity { padding: 0 0 8px 0; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
216
+ #rail-live-activity .la-heading { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px 4px 12px; font-size: 11px; text-transform: uppercase; color: var(--fg-xdim); letter-spacing: 0.08em; }
217
+ .la-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
218
+ .la-entry { display: grid; grid-template-columns: 10px 18px 1fr auto; align-items: center; gap: 4px; padding: 5px 12px; cursor: pointer; border-radius: 4px; font-size: 12px; }
219
+ .la-entry:hover { background: var(--bg-soft); }
220
+ .la-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
221
+ .la-dot-running { background: #2dd4bf; }
222
+ .la-dot-stale { background: var(--fg-xdim, #6b7280); }
223
+ .la-icon { font-family: var(--mono); font-size: 13px; text-align: center; color: var(--fg-dim); }
224
+ .la-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40ch; color: var(--fg); font-family: var(--mono); font-size: 11px; }
225
+ .la-duration { font-family: var(--mono); font-size: 11px; color: var(--fg-xdim); text-align: right; white-space: nowrap; }
226
+ .la-empty { padding: 4px 12px 0 12px; color: var(--fg-xdim); font-size: 11px; font-style: italic; }
227
+ /* Kind icon slots — glyphs set in JS via KIND_ICONS map */
228
+ .la-icon-bash { color: var(--accent-warm, #f0a500); }
229
+ .la-icon-monitor { color: var(--accent, #58a6ff); }
230
+ .la-icon-tool { color: var(--fg-dim, #8b949e); }
231
+ .la-icon-spawn { color: var(--green, #3fb950); }
232
+ @keyframes accent-pulse {
233
+ 0% { box-shadow: inset 2px 0 0 0 #2dd4bf; opacity: 1; }
234
+ 50% { box-shadow: inset 2px 0 0 0 #2dd4bf; opacity: 0.6; }
235
+ 100% { box-shadow: inset 2px 0 0 0 #2dd4bf; opacity: 1; }
236
+ }
237
+ .la-pulsing { animation: accent-pulse 1.5s ease-in-out infinite; }
212
238
  </style>
213
239
  </head>
214
240
  <body data-spawn-id="__SPAWN_ID__">
@@ -226,6 +252,15 @@
226
252
  <div class="rail-header"><span>★ Main Session</span></div>
227
253
  <div class="rail-body" id="rail-main-body"><div class="empty">No in-session conversation captured yet.</div></div>
228
254
  </section>
255
+ <!-- M54 D2 T1 — LIVE ACTIVITY section (additive, between Main Session and Live Spawns).
256
+ Populated by 5s polling JS (D2 T2). CSS @keyframes accent-pulse in <style> block above. -->
257
+ <section id="rail-live-activity">
258
+ <div class="la-heading"><span>⚡ Live Activity</span></div>
259
+ <ul id="la-list" class="la-list">
260
+ <!-- Entries injected by polling JS (T2). Empty section header visible when list is empty. -->
261
+ </ul>
262
+ <div class="la-empty" id="la-empty-msg">No active operations detected.</div>
263
+ </section>
229
264
  <section class="rail-live" data-rail-section="live">
230
265
  <div class="rail-header"><span>Live Spawns</span></div>
231
266
  <div class="rail-body">
@@ -1615,6 +1650,171 @@
1615
1650
  poll();
1616
1651
  setInterval(poll, 5000);
1617
1652
  })();
1653
+
1654
+ // ── M54 D2 T2 — Live Activity Rail ────────────────────────────────────
1655
+ // Polls /api/live-activity every 5s (same cadence as /api/parallelism).
1656
+ // Appends entries to #la-list, applies .la-pulsing on new entries,
1657
+ // removes entries when they disappear from the API response.
1658
+ // Click handler: stopPulse + loadTailUrl (NO auto-switch on arrival).
1659
+ // Additive only — no existing function renamed or removed.
1660
+ (function wireLiveActivity() {
1661
+ const laList = document.getElementById('la-list');
1662
+ const laEmpty = document.getElementById('la-empty-msg');
1663
+ if (!laList) return; // section not present in this page variant
1664
+
1665
+ // Map of id → { element, startedAt, stopPulseTimer }
1666
+ const _current = new Map();
1667
+
1668
+ // Format duration in seconds as human-readable string
1669
+ function fmtDuration(ms) {
1670
+ const s = Math.floor(ms / 1000);
1671
+ if (s < 60) return s + 's';
1672
+ const m = Math.floor(s / 60), rs = s % 60;
1673
+ if (m < 60) return m + 'm ' + rs + 's';
1674
+ const h = Math.floor(m / 60), rm = m % 60;
1675
+ return h + 'h ' + rm + 'm';
1676
+ }
1677
+
1678
+ // Kind icon mapping
1679
+ const KIND_ICONS = { bash: '$', monitor: '👁', tool: '🔧', spawn: '↳' };
1680
+
1681
+ // Load the tailUrl content into the bottom pane (mirrors existing bottom-pane loaders)
1682
+ function loadTailUrl(tailUrl) {
1683
+ const streamEl = document.getElementById('stream');
1684
+ if (!streamEl) return;
1685
+ fetch(tailUrl)
1686
+ .then(function(r) { return r.text(); })
1687
+ .then(function(text) {
1688
+ streamEl.innerHTML = '<pre style="margin:0;padding:12px;font-family:var(--mono);font-size:12px;white-space:pre-wrap;word-break:break-word;">' + _esc(text) + '</pre>';
1689
+ })
1690
+ .catch(function(err) {
1691
+ console.log('[la] tail load error:', err && err.message);
1692
+ });
1693
+ }
1694
+
1695
+ function _esc(s) {
1696
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1697
+ }
1698
+
1699
+ // Remove .la-pulsing from entry element
1700
+ function stopPulse(id) {
1701
+ const item = _current.get(id);
1702
+ if (!item || !item.element) return;
1703
+ item.element.classList.remove('la-pulsing');
1704
+ if (item.stopPulseTimer) { clearTimeout(item.stopPulseTimer); item.stopPulseTimer = null; }
1705
+ }
1706
+
1707
+ // Append a new .la-entry to #la-list
1708
+ function appendActivity(entry) {
1709
+ const li = document.createElement('li');
1710
+ li.className = 'la-entry la-pulsing';
1711
+ li.setAttribute('data-id', entry.id);
1712
+ li.setAttribute('data-kind', entry.kind);
1713
+ li.setAttribute('data-tail-url', entry.tailUrl || '');
1714
+
1715
+ const dot = document.createElement('span');
1716
+ dot.className = 'la-dot la-dot-running';
1717
+ li.appendChild(dot);
1718
+
1719
+ const icon = document.createElement('span');
1720
+ icon.className = 'la-icon la-icon-' + entry.kind;
1721
+ icon.textContent = KIND_ICONS[entry.kind] || '?';
1722
+ li.appendChild(icon);
1723
+
1724
+ const label = document.createElement('span');
1725
+ label.className = 'la-label';
1726
+ const labelText = String(entry.label || '').slice(0, 40);
1727
+ label.textContent = labelText;
1728
+ label.title = entry.label || '';
1729
+ li.appendChild(label);
1730
+
1731
+ const dur = document.createElement('span');
1732
+ dur.className = 'la-duration';
1733
+ dur.textContent = fmtDuration(entry.durationMs || 0);
1734
+ li.appendChild(dur);
1735
+
1736
+ li.addEventListener('click', function() {
1737
+ stopPulse(entry.id);
1738
+ if (entry.tailUrl) loadTailUrl(entry.tailUrl);
1739
+ });
1740
+
1741
+ laList.appendChild(li);
1742
+
1743
+ // Schedule pulse stop after 30s (condition c)
1744
+ const timer = setTimeout(function() { stopPulse(entry.id); }, 30000);
1745
+ _current.set(entry.id, { element: li, startedAt: entry.startedAt, stopPulseTimer: timer });
1746
+
1747
+ if (laEmpty) laEmpty.style.display = 'none';
1748
+ }
1749
+
1750
+ // Remove a .la-entry from #la-list
1751
+ function removeActivity(id) {
1752
+ const item = _current.get(id);
1753
+ if (!item) return;
1754
+ if (item.stopPulseTimer) clearTimeout(item.stopPulseTimer);
1755
+ if (item.element && item.element.parentNode) item.element.parentNode.removeChild(item.element);
1756
+ _current.delete(id);
1757
+ if (laEmpty && _current.size === 0) laEmpty.style.display = '';
1758
+ }
1759
+
1760
+ // Update the duration counter for an existing entry
1761
+ function updateDuration(id, startedAt) {
1762
+ const item = _current.get(id);
1763
+ if (!item || !item.element) return;
1764
+ const dur = item.element.querySelector('.la-duration');
1765
+ if (!dur) return;
1766
+ const start = startedAt ? new Date(startedAt).getTime() : 0;
1767
+ const elapsed = start ? Math.max(0, Date.now() - start) : 0;
1768
+ dur.textContent = fmtDuration(elapsed);
1769
+ }
1770
+
1771
+ // Fetch /api/live-activity, return envelope (or {activities:[]} on error)
1772
+ async function fetchLiveActivity() {
1773
+ try {
1774
+ const r = await fetch('/api/live-activity');
1775
+ if (!r.ok) {
1776
+ console.log('[la] /api/live-activity returned', r.status);
1777
+ return { activities: [] };
1778
+ }
1779
+ return await r.json();
1780
+ } catch (err) {
1781
+ console.log('[la] fetch error:', err && err.message);
1782
+ return { activities: [] };
1783
+ }
1784
+ }
1785
+
1786
+ // Reconcile current DOM state with new API response
1787
+ function reconcile(activities) {
1788
+ // Build a Map of new activities by id
1789
+ const newById = new Map();
1790
+ for (const entry of (activities || [])) {
1791
+ newById.set(entry.id, entry);
1792
+ }
1793
+
1794
+ // Remove entries no longer present
1795
+ for (const id of Array.from(_current.keys())) {
1796
+ if (!newById.has(id)) removeActivity(id);
1797
+ }
1798
+
1799
+ // Add new entries; update durations for existing ones
1800
+ for (const [id, entry] of newById) {
1801
+ if (_current.has(id)) {
1802
+ updateDuration(id, entry.startedAt);
1803
+ } else {
1804
+ appendActivity(entry);
1805
+ }
1806
+ }
1807
+ }
1808
+
1809
+ // Initial poll + 5s interval (same cadence as /api/parallelism)
1810
+ async function poll() {
1811
+ const data = await fetchLiveActivity();
1812
+ reconcile(data.activities);
1813
+ }
1814
+
1815
+ poll();
1816
+ setInterval(poll, 5000);
1817
+ })();
1618
1818
  })();
1619
1819
  </script>
1620
1820
  </body>
@@ -412,6 +412,52 @@ No command file ships a bare `Task(...)` or `claude -p` line outside of a wrappe
412
412
 
413
413
  Rationale: the pre-M41 convention silently wrote `N/A` tokens because no caller parsed the `usage` envelope. The wrapper is the single place that parses it. Bypassing the wrapper re-introduces blind spots.
414
414
 
415
+ ## Mandatory Preflight Before Spawn (M55 — v3.25.10+)
416
+
417
+ Every command that spawns a Task subagent or invokes `claude -p` MUST run `gsd-t preflight` first. Hard-fails on any `severity:"error"` check (wrong branch, occupied required port). Non-error checks (warn/info) record but do not block.
418
+
419
+ ```bash
420
+ gsd-t preflight --json > /tmp/gsd-t-preflight.json || exit 4
421
+ ```
422
+
423
+ Catches drift early — wrong branch, port collision, DRAFT contracts past PARTITIONED — before any LLM work fires. Same envelope is consumed by `gsd-t verify-gate` Track 1, so failing fast at execute time saves the verify round-trip.
424
+
425
+ Library: `bin/cli-preflight.cjs::runPreflight({projectDir, checks?})`.
426
+ Contract: `.gsd-t/contracts/cli-preflight-contract.md` v1.0.0 STABLE.
427
+
428
+ ## Brief-First Worker Rule (M55 — v3.25.10+)
429
+
430
+ Every parallel worker prompt scaffold MUST thread `$BRIEF_PATH` — a ≤2,500-token JSON snapshot generated once per spawn by `bin/gsd-t-context-brief.cjs`. The brief replaces the 30–60k context re-read every parallel worker would otherwise perform (CLAUDE.md + contracts + scope + relevant code) — the dominant ITPM-relief lever in M55.
431
+
432
+ ```bash
433
+ SPAWN_ID="execute-${DOMAIN}-$(date -u +%Y%m%dT%H%M%SZ)"
434
+ gsd-t brief --kind execute --domain "${DOMAIN}" --spawn-id "${SPAWN_ID}" --out ".gsd-t/briefs/${SPAWN_ID}.json"
435
+ export BRIEF_PATH=".gsd-t/briefs/${SPAWN_ID}.json"
436
+ ```
437
+
438
+ The 3 validation-subagent protocols (`templates/prompts/{qa,red-team,design-verify}-subagent.md`) carry the canonical instruction "If you're about to grep, read, or run a test, check the brief first at `$BRIEF_PATH`." Workers grep the brief instead of re-walking the repo.
439
+
440
+ `.gsd-t/briefs/` is gitignored — briefs are per-spawn ephemera, not committed artifacts.
441
+
442
+ Library: `bin/gsd-t-context-brief.cjs::generateBrief(...)`.
443
+ Contract: `.gsd-t/contracts/context-brief-contract.md` v1.0.0 STABLE.
444
+
445
+ ## Two-Track Verify-Gate (M55 — v3.25.10+)
446
+
447
+ `gsd-t verify-gate --json` is the canonical pre-merge gate. Track 1 = D1 preflight envelope (hard-fail on any `severity:"error"` check). Track 2 = D2 parallel-CLI substrate fans out off-the-shelf CLIs (`tsc`, `biome`/`ruff`, `npm test`, `knip`, `gitleaks`, `scc`/`lizard`). Both tracks always run; both report.
448
+
449
+ ```bash
450
+ gsd-t verify-gate --json > /tmp/gate.json || exit 4
451
+ cat /tmp/gate.json | gsd-t verify-gate-judge > /tmp/judge-prompt.txt
452
+ ```
453
+
454
+ `top-level ok = (skipTrack1 || track1.ok) && (skipTrack2 || track2.ok)` — purely deterministic. The LLM judge sees only the ≤500-token deterministic `summary`; never raw stdout/stderr. The judge confirms or contradicts the deterministic verdict; it never overrides `ok`.
455
+
456
+ Defensive on missing `.gsd-t/ratelimit-map.json` → fallback `maxConcurrency=2` with a structured note.
457
+
458
+ Library: `bin/gsd-t-verify-gate.cjs::runVerifyGate(...)` + `bin/gsd-t-verify-gate-judge.cjs::buildJudgePrompt(...)`.
459
+ Contract: `.gsd-t/contracts/verify-gate-contract.md` v1.0.0 STABLE.
460
+
415
461
  ## Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation
416
462
 
417
463
  Every GSD-T command spawns detached, unconditionally. There is no `--watch`, no `--in-session`, no `--headless` opt-in, no context-meter threshold that reroutes, no low-water-mark bypass. The dialog channel is reserved for human↔Claude conversation; everything else is a detached headless child. Interactive session shows a launch banner + live-transcript URL + event-stream path, then exits. Results surface via the read-back banner on the user's next message.
@@ -4,6 +4,9 @@ You are the Design Verification Agent. Your ONLY job is to visually compare the
4
4
 
5
5
  **FAIL-BY-DEFAULT.** Every visual element starts UNVERIFIED. You must prove each one matches — never assume. "Looks close" is not a verdict. "Appears to match" is not a verdict. The only valid verdicts are `✅ MATCH` (with proof) or `❌ DEVIATION` (with specific values).
6
6
 
7
+ <!-- M55-D5: brief-first rule -->
8
+ **Brief first.** If you're about to grep, read, or run a test, check the brief first at `$BRIEF_PATH` (a ≤2,500-token JSON snapshot of design contracts + scope + design tokens + relevant component paths, generated by `bin/gsd-t-context-brief.cjs --kind design-verify`). The brief lists the design contracts, design-token files, and component paths in scope for this verification — your starting checklist. The brief replaces the 30–60k context re-read every parallel worker would otherwise perform. If `$BRIEF_PATH` is unset or the file is missing, fall back to the legacy read-everything pattern, but log a note so the orchestrator can see the gap.
9
+
7
10
  ## Step 0: Element Count Reconciliation (run BEFORE anything else)
8
11
 
9
12
  A missing widget is the easiest deviation to miss in a 30-row comparison table — and the most catastrophic.
@@ -2,6 +2,9 @@
2
2
 
3
3
  You are the QA agent. Your sole job is test generation, execution, and gap reporting. You write ZERO feature code. You never modify implementation files — only test files and reports.
4
4
 
5
+ <!-- M55-D5: brief-first rule -->
6
+ **Brief first.** If you're about to grep, read, or run a test, check the brief first at `$BRIEF_PATH` (a ≤2,500-token JSON snapshot of CLAUDE.md + contracts + scope + constraints, generated by `bin/gsd-t-context-brief.cjs`). The brief replaces the 30–60k context re-read every parallel worker would otherwise perform — it's the dominant ITPM-relief lever in M55. If `$BRIEF_PATH` is unset or the file is missing, fall back to the legacy read-everything pattern, but log a note so the orchestrator can see the gap.
7
+
5
8
  ## What to Do
6
9
 
7
10
  1. **Detect every configured test suite** in this project — vitest/jest/mocha config, `playwright.config.*`, `cypress.config.*`. Run EVERY suite that exists.
@@ -2,6 +2,9 @@
2
2
 
3
3
  You are a Red Team QA adversary. Your job is to BREAK the code that was just written for this domain. You operate with inverted incentives — your value is measured by REAL bugs found, not tests passed.
4
4
 
5
+ <!-- M55-D5: brief-first rule -->
6
+ **Brief first.** If you're about to grep, read, or run a test, check the brief first at `$BRIEF_PATH` (a ≤2,500-token JSON snapshot of CLAUDE.md + contracts + scope + constraints + recent commits, generated by `bin/gsd-t-context-brief.cjs --kind red-team`). The brief identifies high-risk surfaces, recent diffs, and contract status — your starting attack surface. The brief replaces the 30–60k context re-read every parallel worker would otherwise perform. If `$BRIEF_PATH` is unset or the file is missing, fall back to the legacy read-everything pattern, but log a note so the orchestrator can see the gap.
7
+
5
8
  ## Hard Rules
6
9
 
7
10
  - **Bugs found = value.** A short attack list is failure.