jackherd 1.6.0 → 1.7.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/INSTALL.md +176 -0
  3. package/README.md +260 -315
  4. package/assets/branding/README.md +7 -3
  5. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  6. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  7. package/assets/branding/jackherd-logo-dark.svg +1 -1
  8. package/assets/icons/bark-dark.svg +1 -0
  9. package/assets/icons/bark.svg +1 -0
  10. package/assets/icons/bowl-dark.svg +1 -0
  11. package/assets/icons/bowl.svg +1 -0
  12. package/assets/icons/decide-dark.svg +1 -0
  13. package/assets/icons/decide.svg +1 -0
  14. package/assets/icons/ear-dark.svg +1 -0
  15. package/assets/icons/ear.svg +1 -0
  16. package/assets/icons/lanes-dark.svg +1 -0
  17. package/assets/icons/lanes.svg +1 -0
  18. package/assets/icons/plan-dark.svg +1 -0
  19. package/assets/icons/plan.svg +1 -0
  20. package/assets/icons/space.svg +1 -0
  21. package/dist/{chunk-QJSXHHQ3.js → chunk-OBUWTQ6Z.js} +25 -0
  22. package/dist/config.d.ts +14 -3
  23. package/dist/config.js +111 -10
  24. package/dist/plan-format.d.ts +180 -77
  25. package/dist/plan-format.js +292 -93
  26. package/docs/lanes-dark.svg +50 -0
  27. package/docs/lanes.svg +50 -1
  28. package/docs/skills-dark.svg +65 -0
  29. package/docs/skills.svg +65 -1
  30. package/docs/workflow-dark.svg +138 -0
  31. package/docs/workflow.svg +135 -169
  32. package/herdr-plugin/herdr-plugin.toml +6 -6
  33. package/herdr-plugin/open-plans.ts +1 -1
  34. package/herdr-plugin/restore-plans.ts +7 -7
  35. package/install.sh +1 -1
  36. package/kit/hooks/hooks/approval-gate/decision.ts +61 -0
  37. package/kit/hooks/hooks/approval-gate/herdr-panes.ts +24 -0
  38. package/kit/hooks/hooks/approval-gate/index.ts +69 -0
  39. package/kit/hooks/hooks/approval-gate/lane.ts +14 -0
  40. package/kit/hooks/hooks/approval-gate/request.ts +111 -0
  41. package/kit/hooks/hooks/approval-gate/run.ts +94 -0
  42. package/kit/hooks/hooks/approval-gate/settings.ts +26 -0
  43. package/kit/hooks/hooks/approval-gate/store.ts +70 -0
  44. package/kit/hooks/hooks/approval-gate/transcripts.ts +40 -0
  45. package/kit/hooks/hooks/merge-guard/approval.ts +11 -74
  46. package/kit/hooks/hooks/merge-guard/run.ts +2 -1
  47. package/kit/hooks/hooks/model-gate/agent-definition.ts +13 -0
  48. package/kit/hooks/hooks/model-gate/index.ts +58 -0
  49. package/kit/hooks/hooks/model-gate/run.ts +63 -0
  50. package/kit/hooks/hooks/model-log/index.ts +60 -0
  51. package/kit/hooks/hooks/model-log/run.ts +57 -0
  52. package/kit/hooks/hooks/model-log/store.ts +40 -0
  53. package/kit/hooks/hooks/model-log/transcript.ts +37 -0
  54. package/kit/hooks/hooks/shared/checkout.ts +22 -0
  55. package/kit/hooks/hooks/shared/command.ts +6 -0
  56. package/kit/hooks/hooks/shared/config.ts +21 -0
  57. package/kit/hooks/hooks/shared/dialog-answer.ts +5 -0
  58. package/kit/hooks/hooks/shared/dialog-answers.ts +74 -0
  59. package/kit/hooks/hooks/shared/files.ts +19 -0
  60. package/kit/hooks/hooks/shared/fresh.ts +4 -0
  61. package/kit/hooks/hooks/shared/json.ts +4 -0
  62. package/kit/hooks/hooks/shared/model-family.ts +48 -0
  63. package/kit/hooks/hooks/shared/plan-model-table.ts +41 -0
  64. package/kit/hooks/hooks/shared/private-directory.ts +29 -0
  65. package/kit/hooks/hooks/shared/repository-directory.ts +14 -0
  66. package/kit/hooks/hooks/shared/subagent-phase.ts +99 -0
  67. package/kit/hooks/hooks.json +25 -0
  68. package/kit/workflow/skills/follow/SKILL.md +4 -3
  69. package/kit/workflow/skills/implement/SKILL.md +6 -6
  70. package/kit/workflow/skills/orchestrate/SKILL.md +113 -30
  71. package/kit/workflow/skills/plan/SKILL.md +5 -5
  72. package/kit/workflow/skills/start/SKILL.md +2 -2
  73. package/package.json +4 -1
  74. package/schemas/jackherd.schema.json +43 -5
  75. package/src/check/command.ts +31 -4
  76. package/src/check/deferral-arrows.ts +1 -1
  77. package/src/check/parallel-report.ts +23 -0
  78. package/src/check/plan-structure-check.ts +120 -53
  79. package/src/cli.ts +8 -2
  80. package/src/config/nested-quantifier.ts +73 -0
  81. package/src/config/repository-directory.ts +11 -0
  82. package/src/config/schema.ts +63 -8
  83. package/src/plan-format/board-prose.ts +36 -0
  84. package/src/plan-format/dependencies.ts +227 -0
  85. package/src/plan-format/documentation-gate.ts +16 -0
  86. package/src/plan-format/index.ts +23 -1
  87. package/src/plan-format/lanes.ts +35 -1
  88. package/src/plan-format/model-column.ts +52 -0
  89. package/src/plan-format/phases.ts +16 -1
  90. package/src/plan-format/plan-structure.ts +10 -28
  91. package/src/plan-format/progress.ts +7 -29
  92. package/src/plans/command.ts +3 -3
  93. package/src/plans/current-phase.ts +71 -0
  94. package/src/plans/ensure-panes.ts +2 -2
  95. package/src/plans/herd-command.ts +4 -0
  96. package/src/plans/lane-discovery.ts +14 -2
  97. package/src/plans/lane-tickets.ts +69 -0
  98. package/src/plans/layout.ts +182 -88
  99. package/src/plans/live.ts +5 -1
  100. package/src/plans/phase-models-read.ts +37 -0
  101. package/src/plans/phase-models.ts +70 -0
  102. package/src/plans/plan-panes.ts +8 -7
  103. package/src/plans/pull-requests.ts +78 -4
  104. package/src/plans/selection.ts +1 -40
  105. package/src/plans/settings.ts +9 -3
  106. package/src/plans/shown-with-more.ts +5 -0
  107. package/src/plans/snapshot.ts +82 -8
  108. package/src/plans/sprint-html.ts +2 -2
  109. package/src/plans/supervisor.ts +5 -4
  110. package/src/plans/symbols.ts +9 -6
  111. package/src/plans/texts.ts +30 -13
  112. package/src/plans/view.ts +1 -0
  113. package/src/sync/herdr-plugin.ts +1 -1
  114. package/src/sync/next-steps.ts +1 -1
  115. package/src/sync/render.ts +2 -1
  116. package/src/sync/setup-questions.ts +2 -2
  117. package/src/terminal/text.ts +4 -0
  118. package/src/terminal/types.ts +1 -1
  119. package/src/vocabulary/index.ts +39 -0
  120. package/src/watch/approvals.ts +96 -0
  121. package/src/watch/command.ts +154 -50
  122. package/src/watch/failed-tests.ts +52 -0
  123. package/src/watch/failure-read.ts +56 -0
  124. package/src/watch/failures.ts +107 -0
  125. package/src/watch/feedback-item.ts +21 -0
  126. package/src/watch/feedback-read.ts +2 -1
  127. package/src/watch/feedback.ts +37 -28
  128. package/src/watch/gh-process.ts +53 -0
  129. package/src/watch/gh.ts +10 -20
  130. package/src/watch/heartbeat-progress.ts +1 -1
  131. package/src/watch/lane-model.ts +54 -0
  132. package/src/watch/lane-parallel.ts +46 -0
  133. package/src/watch/lane-progress.ts +28 -4
  134. package/src/watch/lane-report.ts +62 -9
  135. package/src/watch/lane-sources.ts +1 -1
  136. package/src/watch/no-finding.ts +47 -0
  137. package/src/watch/plan-badges-read.ts +19 -7
  138. package/src/watch/plan-badges.ts +59 -7
  139. package/src/watch/plan-views.ts +5 -5
  140. package/src/watch/previews.ts +3 -9
  141. package/src/watch/pull-requests.ts +12 -2
  142. package/src/watch/recorded-reads.ts +25 -0
  143. package/src/watch/source-fields.ts +4 -0
  144. package/src/watch/source-tickets.ts +99 -0
  145. package/src/watch/sources.ts +62 -15
  146. package/src/watch/texts.ts +21 -1
  147. package/src/watch/watch-defaults.ts +8 -0
  148. package/src/watch/watch-events.ts +64 -4
  149. package/src/watch/watcher-lock.ts +1 -1
  150. package/src/watch/workflow-runs.ts +5 -7
  151. package/docs/lanes.mmd +0 -9
  152. package/docs/skills.mmd +0 -10
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ADDRESS_FORM,
3
+ APPROVAL_TEXTS,
3
4
  LANGUAGE_NAME,
4
5
  PLAN_LANGUAGE,
5
6
  PLAN_STATUS,
