yadflow 2.11.1 → 2.13.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 CHANGED
@@ -1,9 +1,9 @@
1
- ## [2.11.1](https://github.com/abdelrahmannasr/yadflow/compare/v2.11.0...v2.11.1) (2026-06-15)
1
+ # [2.13.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.12.0...v2.13.0) (2026-06-16)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * **doctor:** scope platform-CLI auth probe to the hub host ([#68](https://github.com/abdelrahmannasr/yadflow/issues/68)) ([3cb2801](https://github.com/abdelrahmannasr/yadflow/commit/3cb28011c80645e0ff42e544a9b3d933231daeb3))
6
+ * **docs:** make the report the main documentation, mount the SPA under /app/ ([#71](https://github.com/abdelrahmannasr/yadflow/issues/71)) ([1993e4d](https://github.com/abdelrahmannasr/yadflow/commit/1993e4dc282df281474ca1923acd52dbd1262dcb))
7
7
 
8
8
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
9
9
 
package/cli/docs.mjs CHANGED
@@ -43,11 +43,12 @@ export function deployTargetFromHub(hub = {}) {
43
43
  }
44
44
 
45
45
  // The Vite `base` for one site: join the project base path (from docs.json) with the per-site
46
- // subpath. Per-epic sites nest under epics/<id>/; the overview is the Pages root. Always a
46
+ // subpath. The overview SPA mounts under `app/` so the hand-maintained report.html can own the
47
+ // Pages root (`<base>/`) as the main documentation; per-epic sites nest under epics/<id>/. Always a
47
48
  // leading+trailing slash so it works as a Vite base and a router basename.
48
49
  export function siteBasePath(docs = {}, { epic, overview } = {}) {
49
50
  const root = (docs.basePath || '/').replace(/\/+$/, '') || '';
50
- const sub = overview ? '' : epic ? `/epics/${epic}` : '';
51
+ const sub = overview ? '/app' : epic ? `/epics/${epic}` : '';
51
52
  const joined = `${root}${sub}`.replace(/\/+/g, '/');
52
53
  return joined ? `${joined.replace(/\/$/, '')}/` : '/';
53
54
  }
@@ -113,12 +114,13 @@ export function docsStale(manifest, { artifactHash, repoHeads = {}, templateVers
113
114
 
114
115
  // ---- pure: the Pages CI workflow (committed by `yad docs sync --wire`) ---------------------------
115
116
  // GitHub Actions deploy-pages, or a GitLab `pages` job. Both assemble a single `public/` tree: the
116
- // overview at the root and every per-epic site under `epics/<id>/` matching siteBasePath's nesting
117
- // (overview at `<base>/`, epics at `<base>/epics/EP-<slug>/`). A concurrency group prevents the
117
+ // hand-maintained report.html owns the root (`<base>/` the main documentation), the overview SPA
118
+ // mounts under `app/`, and every per-epic site nests under `epics/<id>/` matching siteBasePath
119
+ // (overview at `<base>/app/`, epics at `<base>/epics/EP-<slug>/`). A concurrency group prevents the
118
120
  // deploy from retriggering. The shared shell script keeps the two platforms byte-for-byte aligned.
119
121
  const BUILD_PUBLIC = [
120
122
  'mkdir -p public',
121
- 'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && cp -r docs/sdlc-site/dist/. public/; fi',
123
+ 'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && mkdir -p public/app && cp -r docs/sdlc-site/dist/. public/app/ && cp docs/sdlc-site/public/report.html public/index.html && cp docs/sdlc-site/public/report.html public/report.html; fi',
122
124
  'for d in epics/*/docs-site; do [ -d "$d" ] || continue; id=$(basename "$(dirname "$d")"); (cd "$d" && npm ci && npm run build) && mkdir -p "public/epics/$id" && cp -r "$d/dist/." "public/epics/$id/"; done',
123
125
  ];
124
126
  export function pagesWorkflow(platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.11.1",
3
+ "version": "2.13.0",
4
4
  "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -18,10 +18,17 @@ repo uses. Each reads conventions established by earlier steps — it invents no
18
18
 
19
19
  ## 1. spec-link (`templates/checks/spec-link.sh`)
20
20
 
21
- - Collects the `Task:` trailers across `<base>..HEAD`.
22
- - **FAIL** if there is no `Task:` trailer (the change does not link a story/spec).
23
- - For each `Task: <story>-<task>`, strips the `-T<NN>` suffix to get `<story>` and requires
24
- `specs/<story>/link.md` to exist. **FAIL** if missing.
21
+ - Checks every non-merge commit in `<base>..HEAD` **per commit** (not aggregated across the range),
22
+ so the report names each offending commit and one bad commit never masks the rest.
23
+ - Maintenance commits are **exempt**: a Conventional-Commits subject of type `ci`, `chore`, `build`,
24
+ or `test` (optional `(scope)` / breaking `!`) **PASSes** without a link — CI wiring, dependency
25
+ bumps, and test-infra changes legitimately link no story.
26
+ - For every other commit, requires a `Task: <story>-<task>` trailer. **FAIL** if absent.
27
+ - The trailer must be a well-formed `<story>-T<NN>` id. **FAIL** on a malformed trailer (e.g.
28
+ `EP-demo-S01` with no `-T<NN>`) rather than letting it slip through the suffix-strip.
29
+ - Strips the `-T<NN>` suffix from the task to get `<story>` and requires `specs/<story>/link.md` to
30
+ exist. **FAIL** if missing.
31
+ - An empty range (no non-merge commits) **PASSes**.
25
32
  - Portable across bash 3.2 (macOS) and 4+ (no `mapfile`).
26
33
  - **Fails closed** when `<base>` can't be resolved (so a shallow clone / wrong base never PASSes blind).
27
34
 
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # spec-link gate (Phase 3 build plan §C).
3
- # The change must link a real story/spec: every commit range under review must carry a
3
+ # Every NON-MAINTENANCE commit must link a real story/spec: it must carry a
4
4
  # `Task: <story>-<task>` trailer whose <story> resolves to a specs/<story>/link.md.
5
- # Fail if the link is missing no unlinked code reaches merge.
5
+ # Maintenance commits (ci/chore/build/test) are EXEMPTCI wiring, dependency bumps,
6
+ # and test-infra changes legitimately link no story. Checked per commit (not aggregated
7
+ # across the range), so the report names every offending commit.
6
8
  set -euo pipefail
7
9
 
8
10
  BASE="${1:-${SDLC_BASE:-origin/main}}"
@@ -14,25 +16,48 @@ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
14
16
  fi
15
17
  RANGE="${BASE}..HEAD"
16
18
 
17
- # Portable across bash 3.2 (macOS) and 4+ — no mapfile.
18
- tasks="$(git log "$RANGE" --format='%(trailers:key=Task,valueonly)' | sed '/^$/d' | sort -u)"
19
+ # Conventional-Commits types exempt from the spec-link requirement (optional (scope) and breaking !).
20
+ EXEMPT='ci|chore|build|test'
19
21
 
20
- if [ -z "$tasks" ]; then
21
- echo "FAIL [spec-link]: no 'Task: <story>-<task>' trailer in ${RANGE} — change does not link a story/spec."
22
- exit 1
22
+ # Portable across bash 3.2 (macOS) and 4+ — no mapfile; feed the loop via heredoc (not a pipe) so
23
+ # the failure count survives the loop body.
24
+ commits="$(git rev-list --no-merges "$RANGE")"
25
+ if [ -z "$commits" ]; then
26
+ echo "PASS [spec-link]: no non-merge commits in ${RANGE}"
27
+ exit 0
23
28
  fi
24
29
 
25
30
  rc=0
26
- while IFS= read -r t; do
27
- [ -z "$t" ] && continue
28
- story="$(printf '%s' "$t" | sed -E 's/-T[0-9]+$//')"
31
+ while IFS= read -r sha; do
32
+ [ -z "$sha" ] && continue
33
+ short="$(git log -1 --format=%h "$sha")"
34
+ subject="$(git log -1 --format=%s "$sha")"
35
+ if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
36
+ echo "PASS [spec-link]: ${short} '${subject}' — maintenance commit (exempt)"
37
+ continue
38
+ fi
39
+ task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
40
+ if [ -z "$task" ]; then
41
+ echo "FAIL [spec-link]: ${short} '${subject}' has no 'Task:' trailer"
42
+ rc=1
43
+ continue
44
+ fi
45
+ # The trailer must be a real <story>-T<NN> id. Without this guard a malformed trailer
46
+ # (e.g. 'EP-demo-S01' with no -T<NN>) would survive the suffix-strip unchanged and PASS
47
+ # whenever specs/<that>/link.md happens to exist.
48
+ if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
49
+ echo "FAIL [spec-link]: ${short} '${subject}' has a malformed Task trailer '${task}' (expected <story>-T<NN>)."
50
+ rc=1
51
+ continue
52
+ fi
53
+ story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
29
54
  if [ -f "specs/${story}/link.md" ]; then
30
- echo "PASS [spec-link]: ${t} -> specs/${story}/link.md"
55
+ echo "PASS [spec-link]: ${short} ${task} -> specs/${story}/link.md"
31
56
  else
32
- echo "FAIL [spec-link]: ${t} references specs/${story}/ but link.md is missing."
57
+ echo "FAIL [spec-link]: ${short} ${task} references specs/${story}/ but link.md is missing."
33
58
  rc=1
34
59
  fi
35
60
  done <<EOF
36
- $tasks
61
+ $commits
37
62
  EOF
38
63
  exit "$rc"
@@ -9,7 +9,14 @@
9
9
  # pipeline's default stage regardless of any `stages:` order the foreign root defines — a foreign
10
10
  # `stages:` list can neither break them nor reorder them. Job names are yad-prefixed to avoid
11
11
  # colliding with the host project's own job names.
12
- default:
12
+ #
13
+ # `image:`/`tags:` live on the .sdlc_mr_only anchor below — NOT a top-level `default:`. An included
14
+ # top-level `default:` merges into the HOST root pipeline; if the host already sets a top-level
15
+ # `image:`, GitLab rejects the whole config ("image is defined in top-level and 'default:' entry").
16
+ # Putting them on the anchor scopes them to the gate jobs (which all `extends: .sdlc_mr_only`) and
17
+ # never touches the host root.
18
+
19
+ .sdlc_mr_only:
13
20
  image: node:20
14
21
  # Runner selection. These jobs need a docker/dind executor; on instances whose runners are all
15
22
  # tag-locked (run_untagged: false) an untagged image job never starts. Set the YAD_RUNNER_TAGS
@@ -19,8 +26,6 @@ default:
19
26
  # instead strand it `pending` — if so, set the variable to a real tag. Single value only:
20
27
  # `tags: [$VAR]` is one tag equal to the whole variable, not a comma-split.
21
28
  tags: [$YAD_RUNNER_TAGS]
22
-
23
- .sdlc_mr_only:
24
29
  rules:
25
30
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
26
31
 
@@ -45,6 +50,7 @@ yad-build-test-lint:
45
50
  variables:
46
51
  YAD_TEST_MAX_WORKERS: "2" # cap jest/vitest test workers in CI; ignored by other runners
47
52
  script:
53
+ - npm ci
48
54
  - bash checks/build-test-lint.sh
49
55
 
50
56
  # Pattern gates: commit subject + MR title + MR body all follow the convention (profile: code).
@@ -14,34 +14,69 @@ import { RoleSelectPage } from './pages/RoleSelectPage';
14
14
  import { StakeholderDocPage } from './pages/StakeholderDocPage';
15
15
  import { usePlayback } from './hooks/usePlayback';
16
16
  import { useFlowStore } from './store/useFlowStore';
17
+ import { Icon } from './components/shared/Icon';
18
+
19
+ const railStyle = {
20
+ borderColor: 'var(--color-border-default)',
21
+ background: 'var(--color-bg-primary)',
22
+ };
17
23
 
18
24
  function Dashboard() {
19
25
  usePlayback();
20
26
  const isLogsPanelOpen = useFlowStore((s) => s.isLogsPanelOpen);
27
+ const isLeftPanelOpen = useFlowStore((s) => s.isLeftPanelOpen);
28
+ const isRightPanelOpen = useFlowStore((s) => s.isRightPanelOpen);
29
+ const toggleLeftPanel = useFlowStore((s) => s.toggleLeftPanel);
30
+ const toggleRightPanel = useFlowStore((s) => s.toggleRightPanel);
21
31
 
22
32
  return (
23
33
  <div className="flex flex-1 overflow-hidden">
24
- {/* Left Sidebar */}
25
- <aside
26
- className="w-80 flex-none flex flex-col border-r z-10"
27
- style={{
28
- borderColor: 'var(--color-border-default)',
29
- background: 'var(--color-bg-primary)',
30
- }}
31
- >
32
- {/* Path Selection */}
33
- <div className="p-4 border-b overflow-y-auto" style={{ borderColor: 'var(--color-border-default)' }}>
34
- <PathSelector />
35
- </div>
34
+ {/* Left Sidebar (collapsible — collapse to widen the canvas) */}
35
+ {isLeftPanelOpen ? (
36
+ <aside
37
+ className="w-80 flex-none flex flex-col border-r z-10"
38
+ style={railStyle}
39
+ >
40
+ {/* Collapse header */}
41
+ <div className="flex items-center justify-between px-4 pt-3 pb-1">
42
+ <span className="text-[10px] font-bold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
43
+ Navigator
44
+ </span>
45
+ <button
46
+ onClick={toggleLeftPanel}
47
+ title="Collapse panel"
48
+ aria-label="Collapse navigator panel"
49
+ className="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
50
+ >
51
+ <Icon name="left_panel_close" size={18} />
52
+ </button>
53
+ </div>
36
54
 
37
- {/* Step Timeline */}
38
- <div className="flex-1 overflow-y-auto p-4">
39
- <StepList />
40
- </div>
55
+ {/* Path Selection */}
56
+ <div className="p-4 border-b overflow-y-auto" style={{ borderColor: 'var(--color-border-default)' }}>
57
+ <PathSelector />
58
+ </div>
41
59
 
42
- {/* Footer */}
43
- <SidebarFooter />
44
- </aside>
60
+ {/* Step Timeline */}
61
+ <div className="flex-1 overflow-y-auto p-4">
62
+ <StepList />
63
+ </div>
64
+
65
+ {/* Footer */}
66
+ <SidebarFooter />
67
+ </aside>
68
+ ) : (
69
+ <div className="w-10 flex-none flex flex-col items-center border-r z-10 pt-3" style={railStyle}>
70
+ <button
71
+ onClick={toggleLeftPanel}
72
+ title="Expand navigator"
73
+ aria-label="Expand navigator panel"
74
+ className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
75
+ >
76
+ <Icon name="left_panel_open" size={18} />
77
+ </button>
78
+ </div>
79
+ )}
45
80
 
46
81
  {/* Main Canvas Area */}
47
82
  <main className="flex-1 flex flex-col overflow-hidden relative flow-grid"
@@ -65,8 +100,21 @@ function Dashboard() {
65
100
  <PlaybackBar />
66
101
  </main>
67
102
 
68
- {/* Right Detail Panel */}
69
- <RightPanel />
103
+ {/* Right Detail Panel (collapsible — collapse to widen the canvas) */}
104
+ {isRightPanelOpen ? (
105
+ <RightPanel />
106
+ ) : (
107
+ <div className="w-10 flex-none flex flex-col items-center border-l z-10 pt-3" style={railStyle}>
108
+ <button
109
+ onClick={toggleRightPanel}
110
+ title="Expand details"
111
+ aria-label="Expand step-details panel"
112
+ className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
113
+ >
114
+ <Icon name="right_panel_open" size={18} />
115
+ </button>
116
+ </div>
117
+ )}
70
118
  </div>
71
119
  );
72
120
  }
@@ -75,6 +75,26 @@ export const FlowCanvas: React.FC = () => {
75
75
 
76
76
  const messages = currentStep?.messages || [];
77
77
 
78
+ // Optional loop-back arc, swept below the canvas to read as "the pipeline repeats".
79
+ // Rendered only when both endpoint nodes exist (e.g. the SDLC-overview's
80
+ // trust-log → product-hub cycle); a no-op for diagrams without them.
81
+ const loopBack = useMemo(() => {
82
+ if (dims.width === 0) return null;
83
+ const a = COMPONENTS.find((c) => c.id === 'trust-log');
84
+ const b = COMPONENTS.find((c) => c.id === 'product-hub');
85
+ if (!a || !b) return null;
86
+ const x1 = (a.position.x / 100) * dims.width;
87
+ const y1 = (a.position.y / 100) * dims.height;
88
+ const x2 = (b.position.x / 100) * dims.width;
89
+ const y2 = (b.position.y / 100) * dims.height;
90
+ const yb = dims.height * 0.985;
91
+ return {
92
+ d: `M ${x1} ${y1} C ${x1} ${yb}, ${x2} ${yb}, ${x2} ${y2}`,
93
+ labelX: (x1 + x2) / 2,
94
+ labelY: dims.height * 0.92,
95
+ };
96
+ }, [dims]);
97
+
78
98
  return (
79
99
  <div ref={containerRef} className="relative h-full w-full overflow-hidden">
80
100
  {/* Canvas Header Overlay */}
@@ -159,6 +179,33 @@ export const FlowCanvas: React.FC = () => {
159
179
  containerHeight={dims.height}
160
180
  />
161
181
  ))}
182
+
183
+ {/* Loop-back: the pipeline repeats per epic */}
184
+ {loopBack && (
185
+ <g>
186
+ <path
187
+ d={loopBack.d}
188
+ fill="none"
189
+ stroke="#ff6490"
190
+ strokeWidth={1.5}
191
+ strokeDasharray="6 5"
192
+ opacity={0.55}
193
+ markerEnd="url(#arrowhead)"
194
+ />
195
+ <text
196
+ x={loopBack.labelX}
197
+ y={loopBack.labelY}
198
+ textAnchor="middle"
199
+ fontSize={12}
200
+ fontWeight={700}
201
+ letterSpacing={0.6}
202
+ fill="#ff8db0"
203
+ opacity={0.85}
204
+ >
205
+ ↺ pipeline repeats per epic
206
+ </text>
207
+ </g>
208
+ )}
162
209
  </svg>
163
210
  )}
164
211
 
@@ -12,6 +12,7 @@ export function RightPanel() {
12
12
  const getCurrentStep = useFlowStore((s) => s.getCurrentStep);
13
13
  const activeStepIndex = useFlowStore((s) => s.activeStepIndex);
14
14
  const selectedPath = useFlowStore((s) => s.selectedPath);
15
+ const toggleRightPanel = useFlowStore((s) => s.toggleRightPanel);
15
16
  const navigate = useNavigate();
16
17
  const step = getCurrentStep();
17
18
 
@@ -29,7 +30,14 @@ export function RightPanel() {
29
30
  <h3 className="text-slate-100 text-sm font-bold font-display uppercase tracking-wider">
30
31
  Step Details
31
32
  </h3>
32
- <Icon name="close" size={20} className="text-slate-500 cursor-pointer hover:text-white" />
33
+ <button
34
+ onClick={toggleRightPanel}
35
+ title="Collapse panel"
36
+ aria-label="Collapse step-details panel"
37
+ className="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
38
+ >
39
+ <Icon name="right_panel_close" size={18} />
40
+ </button>
33
41
  </div>
34
42
 
35
43
  {!step ? (
@@ -43,15 +43,6 @@ export const StepList = () => {
43
43
  }
44
44
  };
45
45
 
46
- // Reset expanded state when active step changes externally (playback)
47
- const prevActiveRef = { current: activeStepIndex };
48
- if (prevActiveRef.current !== activeStepIndex) {
49
- prevActiveRef.current = activeStepIndex;
50
- if (expandedIndex !== null && expandedIndex !== activeStepIndex) {
51
- // Auto-expand the new active step during playback
52
- }
53
- }
54
-
55
46
  return (
56
47
  <div className="flex flex-col gap-2">
57
48
  <div className="flex items-center justify-between mb-2">
@@ -28,13 +28,16 @@ export function CommandPalette() {
28
28
  return () => window.removeEventListener('keydown', handler);
29
29
  }, [isOpen, toggle]);
30
30
 
31
- // Reset on open
32
- useEffect(() => {
31
+ // Reset on open — adjust state during render (not inside an effect) so it does
32
+ // not trigger an extra cascading render (react-hooks/set-state-in-effect).
33
+ const [wasOpen, setWasOpen] = useState(isOpen);
34
+ if (isOpen !== wasOpen) {
35
+ setWasOpen(isOpen);
33
36
  if (isOpen) {
34
37
  setQuery('');
35
38
  setSelectedIndex(0);
36
39
  }
37
- }, [isOpen]);
40
+ }
38
41
 
39
42
  const results = useMemo(() => {
40
43
  if (!query.trim()) {
@@ -9,13 +9,22 @@ export function useAnimationQueue() {
9
9
  const [animKey, setAnimKey] = useState(0);
10
10
  const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
11
11
 
12
- // When step changes, reset and schedule completion events
12
+ // Reset the pulse state the instant the step (or speed) changes done during
13
+ // render via the "adjust state on change" pattern rather than inside the effect,
14
+ // so it does not trigger an extra cascading render (react-hooks/set-state-in-effect).
15
+ const stepKey = `${activeStepIndex}-${speed}`;
16
+ const [resetKey, setResetKey] = useState(stepKey);
17
+ if (stepKey !== resetKey) {
18
+ setResetKey(stepKey);
19
+ setCompletedTargets(new Set());
20
+ setAnimKey((k) => k + 1);
21
+ }
22
+
23
+ // When step changes, schedule the target-pulse completion events.
13
24
  useEffect(() => {
14
25
  // Clear previous timers
15
26
  timersRef.current.forEach(clearTimeout);
16
27
  timersRef.current = [];
17
- setCompletedTargets(new Set());
18
- setAnimKey((k) => k + 1);
19
28
 
20
29
  // Get messages from the current step
21
30
  const step = useFlowStore.getState().getCurrentStep();
@@ -30,7 +30,7 @@ export function usePlayback() {
30
30
  return () => {
31
31
  if (elapsedRef.current) clearInterval(elapsedRef.current);
32
32
  };
33
- }, [playbackState]);
33
+ }, [playbackState, tickElapsed, resetElapsed]);
34
34
 
35
35
  // Generate logs on step change
36
36
  useEffect(() => {
@@ -56,7 +56,7 @@ export function usePlayback() {
56
56
  });
57
57
  }, msg.delay / speed);
58
58
  }
59
- }, [activeStepIndex, speed]);
59
+ }, [activeStepIndex, speed, addLog, getCurrentSteps]);
60
60
 
61
61
  useEffect(() => {
62
62
  if (playbackState !== "playing") {
@@ -96,5 +96,5 @@ export function usePlayback() {
96
96
  return () => {
97
97
  if (timerRef.current) clearTimeout(timerRef.current);
98
98
  };
99
- }, [playbackState, activeStepIndex, speed, animatingMessages]);
99
+ }, [playbackState, activeStepIndex, speed, animatingMessages, getCurrentSteps, nextStep, setAnimatingMessages]);
100
100
  }
@@ -26,6 +26,8 @@ interface FlowStore {
26
26
  isReferencePanelOpen: boolean;
27
27
  isCommandPaletteOpen: boolean;
28
28
  isLogsPanelOpen: boolean;
29
+ isLeftPanelOpen: boolean;
30
+ isRightPanelOpen: boolean;
29
31
 
30
32
  // Logs
31
33
  logs: LogEntry[];
@@ -57,6 +59,8 @@ interface FlowStore {
57
59
  toggleReferencePanel: () => void;
58
60
  toggleCommandPalette: () => void;
59
61
  toggleLogsPanel: () => void;
62
+ toggleLeftPanel: () => void;
63
+ toggleRightPanel: () => void;
60
64
 
61
65
  // Logs
62
66
  addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
@@ -90,6 +94,8 @@ export const useFlowStore = create<FlowStore>((set, get) => ({
90
94
  isReferencePanelOpen: false,
91
95
  isCommandPaletteOpen: false,
92
96
  isLogsPanelOpen: false,
97
+ isLeftPanelOpen: true,
98
+ isRightPanelOpen: true,
93
99
  logs: [],
94
100
  elapsedTime: 0,
95
101
 
@@ -157,6 +163,8 @@ export const useFlowStore = create<FlowStore>((set, get) => ({
157
163
  toggleReferencePanel: () => set((s) => ({ isReferencePanelOpen: !s.isReferencePanelOpen })),
158
164
  toggleCommandPalette: () => set((s) => ({ isCommandPaletteOpen: !s.isCommandPaletteOpen })),
159
165
  toggleLogsPanel: () => set((s) => ({ isLogsPanelOpen: !s.isLogsPanelOpen })),
166
+ toggleLeftPanel: () => set((s) => ({ isLeftPanelOpen: !s.isLeftPanelOpen })),
167
+ toggleRightPanel: () => set((s) => ({ isRightPanelOpen: !s.isRightPanelOpen })),
160
168
 
161
169
  // Logs
162
170
  addLog: (entry) => {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-docs-overview
3
- description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. Folds the legacy hand-maintained docs/index.html report into the site as report.html (linked from the nav) and deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
3
+ description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. The hand-maintained report is the MAIN documentation at the Pages root (report.html, also served as index.html); the interactive SPA mounts under `app/` and is reached from it (and links back). Deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
4
4
  ---
5
5
 
6
6
  # SDLC — Author the Overview Site (project-level, the pipeline as a living map)
@@ -8,8 +8,10 @@ description: 'Generates the project-level SDLC-overview interactive site — the
8
8
  **Goal:** Render the **whole yadflow pipeline** — every stage from setup → ship — as an interactive site,
9
9
  reusing the same shell as the per-epic docs (`skills/yad-docs/templates/app/`). Where `yad-docs`
10
10
  animates one epic's flows, this animates the **workflow itself**: the front gates, the build half, the
11
- automation dial, the setup connectors. It is the regenerable successor to the hand-maintained
12
- overview report, which is folded into this site as `public/report.html`.
11
+ automation dial, the setup connectors. The hand-maintained overview report stays the **main
12
+ documentation at the Pages root** (`<base>/`, served from `public/report.html` and `public/index.html`);
13
+ this interactive SPA mounts under `<base>/app/` and is reached from the report — the report links
14
+ forward to `app/`, the app links back to the report root.
13
15
 
14
16
  This is **project documentation, not a gated state** — there is no epic, no `state.json`, no approvals.
15
17
  It only reads the pipeline definition and writes a project-level site. When a docs target is connected
@@ -70,7 +72,8 @@ determinism rules as `yad-docs`: stable-ID sort by skill pipeline order / phase,
70
72
  timestamps in the data files), theme the `:root` of `index.css` from **yadflow's brand palette** — the
71
73
  the legacy report's `:root`: `--accent: #2471a3` and the node colors (`--artifact-*`, `--gate-*`,
72
74
  `--earns-*`, `--locked-*`, `--sentinel-*`) — and substitute the Vite base from `.sdlc/docs.json`
73
- `basePath` (the overview sits at the base root, e.g. `/<repo>/`).
75
+ `basePath` **with `app/` appended** (the SPA mounts under `<base>/app/`, e.g. `/<repo>/app/`, so the
76
+ report can own the root). `siteBasePath(docs, { overview: true })` in `cli/docs.mjs` computes this.
74
77
 
75
78
  ### Step 4 — Write the overview build manifest (the staleness baseline)
76
79
  Write `docs/sdlc-site/.docs-build.json` — `yad-docs-sync` compares against it:
@@ -91,13 +94,16 @@ doc-shell upgrade triggers a rebuild). `skillCount` rides along in the manifest
91
94
  — it is **not** a separate hash input, since `module-help.csv` already moves whenever the skill set does.
92
95
  Not per-epic artifacts/repo heads.
93
96
 
94
- ### Step 5 — Fold the legacy report into the site
95
- Relocate the hand-maintained static report into the generated site as `docs/sdlc-site/public/report.html`
96
- (Vite copies `public/` verbatim into `dist/`, so it publishes alongside the app at `<base>/report.html`),
97
- and link it from the app nav (a "Full report" link in `TopNavBar`). The interactive overview becomes the
98
- primary documentation and the legacy report rides along as its detailed companion no orphaned
99
- `docs/index.html` at the repo root. This generalizes the standing rule that feature work hand-updates the
100
- report: the overview site now **regenerates** instead.
97
+ ### Step 5 — The report is the main documentation; the SPA is reached from it
98
+ The hand-maintained static report lives at `docs/sdlc-site/public/report.html` and is the **primary
99
+ documentation at the Pages root**. The deploy (`BUILD_PUBLIC` in `cli/docs.mjs`, mirrored by the Pages CI
100
+ workflow) copies it to **both** `public/index.html` (the landing `<base>/`) and `public/report.html`
101
+ (so the `<base>/report.html` URL keeps working), and copies the built SPA into `public/app/`. Wire the
102
+ two cross-links: the report links **forward** to the interactive map with a relative `app/` href (its
103
+ hero CTA); the app's `TopNavBar` "Full report" link points **back** to the report root
104
+ (`import.meta.env.BASE_URL` with the trailing `app/` stripped). No orphaned `docs/index.html` at the repo
105
+ root. This generalizes the standing rule that feature work hand-updates the report: the overview SPA now
106
+ **regenerates** instead, while the report stays the front door.
101
107
 
102
108
  ### Step 6 — Build / deploy (`action`)
103
109
  - `action: generate` (default) — generate source + manifest; stop.
@@ -106,8 +112,9 @@ report: the overview site now **regenerates** instead.
106
112
 
107
113
  ### Step 7 — Stop. Report (no gate, no epic)
108
114
  Report: the site path (`docs/sdlc-site/`), the data files produced, that the theme is the yadflow brand
109
- palette, the deploy URL or "build-only", the staleness baseline, and that the legacy report is folded in
110
- at `public/report.html` (linked from the nav). Never touches any epic state.
115
+ palette, the deploy URL or "build-only", the staleness baseline, and that the report is the main
116
+ documentation at `<base>/` (`public/index.html` + `public/report.html`) with the interactive SPA mounted
117
+ under `<base>/app/` and cross-linked. Never touches any epic state.
111
118
 
112
119
  ## Hard rules
113
120