dev-loops 0.6.0 → 0.7.2

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 (203) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +8 -7
  3. package/.claude/agents/fixer.md +1 -0
  4. package/.claude/agents/refiner.md +2 -2
  5. package/.claude/agents/review.md +4 -3
  6. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  7. package/.claude/commands/loop-continue.md +15 -0
  8. package/.claude/commands/loop-enqueue.md +22 -0
  9. package/.claude/commands/loop-grill.md +17 -0
  10. package/.claude/commands/{info.md → loop-info.md} +2 -2
  11. package/.claude/commands/loop-queue-status.md +24 -0
  12. package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
  13. package/.claude/commands/{start.md → loop-start.md} +1 -1
  14. package/.claude/commands/{status.md → loop-status.md} +1 -1
  15. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  16. package/.claude/hooks/_hook-decisions.mjs +138 -15
  17. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  18. package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
  19. package/.claude/skills/dev-loop/SKILL.md +16 -12
  20. package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
  21. package/.claude/skills/docs/anti-patterns.md +5 -3
  22. package/.claude/skills/docs/artifact-authority-contract.md +30 -11
  23. package/.claude/skills/docs/confirmation-rules.md +1 -0
  24. package/.claude/skills/docs/contract-style-guide.md +37 -0
  25. package/.claude/skills/docs/copilot-loop-operations.md +19 -15
  26. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  27. package/.claude/skills/docs/debt-remediation-contract.md +1 -1
  28. package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
  29. package/.claude/skills/docs/issue-intake-procedure.md +26 -19
  30. package/.claude/skills/docs/local-planning-flow.md +1 -1
  31. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  32. package/.claude/skills/docs/merge-preconditions.md +30 -12
  33. package/.claude/skills/docs/plan-file-contract.md +1 -1
  34. package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
  35. package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
  36. package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
  37. package/.claude/skills/docs/spike-mode-contract.md +14 -7
  38. package/.claude/skills/docs/stop-conditions.md +30 -15
  39. package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
  40. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  41. package/.claude/skills/docs/validation-policy.md +4 -5
  42. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  43. package/.claude/skills/local-implementation/SKILL.md +62 -145
  44. package/.claude/skills/loop-grill/SKILL.md +163 -0
  45. package/AGENTS.md +1 -0
  46. package/CHANGELOG.md +101 -0
  47. package/README.md +9 -9
  48. package/agents/dev-loop.agent.md +11 -10
  49. package/agents/developer.agent.md +1 -1
  50. package/agents/docs.agent.md +1 -1
  51. package/agents/fixer.agent.md +2 -1
  52. package/agents/quality.agent.md +1 -1
  53. package/agents/refiner.agent.md +3 -3
  54. package/agents/review.agent.md +5 -4
  55. package/cli/index.mjs +35 -42
  56. package/extension/README.md +4 -4
  57. package/extension/checks.ts +1 -5
  58. package/extension/harness-types.ts +1 -0
  59. package/extension/index.ts +6 -0
  60. package/extension/pi-extension-adapter.ts +2 -0
  61. package/extension/presentation.ts +7 -19
  62. package/package.json +12 -12
  63. package/scripts/_core-helpers.mjs +2 -0
  64. package/scripts/claude/headless-dev-loop.mjs +1 -1
  65. package/scripts/docs/validate-rule-ownership.mjs +442 -0
  66. package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
  67. package/scripts/github/_review-thread-mutations.mjs +5 -2
  68. package/scripts/github/capture-review-threads.mjs +2 -9
  69. package/scripts/github/comment-issue.mjs +2 -9
  70. package/scripts/github/create-label.mjs +133 -0
  71. package/scripts/github/create-pr.mjs +119 -10
  72. package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
  73. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  74. package/scripts/github/edit-pr.mjs +259 -0
  75. package/scripts/github/fetch-ci-logs.mjs +2 -9
  76. package/scripts/github/list-issues.mjs +2 -9
  77. package/scripts/github/list-review-threads.mjs +277 -0
  78. package/scripts/github/manage-sub-issues.mjs +46 -10
  79. package/scripts/github/offer-human-handoff.mjs +8 -5
  80. package/scripts/github/post-gate-findings.mjs +20 -5
  81. package/scripts/github/probe-ci-status.mjs +8 -2
  82. package/scripts/github/probe-copilot-review.mjs +21 -14
  83. package/scripts/github/ready-for-review.mjs +26 -8
  84. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  85. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  86. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  87. package/scripts/github/request-copilot-review.mjs +98 -26
  88. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  89. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  90. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  91. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  92. package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
  93. package/scripts/github/verify-briefing-prefixes.mjs +191 -0
  94. package/scripts/github/verify-fresh-review-context.mjs +226 -32
  95. package/scripts/github/view-pr.mjs +150 -0
  96. package/scripts/github/wait-pr-checks.mjs +171 -0
  97. package/scripts/github/write-gate-context.mjs +458 -65
  98. package/scripts/github/write-gate-findings-log.mjs +128 -2
  99. package/scripts/lib/jq-output.mjs +18 -0
  100. package/scripts/loop/_post-convergence-change.mjs +211 -0
  101. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  102. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  103. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  104. package/scripts/loop/check-retro-tooling.mjs +14 -3
  105. package/scripts/loop/checkpoint-contract.mjs +7 -4
  106. package/scripts/loop/cleanup-worktree.mjs +12 -3
  107. package/scripts/loop/conductor-monitor.mjs +12 -18
  108. package/scripts/loop/copilot-pr-handoff.mjs +162 -14
  109. package/scripts/loop/debt-remediate.mjs +24 -12
  110. package/scripts/loop/detect-change-scope.mjs +38 -9
  111. package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
  112. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  113. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  114. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  115. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  116. package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
  117. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  118. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  119. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  120. package/scripts/loop/ensure-worktree.mjs +8 -3
  121. package/scripts/loop/info.mjs +12 -4
  122. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  123. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  124. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  125. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  126. package/scripts/loop/inspect-run.mjs +8 -2
  127. package/scripts/loop/outer-loop.mjs +8 -4
  128. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  129. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  130. package/scripts/loop/pre-flight-gate.mjs +9 -9
  131. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  132. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  133. package/scripts/loop/provision-worktree.mjs +74 -3
  134. package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
  135. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  136. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  137. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  138. package/scripts/loop/run-queue.mjs +18 -9
  139. package/scripts/loop/run-refinement-audit.mjs +8 -9
  140. package/scripts/loop/run-watch-cycle.mjs +2 -9
  141. package/scripts/loop/sanctioned-commands.mjs +106 -0
  142. package/scripts/loop/steer-loop.mjs +29 -16
  143. package/scripts/loop/validate-pr-body-spec.mjs +223 -0
  144. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  145. package/scripts/pages/build-site.mjs +59 -8
  146. package/scripts/pages/build-state-atlas.mjs +443 -0
  147. package/scripts/projects/_resolve-project.mjs +1 -0
  148. package/scripts/projects/add-queue-item.mjs +60 -54
  149. package/scripts/projects/archive-done-items.mjs +49 -84
  150. package/scripts/projects/ensure-queue-board.mjs +10 -36
  151. package/scripts/projects/list-queue-items.mjs +59 -382
  152. package/scripts/projects/move-queue-item.mjs +21 -449
  153. package/scripts/projects/reconcile-queue.mjs +253 -0
  154. package/scripts/projects/reorder-queue-item.mjs +43 -68
  155. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  156. package/scripts/projects/sync-item-status.mjs +15 -10
  157. package/scripts/refine/_refine-helpers.mjs +21 -5
  158. package/scripts/refine/exit-spike.mjs +18 -8
  159. package/scripts/refine/promote-plan.mjs +22 -16
  160. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  161. package/scripts/refine/refine-plan-file.mjs +13 -4
  162. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  163. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  164. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  165. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  166. package/scripts/refine/validate-plan-file.mjs +1 -1
  167. package/scripts/refine/validate-spike-file.mjs +1 -1
  168. package/scripts/refine/verify.mjs +11 -6
  169. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  170. package/scripts/release/extract-changelog-section.mjs +16 -2
  171. package/skills/copilot-pr-followup/SKILL.md +60 -34
  172. package/skills/dev-loop/SKILL.md +11 -7
  173. package/skills/docs/acceptance-criteria-verification.md +8 -3
  174. package/skills/docs/anti-patterns.md +5 -3
  175. package/skills/docs/artifact-authority-contract.md +30 -11
  176. package/skills/docs/confirmation-rules.md +1 -0
  177. package/skills/docs/contract-style-guide.md +37 -0
  178. package/skills/docs/copilot-loop-operations.md +19 -15
  179. package/skills/docs/cross-harness-regression-contract.md +60 -0
  180. package/skills/docs/debt-remediation-contract.md +1 -1
  181. package/skills/docs/epic-tree-refinement-procedure.md +25 -22
  182. package/skills/docs/issue-intake-procedure.md +26 -19
  183. package/skills/docs/local-planning-flow.md +1 -1
  184. package/skills/docs/local-planning-worked-example.md +1 -1
  185. package/skills/docs/merge-preconditions.md +30 -12
  186. package/skills/docs/plan-file-contract.md +1 -1
  187. package/skills/docs/pr-lifecycle-contract.md +44 -35
  188. package/skills/docs/public-dev-loop-contract.md +60 -44
  189. package/skills/docs/required-rules.json +149 -0
  190. package/skills/docs/retrospective-checkpoint-contract.md +106 -79
  191. package/skills/docs/spike-mode-contract.md +14 -7
  192. package/skills/docs/stop-conditions.md +30 -15
  193. package/skills/docs/tracker-first-loop-state.md +13 -8
  194. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  195. package/skills/docs/validation-policy.md +4 -5
  196. package/skills/docs/workflow-handoff-contract.md +39 -0
  197. package/skills/local-implementation/SKILL.md +62 -145
  198. package/skills/loop-grill/SKILL.md +165 -0
  199. package/.claude/commands/continue.md +0 -15
  200. package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
  201. package/scripts/loop/conductor.mjs +0 -233
  202. package/scripts/loop/detect-stale-runner.mjs +0 -265
  203. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -8,6 +8,7 @@