@@ -10,7 +11,10 @@ import {
10
11
  isPlanLanguage,
11
12
  phaseHeading,
12
13
  sectionHeading
13
- } from "./chunk-QJSXHHQ3.js";
14
+ } from "./chunk-OBUWTQ6Z.js";
15
+
16
+ // src/plan-format/lanes.ts
17
+ import { posix } from "path";
14
18
 
15
19
  // src/plan-format/terminal-text.ts
16
20
  var UNSAFE_RANGES = [
@@ -88,63 +92,12 @@ var allTables = (lines) => {
88
92
  return tables;
89
93
  };
90
94
 
91
- // src/plan-format/drift.ts
92
- var OPEN_TABLE_CELL = /⬜/;
93
- var DRIFT_WORDS = {
94
- de: {
95
- inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Aktiv)\b/i,
96
- completion: /\b(done|abgeschlossen|umgesetzt|implementiert|implemented|fertig)\b/i,
97
- open: /\b(offen|ausstehend|deferred|zurückgestellt)\b/i,
98
- architecture: /\b(arc42|ADR|Architekturdokumentation|Dokumentation)\b/i,
99
- architectureHeading: "Architekturdokumentation",
100
- selfReported: (statusText) => `Status nennt offene Doku-Arbeit: "${statusText.trim()}"`,
101
- silent: "arc42-Abschlussphase hat offene \u2B1C-Zeilen trotz abgeschlossenem Status"
102
- },
103
- en: {
104
- inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Active)\b/i,
105
- completion: /\b(done|completed|implemented|finished)\b/i,
106
- open: /\b(open|outstanding|pending|deferred)\b/i,
107
- architecture: /\b(arc42|ADR|architecture documentation|documentation)\b/i,
108
- architectureHeading: "Architecture documentation",
109
- selfReported: (statusText) => `Status names open documentation work: "${statusText.trim()}"`,
110
- silent: "The arc42 closing phase has open \u2B1C rows despite a completed status"
111
- }
112
- };
113
- var statusLine = (content, name) => content.match(new RegExp(`^\\*\\*${escapeRegExp(name)}:\\*\\*\\s*(.+)$`, "m"))?.[1];
114
- var architectureSection = (content, heading) => content.match(new RegExp(`#{2,4}[^\\n]*${escapeRegExp(heading)}[^\\n]*\\n([\\s\\S]*?)(?=\\n#{1,4} |$)`))?.[1];
115
- var findDrift = (file, content, language) => {
116
- const statusText = statusLine(content, PLAN_VOCABULARY[language].header.status);
117
- const words = DRIFT_WORDS[language];
118
- if (!statusText || words.inFlight.test(statusText.trim())) return [];
119
- const completed = words.completion.test(statusText);
120
- const caveat = words.open.test(statusText);
121
- const selfReported = completed && caveat && words.architecture.test(statusText) ? words.selfReported(statusText) : null;
122
- const section = completed && !caveat ? architectureSection(content, words.architectureHeading) : void 0;
123
- const silent = section && OPEN_TABLE_CELL.test(section) ? words.silent : null;
124
- return [selfReported, silent].filter((reason) => reason !== null).map((reason) => ({ file, reason }));
125
- };
126
-
127
- // src/plan-format/header.ts
128
- var DECISION_ROW = /^\|\s*\*?\*?D\d+/gm;
129
- var readPlanHeader = (markdown, language, fallbackTitle) => {
130
- const { header } = PLAN_VOCABULARY[language];
131
- const status = headerField(markdown, header.status);
132
- return {
133
- title: firstHeading(markdown, fallbackTitle),
134
- date: headerField(markdown, header.date),
135
- status,
136
- statusWord: classifyStatus(language, status),
137
- scope: headerField(markdown, header.scope)
138
- };
139
- };
140
- var countDecisions = (markdown) => (markdown.match(DECISION_ROW) ?? []).length;
141
-
142
95
  // src/plan-format/lanes.ts
143
- import { posix } from "path";
144
96
  var LANE_NAME = /`([^`]+)`/;
145
97
  var CONTINUATION = "\u21B3";
146
98
  var PLAN_LINK = /\[([^\]]+)\]\(([^)]+)\)/;
147
99
  var PHASE_ID_PREFIX = "P";
100
+ var PHASES_COLUMN = 3;
148
101
  var CLAUSE_SEPARATOR = /([;,]|\.(?=\s))\s*/;
149
102
  var ENTRY_END = /* @__PURE__ */ new Set([";", "."]);
150
103
  var phaseIdPatterns = (language) => {
@@ -214,6 +167,39 @@ var parseSprintLanes = (markdown, language) => {
214
167
  };
215
168
  var sprintTitleOf = (markdown, fallback) => firstHeading(markdown, fallback);
216
169
  var resolvePlanPath = (sprintDirectory, href) => posix.normalize(posix.join(sprintDirectory, href.split("#")[0]));
170
+ var changedOpenLaneRows = (markdown, language, previousLines) => {
171
+ const start = sectionStart(markdown, PLAN_VOCABULARY[language].sections.lanes);
172
+ if (start === -1) return [];
173
+ const closed = new Set(
174
+ parseSprintLanes(markdown, language).filter((lane) => lane.closed).map((lane) => lane.name)
175
+ );
176
+ const rows = [];
177
+ let previous = null;
178
+ for (const { index, line, cells } of firstTableRows(
179
+ sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes)
180
+ )) {
181
+ const lane = laneNameOf(cells[0] ?? "", previous);
182
+ previous = lane ?? previous;
183
+ if (lane !== null && !closed.has(lane) && !previousLines.has(line))
184
+ rows.push({ line: start + index + 1, lane, phases: cells[PHASES_COLUMN] ?? "" });
185
+ }
186
+ return rows;
187
+ };
188
+
189
+ // src/plan-format/board-prose.ts
190
+ var PHASE_CELL_LIMIT = 250;
191
+ var PROSE_REASON = { long: "long", sideOrder: "side-order" };
192
+ var reasonOf = (phases, marker) => {
193
+ if (phases.includes(marker)) return PROSE_REASON.sideOrder;
194
+ return phases.length > PHASE_CELL_LIMIT ? PROSE_REASON.long : null;
195
+ };
196
+ var boardRowsWithProse = (markdown, language, previousLines) => {
197
+ const { sideOrderMarker } = PLAN_VOCABULARY[language];
198
+ return changedOpenLaneRows(markdown, language, previousLines).flatMap(({ line, lane, phases }) => {
199
+ const reason = reasonOf(phases, sideOrderMarker);
200
+ return reason === null ? [] : [{ line, lane, reason, length: phases.length }];
201
+ });
202
+ };
217
203
 
218
204
  // src/plan-format/phases.ts
219
205
  var REVIEW_GATE = /^\s*\*\*review[- ]gate\b/i;
@@ -223,7 +209,7 @@ var SECTION_HEADING = /^##\s+(.+?)\s*$/;
223
209
  var TOP_HEADING = /^#\s/;
224
210
  var TABLE_STATUS = { done: "\u2705", open: "\u2B1C" };
225
211
  var ROW_NUMBER = /^\d+(\.\d+)*[a-z]?$/;
226
- var NUMBERED_SECTION = /^##\s+(\d+[a-z]?)\s*[—–:-]\s*(.*)$/;
212
+ var NUMBERED_SECTION = new RegExp(`^##\\s+${PHASE_ID_PREFIX}?(\\d+[a-z]?)\\s*[\u2014\u2013:-]\\s*(.*)$`);
227
213
  var SECTION_POINTER = /`##\s+([^`]+?)\s*`/;
228
214
  var DEFERRED_TO = new RegExp(`\\s\u2192\\s*${PHASE_ID_PREFIX}(\\d+[a-z]?)\\s*$`, "i");
229
215
  var PHASE_ARROW = new RegExp(`\\s*\u2192\\s*${PHASE_ID_PREFIX}(\\d+[a-z]?)\\b`, "i");
@@ -334,6 +320,13 @@ var notePointer = (pointers, phase, line) => {
334
320
  if (phase && pointer) pointers.set(phase, pointer);
335
321
  };
336
322
  var headingOf = (line, phaseHeading2) => line.match(phaseHeading2) ?? line.match(NUMBERED_SECTION);
323
+ var phaseHeadings = (markdown, language) => {
324
+ const phaseHeading2 = phaseHeadingPattern(language);
325
+ return markdown.split("\n").flatMap((text, index) => {
326
+ const heading = headingOf(text, phaseHeading2);
327
+ return heading ? [{ line: index + 1, id: (heading[1] ?? "").toLowerCase(), title: terminalSafe(heading[2] ?? "").trim() }] : [];
328
+ });
329
+ };
337
330
  var parsePlanPhases = (markdown, language) => {
338
331
  const phaseHeading2 = phaseHeadingPattern(language);
339
332
  const pointers = /* @__PURE__ */ new Map();
@@ -355,11 +348,232 @@ var parsePlanPhases = (markdown, language) => {
355
348
  return withArrivedItems(withPointedSections(phases, pointers, markdown));
356
349
  };
357
350
 
351
+ // src/plan-format/dependencies.ts
352
+ var ID = "(\\d+[a-z]?)";
353
+ var PHASE_ID = new RegExp(`^\\s*${PHASE_ID_PREFIX}?${ID}\\b`, "i");
354
+ var PHASE_RANGE = new RegExp(`^${PHASE_ID_PREFIX}?(\\d+)\\s*[\u2013-]\\s*${PHASE_ID_PREFIX}?(\\d+)$`, "i");
355
+ var BACKTICKED = /`([^`]+)`/g;
356
+ var NO_VALUE = /^[—–-]?$/;
357
+ var TRAILING_NOTE = /\s*\([^)]*\)\s*$/;
358
+ var GLOB_CHARACTER = /[*?{[]/;
359
+ var MAX_RANGE = 50;
360
+ var phaseIdOf = (cell) => PHASE_ID.exec(cell)?.[1]?.toLowerCase() ?? null;
361
+ var splitOutsideParentheses = (cell, separator) => {
362
+ const entries = [];
363
+ let depth = 0;
364
+ let start = 0;
365
+ for (const [index, character] of [...cell].entries()) {
366
+ if (character === "(") depth += 1;
367
+ else if (character === ")") depth = Math.max(depth - 1, 0);
368
+ else if (character === separator && depth === 0) {
369
+ entries.push(cell.slice(start, index));
370
+ start = index + 1;
371
+ }
372
+ }
373
+ return [...entries, cell.slice(start)];
374
+ };
375
+ var rangeOf = (entry) => {
376
+ const match = PHASE_RANGE.exec(entry);
377
+ if (!match) return null;
378
+ const [first, last] = [Number(match[1]), Number(match[2])];
379
+ if (first > last || last - first >= MAX_RANGE) return [];
380
+ return Array.from({ length: last - first + 1 }, (_, offset) => String(first + offset));
381
+ };
382
+ var dependsOf = (cell, language) => {
383
+ const noDependency = PLAN_VOCABULARY[language].noDependency;
384
+ const entries = splitOutsideParentheses(cell, ",").map((entry) => entry.trim()).filter((entry) => !NO_VALUE.test(entry) && !noDependency.includes(entry.toLowerCase()));
385
+ const after = entries.flatMap((entry) => rangeOf(entry) ?? phaseIdOf(entry) ?? []);
386
+ const external = entries.filter((entry) => {
387
+ const spanned = rangeOf(entry);
388
+ return spanned === null ? phaseIdOf(entry) === null : spanned.length === 0;
389
+ });
390
+ return { after, external };
391
+ };
392
+ var pathOf = (entry) => {
393
+ if (/\s/.test(entry)) return null;
394
+ const glob = entry.search(GLOB_CHARACTER);
395
+ const literal = glob === -1 ? entry : entry.slice(0, Math.max(entry.lastIndexOf("/", glob), 0));
396
+ const path = literal.replace(/^\.\//, "").replace(/\/+$/, "");
397
+ return path === "." ? "" : path;
398
+ };
399
+ var likePattern = (language) => {
400
+ const { writesLike, writesLikeJoiners } = PLAN_VOCABULARY[language];
401
+ const words = (list) => list.map(escapeRegExp).join("|");
402
+ const target = `${PHASE_ID_PREFIX}?${ID}`;
403
+ const joiner = `(?:\\s*,\\s*|\\s+(?:${words(writesLikeJoiners)})\\s+|\\s*,\\s*(?:${words(writesLikeJoiners)})\\s+)`;
404
+ return {
405
+ start: new RegExp(`(?<![\\p{L}\\d])(?:${words(writesLike)})\\s+${target}\\b(?:${joiner}${target}\\b)*`, "iu"),
406
+ id: new RegExp(`${PHASE_ID_PREFIX}?${ID}\\b`, "gi")
407
+ };
408
+ };
409
+ var writesLikeOf = (cell, language) => {
410
+ const { start, id } = likePattern(language);
411
+ const found = start.exec(cell)?.[0];
412
+ if (found === void 0) return [];
413
+ const afterWord = found.replace(/^\S+\s+/, "");
414
+ return [...afterWord.matchAll(id)].map((match) => (match[1] ?? "").toLowerCase());
415
+ };
416
+ var writesOf = (cell, language) => ({
417
+ writes: [...cell.matchAll(BACKTICKED)].flatMap((match) => pathOf(match[1] ?? "") ?? []),
418
+ writesLike: writesLikeOf(cell, language)
419
+ });
420
+ var markersOf = (cell, language) => {
421
+ const release = new RegExp(`^${escapeRegExp(PLAN_VOCABULARY[language].releaseMarker)}(?:\\s|$)`, "i");
422
+ return cell.split(";").map((entry) => entry.replace(TRAILING_NOTE, "").trim()).filter((entry) => !NO_VALUE.test(entry) && !release.test(entry));
423
+ };
424
+ var parseDependencyTable = (markdown, language) => {
425
+ const { sections, columns } = PLAN_VOCABULARY[language];
426
+ const { header, rows } = firstTableWithHeader(subsectionLines(markdown, sections.dependencies));
427
+ const [after, writes, marker] = [columns.dependsOn, columns.writes, columns.serialMarker].map(
428
+ (name) => header.indexOf(name)
429
+ );
430
+ if (after === -1 || writes === -1 || marker === -1) return null;
431
+ return rows.flatMap((cells) => {
432
+ const id = phaseIdOf(cells[0] ?? "");
433
+ if (id === null) return [];
434
+ return [
435
+ {
436
+ id,
437
+ ...dependsOf(cells[after] ?? "", language),
438
+ ...writesOf(cells[writes] ?? "", language),
439
+ serialMarkers: markersOf(cells[marker] ?? "", language)
440
+ }
441
+ ];
442
+ });
443
+ };
444
+ var phasesWithoutDependencyRow = (markdown, language) => {
445
+ const table = parseDependencyTable(markdown, language);
446
+ if (table === null) return null;
447
+ const listed = new Set(table.map(({ id }) => id));
448
+ return phaseHeadings(markdown, language).filter(({ id }) => !listed.has(id));
449
+ };
450
+ var mergeSets = (sets) => sets.some((set) => set === null) ? null : sets.flatMap((set) => set ?? []);
451
+ var writeSetOf = (row, byId, chain) => {
452
+ if (chain.includes(row.id)) return null;
453
+ const targets = row.writesLike.map((id) => byId.get(id));
454
+ const like = mergeSets(targets.map((target) => target ? writeSetOf(target, byId, [...chain, row.id]) : null));
455
+ return like === null ? null : [...row.writes, ...like];
456
+ };
457
+ var overlaps = (left, right) => left === "" || right === "" || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
458
+ var sharesMarker = (first, second) => {
459
+ const markers = new Set(second.serialMarkers.map((marker) => marker.trim().toLowerCase()));
460
+ return first.serialMarkers.some((marker) => markers.has(marker.trim().toLowerCase()));
461
+ };
462
+ var collides = (first, second) => sharesMarker(first, second) || first.writes.some((path) => second.writes.some((other) => overlaps(path, other)));
463
+ var candidateOf = (row, byId) => {
464
+ const writes = writeSetOf(row, byId, []);
465
+ return writes === null || writes.length === 0 ? [] : [{ id: row.id, writes, serialMarkers: row.serialMarkers }];
466
+ };
467
+ var parallelGroups = (rows, doneIds) => {
468
+ const byId = new Map(rows.map((row) => [row.id, row]));
469
+ const ready = rows.filter((row) => !doneIds.has(row.id) && row.external.length === 0 && row.after.every((id) => doneIds.has(id))).flatMap((row) => candidateOf(row, byId));
470
+ const groups = [];
471
+ for (const candidate of ready) {
472
+ const group = groups.find((members) => members.every((member) => !collides(member, candidate)));
473
+ if (group) group.push(candidate);
474
+ else groups.push([candidate]);
475
+ }
476
+ return groups.map((group) => group.map(({ id }) => id));
477
+ };
478
+ var phasesText = (phases) => phases.map((id) => `${PHASE_ID_PREFIX}${id}`).join(" \u2225 ");
479
+ var donePhaseIds = (markdown, language) => new Set(
480
+ parsePlanPhases(markdown, language).filter(({ items }) => items.length > 0 && items.every((item) => item.done)).map(({ id }) => id)
481
+ );
482
+ var parallelGroupsOfPlan = (markdown, language) => {
483
+ const table = parseDependencyTable(markdown, language);
484
+ return table === null ? null : parallelGroups(table, donePhaseIds(markdown, language));
485
+ };
486
+
487
+ // src/plan-format/documentation-gate.ts
488
+ var DOCUMENTATION_WORDS = Object.values(PLAN_VOCABULARY).map(
489
+ (vocabulary) => vocabulary.sections.documentation.toLowerCase()
490
+ );
491
+ var isDocumentationGateItem = ({ gate, text }) => gate === true && DOCUMENTATION_WORDS.some((word) => text.toLowerCase().startsWith(word));
492
+ var onlyDocumentationOpen = ({ items }) => {
493
+ const open = items.filter((item) => !item.done);
494
+ return open.length === 1 && isDocumentationGateItem(open[0]);
495
+ };
496
+
497
+ // src/plan-format/drift.ts
498
+ var OPEN_TABLE_CELL = /⬜/;
499
+ var DRIFT_WORDS = {
500
+ de: {
501
+ inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Aktiv)\b/i,
502
+ completion: /\b(done|abgeschlossen|umgesetzt|implementiert|implemented|fertig)\b/i,
503
+ open: /\b(offen|ausstehend|deferred|zurückgestellt)\b/i,
504
+ architecture: /\b(arc42|ADR|Architekturdokumentation|Dokumentation)\b/i,
505
+ architectureHeading: "Architekturdokumentation",
506
+ selfReported: (statusText) => `Status nennt offene Doku-Arbeit: "${statusText.trim()}"`,
507
+ silent: "arc42-Abschlussphase hat offene \u2B1C-Zeilen trotz abgeschlossenem Status"
508
+ },
509
+ en: {
510
+ inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Active)\b/i,
511
+ completion: /\b(done|completed|implemented|finished)\b/i,
512
+ open: /\b(open|outstanding|pending|deferred)\b/i,
513
+ architecture: /\b(arc42|ADR|architecture documentation|documentation)\b/i,
514
+ architectureHeading: "Architecture documentation",
515
+ selfReported: (statusText) => `Status names open documentation work: "${statusText.trim()}"`,
516
+ silent: "The arc42 closing phase has open \u2B1C rows despite a completed status"
517
+ }
518
+ };
519
+ var statusLine = (content, name) => content.match(new RegExp(`^\\*\\*${escapeRegExp(name)}:\\*\\*\\s*(.+)$`, "m"))?.[1];
520
+ var architectureSection = (content, heading) => content.match(new RegExp(`#{2,4}[^\\n]*${escapeRegExp(heading)}[^\\n]*\\n([\\s\\S]*?)(?=\\n#{1,4} |$)`))?.[1];
521
+ var findDrift = (file, content, language) => {
522
+ const statusText = statusLine(content, PLAN_VOCABULARY[language].header.status);
523
+ const words = DRIFT_WORDS[language];
524
+ if (!statusText || words.inFlight.test(statusText.trim())) return [];
525
+ const completed = words.completion.test(statusText);
526
+ const caveat = words.open.test(statusText);
527
+ const selfReported = completed && caveat && words.architecture.test(statusText) ? words.selfReported(statusText) : null;
528
+ const section = completed && !caveat ? architectureSection(content, words.architectureHeading) : void 0;
529
+ const silent = section && OPEN_TABLE_CELL.test(section) ? words.silent : null;
530
+ return [selfReported, silent].filter((reason) => reason !== null).map((reason) => ({ file, reason }));
531
+ };
532
+
533
+ // src/plan-format/header.ts
534
+ var DECISION_ROW = /^\|\s*\*?\*?D\d+/gm;
535
+ var readPlanHeader = (markdown, language, fallbackTitle) => {
536
+ const { header } = PLAN_VOCABULARY[language];
537
+ const status = headerField(markdown, header.status);
538
+ return {
539
+ title: firstHeading(markdown, fallbackTitle),
540
+ date: headerField(markdown, header.date),
541
+ status,
542
+ statusWord: classifyStatus(language, status),
543
+ scope: headerField(markdown, header.scope)
544
+ };
545
+ };
546
+ var countDecisions = (markdown) => (markdown.match(DECISION_ROW) ?? []).length;
547
+
548
+ // src/plan-format/model-column.ts
549
+ var NOTE = /\([^)]*\)/g;
550
+ var ENTRY_SEPARATOR = /[/,+→]/;
551
+ var TOKEN_SEPARATOR = /[^a-z0-9]+/;
552
+ var NAME = /^[a-z]+$/;
553
+ var NUMBER = /^\d+$/;
554
+ var tokensOf = (text) => text.toLowerCase().split(TOKEN_SEPARATOR);
555
+ var modelOf = (entry) => {
556
+ const tokens = tokensOf(entry);
557
+ const names = tokens.filter((token) => NAME.test(token));
558
+ return names.length > 0 ? { names, version: tokens.filter((token) => NUMBER.test(token)) } : null;
559
+ };
560
+ var columnModels = (column) => column.replace(NOTE, "").split(ENTRY_SEPARATOR).flatMap((entry) => modelOf(entry) ?? []);
561
+ var startsWith = (tokens, prefix) => prefix.every((part, index) => tokens[index] === part);
562
+ var fitsName = (tokens, name, version) => tokens.some(
563
+ (token, index) => token === name && (startsWith(tokens.slice(index + 1), version) || startsWith(tokens.slice(index - version.length, index), version))
564
+ );
565
+ var fits = (tokens, { names, version }) => names.some((name) => fitsName(tokens, name, version));
566
+ var differsFromColumn = (column, modelId) => {
567
+ const models = columnModels(column);
568
+ const tokens = tokensOf(modelId);
569
+ return models.length > 0 && !models.some((model) => fits(tokens, model));
570
+ };
571
+
358
572
  // src/plan-format/phase-tally.ts
359
573
  var DONE_MARK = /^\s*[-*]\s\[x\]|\|\s*✅\s*\|/gim;
360
574
  var OPEN_MARK = /^\s*[-*]\s\[ \]|\|\s*⬜\s*\|/gm;
361
575
  var TRAILING_SEPARATOR = /[:—–-]\s*$/;
362
- var PHASE_ID = /^(\d+[a-z]?)/;
576
+ var PHASE_ID2 = /^(\d+[a-z]?)/;
363
577
  var modelFor = (markdown, phaseWord, phaseTitle) => {
364
578
  const prefix = new RegExp(`^${escapeRegExp(phaseWord)}\\s*`, "i");
365
579
  const number = /^(\d+)/.exec(phaseTitle.replace(prefix, ""))?.[1];
@@ -371,7 +585,7 @@ var sectionOf = (section, heading, markdown, phaseWord) => {
371
585
  const match = heading.exec(section);
372
586
  if (!match) return null;
373
587
  const title = match[1].replace(TRAILING_SEPARATOR, "").trim();
374
- const phaseId = PHASE_ID.exec(title)?.[1]?.toLowerCase() ?? "";
588
+ const phaseId = PHASE_ID2.exec(title)?.[1]?.toLowerCase() ?? "";
375
589
  const targets = section.split("\n").flatMap((line) => deferralTarget(line, phaseId) ?? []);
376
590
  const done = (section.match(DONE_MARK) ?? []).length;
377
591
  const open = (section.match(OPEN_MARK) ?? []).length - targets.length;
@@ -436,45 +650,18 @@ var mergedPhasesWithOpenBoxes = (markdown, base, language) => {
436
650
  };
437
651
  return phaseSections(markdown, language).filter(({ id, openLines }) => openLines.length > 0 && isMerged(id)).map(({ line, id, title, openLines }) => ({ line, id, title, openLines }));
438
652
  };
439
- var boardRowsWithoutPhase = (markdown, language, previousLines) => {
440
- const start = sectionStart(markdown, PLAN_VOCABULARY[language].sections.lanes);
441
- if (start === -1) return [];
442
- const closed = new Set(
443
- parseSprintLanes(markdown, language).filter((lane) => lane.closed).map((lane) => lane.name)
444
- );
445
- const rows = [];
446
- let previous = null;
447
- for (const { index, line, cells } of firstTableRows(
448
- sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes)
449
- )) {
450
- const [name = "", , , phases = ""] = cells;
451
- const lane = laneNameOf(name, previous);
452
- previous = lane ?? previous;
453
- const counts = lane !== null && !closed.has(lane) && !previousLines.has(line);
454
- if (counts && phaseIdsOfCell(phases, language).length === 0) rows.push({ line: start + index + 1, lane, phases });
455
- }
456
- return rows;
457
- };
653
+ var boardRowsWithoutPhase = (markdown, language, previousLines) => changedOpenLaneRows(markdown, language, previousLines).filter(
654
+ ({ phases }) => phaseIdsOfCell(phases, language).length === 0
655
+ );
458
656
 
459
657
  // src/plan-format/progress.ts
460
658
  var sumPhases = (phases) => ({
461
659
  done: phases.reduce((sum, phase) => sum + phase.done, 0),
462
660
  total: phases.reduce((sum, phase) => sum + phase.total, 0)
463
661
  });
464
- var baseId = (id) => id.replace(/[a-z]$/, "");
465
- var findPhase = (phases, id) => phases.find((phase) => phase.id === id) ?? phases.find((phase) => phase.id === baseId(id));
466
- var keptPhaseIds = (phases, ids) => {
467
- const shown = new Set(ids.flatMap((id) => phases.filter((phase) => phase.id === id)));
468
- return ids.filter((id) => {
469
- const phase = findPhase(phases, id);
470
- if (!phase || phase.id === id) return true;
471
- if (shown.has(phase)) return false;
472
- shown.add(phase);
473
- return true;
474
- });
475
- };
662
+ var phaseById = (phases, id) => phases.find((phase) => phase.id === id);
476
663
  var phaseProgress = (phases, id) => {
477
- const phase = findPhase(phases, id);
664
+ const phase = phaseById(phases, id);
478
665
  const items = phase?.items ?? [];
479
666
  return {
480
667
  id,
@@ -499,12 +686,11 @@ var newestStepWith = (steps, pick) => {
499
686
  }
500
687
  return null;
501
688
  };
502
- var shownPhase = (phases, id) => phases.find((phase) => phase.id === id) ?? phases.find((phase) => phase.id === baseId(id));
503
689
  var earlierPhase = (steps, before, id) => {
504
690
  const planHref = steps[before].step.planHref;
505
691
  for (let stepIndex = before - 1; stepIndex >= 0; stepIndex--) {
506
692
  if (steps[stepIndex].step.planHref !== planHref) continue;
507
- const phase = shownPhase(steps[stepIndex].phases, id);
693
+ const phase = phaseById(steps[stepIndex].phases, id);
508
694
  if (phase) return { phase, current: { stepIndex, phaseId: phase.id } };
509
695
  }
510
696
  return null;
@@ -539,14 +725,14 @@ var currentPhaseOf = (steps) => newestRowPhase(steps) ?? phaseFromTicks(steps);
539
725
  var laneProgress = (lane, plans) => {
540
726
  const steps = lane.steps.map((step) => {
541
727
  const phases = step.planHref ? plans[step.planHref] : void 0;
542
- return { step, phases: phases ? keptPhaseIds(phases, step.phaseIds).map((id) => phaseProgress(phases, id)) : [] };
728
+ return { step, phases: phases ? step.phaseIds.map((id) => phaseProgress(phases, id)) : [] };
543
729
  });
544
730
  return { lane, steps, current: currentPhaseOf(steps), ...sumPhases(steps.flatMap((step) => step.phases)) };
545
731
  };
546
732
  var currentPhase = (progress) => {
547
733
  if (!progress.current) return null;
548
734
  const { stepIndex, phaseId } = progress.current;
549
- return progress.steps[stepIndex].phases.find((phase) => phase.id === phaseId) ?? null;
735
+ return phaseById(progress.steps[stepIndex].phases, phaseId) ?? null;
550
736
  };
551
737
  var activeItem = (phase) => phase.items.find((item) => !item.done) ?? null;
552
738
  var shortTitle = (progress) => {
@@ -595,40 +781,53 @@ var readPhaseDependencies = (planMarkdown, language) => {
595
781
  };
596
782
  export {
597
783
  ADDRESS_FORM,
784
+ APPROVAL_TEXTS,
598
785
  LANGUAGE_NAME,
786
+ PHASE_CELL_LIMIT,
599
787
  PHASE_ID_PREFIX,
600
788
  PLAN_LANGUAGE,
601
789
  PLAN_STATUS,
602
790
  PLAN_VOCABULARY,
791
+ PROSE_REASON,
603
792
  RESPONSE_ADDRESS,
604
793
  SELF_MERGE_LABEL,
605
794
  activeItem,
795
+ boardRowsWithProse,
606
796
  boardRowsWithoutPhase,
607
797
  classifyStatus,
798
+ columnModels,
608
799
  countDecisions,
609
800
  currentPhase,
610
801
  currentPhaseOf,
611
802
  deferralTarget,
803
+ differsFromColumn,
612
804
  endsPhase,
613
805
  findDrift,
614
806
  hasLanesTable,
615
807
  isCheckboxLine,
808
+ isDocumentationGateItem,
616
809
  isOpenCheckboxLine,
617
810
  isPlanLanguage,
618
811
  isReviewGateLine,
619
812
  isTickedUnmerged,
620
- keptPhaseIds,
621
813
  laneNameOf,
622
814
  laneProgress,
623
815
  mergedPhasesWithOpenBoxes,
624
816
  misplacedDeferrals,
625
817
  newPhasesWithoutReviewGate,
818
+ onlyDocumentationOpen,
819
+ parallelGroups,
820
+ parallelGroupsOfPlan,
821
+ parseDependencyTable,
626
822
  parsePlanPhases,
627
823
  parseSprintLanes,
628
824
  phaseHeading,
629
825
  phaseHeadingPattern,
826
+ phaseHeadings,
630
827
  phaseIdsOfCell,
631
828
  phaseIdsOfPlan,
829
+ phasesText,
830
+ phasesWithoutDependencyRow,
632
831
  readPhaseDependencies,
633
832
  readPlanHeader,
634
833
  readSprintDocument,
@@ -0,0 +1,50 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="960" height="440" viewBox="0 0 960 440" role="img" aria-labelledby="t d"><title id="t">What runs where</title><desc id="d">The coordinator, running /jh-orchestrate in the main checkout, prompts two lanes, each in its own git worktree, and asks you the open decisions as a choice, with one approval per pull request. The lanes send their questions back as text. jh watch reads each lane's state, pull request and checks and wakes the coordinator. Small dots travel along the lines to show the messages.</desc><defs><style>.s{font-family:ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif}.m{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace}@keyframes k1{0%{opacity:0;transform:translate(388px,182px)}2.22%{opacity:0;transform:translate(388px,182px)}2.56%{opacity:1;transform:translate(388px,182px)}3.89%{opacity:1;transform:translate(420.4px,173px)}5.56%{opacity:1;transform:translate(451.7px,162.1px)}7.22%{opacity:1;transform:translate(482.1px,150.1px)}8.89%{opacity:1;transform:translate(512px,137.2px)}10.56%{opacity:1;transform:translate(541.5px,124px)}12.22%{opacity:1;transform:translate(571px,111px)}13.89%{opacity:1;transform:translate(600.6px,98.7px)}15.56%{opacity:1;transform:translate(630.7px,87.5px)}17.22%{opacity:1;transform:translate(661.4px,77.9px)}18.89%{opacity:1;transform:translate(693px,70.5px)}20.56%{opacity:1;transform:translate(725.8px,65.7px)}22.22%{opacity:0;transform:translate(760px,64px)}100%{opacity:0;transform:translate(760px,64px)}}.k1{animation:k1 9s linear infinite both}@keyframes k2{0%{opacity:0;transform:translate(388px,226px)}8.89%{opacity:0;transform:translate(388px,226px)}9.22%{opacity:1;transform:translate(388px,226px)}10.56%{opacity:1;transform:translate(420.4px,235.1px)}12.22%{opacity:1;transform:translate(451.7px,246px)}13.89%{opacity:1;transform:translate(482.1px,258.2px)}15.56%{opacity:1;transform:translate(512px,271.3px)}17.22%{opacity:1;transform:translate(541.5px,284.8px)}18.89%{opacity:1;transform:translate(571px,298px)}20.56%{opacity:1;transform:translate(600.6px,310.6px)}22.22%{opacity:1;transform:translate(630.7px,322px)}23.89%{opacity:1;transform:translate(661.4px,331.8px)}25.56%{opacity:1;transform:translate(693px,339.3px)}27.22%{opacity:1;transform:translate(725.8px,344.3px)}28.89%{opacity:0;transform:translate(760px,346px)}100%{opacity:0;transform:translate(760px,346px)}}.k2{animation:k2 9s linear infinite both}@keyframes k3{0%{opacity:0;transform:translate(770px,94px)}24.44%{opacity:0;transform:translate(770px,94px)}24.78%{opacity:1;transform:translate(770px,94px)}25.56%{opacity:1;transform:translate(762.1px,107.5px)}26.67%{opacity:1;transform:translate(753.5px,119.9px)}27.78%{opacity:1;transform:translate(744.3px,131.3px)}28.89%{opacity:1;transform:translate(734.7px,141.7px)}30%{opacity:1;transform:translate(724.7px,151.1px)}31.11%{opacity:1;transform:translate(714.5px,159.6px)}32.22%{opacity:1;transform:translate(704.2px,167.2px)}33.33%{opacity:1;transform:translate(694px,174px)}34.44%{opacity:1;transform:translate(683.9px,179.9px)}35.56%{opacity:1;transform:translate(674.2px,185px)}36.67%{opacity:1;transform:translate(664.8px,189.4px)}37.78%{opacity:0;transform:translate(656px,193px)}100%{opacity:0;transform:translate(656px,193px)}}.k3{animation:k3 9s linear infinite both}@keyframes k4{0%{opacity:0;transform:translate(770px,316px)}28.89%{opacity:0;transform:translate(770px,316px)}29.22%{opacity:1;transform:translate(770px,316px)}30%{opacity:1;transform:translate(762.1px,302.6px)}31.11%{opacity:1;transform:translate(753.5px,290.3px)}32.22%{opacity:1;transform:translate(744.3px,279.1px)}33.33%{opacity:1;transform:translate(734.7px,268.9px)}34.44%{opacity:1;transform:translate(724.7px,259.7px)}35.56%{opacity:1;transform:translate(714.5px,251.5px)}36.67%{opacity:1;transform:translate(704.2px,244.2px)}37.78%{opacity:1;transform:translate(694px,237.8px)}38.89%{opacity:1;transform:translate(683.9px,232.2px)}40%{opacity:1;transform:translate(674.2px,227.4px)}41.11%{opacity:1;transform:translate(664.8px,223.3px)}42.22%{opacity:0;transform:translate(656px,220px)}100%{opacity:0;transform:translate(656px,220px)}}.k4{animation:k4 9s linear infinite both}@keyframes k5{0%{opacity:0;transform:translate(600px,205px)}42.22%{opacity:0;transform:translate(600px,205px)}42.56%{opacity:1;transform:translate(600px,205px)}43.24%{opacity:1;transform:translate(584.4px,205px)}44.26%{opacity:1;transform:translate(567.8px,205px)}45.28%{opacity:1;transform:translate(550.3px,205px)}46.3%{opacity:1;transform:translate(532.1px,205px)}47.31%{opacity:1;transform:translate(513.6px,205px)}48.33%{opacity:1;transform:translate(494.8px,205px)}49.35%{opacity:1;transform:translate(475.9px,205px)}50.37%{opacity:1;transform:translate(457.2px,205px)}51.39%{opacity:1;transform:translate(438.8px,205px)}52.41%{opacity:1;transform:translate(421.1px,205px)}53.43%{opacity:1;transform:translate(404px,205px)}54.44%{opacity:0;transform:translate(388px,205px)}100%{opacity:0;transform:translate(388px,205px)}}.k5{animation:k5 9s linear infinite both}@keyframes k6{0%{opacity:0;transform:translate(760px,346px)}48.89%{opacity:0;transform:translate(760px,346px)}49.22%{opacity:1;transform:translate(760px,346px)}50.37%{opacity:1;transform:translate(725.8px,344.2px)}51.85%{opacity:1;transform:translate(693px,339.3px)}53.33%{opacity:1;transform:translate(661.4px,331.8px)}54.81%{opacity:1;transform:translate(630.7px,322px)}56.3%{opacity:1;transform:translate(600.6px,310.6px)}57.78%{opacity:1;transform:translate(571px,298px)}59.26%{opacity:1;transform:translate(541.5px,284.8px)}60.74%{opacity:1;transform:translate(512px,271.3px)}62.22%{opacity:1;transform:translate(482.1px,258.2px)}63.7%{opacity:1;transform:translate(451.7px,246px)}65.19%{opacity:1;transform:translate(420.4px,235.1px)}66.67%{opacity:0;transform:translate(388px,226px)}100%{opacity:0;transform:translate(388px,226px)}}.k6{animation:k6 9s linear infinite both}@keyframes k7{0%{opacity:0;transform:translate(300px,204px)}67.78%{opacity:0;transform:translate(300px,204px)}68.11%{opacity:1;transform:translate(300px,204px)}68.7%{opacity:1;transform:translate(287.1px,202.6px)}69.63%{opacity:1;transform:translate(273.5px,201.5px)}70.56%{opacity:1;transform:translate(259.3px,200.6px)}71.48%{opacity:1;transform:translate(244.6px,200px)}72.41%{opacity:1;transform:translate(229.5px,199.6px)}73.33%{opacity:1;transform:translate(214.2px,199.5px)}74.26%{opacity:1;transform:translate(198.8px,199.6px)}75.19%{opacity:1;transform:translate(183.4px,200px)}76.11%{opacity:1;transform:translate(168.1px,200.6px)}77.04%{opacity:1;transform:translate(153px,201.5px)}77.96%{opacity:1;transform:translate(138.3px,202.6px)}78.89%{opacity:0;transform:translate(124px,204px)}100%{opacity:0;transform:translate(124px,204px)}}.k7{animation:k7 9s linear infinite both}@keyframes k8{0%{opacity:0;transform:translate(124px,204px)}83.33%{opacity:0;transform:translate(124px,204px)}83.67%{opacity:1;transform:translate(124px,204px)}84.26%{opacity:1;transform:translate(138.3px,202.6px)}85.19%{opacity:1;transform:translate(153px,201.5px)}86.11%{opacity:1;transform:translate(168.1px,200.6px)}87.04%{opacity:1;transform:translate(183.4px,200px)}87.96%{opacity:1;transform:translate(198.8px,199.6px)}88.89%{opacity:1;transform:translate(214.2px,199.5px)}89.81%{opacity:1;transform:translate(229.5px,199.6px)}90.74%{opacity:1;transform:translate(244.6px,200px)}91.67%{opacity:1;transform:translate(259.3px,200.6px)}92.59%{opacity:1;transform:translate(273.5px,201.5px)}93.52%{opacity:1;transform:translate(287.1px,202.6px)}94.44%{opacity:0;transform:translate(300px,204px)}100%{opacity:0;transform:translate(300px,204px)}}.k8{animation:k8 9s linear infinite both}@media (prefers-reduced-motion:reduce){*{animation:none!important}}</style><clipPath id="jack-clip"><rect width="240" height="240" rx="50"/></clipPath><symbol id="jack" viewBox="0 0 240 240"><rect width="240" height="240" rx="50" fill="#212E34"/><g clip-path="url(#jack-clip)"><g transform="translate(34 8)"><path fill="#4A4E51" d="M80 50.5c.2 .1 .2 .1 1 1c-.2 .6-.3 1.3-.5 2c-.7 .4-.7 .4-4 2c-6 3.3-6 3.3-7 6.5c1.2-.4 2.3-.6 3.5-1c.8 0 1.7 0 2.5 0c.2 .1 .2 .1 1 1c-.5 2-.5 2-5.5 6.5c-3 5-3 5-5.5 11c.2 .3 .2 .3 1 2c.2 .3 .3 .7 .5 1c-.7 .7-1.3 1.3-2 2c-1.3-.2-2.6-.3-4-.5c0-1.2 0-2.3 0-3.5c-.2 .1-.2 .1-1.5 .5c-1.2 2.7-2.4 5.3-3.5 8c-1 2.1-2 4.2-3 6.3c-2.5 5.7-2.5 5.7-3 8.2c.5 0 1 0 1.5 0c.1-.8 .4-1.7 .5-2.5c.6 0 1.3 0 2 0c.5 2 1 3.9 1.6 6c1 4 2.2 8 3.4 11.9c2.1 8.4 1.1 15.5-3 23.1c-.6-.2-1.3-.3-2-.5c-.1-1.5-.3-3-.5-4.6c-.3-2.6-.5-5.2-1-7.9c-.5 0-1 0-1.5 0c-.1 1.7-.4 3.3-.5 5c-2.7 10.2-2.7 10.2-6 13.5c-6.1-3.1-9-14.9-11-21c-1.1-8.2 .9-14.7 3.5-22.5c-8.4 8.3-8.3 20.5-9 31.5c-.6-.2-1.3-.3-2-.5c-5.4-8.9-5.1-15-3-25c.5-1 1-2 1.5-3c-3 1.5-3 1.5-6.5 5c-.4 0-.6 0-1 0c.8-6.5 3.2-10.2 7.5-15c.6-.8 .6-.8 3.5-5c2.6-2.6 5.2-5.1 8-7.5c.6-.6 .6-.6 3.5-3.5c-.5-2-.5-2-1-3.5c3.6-6 6.5-9.2 12.5-13c.4-.6 .6-1.3 1-2c-.5-.6-1-1.3-1.5-2c2-3 2-3 4.5-4.5c.5 1 .5 1 1 3.5c2.5-1 2.5-1 8.5-5.5c.3 0 .7 0 1 0c-.1-.4-.1-.4-.5-2.5c4.3-3.3 10-2.5 15-1zM153 69.5c1.2 .2 2.3 .3 3.5 .5c.3 .7 .7 1.3 1 2c.3 0 .7 0 1 0c.5 1.2 1 2.3 1.5 3.5c3.4 7.6 6.2 13.7 12.2 19.6c3.3 3.4 3.3 3.4 3.8 5.9c-.2 .2-.3 .3-.5 .5c.1 .7 .1 .7 .5 4.5c-.5 .3-1 .7-1.5 1c-.8-.3-1.7-.7-2.5-1c.3 .7 .7 1.3 1 2c1.2 5.8 1.4 10 1 16c-.7 0-1.3 0-2 0c-.4-1.3-.8-2.7-1.3-4.1c-2.1-6.1-5.2-11.4-8.7-16.9c-.2-.8-.2-.8-1.5-5c-.3-.6-.3-.6-2.1-3.5c-1.9-3.5-1.9-3.5-2.9-8.5c.7 0 1.3 0 2 0c.5 .5 1 1 1.5 1.5c-.8-2.3-1.7-4.6-2.5-7c-.7-2-1.3-4-2-6c-.7-1-1.3-2-2-3c.2-.7 .3-1.3 .5-2zM86.5 100.5c1 0 2 0 3 0c1.3 .8 2.7 1.7 4 2.5c-2.5 2.5-2.5 2.5-8 5.5c-5.5 4.3-8.6 7-10 14c-.8 2.2-1.7 4.3-2.5 6.5c-1.2 4.6-.5 9.3 0 14c-.5 0-1 0-1.5 0c-.2 2-.3 3.9-.5 6c-.7 0-1.3 0-2 0c-.3-.6-.3-.6-2-3.5c-1-.8-2-1.7-3-2.5c-.5-1.5-.5-1.5 1.5-8.5c.3-10.3-1-18.2-4-28c0-4 0-4 1-6c2 2 2 2 3 6.5c2.5-.5 2.5-.5 8-4c9.8-3.5 9.8-3.5 13-2.5zM43 161.5c2.5 2.5 2.5 2.5 3 5c-1.5 2.5-1.5 2.5-4 5c.5 3.5 .5 3.5 4 6.5c-.1 1.2-.4 2.3-.5 3.5c-5.3 3.5-5.3 3.5-8.5 3.5c-6-2.5-6-2.5-11.5-7c0-3 0-3 4-8c9.5-8.5 9.5-8.5 13.5-8.5z"/><path fill="#9B9D9E" d="M142.5 60.5c.5 1 .5 1-.5 4.5c1.2 1.5 2.3 3 3.5 4.5c2 4.5 2 4.5 2.5 9.5c-.5 .2-1 .3-1.5 .5c-.1 .2-.1 .2-.5 1c-2-1-3.9-2-6-3c-.8-.3-1.7-.7-2.5-1c-.3-.7-.7-1.3-1-2c.3-.5 .7-1 1-1.5c-.2-.7-.3-1.3-.5-2c-3.5 .5-3.5 .5-7.5 4c-.5 3-.5 3 0 5.5c-2 4-2 4-3.5 5.5c-.3 0-.7 0-1 0c0 .7 0 1.3 0 2c-2-1-2-1-2.5-2.5c.3-13.7 .3-13.7 4.8-18.5c4.1-3.9 6.3-4.8 11.7-6c1-.5 1-.5 3.5-.5zM98 70.5c.5 .3 1 .7 1.5 1c1.3 .3 2.7 .7 4 1c4.4 5.8 6.5 10.3 6.5 17.5c-2 2-2 2-5 2.5c-.3 .2-.3 .2-2 1c-.2-.2-.2-.2-1.5-1c.5-3 .5-3 1-5c-.7-.8-1.3-1.7-2-2.5c-.2-1.7-.3-3.3-.5-5c-4.5-2.7-8.5-3-13.5-2.5c-.1 .3-.1 .3-.5 1.5c1.3 .8 2.7 1.7 4 2.5c-.1 .4-.1 .4-.5 2.5c-2.3 1.2-4.6 2.3-7 3.5c-5.5 5.5-5.5 5.5-6.5 7c0 .5 0 1 0 1.5c.9-.1 .9-.1 5.5-.5c1 1.3 2 2.7 3 4c-.2 .5-.3 1-.5 1.5c-1.7 .5-3.2 1-5 1.6c-5.5 1.9-5.5 1.9-11 5.4c-.4 .1-.4 .1-2.5 .5c-1.5-1.5-1.5-1.5-2.5-6c-.4-.3-.6-.7-1-1c0-6.7 .9-11.7 3-18c.2-.6 .2-.6 1-3.5c7.8-9.7 20.5-14.1 32-9.5z"/><path fill="#FAF9F8" d="M103.5 40c0 .4 0 .6 0 1c2.2-.1 4.3-.4 6.5-.5c7 0 7 0 9.5 1c-.2 .6-.3 1.3-.5 2c-1.8 .6-3.7 1.3-5.5 2c-.2 .5-.3 1-.5 1.5c.7-.1 1.3-.4 2-.5c13.4-.3 13.4-.3 19.5 3.5c-.3 .6-.7 1.3-1 2c-1 0-2 0-3 0c0 .5 0 1 0 1.5c1.2 .1 1.2 .1 7 .5c3.5 2 3.5 2 5.5 4.5c0 1 0 2 0 3c-3.3 .9-6.7 1.6-10 2.5c-7.5 5-7.5 5-8.5 9c-.3 4.5-.4 9-.5 13.5c1.5 1 1.5 1 7.5 2.5c.9 .6 .9 .6 5.5 3.5c2 1 2 1 9 2.5c7.9 4 11.3 8.3 15 16.5c0 .5 0 1 0 1.5c-1 .2-2 .3-3 .5c.5 1.8 1 3.7 1.5 5.5c.3 8.5-.8 13.6-6 20.5c-.7-.2-1.3-.3-2-.5c0-1.7 0-3.3 0-5c-.7 .2-1.3 .3-2 .5c-1.3 2.7-2.7 5.3-4 8c-3.2 3.7-6.4 6.2-11 8c-1-1-1-1-1.5-3c-2.5 1.3-4.8 2.9-7 4.5c-6.3 2.8-10.8 3.2-17.6 1c-1.3-.5-2.6-1-3.9-1.5c.2 1.2 .3 2.3 .5 3.5c-.2 .3-.3 .7-.5 1c-5.9 0-9-1.8-14-5c-3-3.5-3-3.5-3-4.5c-.7 0-1.3 0-2 0c.5 2.2 1 4.3 1.5 6.5c-.3 .3-.7 .7-1 1c-4.5-.9-7.6-3.6-11-6.5c-4-6-4-9.3-4-16.5c2.5-7.5 2.5-7.5 3.5-9.5c.3-1.7 .7-3.3 1-5c6.4-8 14.7-13 24.5-16c6.4-6.3 6.4-6.3 8.5-10.5c0-6-1.6-9.8-4.5-15c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-1.3-.7-2.7-1.3-4-2c-5.7-1.9-10.2-.3-15.5 1.5c-10 5.5-10 5.5-13 10c-.3-.2-.3-.2-2-1c1.3-6.8 4.6-11.7 9.5-16.5c.5-.3 1-.6 1.5-1c.2-.5 .3-1 .5-1.5c-1 .1-1 .1-6 1c-.3-.4-.7-.6-1-1c.5-2 .5-2 4-6c5-2.5 5-2.5 7.5-3c0-.5 0-1 0-1.5c-10.5-1.5-18.5 1-27 7c-.5 .9-1 1.6-1.5 2.5c-.9 .6-.9 .6-5.5 4c-4 3.5-7.6 7-11 11c-4.6 3.8-8.7 6.8-14 9.5c-.7-.3-1.3-.7-2-1c.2-.7 .3-1.3 .5-2c1.8-1 3.7-2 5.5-3c5.2-4.2 9.7-8.5 14-13.5c3.1-2.8 6.2-5.5 9.5-8c1.3-1.3 2.6-2.6 4-4c5.6-3.6 10.4-6.1 17-7c.5-1 1-2 1.5-3c7.2-2.4 12.5-2.2 20-.5c.2 .1 .2 .1 1 .5c3.5 0 3.5 0 8.5-3.5c1-.5 1-.5 3-.5zM118 92.5c-1 1.2-2 2.3-3 3.5c.5 2.5 .5 2.5 3 5c1.8 .8 3.7 1.7 5.5 2.5c.5 1 .5 1 .5 4.5c-4 4.8-6.1 5-12.5 6c0 .7 0 1.3 0 2c4.7 1.5 7.7 .2 12-1.5c1.2-.7 2.3-1.3 3.5-2c1.3 .3 2.7 .7 4 1c3.5 0 3.5 0 5.5-1.5c-.2-.5-.3-1-.5-1.5c-1.7-.3-3.3-.7-5-1c-2.5-2-2.5-2-3-5.5c2-3 2-3 4-5.5c.5-3.5 .5-3.5-1.5-6.5c-4.3-2.2-8.2-1-12.5 .5zM146 58.5c5.8 1.1 8.3 3.9 12.5 8c3.7 5.9 4.4 7.3 5 14c2.8 6.2 5.4 10 10.5 14.5c5.2 5.2 7.2 10.3 9 17.5c0 3.5 0 3.5-2 11c-.2 1.3-.2 1.3-1 7.5c-3.8 10.7-3.8 10.7-8.5 13.5c-.5 .2-.5 .2-3 1c-1 1.3-2 2.7-3 4c-4.5 3-4.5 3-10.5 5c-2.7-.6-5.3-1.3-8-2c-1.7 1.2-3.3 2.3-5 3.5c-2 1-2 1-4 .5c.5-1.5 .5-1.5 6-5c.3-.3 .3-.3 2-2c2.4 0 4.9 0 7.5 0c.5 .5 1 1 1.5 1.5c4.3-.6 7.1-2.3 10.5-5c3.5-5.5 3.5-5.5 4-7c1.5 0 2.9 0 4.5 0c4.3-8.7 4.6-14 2.5-23.5c-.3-1-.7-2-1-3c.3 .1 .3 .1 2 .5c1.5 2.5 1.5 2.5 1.5 4c.5-.2 1-.3 1.5-.5c-.8-10-4.3-16-11.4-22.8c-3.7-3.8-5.5-6.9-7.6-11.7c-.7-1.5-1.5-2.9-2.3-4.4c-1.7-3.6-1.7-3.6-1.7-5.6c.3-.2 .7-.3 1-.5c-.5-3-.5-3-4.5-7c-2-1-3.9-2-6-3c-.7-.4-1.3-.6-2-1c0-.6 0-1.3 0-2zM33.5 83.5c-1 4-5.1 6.4-8 9c-8.6 8.1-14.6 17.2-15.2 29.2c.3 8.1 .3 8.1 2.2 11.8c.5 0 1 0 1.5 0c.4-2.9 .6-6 1-9c.6 0 1.3 0 2 0c.2 1.3 .4 2.7 .6 4.1c1.8 9.3 5.3 14.8 12.9 20.4c2.3 1.2 4.6 2.3 7 3.5c.7 .6 .7 .6 4.5 3.5c3.5 0 3.5 0 8.5-1.5c1 0 2 0 3 0c-1 2-1 2-2.5 3.5c-6.5 1.7-9.6 .9-16-1.5c-1.1-1-2.3-2-3.5-3c-1.5-.7-2.9-1.3-4.5-2c-3.9-3.4-6.9-6.5-9.5-11c-1.1-.5-2.3-1-3.5-1.5c-5.9-6.7-8-12.7-8-21.5c2.4-12.2 7.2-19.8 16.5-28c4.5-4 4.5-4 6-5.5c3.5-1 3.5-1 5-.5zM50.5 172c2.5 1 2.5 1 7 6.5c6 6.2 12 11.8 18.8 17.1c5.7 4.5 11.3 9.1 16.7 13.9c1.3 1.2 2.7 2.3 4 3.5c.7 .7 1.3 1.3 2 2c-.2 .8-.3 1.7-.5 2.5c-6 2.9-11.7 4-18.5 4c-2.6-2.6-4.3-5.5-5.5-9c-.3 .1-.3 .1-1.5 .5c0 2.3 0 4.6 0 7c-.3 .5-.7 1-1 1.5c-13.3 0-24.6-4.6-34.5-13.5c-4.5-4.5-4.5-4.5-8.5-11c.5-2.5 .5-2.5 4.5-7c5-3 5-3 8-3.5c.5 .3 1 .7 1.5 1c2-.5 2-.5 4-2c.9-4.2 1.7-8.3 2.5-12.5c.4-.3 .6-.7 1-1z"/><path fill="#03B5BD" d="M58.5 149.5c2.5 .5 2.5 .5 7 3.5c9.8 5.2 18.9 7.9 30 8.5c2.2 .5 4.3 1 6.5 1.5c7 0 7 0 9-.5c2.3-.2 4.6-.3 7-.5c3.2-.9 6.3-1.9 9.5-3c1.3-.2 2.7-.3 4-.5c.2 .2 .3 .3 .5 .5c-.8 6.3-4.1 10.2-8 15c-.2 .5-.3 1-.5 1.5c1.7-1 3.3-2 5-3c.7 .2 1.3 .3 2 .5c-3 12.2-9.3 20.3-17.5 29.5c-.7 .7-.7 .7-4 4.5c-1 .8-2 1.7-3 2.5c-3.5-.5-3.5-.5-8-5c-2.3-1.8-4.7-3.7-7-5.5c-2.7-2.2-5.3-4.3-8-6.5c-.8-.6-.8-.6-4.5-3.5c-11-9.3-20.7-18.8-28.5-31c3-4 3-4 8.5-8.5z"/><ellipse cx="94" cy="94" rx="6.5" ry="6.5" fill="#212E34"/><ellipse cx="135" cy="85.6" rx="6.2" ry="5.8" fill="#212E34"/><g fill="#FAF9F8"><circle cx="91.5" cy="91.5" r="2.1"/><circle cx="134.6" cy="84.3" r="2"/></g><ellipse cx="124" cy="93.8" rx="3.4" ry="1.1" fill="#9B9D9E" transform="rotate(-6 124 93.8)"/></g></g><rect x="5" y="5" width="230" height="230" rx="45" fill="none" stroke="#5B6B73" stroke-width="10"/></symbol></defs>
2
+ <rect x="1" y="1" width="958" height="438" rx="18" fill="#151D22" stroke="#34434B" stroke-width="1.5"/>
3
+ <path d="M389.1 186.4 403.2 182.6 417.1 178.5 430.8 174 444.2 169.3 457.5 164.3 470.6 159 483.6 153.6 496.5 148.1 509.2 142.4 521.9 136.7 534.6 131 547.1 125.3 559.7 119.6 572.3 114 584.9 108.5 597.5 103.2 610.2 98.1 623 93.3 635.9 88.7 648.9 84.4 662.1 80.5 675.4 76.9 688.9 73.8 702.6 71.2 716.6 69 730.8 67.4 745.2 66.4 760 66 A2 2 0 0 1 760 62 L760 62 745.1 62.2 730.4 63.1 716 64.5 701.9 66.5 687.9 69 674.2 72 660.7 75.4 647.3 79.2 634.1 83.3 621 87.8 608.1 92.5 595.2 97.5 582.4 102.7 569.7 108 557 113.5 544.4 119 531.7 124.6 519 130.1 506.3 135.7 493.5 141.1 480.6 146.5 467.7 151.7 454.6 156.7 441.4 161.5 428.1 166 414.6 170.2 400.9 174.1 386.9 177.6 A4.5 4.5 0 0 1 389.1 186.4Z" fill="#21BEC5"/>
4
+ <path d="M386.9 230.4 400.9 233.9 414.6 237.8 428.1 242 441.4 246.6 454.6 251.5 467.7 256.5 480.6 261.8 493.5 267.3 506.3 272.8 519 278.4 531.7 284.1 544.3 289.8 557 295.4 569.7 301 582.4 306.4 595.2 311.7 608.1 316.8 621 321.6 634.1 326.2 647.3 330.4 660.7 334.3 674.2 337.8 687.9 340.8 701.8 343.4 716 345.4 730.4 346.9 745.1 347.8 760 348 A2 2 0 0 1 760 344 L760 344 745.2 343.6 730.8 342.5 716.6 340.9 702.6 338.7 688.9 336 675.4 332.8 662.1 329.2 648.9 325.2 635.9 320.9 623 316.2 610.2 311.2 597.5 306 584.9 300.6 572.3 295 559.7 289.3 547.2 283.5 534.6 277.7 522 271.9 509.3 266.1 496.5 260.3 483.6 254.7 470.6 249.2 457.5 243.9 444.2 238.8 430.8 234 417.1 229.5 403.2 225.4 389.1 221.6 A4.5 4.5 0 0 1 386.9 230.4Z" fill="#21BEC5"/>
5
+ <path d="M300.4 200.5 294.9 200 289.3 199.4 283.6 199 277.7 198.6 271.7 198.2 265.6 197.9 259.4 197.6 253.2 197.3 246.8 197.1 240.4 197 233.9 196.9 227.4 196.8 220.8 196.8 214.2 196.9 207.6 197 201 197.1 194.4 197.3 187.7 197.5 181.1 197.8 174.5 198.1 168 198.4 161.5 198.8 155 199.3 148.6 199.8 142.3 200.3 136 200.9 129.9 201.6 123.8 202.3 A1.8 1.8 0 0 1 124.2 205.7 L124.2 205.7 130.2 205.2 136.4 204.7 142.6 204.2 148.9 203.8 155.3 203.4 161.7 203.1 168.2 202.8 174.7 202.6 181.3 202.4 187.9 202.2 194.5 202.1 201.1 202.1 207.7 202.1 214.2 202.1 220.8 202.2 227.3 202.3 233.8 202.5 240.2 202.7 246.6 203 252.9 203.3 259.1 203.7 265.2 204.1 271.3 204.5 277.2 205 283 205.6 288.7 206.2 294.2 206.8 299.6 207.5 A3.5 3.5 0 0 1 300.4 200.5Z" fill="#21BEC5"/>
6
+ <path d="M770 94C740 150 690 180 656 193" fill="none" stroke="#7E8A90" stroke-width="2.4" stroke-linecap="round" stroke-dasharray="0.1 7"/>
7
+ <path d="M770 316C740 260 690 232 656 220" fill="none" stroke="#7E8A90" stroke-width="2.4" stroke-linecap="round" stroke-dasharray="0.1 7"/>
8
+ <path d="M600 205C540 205 450 205 388 205" fill="none" stroke="#21BEC5" stroke-width="2.4" stroke-linecap="round"/>
9
+ <path d="M398 199l-8 6 8 6" fill="none" stroke="#21BEC5" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <circle class="k1" opacity="0" r="4.6" fill="#21BEC5" stroke="#151D22" stroke-width="2"/>
11
+ <circle class="k2" opacity="0" r="4.6" fill="#21BEC5" stroke="#151D22" stroke-width="2"/>
12
+ <circle class="k3" opacity="0" r="3.6" fill="#7E8A90" stroke="#151D22" stroke-width="2"/>
13
+ <circle class="k4" opacity="0" r="3.6" fill="#7E8A90" stroke="#151D22" stroke-width="2"/>
14
+ <circle class="k5" opacity="0" r="3.8" fill="#21BEC5" stroke="#151D22" stroke-width="2"/>
15
+ <circle class="k6" opacity="0" r="4.6" fill="#F0B455" stroke="#151D22" stroke-width="2"/>
16
+ <circle class="k7" opacity="0" r="4.6" fill="#F0B455" stroke="#151D22" stroke-width="2"/>
17
+ <circle class="k8" opacity="0" r="4.6" fill="#21BEC5" stroke="#151D22" stroke-width="2"/>
18
+ <ellipse cx="344" cy="255" rx="46" ry="5" fill="#BCC5CA" opacity="0.12"/>
19
+ <use href="#jack" x="300" y="160" width="88" height="88"/>
20
+ <ellipse cx="790.0" cy="100" rx="34" ry="4" fill="#BCC5CA" opacity="0.12"/>
21
+ <rect x="760" y="34" width="60" height="60" rx="12" fill="#212E34" stroke="#5B6B73" stroke-width="2.4"/><circle cx="790" cy="64" r="11.4" fill="#21BEC5"/>
22
+ <g fill="none" stroke="#21BEC5" stroke-width="3" stroke-linecap="round"><path d="M824 30l7-9"/><path d="M830 38l9-3.4"/></g>
23
+ <ellipse cx="790.0" cy="382" rx="34" ry="4" fill="#BCC5CA" opacity="0.12"/>
24
+ <rect x="760" y="316" width="60" height="60" rx="12" fill="#212E34" stroke="#5B6B73" stroke-width="2.4"/><circle cx="790" cy="346" r="11.4" fill="#21BEC5"/>
25
+ <g fill="none" stroke="#21BEC5" stroke-width="3" stroke-linecap="round"><path d="M824 312l7-9"/><path d="M830 320l9-3.4"/></g>
26
+ <rect x="600" y="177" width="56" height="56" rx="11.2" fill="#212E34" stroke="#5B6B73" stroke-width="2.2"/>
27
+ <circle cx="622" cy="205" r="5.5" fill="#21BEC5"/>
28
+ <g fill="none" stroke="#21BEC5" stroke-width="3" stroke-linecap="round"><path d="M630 196a12 12 0 0 1 0 18"/><path d="M637 190a20 20 0 0 1 0 30"/></g>
29
+ <rect x="64" y="174" width="60" height="60" rx="12" fill="#212E34" stroke="#5B6B73" stroke-width="2.4"/>
30
+ <circle cx="94" cy="197" r="7.5" fill="#FAF9F8"/><path d="M81 220a13 12 0 0 1 26 0z" fill="#FAF9F8"/>
31
+ <text x="344" y="276" class="s" font-size="15" fill="#E6EDF3" font-weight="700" text-anchor="middle">Coordinator</text>
32
+ <text x="344" y="296" class="m" font-size="13" fill="#9B9D9E" text-anchor="middle">/jh-orchestrate</text>
33
+ <text x="344" y="314" class="s" font-size="12" fill="#9B9D9E" text-anchor="middle">main checkout</text>
34
+ <text x="790" y="124" class="s" font-size="15" fill="#E6EDF3" font-weight="700" text-anchor="middle">Lane a</text>
35
+ <text x="790" y="143" class="m" font-size="13" fill="#9B9D9E" text-anchor="middle">worktree a</text>
36
+ <text x="790" y="406" class="s" font-size="15" fill="#E6EDF3" font-weight="700" text-anchor="middle">Lane b</text>
37
+ <text x="790" y="425" class="m" font-size="13" fill="#9B9D9E" text-anchor="middle">worktree b</text>
38
+ <text x="628" y="259" class="m" font-size="14" fill="#E6EDF3" font-weight="700" text-anchor="middle">jh watch</text>
39
+ <text x="94" y="262" class="s" font-size="15" fill="#E6EDF3" font-weight="700" text-anchor="middle">You</text>
40
+ <text x="212" y="172" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="middle">open decisions,</text>
41
+ <text x="212" y="189" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="middle">one approval per PR</text>
42
+ <text x="212" y="232" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="middle">asked as a choice</text>
43
+ <text x="520" y="104" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="end">prompts →</text>
44
+ <text x="520" y="121" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="end">← questions as text</text>
45
+ <text x="520" y="310" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="end">prompts →</text>
46
+ <text x="520" y="327" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="end">← questions as text</text>
47
+ <text x="494" y="196" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="middle">wakes</text>
48
+ <text x="682" y="210" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="start">state, PR,</text>
49
+ <text x="682" y="226" class="s" font-size="12.5" fill="#9B9D9E" text-anchor="start">checks</text>
50
+ </svg>