8
8
  import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
9
9
  import { dirname, join, resolve, parse as parsePath } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { buildStateAtlasHtml } from './build-state-atlas.mjs';
11
12
 
12
13
  const REPO_ROOT_DEFAULT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
13
14
 
@@ -46,27 +47,59 @@ export const DECKS = [
46
47
  // Resolve a deck's published filename: distinct outFile when set, else file.
47
48
  const deckOut = (deck) => deck.outFile ?? deck.file;
48
49
 
50
+ // The State atlas: a generated page (site/state-atlas.html) rendering every
51
+ // dev-loops state machine as mermaid diagrams straight from the code's tables.
52
+ export const STATE_ATLAS = { file: 'state-atlas.html', label: 'State atlas' };
53
+
49
54
  // The other resources linked from the navigation, in order.
50
55
  export const NAV_LINKS = [
51
56
  ...ARTICLES.map((a) => ({ file: a.file, label: a.navLabel })),
52
57
  ...DECKS.map((d) => ({ file: deckOut(d), label: d.navLabel })),
58
+ { file: STATE_ATLAS.file, label: STATE_ATLAS.label },
53
59
  ];
54
60
 
55
61
  // Nav styling, appended to each article page's own <style> block so it reuses
56
62
  // the article design-system variables (--heading/--kicker/--accent-soft).
57
63
  const NAV_CSS = `
58
- .site-nav { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1.1rem; max-width: 64rem; margin: 0 auto; padding: 0.9rem clamp(1.1rem, 5vw, 2rem); border-bottom: 1px solid rgba(148, 163, 184, 0.16); }
64
+ .site-nav { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1.1rem; max-width: 48rem; margin: 0 auto; padding: 0.9rem clamp(1.1rem, 5vw, 2rem); border-bottom: 1px solid rgba(148, 163, 184, 0.16); }
65
+ @media (min-width: 900px) { .site-nav { max-width: 56rem; } }
59
66
  .site-nav-brand { font-weight: 700; letter-spacing: -0.01em; color: var(--heading); text-decoration: none; border: 0; margin-right: auto; }
60
67
  .site-nav-links { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; }
61
68
  .site-nav a { color: var(--kicker); text-decoration: none; font-size: 0.9rem; border: 0; }
62
- .site-nav a:hover { color: var(--accent-soft); }`;
69
+ .site-nav a:hover { color: var(--accent-soft); }
70
+ .site-nav-gh { display: inline-flex; align-items: center; }
71
+ .site-nav-gh svg { width: 1.125rem; height: 1.125rem; fill: currentColor; display: block; }`;
72
+
73
+ // The repository the site links to from its nav. Derived from the effective
74
+ // repoRoot's package.json repository.url (single source of truth) so a repo
75
+ // rename/move updates the nav link too, rather than drifting from a hardcoded
76
+ // copy. Resolved inside buildSite from its repoRoot param (not at module load)
77
+ // so --repo-root / buildSite({ repoRoot }) reads the right package.json and no
78
+ // I/O runs merely on import. The '.git' suffix is stripped for the web URL.
79
+ // npm's `repository` field may be a string ("github:owner/repo" or a full URL)
80
+ // or an object { type, url }. Accept both; fail with an explicit, actionable
81
+ // message when neither yields a URL, rather than an implicit TypeError.
82
+ export async function resolveRepoUrl(repoRoot) {
83
+ const { repository } = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
84
+ const raw = typeof repository === 'string' ? repository : repository?.url;
85
+ if (!raw) {
86
+ throw new Error(`cannot resolve repo URL: package.json at ${repoRoot} has no repository.url`);
87
+ }
88
+ return raw
89
+ .replace(/^github:/, 'https://github.com/')
90
+ .replace(/\.git$/, '');
91
+ }
92
+ // Inline SVG (GitHub octicon mark) so it renders under the page's strict CSP
93
+ // (img-src is data:-only; no external icon).
94
+ const GITHUB_ICON = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>';
63
95
 
64
- function navMarkup() {
96
+ function navMarkup(repoUrl) {
65
97
  const links = NAV_LINKS.map((l) => ` <a href="${l.file}">${l.label}</a>`).join('\n');
66
98
  return `<nav class="site-nav" aria-label="dev-loops resources">
67
99
  <a class="site-nav-brand" href="index.html">dev-loops</a>
68
100
  <div class="site-nav-links">
69
101
  ${links}
102
+ <a class="site-nav-gh" href="${repoUrl}" aria-label="dev-loops on GitHub">${GITHUB_ICON}</a>
70
103
  </div>
71
104
  </nav>`;
72
105
  }
@@ -75,13 +108,13 @@ ${links}
75
108
  // markup right after <body>. Idempotent enough for assembly (each source file
76
109
  // is read once). Throws if the page lacks the expected anchors so a structural
77
110
  // drift fails the build rather than publishing an un-navigable page.
78
- export function injectNav(html) {
111
+ export function injectNav(html, repoUrl) {
79
112
  if (!html.includes('</style>') || !/<body[^>]*>/.test(html)) {
80
113
  throw new Error('cannot inject nav: page is missing a <style> block or <body> tag');
81
114
  }
82
115
  return html
83
116
  .replace('</style>', `${NAV_CSS}\n</style>`)
84
- .replace(/<body([^>]*)>/, `<body$1>\n ${navMarkup()}`);
117
+ .replace(/<body([^>]*)>/, `<body$1>\n ${navMarkup(repoUrl)}`);
85
118
  }
86
119
 
87
120
  export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
@@ -100,14 +133,16 @@ export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
100
133
  await rm(out, { recursive: true, force: true });
101
134
  await mkdir(out, { recursive: true });
102
135
 
136
+ const repoUrl = await resolveRepoUrl(repoRoot);
137
+
103
138
  // Landing page: the intro article, navigable, published as index.html.
104
139
  const landingHtml = await readFile(join(articlesDir, LANDING.file), 'utf8');
105
- await writeFile(join(out, 'index.html'), injectNav(landingHtml), 'utf8');
140
+ await writeFile(join(out, 'index.html'), injectNav(landingHtml, repoUrl), 'utf8');
106
141
 
107
142
  // Deep-dive article: published with the same nav so the set is navigable.
108
143
  for (const article of ARTICLES) {
109
144
  const html = await readFile(join(articlesDir, article.file), 'utf8');
110
- await writeFile(join(out, article.file), injectNav(html), 'utf8');
145
+ await writeFile(join(out, article.file), injectNav(html, repoUrl), 'utf8');
111
146
  }
112
147
 
113
148
  // Decks: self-contained slide renders, copied as-is (no nav injection).
@@ -115,9 +150,25 @@ export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
115
150
  await cp(join(decksDir, deck.file), join(out, deckOut(deck)));
116
151
  }
117
152
 
153
+ // State atlas: generated from the core state tables at build time, then given
154
+ // the same shared nav as the article pages so it can never drift from the code.
155
+ await writeFile(join(out, STATE_ATLAS.file), injectNav(buildStateAtlasHtml(), repoUrl), 'utf8');
156
+
157
+ // Vendored mermaid runtime the atlas page references (Pages has no CSP, so we
158
+ // load it as an external asset rather than inlining ~3MB into the page).
159
+ const mermaidSrc = join(repoRoot, 'scripts', 'loop', 'inspect-run-viewer', 'vendor', 'mermaid.min.js');
160
+ await mkdir(join(out, 'assets'), { recursive: true });
161
+ await cp(mermaidSrc, join(out, 'assets', 'mermaid.min.js'));
162
+
118
163
  return {
119
164
  out,
120
- files: ['index.html', ...ARTICLES.map((a) => a.file), ...DECKS.map((d) => deckOut(d))],
165
+ files: [
166
+ 'index.html',
167
+ ...ARTICLES.map((a) => a.file),
168
+ ...DECKS.map((d) => deckOut(d)),
169
+ STATE_ATLAS.file,
170
+ 'assets/mermaid.min.js',
171
+ ],
121
172
  };
122
173
  }
123
174
 
@@ -0,0 +1,443 @@
1
+ // Generates the "State atlas" page for the GitHub Pages site: every dev-loops
2
+ // state machine rendered as a mermaid diagram, emitted DETERMINISTICALLY from
3
+ // the code's own exported tables so the page can never drift from the code.
4
+ //
5
+ // Five diagrams come straight from packages/core/src/loop tables (copilot loop,
6
+ // reviewer loop, outer conductor routing, the public dev-loop gate hub, and the
7
+ // PR lifecycle contract); one more (the release pipeline workflow) is authored
8
+ // from a documented, table-less source.
9
+ //
10
+ // The page reuses the article design system (docs/articles/introducing-dev-loops.html):
11
+ // the same :root tokens, body gradient, and typography. It intentionally defines
12
+ // a <style> block and a <body> tag so build-site.mjs can inject the shared nav
13
+ // (NAV_CSS references --heading/--kicker/--accent-soft, all declared here).
14
+ import { STATE, TRANSITIONS } from '../../packages/core/src/loop/copilot-loop-state.mjs';
15
+ import { REVIEWER_STATE, REVIEWER_TRANSITIONS } from '../../packages/core/src/loop/reviewer-loop-state.mjs';
16
+ import { OUTER_STATE, OUTER_TRANSITIONS } from '../../packages/core/src/loop/conductor-routing.mjs';
17
+ import { PUBLIC_DEV_LOOP_GATE_CONTRACT } from '../../packages/core/src/loop/public-dev-loop-routing-contract.mjs';
18
+ import { PR_LIFECYCLE_STATES, PR_LIFECYCLE_TRANSITIONS } from '../../packages/core/src/loop/pr-lifecycle.mjs';
19
+
20
+ // Classify a state/gate id by name into one of four visual classes. Colors are
21
+ // drawn from the site's own dark palette (accent violet, kicker blue,
22
+ // accent-soft, and the card-border slate) so diagrams sit natively in the skin.
23
+ const WAIT = /waiting|watch/;
24
+ const STOP = /blocked|unavailable|stop|needs_human|reconcile|invalidated|conflict/;
25
+ const TERM = /done|terminal|complete|merged/;
26
+ // TERM is tested before STOP so names carrying both (e.g. stop_done_terminal)
27
+ // classify as terminal, not blocked. `conflict` is a stop keyword: a conflicted
28
+ // head is a blocked/detour state, never active progress.
29
+ function classify(name) {
30
+ if (WAIT.test(name)) return 'wait';
31
+ if (TERM.test(name)) return 'term';
32
+ if (STOP.test(name)) return 'stop';
33
+ return 'act';
34
+ }
35
+
36
+ // classDef colors sourced from the article design tokens: --accent #a78bfa,
37
+ // --kicker #93c5fd, --accent-soft #ddd6fe, slate #94a3b8, ground #0f172a.
38
+ const CLASSDEFS = [
39
+ ' classDef act fill:#171532,stroke:#a78bfa,stroke-width:1px,color:#e5e7eb;',
40
+ ' classDef wait fill:#0f1d33,stroke:#93c5fd,stroke-width:1px,color:#e5e7eb;',
41
+ ' classDef term fill:#1c1a3a,stroke:#ddd6fe,stroke-width:1.5px,color:#f8fafc;',
42
+ ' classDef stop fill:#1a2233,stroke:#94a3b8,stroke-width:1px,color:#cbd5e1,stroke-dasharray:4 2;',
43
+ ];
44
+
45
+ // A state with no legal transitions in its table is terminal → edge to [*].
46
+ function edgesFromTransitions(values, transitions) {
47
+ const edges = [];
48
+ for (const s of values) {
49
+ const targets = transitions[s] ?? [];
50
+ if (targets.length === 0) edges.push([s, '[*]']);
51
+ else for (const t of targets) edges.push([s, t]);
52
+ }
53
+ return edges;
54
+ }
55
+
56
+ // Note: mermaid sources are HTML-escaped (& then <) when embedded into the page
57
+ // (see sectionMarkup), so the browser's parser hands mermaid the exact source
58
+ // back via textContent. Every id here is snake_case and every label is HTML-free,
59
+ // so the escaping is defense-in-depth against future drift, not load-bearing:
60
+ // today's sources contain `>` (in `-->` arrows) but never `<` or `&`, and the
61
+ // arrows survive verbatim in the built bytes for the atlas tests.
62
+ function renderStateDiagram(edges, states) {
63
+ const lines = ['stateDiagram-v2'];
64
+ for (const [a, b] of edges) lines.push(` ${a} --> ${b}`);
65
+ lines.push(...CLASSDEFS);
66
+ for (const s of states) lines.push(` class ${s} ${classify(s)}`);
67
+ return lines.join('\n');
68
+ }
69
+
70
+ // The public dev-loop gate contract as a router → gates flowchart hub: one edge
71
+ // per row, labelled with routeKind; each gate node shows gate id + strategy.
72
+ // Gates carry their own routeKind, so color by it (route→act, wait→wait,
73
+ // stop/needs_reconcile→stop) instead of the name heuristic — e.g.
74
+ // waiting_for_merge_authorization names "waiting" but its routeKind is stop.
75
+ // Terminal-named gates (stop_done_terminal) still classify as term.
76
+ function gateClass(row) {
77
+ if (TERM.test(row.gate)) return 'term';
78
+ if (row.routeKind === 'route') return 'act';
79
+ if (row.routeKind === 'wait') return 'wait';
80
+ return 'stop';
81
+ }
82
+
83
+ function renderGateFlowchart(contract) {
84
+ const lines = ['flowchart LR', ' router(["dev-loop router"])'];
85
+ for (const row of contract) {
86
+ const strategy = row.selectedStrategy ?? 'none';
87
+ lines.push(` router -->|${row.routeKind}| ${row.gate}["${row.gate} — ${strategy}"]`);
88
+ }
89
+ lines.push(...CLASSDEFS);
90
+ for (const row of contract) lines.push(` class ${row.gate} ${gateClass(row)}`);
91
+ return lines.join('\n');
92
+ }
93
+
94
+ // --- Code-derived diagrams (single source of truth = the exported tables) ---
95
+ const copilotDiagram = renderStateDiagram(
96
+ edgesFromTransitions(Object.values(STATE), TRANSITIONS),
97
+ Object.values(STATE),
98
+ );
99
+ const reviewerDiagram = renderStateDiagram(
100
+ edgesFromTransitions(Object.values(REVIEWER_STATE), REVIEWER_TRANSITIONS),
101
+ Object.values(REVIEWER_STATE),
102
+ );
103
+ const outerDiagram = renderStateDiagram(
104
+ edgesFromTransitions(Object.values(OUTER_STATE), OUTER_TRANSITIONS),
105
+ Object.values(OUTER_STATE),
106
+ );
107
+ const gateDiagram = renderGateFlowchart(PUBLIC_DEV_LOOP_GATE_CONTRACT);
108
+
109
+ // PR lifecycle: the 13-state vocabulary + required transitions exported by
110
+ // packages/core/src/loop/pr-lifecycle.mjs (issue #1193), re-exported here so
111
+ // this module's public surface is unchanged. The L2/L3 state-machine
112
+ // conformance harness (scripts/docs/validate-state-machine-conformance.mjs)
113
+ // imports the same core module directly rather than through this one, so it
114
+ // never pulls in this page generator's diagram-rendering work at load time.
115
+ export { PR_LIFECYCLE_STATES, PR_LIFECYCLE_TRANSITIONS };
116
+ const prLifecycleDiagram = renderStateDiagram(PR_LIFECYCLE_TRANSITIONS, PR_LIFECYCLE_STATES);
117
+
118
+ // --- Statically-authored diagram (documented, table-less source) ---
119
+
120
+ // Release pipeline: the fail-closed gate chain from .github/workflows/release.yml.
121
+ const releasePipelineNodes = [
122
+ 'push_tag',
123
+ 'ancestry',
124
+ 'lockstep',
125
+ 'exists',
126
+ 'changelog',
127
+ 'create',
128
+ 'done_release_published',
129
+ 'done_idempotent_skip',
130
+ 'stop_not_on_main',
131
+ 'stop_core_mismatch',
132
+ 'stop_changelog_missing',
133
+ ];
134
+ const releaseDiagram = [
135
+ 'flowchart TD',
136
+ ' push_tag(["push v* tag"])',
137
+ ' push_tag --> ancestry{"commit is ancestor of origin/main?"}',
138
+ ' ancestry -->|no| stop_not_on_main["stopped: not on main"]',
139
+ ' ancestry -->|yes| lockstep{"@dev-loops/core dep in lockstep?"}',
140
+ ' lockstep -->|no| stop_core_mismatch["stopped: core version mismatch"]',
141
+ ' lockstep -->|yes| exists{"release already exists?"}',
142
+ ' exists -->|yes| done_idempotent_skip["done: idempotent skip"]',
143
+ ' exists -->|no| changelog{"CHANGELOG section extractable?"}',
144
+ ' changelog -->|no| stop_changelog_missing["stopped: changelog missing"]',
145
+ ' changelog -->|yes| create["gh release create --verify-tag"]',
146
+ ' create --> done_release_published["done: release published, fires npm-publish"]',
147
+ ...CLASSDEFS,
148
+ ...releasePipelineNodes.map((n) => ` class ${n} ${classify(n)}`),
149
+ ].join('\n');
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Hand-authored explanatory prose.
153
+ //
154
+ // This is the ONLY hand-written content on the page: the diagrams above are
155
+ // generated from the code tables; the words below are authored from the
156
+ // documented contracts. Keeping the two apart in this module makes the split
157
+ // explicit — if a diagram and its prose disagree, the diagram (the code) wins.
158
+ // ---------------------------------------------------------------------------
159
+
160
+ const INTRO_PROSE = [
161
+ 'dev-loops drives every pull request through closed, deterministic state machines. Workflow control lives in the graph; agent judgment enters only as bounded, explicit inputs, never as hidden orchestration. Exactly one state applies at a time, and each state exposes the legal transitions out of it.',
162
+ "Every diagram on this page is generated at site-build time from the code's own exported tables — the copilot loop's <code>STATE</code>/<code>TRANSITIONS</code>, the reviewer loop's <code>REVIEWER_STATE</code>/<code>REVIEWER_TRANSITIONS</code>, the outer loop's <code>OUTER_STATE</code>/<code>OUTER_TRANSITIONS</code>, the public router's <code>PUBLIC_DEV_LOOP_GATE_CONTRACT</code>, and the PR lifecycle's <code>PR_LIFECYCLE_STATES</code>/<code>PR_LIFECYCLE_TRANSITIONS</code>. They cannot drift from the code: change a transition table and this page changes with it. One diagram (the release pipeline) is authored from a documented contract that has no single code table.",
163
+ 'The state names are not labels for a picture — they are the literal contract identifiers that appear in logs, handoff envelopes, and gate artifacts. Nodes are coloured by role: <strong>active / in-progress</strong> steps the loop advances itself, <strong>waiting-on-external</strong> states that block on something outside the loop (CI, Copilot, a human), <strong>blocked / fail-closed</strong> states that stop or reconcile rather than guess, and <strong>terminal</strong> states where the slice is complete.',
164
+ ];
165
+
166
+ const SECTIONS = [
167
+ {
168
+ id: 'public-gate-hub',
169
+ title: 'Public dev-loop gate hub',
170
+ source: 'packages/core/src/loop/public-dev-loop-routing-contract.mjs — PUBLIC_DEV_LOOP_GATE_CONTRACT',
171
+ prose: [
172
+ 'The single public entrypoint, <code>dev-loop</code>, resolves the canonical current state to exactly one of eleven gates. Each gate carries a closed route-kind — <code>route</code> to an internal strategy, <code>stop</code> for a human decision or terminal work, or <code>wait</code> on an external signal — and the internal strategy it selects.',
173
+ 'This is the table the startup resolver walks on every invocation: it is not a one-time setup step but the routing decision re-made each time. Ambiguous, conflicting, or unsupported state does not get a guessed route — it fails closed to <code>fail_closed_reconcile</code>. Edge labels below are the route-kind; each node names the gate and the strategy it selects.',
174
+ ],
175
+ diagram: gateDiagram,
176
+ },
177
+ {
178
+ id: 'outer-routing',
179
+ title: 'Outer conductor routing',
180
+ source: 'packages/core/src/loop/conductor-routing.mjs — OUTER_STATE + OUTER_TRANSITIONS',
181
+ prose: [
182
+ 'Above the family-local machines, the outer conductor derives one routing outcome per tick: continue waiting, hand off to the Copilot loop, hand off to the reviewer loop, or stay with a live owner. The four active outcomes form a fully-connected core by design — routing is stateless per tick, so each tick re-derives the outcome from fresh detector state rather than following a remembered path.',
183
+ 'The three remaining outcomes — <code>stop_needs_human</code>, <code>done_terminal</code>, and <code>needs_reconcile</code> — are absorbing: once reached, the outer loop does not route onward on its own.',
184
+ ],
185
+ diagram: outerDiagram,
186
+ },
187
+ {
188
+ id: 'pr-lifecycle',
189
+ title: 'PR lifecycle contract',
190
+ source: 'packages/core/src/loop/pr-lifecycle.mjs — PR_LIFECYCLE_STATES + PR_LIFECYCLE_TRANSITIONS',
191
+ prose: [
192
+ 'One PR moves through a stable thirteen-state vocabulary from draft to merge. These identifiers are part of the contract surface even as the helper implementations around them change. Two local gates guard the path — <code>draft_gate</code> (draft to ready-for-review) and <code>pre_approval_gate</code> (before final approval) — and both are fail-closed fan-out reviews that run independent angle chains and must produce clean current-head evidence to pass.',
193
+ 'All gate evidence is per-head: a new push re-opens the gates, and ready-to-draft resets the lifecycle back into draft-stage gating. A conflicted head detours through <code>merge_conflict_resolution</code> before any further gate progression — a conflicted PR is never treated as approval- or merge-ready, even if older gate comments and CI were green. Human approval and merge are explicit external waits, not hidden remediation states.',
194
+ ],
195
+ diagram: prLifecycleDiagram,
196
+ },
197
+ {
198
+ id: 'copilot-loop',
199
+ title: 'Copilot review/fix loop',
200
+ source: 'packages/core/src/loop/copilot-loop-state.mjs — STATE + TRANSITIONS',
201
+ prose: [
202
+ 'The inner loop requests Copilot review, watches for it, and remediates — all scoped to one PR head. Unresolved feedback always routes into fix and reply/resolve, never into a wait: the loop does not sleep on a PR that has open threads. Only once threads are resolved and CI is settled does it re-request or converge.',
203
+ 'Re-requesting is bounded by <code>maxCopilotRounds</code>. At the cap a clean PR routes to <code>round_cap_clean_fallback</code> (proceed to the pre-approval gate) rather than dead-ending on a review that can never come, while a not-clean PR hard-stops at <code>round_cap_reached</code>; a significant head change is the escape hatch that re-opens auto re-request. A separate low-signal heuristic can end the loop early at <code>low_signal_converged</code> when extra rounds yield only minimal actionable feedback. These cap and convergence states are the ones older prose graphs omit.',
204
+ ],
205
+ diagram: copilotDiagram,
206
+ },
207
+ {
208
+ id: 'reviewer-loop',
209
+ title: 'Reviewer loop',
210
+ source: 'packages/core/src/loop/reviewer-loop-state.mjs — REVIEWER_STATE + REVIEWER_TRANSITIONS',
211
+ prose: [
212
+ 'The reviewer side plans a bounded set of review angles, runs them in parallel, merges the results into one draft review, and posts it — then submits. The external-wait boundaries are explicitly named states (<code>waiting_for_review_request</code>, <code>waiting_for_user_submit</code>) rather than implicit pauses, so a wait on a human is always a distinct, inspectable state.',
213
+ 'If the reviewed head goes stale, <code>review_invalidated</code> discards the pending draft and re-enters at <code>review_requested</code> so the review is always produced for the current head.',
214
+ ],
215
+ diagram: reviewerDiagram,
216
+ },
217
+ {
218
+ id: 'release-pipeline',
219
+ title: 'Release pipeline',
220
+ source: '.github/workflows/release.yml (documented workflow)',
221
+ prose: [
222
+ 'Cutting a release is milestone-terminal. The version-bump PR itself goes through the same full gate pipeline as any other change — there is no privileged path to main. Pushing a <code>v*</code> tag is the only manual release step.',
223
+ 'Everything after the tag fails closed: the release commit must be an ancestor of <code>origin/main</code>, the shipped <code>@dev-loops/core</code> dependency must be in major.minor lockstep with the release, an existing release short-circuits idempotently, and the CHANGELOG section must extract before <code>gh release create</code> runs. Any failed check stops the release rather than publishing a partial one.',
224
+ ],
225
+ diagram: releaseDiagram,
226
+ },
227
+ ];
228
+
229
+ function proseParagraphs(prose) {
230
+ return prose.map((p) => ` <p>${p}</p>`).join('\n');
231
+ }
232
+
233
+ // Escape mermaid source for HTML embedding: & first, then <. The HTML parser
234
+ // unescapes these before mermaid reads the element's textContent, so mermaid
235
+ // sees the exact original source (and `-->` arrows pass through untouched).
236
+ const escapeMermaid = (src) => src.replace(/&/g, '&amp;').replace(/</g, '&lt;');
237
+
238
+ function sectionMarkup(s) {
239
+ return ` <section class="atlas-section" id="${s.id}">
240
+ <h2>${s.title}</h2>
241
+ <p class="source">Diagram generated from <code>${s.source}</code></p>
242
+ ${proseParagraphs(s.prose)}
243
+ <div class="diagram"><button class="expand" type="button" aria-label="View diagram fullscreen">⤢ Fullscreen</button><div class="mermaid">
244
+ ${escapeMermaid(s.diagram)}
245
+ </div></div>
246
+ </section>`;
247
+ }
248
+
249
+ // The colour legend mirrors the four classDef classes so readers can map a node
250
+ // colour to its role. Swatch colours are the classDef stroke colours above.
251
+ const LEGEND = [
252
+ { cls: 'act', label: 'active / in-progress', stroke: '#a78bfa' },
253
+ { cls: 'wait', label: 'waiting on external', stroke: '#93c5fd' },
254
+ { cls: 'stop', label: 'blocked / fail-closed', stroke: '#94a3b8' },
255
+ { cls: 'term', label: 'terminal', stroke: '#ddd6fe' },
256
+ ];
257
+
258
+ function legendMarkup() {
259
+ const items = LEGEND.map(
260
+ (l) => ` <li><span class="swatch" style="border-color:${l.stroke}" aria-hidden="true"></span> ${l.label}</li>`,
261
+ ).join('\n');
262
+ return ` <ul class="legend" aria-label="diagram colour key">
263
+ ${items}
264
+ </ul>`;
265
+ }
266
+
267
+ /**
268
+ * Build the full State atlas page HTML (deterministic; no timestamps/randomness).
269
+ *
270
+ * The returned HTML has a single <style> block and a <body> tag so build-site's
271
+ * injectNav can attach the shared nav. It references the vendored mermaid via
272
+ * <script src="assets/mermaid.min.js"> (copied into site/ by build-site) rather
273
+ * than inlining ~3MB.
274
+ *
275
+ * The CSP meta mirrors the article pages' policy (default-src 'none';
276
+ * 'unsafe-inline' styles; data: images; locked base/form) and extends it with
277
+ * script-src 'self' (the copied mermaid asset) + 'unsafe-inline' (the init
278
+ * script below), matching the articles' inline approach — they use
279
+ * 'unsafe-inline', not hash sources.
280
+ *
281
+ * @returns {string}
282
+ */
283
+ export function buildStateAtlasHtml() {
284
+ const intro = INTRO_PROSE.map((p) => ` <p class="lede">${p}</p>`).join('\n');
285
+ const sections = SECTIONS.map(sectionMarkup).join('\n');
286
+ return `<!DOCTYPE html>
287
+ <html lang="en">
288
+ <head>
289
+ <meta charset="utf-8" />
290
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
291
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'" />
292
+ <title>State atlas — dev-loops</title>
293
+ <style>
294
+ :root {
295
+ --ground-1: #08101f;
296
+ --ground-2: #0b1220;
297
+ --ground-3: #0f172a;
298
+ --ink: #e5e7eb;
299
+ --heading: #f8fafc;
300
+ --copy: #cbd5e1;
301
+ --accent: #a78bfa;
302
+ --accent-soft: #ddd6fe;
303
+ --kicker: #93c5fd;
304
+ --card-border: rgba(148, 163, 184, 0.18);
305
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Helvetica Neue", sans-serif;
306
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
307
+ }
308
+
309
+ * { box-sizing: border-box; }
310
+
311
+ body {
312
+ margin: 0;
313
+ font-family: var(--font);
314
+ color: var(--ink);
315
+ overflow-x: hidden;
316
+ -webkit-text-size-adjust: 100%;
317
+ background:
318
+ radial-gradient(circle at 85% 4%, rgba(139, 92, 246, 0.22), transparent 26%),
319
+ radial-gradient(circle at 12% 2%, rgba(59, 130, 246, 0.16), transparent 22%),
320
+ linear-gradient(180deg, var(--ground-1) 0%, var(--ground-2) 38%, var(--ground-3) 100%);
321
+ background-attachment: fixed;
322
+ }
323
+
324
+ .wrap {
325
+ max-width: 48rem;
326
+ margin: 0 auto;
327
+ padding: clamp(2rem, 6vw, 4rem) clamp(1.1rem, 5vw, 2rem) 5rem;
328
+ }
329
+ @media (min-width: 900px) {
330
+ .wrap { max-width: 64rem; }
331
+ }
332
+
333
+ .kicker {
334
+ text-transform: uppercase;
335
+ letter-spacing: 0.14em;
336
+ font-size: 0.72rem;
337
+ color: var(--kicker);
338
+ margin: 0 0 0.6rem;
339
+ font-weight: 600;
340
+ }
341
+
342
+ h1, h2, h3 { color: var(--heading); letter-spacing: -0.02em; margin-top: 0; }
343
+ h1 { font-weight: 760; font-size: clamp(1.9rem, 5vw, 2.7rem); line-height: 1.08; margin-bottom: 0.9rem; }
344
+ h2 { font-weight: 720; font-size: clamp(1.35rem, 3.4vw, 1.85rem); line-height: 1.15; margin: 0 0 0.6rem; }
345
+
346
+ p { line-height: 1.72; font-size: 1.05rem; color: var(--ink); margin: 0 0 1.15rem; }
347
+ strong { color: var(--accent-soft); }
348
+ .lede { color: var(--copy); font-size: clamp(1.02rem, 2.2vw, 1.18rem); line-height: 1.5; margin: 0 0 1.4rem; }
349
+ .source { color: var(--copy); font-size: 0.85rem; margin: 0 0 0.6rem; }
350
+
351
+ code {
352
+ font-family: var(--mono);
353
+ font-size: 0.9em;
354
+ background: rgba(148, 163, 184, 0.12);
355
+ border: 1px solid rgba(148, 163, 184, 0.16);
356
+ border-radius: 6px;
357
+ padding: 0.05em 0.4em;
358
+ color: #e2e8f0;
359
+ overflow-wrap: anywhere;
360
+ }
361
+
362
+ .legend { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; padding: 0; margin: 0 0 2.6rem; font-size: 0.9rem; color: var(--copy); }
363
+ .legend li { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0; }
364
+ .legend .swatch { width: 0.85rem; height: 0.85rem; border-radius: 4px; border: 2px solid; background: rgba(15, 23, 42, 0.6); display: inline-block; }
365
+
366
+ .atlas-section { margin: 0 0 3rem; }
367
+
368
+ .diagram {
369
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.82), rgba(15, 23, 42, 0.6));
370
+ border: 1px solid var(--card-border);
371
+ border-radius: 14px;
372
+ padding: 1.15rem;
373
+ overflow-x: auto;
374
+ }
375
+ .diagram .mermaid { text-align: center; }
376
+ .diagram .mermaid svg { max-width: 100%; height: auto; }
377
+ .diagram { position: relative; }
378
+ .diagram .expand { position: sticky; float: right; top: 0.6rem; right: 0.6rem; margin: 0 0 -2rem; z-index: 2; font: 600 0.72rem var(--font); color: var(--copy); background: rgba(15, 23, 42, 0.85); border: 1px solid var(--card-border); border-radius: 8px; padding: 0.35rem 0.6rem; cursor: pointer; }
379
+ .diagram .expand:hover { color: var(--heading); border-color: var(--accent); }
380
+ .diagram .mermaid { cursor: zoom-in; }
381
+ .diagram .mermaid:fullscreen { cursor: zoom-out; background: #0b1220; display: flex; align-items: center; justify-content: center; padding: 2rem; }
382
+ .diagram .mermaid:-webkit-full-screen { cursor: zoom-out; background: #0b1220; display: flex; align-items: center; justify-content: center; padding: 2rem; }
383
+ .diagram .mermaid:-webkit-full-screen svg { max-width: calc(100vw - 4rem); max-height: calc(100vh - 4rem); width: auto; height: auto; }
384
+ .diagram .mermaid:fullscreen svg { max-width: calc(100vw - 4rem); max-height: calc(100vh - 4rem); width: auto; height: auto; }
385
+ .diagram .mermaid.fs-fallback { position: fixed; inset: 0; z-index: 50; background: #0b1220; display: flex; align-items: center; justify-content: center; padding: 1rem; cursor: zoom-out; overflow: auto; }
386
+ .diagram .mermaid.fs-fallback svg { max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); width: auto; height: auto; }
387
+ </style>
388
+ </head>
389
+ <body>
390
+ <main class="wrap">
391
+ <p class="kicker">dev-loops</p>
392
+ <h1>State atlas</h1>
393
+ ${intro}
394
+ ${legendMarkup()}
395
+ ${sections}
396
+ </main>
397
+ <script src="assets/mermaid.min.js"></script>
398
+ <script>
399
+ let fsInFlight = false;
400
+ document.addEventListener('click', async (e) => {
401
+ if (fsInFlight) return;
402
+ const target = e.target instanceof Element ? e.target : e.target?.parentElement;
403
+ if (!target) return;
404
+ const btn = target.closest('.diagram .expand');
405
+ const box = btn ? btn.parentElement.querySelector('.mermaid') : target.closest('.diagram .mermaid');
406
+ if (!box) return;
407
+ fsInFlight = true;
408
+ try {
409
+ const fsElement = document.fullscreenElement || document.webkitFullscreenElement;
410
+ if (fsElement === box) {
411
+ await (document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
412
+ return;
413
+ }
414
+ if (box.classList.contains('fs-fallback')) { box.classList.remove('fs-fallback'); return; }
415
+ const request = box.requestFullscreen || box.webkitRequestFullscreen;
416
+ if (request) await request.call(box);
417
+ else box.classList.add('fs-fallback'); // iOS Safari: CSS overlay fallback
418
+ } catch { /* fullscreen denied: fall back to the CSS overlay */ box.classList.add('fs-fallback'); }
419
+ finally { fsInFlight = false; }
420
+ });
421
+ mermaid.initialize({
422
+ startOnLoad: true,
423
+ securityLevel: 'strict',
424
+ theme: 'base',
425
+ themeVariables: {
426
+ darkMode: true,
427
+ background: 'transparent',
428
+ primaryColor: '#0f172a',
429
+ primaryTextColor: '#f8fafc',
430
+ primaryBorderColor: '#a78bfa',
431
+ secondaryColor: '#171532',
432
+ tertiaryColor: '#0f1d33',
433
+ lineColor: '#94a3b8',
434
+ textColor: '#e5e7eb',
435
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
436
+ fontSize: '14px',
437
+ },
438
+ });
439
+ </script>
440
+ </body>
441
+ </html>
442
+ `;
443
+ }
@@ -0,0 +1 @@
1
+ export { resolveSettings, parseProjectRef, parseItemRef, resolveProjectSelector, findProject, applyDevloopsBoard } from "@dev-loops/core/projects/resolve-project";