@tyroneross/build-loop 0.36.0 → 0.43.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 (270) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor/rules/build-loop-surface.mdc +12 -11
  6. package/.cursor/rules/skill-index.mdc +33 -0
  7. package/AGENTS.md +213 -34
  8. package/README.md +99 -31
  9. package/agents/advisor.md +4 -4
  10. package/agents/alignment-checker.md +2 -2
  11. package/agents/architecture-scout.md +4 -4
  12. package/agents/build-orchestrator.md +38 -36
  13. package/agents/database-assessor.md +11 -5
  14. package/agents/design-contract-specialist.md +8 -8
  15. package/agents/fact-checker.md +13 -3
  16. package/agents/fix-critique.md +2 -2
  17. package/agents/independent-auditor.md +60 -7
  18. package/agents/leak-scanner.md +82 -0
  19. package/agents/overfitting-reviewer.md +2 -2
  20. package/agents/plan-critic.md +1 -1
  21. package/agents/promotion-reviewer.md +5 -5
  22. package/agents/retrospective-synthesizer.md +138 -35
  23. package/agents/scope-auditor.md +82 -11
  24. package/agents/security-reviewer.md +56 -2
  25. package/agents/self-improvement-architect.md +17 -3
  26. package/agents/transcript-pattern-miner.md +5 -5
  27. package/agents/ui-validator.md +1 -1
  28. package/bin/build-loop-debugger.js +143 -0
  29. package/bin/build-loop-install.js +1 -4
  30. package/bin/build-loop-load-probe.js +345 -0
  31. package/codex-skills/build-loop/SKILL.md +28 -6
  32. package/commands/feedback.md +37 -0
  33. package/dist/src/interactive-verifier.d.ts +1 -14
  34. package/dist/src/interactive-verifier.d.ts.map +1 -1
  35. package/dist/src/interactive-verifier.js +6 -113
  36. package/dist/src/interactive-verifier.js.map +1 -1
  37. package/dist/src/quality.d.ts +5 -0
  38. package/dist/src/quality.d.ts.map +1 -0
  39. package/dist/src/quality.js +81 -0
  40. package/dist/src/quality.js.map +1 -0
  41. package/dist/src/storage.d.ts.map +1 -1
  42. package/dist/src/storage.js +37 -3
  43. package/dist/src/storage.js.map +1 -1
  44. package/docs/agent-surface-policy.md +35 -31
  45. package/docs/memory-setup.md +19 -0
  46. package/hooks/git/pre-push +65 -4
  47. package/hooks/hooks.json +95 -38
  48. package/hooks/pre-commit +20 -1
  49. package/hooks/pre-edit-rally-point.sh +10 -3
  50. package/hooks/session-start-codex-hook-trust.sh +30 -0
  51. package/hooks/session-start-git-hooks.sh +3 -1
  52. package/hooks/session-start-rally-point.sh +52 -4
  53. package/hooks/session-start-worktree-gc.sh +47 -94
  54. package/hooks/stop-transcript-sweep.sh +173 -0
  55. package/hooks/test_closeout.sh +14 -2
  56. package/package.json +8 -7
  57. package/scripts/README.md +1 -1
  58. package/scripts/_paths.py +65 -0
  59. package/scripts/groundwork_exchange.py +1012 -0
  60. package/scripts/install_memory.py +33 -1
  61. package/scripts/lessons_index/ingest.py +13 -2
  62. package/scripts/lessons_index/query.py +36 -13
  63. package/scripts/memory_context/__init__.py +108 -14
  64. package/scripts/memory_graph/__init__.py +5 -1
  65. package/scripts/project_resolver.py +42 -36
  66. package/scripts/sync_plugin_cache.py +37 -2
  67. package/skills/agent-rally-point/SKILL.md +46 -0
  68. package/skills/api-registry-bridge/SKILL.md +1 -1
  69. package/skills/architecture/dead/SKILL.md +1 -1
  70. package/skills/architecture/impact/SKILL.md +1 -1
  71. package/skills/architecture/review/SKILL.md +1 -1
  72. package/skills/architecture/rules/SKILL.md +3 -3
  73. package/skills/architecture/scan/SKILL.md +1 -1
  74. package/skills/architecture/trace/SKILL.md +1 -1
  75. package/skills/attribution-standard/SKILL.md +6 -6
  76. package/skills/auto-decision-capture/SKILL.md +31 -2
  77. package/skills/auto-finding-capture/SKILL.md +28 -1
  78. package/skills/build-loop/SKILL.md +131 -23
  79. package/skills/build-loop/fallbacks.md +16 -21
  80. package/skills/build-loop/phases/ui-validation.md +2 -2
  81. package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
  82. package/skills/build-loop/references/apple-native-planning.md +1 -1
  83. package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
  84. package/skills/build-loop/references/autonomy-dashboard.md +115 -0
  85. package/skills/build-loop/references/capability-routing.md +24 -2
  86. package/skills/build-loop/references/coordination.md +24 -6
  87. package/skills/build-loop/references/experiment-results-template.md +15 -3
  88. package/skills/build-loop/references/leadership.md +1 -1
  89. package/skills/build-loop/references/memory.md +14 -3
  90. package/skills/build-loop/references/modular-systems-pack.md +8 -0
  91. package/skills/build-loop/references/output-style.md +86 -0
  92. package/skills/build-loop/references/phase-1-assess.md +102 -2
  93. package/skills/build-loop/references/phase-2-plan.md +9 -1
  94. package/skills/build-loop/references/phase-3-execute.md +5 -2
  95. package/skills/build-loop/references/phase-4-review.md +85 -8
  96. package/skills/build-loop/references/phase-5-iterate.md +76 -8
  97. package/skills/build-loop/references/phase-6-learn.md +10 -17
  98. package/skills/build-loop/references/privileged-request-broker.md +254 -0
  99. package/skills/build-loop/references/resource-aware-execution.md +183 -0
  100. package/skills/build-loop/references/self-recursive-dev.md +2 -2
  101. package/skills/build-loop/references/status-output-format.md +207 -0
  102. package/skills/build-loop/references/verify-dispatch.md +56 -2
  103. package/skills/building-with-deepagents/SKILL.md +1 -1
  104. package/skills/claim-scope/SKILL.md +185 -0
  105. package/skills/color-engine/SKILL.md +103 -0
  106. package/skills/color-engine/_core.py +464 -0
  107. package/skills/color-engine/color_engine.py +175 -0
  108. package/skills/cost-rca/SKILL.md +61 -0
  109. package/skills/data-plane-worktrees/SKILL.md +139 -0
  110. package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
  111. package/skills/database-practice/SKILL.md +200 -0
  112. package/skills/database-practice/references/diagnostic-queries.sql +126 -0
  113. package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
  114. package/skills/database-practice/scripts/db_table_map.py +1244 -0
  115. package/skills/database-practice/scripts/test_db_table_map.py +514 -0
  116. package/skills/debug-loop/SKILL.md +36 -6
  117. package/skills/debugging-memory/SKILL.md +32 -430
  118. package/skills/debugging-memory/references/pattern-extraction.md +4 -4
  119. package/skills/debugging-memory/references/search.md +32 -120
  120. package/skills/debugging-memory/references/store.md +32 -126
  121. package/skills/debugging-memory/references/subagent-integration.md +1 -1
  122. package/skills/decision-queue/SKILL.md +251 -0
  123. package/skills/decision-queue/assets/template.html +1242 -0
  124. package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
  125. package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
  126. package/skills/defenseclaw-bridge/SKILL.md +2 -2
  127. package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
  128. package/skills/drain-proposals/SKILL.md +53 -0
  129. package/skills/focused-loop-builder/SKILL.md +31 -0
  130. package/skills/focused-loop-builder/references/spec-format.md +27 -0
  131. package/skills/handoff/SKILL.md +169 -8
  132. package/skills/ibr-bridge/SKILL.md +4 -1
  133. package/skills/knowledge/SKILL.md +26 -14
  134. package/skills/knowledge/references/review-mode.md +2 -3
  135. package/skills/knowledge/templates/madr-minimal.md +1 -1
  136. package/skills/mcp-builder/SKILL.md +1 -1
  137. package/skills/model-bakeoff/SKILL.md +48 -10
  138. package/skills/model-tiering/SKILL.md +92 -31
  139. package/skills/native-ax-driver/SKILL.md +38 -5
  140. package/skills/native-ax-driver/scripts/native_driver.py +278 -22
  141. package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
  142. package/skills/optimize/SKILL.md +1 -1
  143. package/skills/plugin-builder/SKILL.md +48 -1
  144. package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
  145. package/skills/plugin-builder/references/distribution.md +13 -2
  146. package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
  147. package/skills/plugin-tests/SKILL.md +2 -2
  148. package/skills/recursive-retrospective/SKILL.md +1 -1
  149. package/skills/repo-closeout/SKILL.md +17 -0
  150. package/skills/repo-closeout/agents/openai.yaml +4 -0
  151. package/skills/repo-maintenance/SKILL.md +179 -0
  152. package/skills/repo-maintenance/agents/openai.yaml +4 -0
  153. package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
  154. package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
  155. package/skills/repo-maintenance/references/safety-protocol.md +106 -0
  156. package/skills/repo-maintenance/references/stack-profiles.md +138 -0
  157. package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
  158. package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
  159. package/skills/repository-intelligence/SKILL.md +189 -0
  160. package/skills/repository-intelligence/agents/openai.yaml +4 -0
  161. package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
  162. package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
  163. package/skills/research/SKILL.md +12 -2
  164. package/skills/root-cause-analysis/SKILL.md +1 -1
  165. package/skills/runtime-parity-verification/SKILL.md +36 -1
  166. package/skills/security-methodology/SKILL.md +23 -10
  167. package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
  168. package/skills/security-methodology/references/cross-source-matrix.md +1 -1
  169. package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
  170. package/skills/security-scan/SKILL.md +55 -15
  171. package/skills/self-improve/SKILL.md +70 -50
  172. package/skills/silent-assumptions/SKILL.md +341 -0
  173. package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
  174. package/skills/spec-writing/SKILL.md +128 -24
  175. package/skills/spec-writing/scripts/check_checklist.py +114 -15
  176. package/skills/ui-design/SKILL.md +6 -4
  177. package/skills/ui-design/references/color-engine.md +132 -0
  178. package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
  179. package/skills/ui-design/references/ui-guidance-sources.md +1 -1
  180. package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
  181. package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
  182. package/plugin-artifacts/codex/AGENTS.md +0 -560
  183. package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
  184. package/plugin-artifacts/codex/LICENSE +0 -202
  185. package/plugin-artifacts/codex/README.md +0 -313
  186. package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
  187. package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
  188. package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
  189. package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
  190. package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
  191. package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
  192. package/plugin-artifacts/codex/references/backlog-system.md +0 -285
  193. package/plugin-artifacts/codex/references/capability-routing.md +0 -231
  194. package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
  195. package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
  196. package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
  197. package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
  198. package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
  199. package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
  200. package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
  201. package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
  202. package/plugin-artifacts/codex/references/leadership.md +0 -72
  203. package/plugin-artifacts/codex/references/memory-systems.md +0 -261
  204. package/plugin-artifacts/codex/references/memory.md +0 -313
  205. package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
  206. package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
  207. package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
  208. package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
  209. package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
  210. package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
  211. package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
  212. package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
  213. package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
  214. package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
  215. package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
  216. package/plugin-artifacts/codex/references/self-review.md +0 -234
  217. package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
  218. package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
  219. package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
  220. package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
  221. package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
  222. package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
  223. package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
  224. package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
  225. package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
  226. package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
  227. package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
  228. package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
  229. package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
  230. package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
  231. package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
  232. package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
  233. package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
  234. package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
  235. package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
  236. package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
  237. package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
  238. package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
  239. package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
  240. package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
  241. package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
  242. package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
  243. package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
  244. package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
  245. package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
  246. package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
  247. package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
  248. package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
  249. package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
  250. package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
  251. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
  252. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
  253. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
  254. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
  255. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
  256. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
  257. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
  258. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
  259. package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
  260. package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
  261. package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
  262. package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
  263. package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
  264. package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
  265. package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
  266. package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
  267. package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
  268. package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
  269. package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
  270. package/scripts/build_codex_plugin_artifact.py +0 -321
@@ -0,0 +1,1242 @@
1
+ <!--
2
+ decision-queue template — READ skills/decision-queue/SKILL.md before editing this.
3
+
4
+ Two zones:
5
+ 1. CONTENT ZONE — window.__META__ and window.__ITEMS__ inside the
6
+ <script id="app-data"> tag. Replace freely per topic.
7
+ 2. PLUMBING ZONE — everything in <script id="app-script">. Tested and
8
+ correct. Do not touch it, and NEVER replace the HEAD_HTML /
9
+ SAVE_BAR_HTML constants with a runtime document.head / DOM capture
10
+ — see SKILL.md "The one rule that matters" for why that corrupts
11
+ the page on every save.
12
+
13
+ This comment block itself must NOT ship in the published artifact —
14
+ strip it (and this note) when you copy this file to scratchpad.
15
+ -->
16
+ <title>Decision Queue</title>
17
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
18
+ <style id="app-style">
19
+ :root {
20
+ --bg: #F5F6F8;
21
+ --surface: #FFFFFF;
22
+ --surface-2: #FAFBFC;
23
+ --border: #E0E4EA;
24
+ --border-strong: #C7CDD7;
25
+ --text: #171B21;
26
+ --text-muted: #5B6472;
27
+ --text-faint: #8A93A2;
28
+ --accent: #2F52D6;
29
+ --accent-soft: #EAEEFC;
30
+ --accent-strong: #1E3AAE;
31
+ --danger: #A5393B;
32
+ --danger-soft: #F7E9E9;
33
+ --good: #2C7A55;
34
+ --good-soft: #E7F3ED;
35
+ --neutral-chip: #EEF0F3;
36
+ --warn: #9A6212;
37
+ --shadow: rgba(20, 24, 30, 0.10);
38
+ }
39
+
40
+ @media (prefers-color-scheme: dark) {
41
+ :root:not([data-theme="light"]) {
42
+ --bg: #0E1116;
43
+ --surface: #161A21;
44
+ --surface-2: #1B2029;
45
+ --border: #2A303B;
46
+ --border-strong: #3A4150;
47
+ --text: #E8EAEE;
48
+ --text-muted: #9BA3B2;
49
+ --text-faint: #6B7382;
50
+ --accent: #7089FF;
51
+ --accent-soft: rgba(112, 137, 255, 0.14);
52
+ --accent-strong: #91A4FF;
53
+ --danger: #E28C8C;
54
+ --danger-soft: rgba(226, 140, 140, 0.12);
55
+ --good: #7BCBA4;
56
+ --warn: #D9A441;
57
+ --good-soft: rgba(123, 203, 164, 0.12);
58
+ --neutral-chip: #232935;
59
+ --shadow: rgba(0, 0, 0, 0.4);
60
+ }
61
+ }
62
+
63
+ :root[data-theme="dark"] {
64
+ --bg: #0E1116;
65
+ --surface: #161A21;
66
+ --surface-2: #1B2029;
67
+ --border: #2A303B;
68
+ --border-strong: #3A4150;
69
+ --text: #E8EAEE;
70
+ --text-muted: #9BA3B2;
71
+ --text-faint: #6B7382;
72
+ --accent: #7089FF;
73
+ --accent-soft: rgba(112, 137, 255, 0.14);
74
+ --accent-strong: #91A4FF;
75
+ --danger: #E28C8C;
76
+ --danger-soft: rgba(226, 140, 140, 0.12);
77
+ --good: #7BCBA4;
78
+ --good-soft: rgba(123, 203, 164, 0.12);
79
+ --neutral-chip: #232935;
80
+ --shadow: rgba(0, 0, 0, 0.4);
81
+ }
82
+
83
+ * { box-sizing: border-box; }
84
+
85
+ body {
86
+ margin: 0;
87
+ background: var(--bg);
88
+ color: var(--text);
89
+ font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
90
+ font-size: 15px;
91
+ line-height: 1.55;
92
+ -webkit-font-smoothing: antialiased;
93
+ }
94
+
95
+ .page {
96
+ max-width: 900px;
97
+ margin: 0 auto;
98
+ padding: 40px 24px 4px;
99
+ }
100
+
101
+ .eyebrow {
102
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
103
+ font-size: 11.5px;
104
+ letter-spacing: 0.09em;
105
+ text-transform: uppercase;
106
+ color: var(--text-faint);
107
+ margin-bottom: 10px;
108
+ }
109
+
110
+ h1 {
111
+ font-family: "Archivo", ui-sans-serif, sans-serif;
112
+ font-weight: 800;
113
+ font-size: clamp(26px, 4vw, 34px);
114
+ letter-spacing: -0.01em;
115
+ text-wrap: balance;
116
+ margin: 0 0 12px;
117
+ }
118
+
119
+ .lede {
120
+ color: var(--text-muted);
121
+ max-width: 62ch;
122
+ font-size: 15.5px;
123
+ margin: 0;
124
+ }
125
+
126
+ .summary-strip {
127
+ display: flex;
128
+ gap: 0;
129
+ margin-top: 24px;
130
+ border: 1px solid var(--border);
131
+ border-radius: 10px;
132
+ overflow: hidden;
133
+ background: var(--surface);
134
+ }
135
+
136
+ .summary-cell {
137
+ flex: 1;
138
+ padding: 14px 18px;
139
+ border-right: 1px solid var(--border);
140
+ }
141
+ .summary-cell:last-child { border-right: none; }
142
+
143
+ .summary-num {
144
+ font-family: "Archivo", sans-serif;
145
+ font-weight: 800;
146
+ font-size: 22px;
147
+ font-variant-numeric: tabular-nums;
148
+ line-height: 1;
149
+ margin-bottom: 4px;
150
+ }
151
+
152
+ .summary-num.accent { color: var(--accent); }
153
+
154
+ .summary-label {
155
+ font-size: 12px;
156
+ color: var(--text-muted);
157
+ }
158
+
159
+ .queue {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 18px;
163
+ margin-top: 24px;
164
+ }
165
+
166
+ .card {
167
+ background: var(--surface);
168
+ border: 1px solid var(--border);
169
+ border-radius: 12px;
170
+ padding: 22px 24px 24px;
171
+ }
172
+
173
+ .card-head {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: flex-start;
177
+ gap: 16px;
178
+ margin-bottom: 6px;
179
+ }
180
+
181
+ .card-num {
182
+ font-family: "IBM Plex Mono", monospace;
183
+ font-size: 12px;
184
+ color: var(--text-faint);
185
+ margin-bottom: 6px;
186
+ }
187
+
188
+ .card-title {
189
+ font-family: "Archivo", sans-serif;
190
+ font-weight: 700;
191
+ font-size: 18px;
192
+ letter-spacing: -0.005em;
193
+ text-wrap: balance;
194
+ margin: 0;
195
+ }
196
+
197
+ .meta-row {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ gap: 8px;
201
+ margin-top: 10px;
202
+ align-items: center;
203
+ }
204
+
205
+ .chip {
206
+ font-family: "IBM Plex Mono", monospace;
207
+ font-size: 11px;
208
+ padding: 3px 8px;
209
+ border-radius: 5px;
210
+ background: var(--neutral-chip);
211
+ color: var(--text-muted);
212
+ white-space: nowrap;
213
+ }
214
+
215
+ .chip.class-production { background: var(--danger-soft); color: var(--danger); }
216
+ .chip.class-safe { background: var(--good-soft); color: var(--good); }
217
+
218
+ .chip.repo {
219
+ max-width: 320px;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ }
223
+
224
+ .status-chip {
225
+ font-family: "IBM Plex Mono", monospace;
226
+ font-size: 11px;
227
+ padding: 3px 8px;
228
+ border-radius: 5px;
229
+ white-space: nowrap;
230
+ }
231
+ .status-chip.saved { background: var(--good-soft); color: var(--good); }
232
+ .status-chip.pending { background: var(--neutral-chip); color: var(--text-faint); }
233
+ /* Status carries in text colour, not a filled badge (Signal / S:N rule). */
234
+ .chip.stale { background: none; padding-left: 0; color: var(--warn); font-weight: 600; }
235
+ /* Options are a radio group: fieldset+legend give screen readers the question
236
+ that the choices belong to. Reset the UA chrome so it still looks flat. */
237
+ .options-group { border: 0; margin: 0; padding: 0; min-width: 0; }
238
+ .options-group > legend { padding: 0; }
239
+ body.filter-unanswered .card.answered { display: none; }
240
+ .filter-toggle {
241
+ display: inline-flex; align-items: center; gap: 8px;
242
+ font-size: 12px; color: var(--text-faint); cursor: pointer;
243
+ min-height: 44px;
244
+ }
245
+ .filter-toggle input { width: 16px; height: 16px; accent-color: var(--accent); }
246
+ .draft-note { color: var(--warn); }
247
+
248
+ .section {
249
+ margin-top: 16px;
250
+ padding-top: 16px;
251
+ border-top: 1px solid var(--border);
252
+ }
253
+ .card-head + .section { margin-top: 18px; }
254
+
255
+ .section-label {
256
+ font-family: "IBM Plex Mono", monospace;
257
+ font-size: 11px;
258
+ letter-spacing: 0.08em;
259
+ text-transform: uppercase;
260
+ color: var(--text-faint);
261
+ margin-bottom: 6px;
262
+ }
263
+
264
+ .section p { margin: 0; color: var(--text); }
265
+ .section p + p { margin-top: 8px; }
266
+ .section p.muted { color: var(--text-muted); }
267
+
268
+ .options {
269
+ list-style: none;
270
+ margin: 4px 0 0;
271
+ padding: 0;
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: 8px;
275
+ }
276
+
277
+ .opt-label {
278
+ display: flex;
279
+ gap: 10px;
280
+ padding: 10px 12px;
281
+ background: var(--surface-2);
282
+ border: 1px solid var(--border);
283
+ border-radius: 8px;
284
+ font-size: 14.5px;
285
+ cursor: pointer;
286
+ align-items: flex-start;
287
+ transition: border-color 120ms ease, background 120ms ease;
288
+ }
289
+ .opt-label:hover { border-color: var(--border-strong); }
290
+ .opt-label.checked { border-color: var(--accent); background: var(--accent-soft); }
291
+
292
+ .opt-radio {
293
+ margin-top: 3px;
294
+ accent-color: var(--accent);
295
+ flex-shrink: 0;
296
+ width: 15px;
297
+ height: 15px;
298
+ cursor: pointer;
299
+ }
300
+ .opt-radio:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
301
+
302
+ .opt-letter {
303
+ font-family: "IBM Plex Mono", monospace;
304
+ font-weight: 500;
305
+ font-size: 13px;
306
+ color: var(--text-faint);
307
+ margin-right: 2px;
308
+ }
309
+
310
+ .comment-field {
311
+ margin-top: 10px;
312
+ }
313
+
314
+ .comment-field label {
315
+ display: block;
316
+ margin-bottom: 6px;
317
+ }
318
+
319
+ textarea.comment-input {
320
+ width: 100%;
321
+ min-height: 60px;
322
+ resize: vertical;
323
+ font-family: "IBM Plex Sans", sans-serif;
324
+ font-size: 14px;
325
+ line-height: 1.5;
326
+ padding: 10px 12px;
327
+ border: 1px solid var(--border);
328
+ border-radius: 8px;
329
+ background: var(--surface-2);
330
+ color: var(--text);
331
+ }
332
+ textarea.comment-input::placeholder { color: var(--text-faint); }
333
+ textarea.comment-input:focus-visible {
334
+ outline: 2px solid var(--accent);
335
+ outline-offset: 1px;
336
+ border-color: var(--accent);
337
+ }
338
+
339
+ .recommendation {
340
+ margin-top: 16px;
341
+ padding: 14px 16px;
342
+ background: var(--accent-soft);
343
+ border: 1px solid var(--accent);
344
+ border-radius: 8px;
345
+ }
346
+ .recommendation .section-label { color: var(--accent-strong); }
347
+ .recommendation p { color: var(--text); }
348
+
349
+ .inferred-note {
350
+ font-size: 12px;
351
+ color: var(--text-faint);
352
+ margin-top: 4px;
353
+ }
354
+
355
+ footer {
356
+ margin: 32px 0 24px;
357
+ padding-top: 20px;
358
+ border-top: 1px solid var(--border);
359
+ font-size: 12.5px;
360
+ color: var(--text-faint);
361
+ }
362
+
363
+ .save-bar-shell {
364
+ position: sticky;
365
+ bottom: 16px;
366
+ max-width: 900px;
367
+ margin: 0 auto;
368
+ padding: 0 24px;
369
+ z-index: 10;
370
+ }
371
+
372
+ .save-bar {
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: space-between;
376
+ gap: 16px;
377
+ flex-wrap: wrap;
378
+ padding: 14px 18px;
379
+ background: var(--surface);
380
+ border: 1px solid var(--border-strong);
381
+ border-radius: 12px;
382
+ box-shadow: 0 8px 24px var(--shadow);
383
+ }
384
+
385
+ .save-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
386
+
387
+ .save-count {
388
+ font-family: "IBM Plex Mono", monospace;
389
+ font-size: 12.5px;
390
+ color: var(--text-muted);
391
+ font-variant-numeric: tabular-nums;
392
+ }
393
+
394
+ .save-status { font-size: 12.5px; color: var(--text-faint); }
395
+ .save-status.error { color: var(--danger); }
396
+ .save-status.ok { color: var(--good); }
397
+
398
+ .save-btn {
399
+ font-family: "IBM Plex Sans", sans-serif;
400
+ font-weight: 600;
401
+ font-size: 14px;
402
+ padding: 10px 20px;
403
+ border-radius: 8px;
404
+ border: 1px solid var(--accent);
405
+ background: var(--accent);
406
+ color: #fff;
407
+ cursor: pointer;
408
+ white-space: nowrap;
409
+ flex-shrink: 0;
410
+ }
411
+ .save-btn:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
412
+ .save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
413
+ .save-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
414
+
415
+ @media (max-width: 560px) {
416
+ .summary-strip { flex-direction: column; }
417
+ .summary-cell { border-right: none; border-bottom: 1px solid var(--border); }
418
+ .summary-cell:last-child { border-bottom: none; }
419
+ .save-bar { flex-direction: column; align-items: stretch; }
420
+ .save-btn { width: 100%; }
421
+ }
422
+ </style>
423
+
424
+ <div class="page" id="root"></div>
425
+
426
+ <div class="save-bar-shell" id="save-bar-shell">
427
+ <div class="save-bar" id="save-bar">
428
+ <div class="save-info">
429
+ <div class="save-count" id="save-count">0 answered</div>
430
+ <div class="save-status" id="save-status" role="status" aria-live="polite">Select a response on any card, then save.</div>
431
+ </div>
432
+ <label class="filter-toggle" for="filter-unanswered">
433
+ <input type="checkbox" id="filter-unanswered"> Show unanswered only
434
+ </label>
435
+ <button class="save-btn" id="save-btn" type="button">Save responses</button>
436
+ </div>
437
+ </div>
438
+
439
+ <script id="app-data">
440
+ /* ===== CONTENT ZONE — edit META and ITEMS per topic; leave everything below untouched ===== */
441
+
442
+ window.__META__ = {
443
+ eyebrow: "Source · scope · date read",
444
+ title: "N decisions are waiting on you",
445
+ lede: "One sentence framing what this batch of decisions has in common and why it's stalled on the reader specifically.",
446
+ /* Fixed context cells shown before the live "Answered" counter, which app-script appends automatically. */
447
+ summaryCells: [
448
+ { num: "N", label: "Pending decisions" }
449
+ ],
450
+ footer: "Source and date read, plus a one-line disclaimer about what is fact vs. recommendation and what saving here does and does not do."
451
+ };
452
+
453
+ window.__ITEMS__ = [
454
+ {
455
+ id: "example-item",
456
+ num: "01",
457
+ opened: "2026-01-01",
458
+ touched: "2026-01-01",
459
+ repo: "example-repo",
460
+ classChip: "",
461
+ typeLabel: "type · class",
462
+ priority: "P1",
463
+ title: "One-line name of the decision",
464
+ decision: "What action is on the table, stated as a full sentence — the thing being decided, not a summary of the situation.",
465
+ why: "Why this specifically needs the reader's input rather than being handled automatically — what makes it their call and not an agent's.",
466
+ whyInferred: false,
467
+ impact: "What actually changes for the app, the user, or the team depending on the outcome — concrete, not hand-wavy.",
468
+ options: [
469
+ { letter: "A", text: "First option, stated as an action." },
470
+ { letter: "B", text: "Second option, a real alternative, not a straw man." },
471
+ { letter: "C", text: "Third option — often 'defer' or 'reject', stated with its real cost." }
472
+ ],
473
+ recommendation: "Which option and why, in one or two sentences that name the actual tradeoff being made.",
474
+ selected: null,
475
+ comment: "",
476
+ respondedAt: null
477
+ }
478
+ ];
479
+ </script>
480
+
481
+ <script id="app-script">
482
+ /* ===== PLUMBING ZONE — do not edit. See SKILL.md before touching HEAD_HTML/SAVE_BAR_HTML. ===== */
483
+ (function () {
484
+ var items = window.__ITEMS__;
485
+ var meta = window.__META__;
486
+
487
+ /* HEAD_HTML and SAVE_BAR_HTML are hardcoded strings, not document.head / DOM
488
+ captures. The claude.ai viewer injects its own bootstrap script into
489
+ <head> before this script runs; capturing document.head.innerHTML at
490
+ runtime bakes that injected script into the saved page, and it collides
491
+ with the fresh copy the viewer injects on the next load — the page loses
492
+ all styling. Keep these as literal strings; if you edit the CSS above,
493
+ copy the change into this string too. */
494
+ var HEAD_HTML = `<title>Decision Queue</title>
495
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
496
+ <style id="app-style">
497
+ :root {
498
+ --bg: #F5F6F8;
499
+ --surface: #FFFFFF;
500
+ --surface-2: #FAFBFC;
501
+ --border: #E0E4EA;
502
+ --border-strong: #C7CDD7;
503
+ --text: #171B21;
504
+ --text-muted: #5B6472;
505
+ --text-faint: #8A93A2;
506
+ --accent: #2F52D6;
507
+ --accent-soft: #EAEEFC;
508
+ --accent-strong: #1E3AAE;
509
+ --danger: #A5393B;
510
+ --danger-soft: #F7E9E9;
511
+ --good: #2C7A55;
512
+ --good-soft: #E7F3ED;
513
+ --neutral-chip: #EEF0F3;
514
+ --warn: #9A6212;
515
+ --shadow: rgba(20, 24, 30, 0.10);
516
+ }
517
+
518
+ @media (prefers-color-scheme: dark) {
519
+ :root:not([data-theme="light"]) {
520
+ --bg: #0E1116;
521
+ --surface: #161A21;
522
+ --surface-2: #1B2029;
523
+ --border: #2A303B;
524
+ --border-strong: #3A4150;
525
+ --text: #E8EAEE;
526
+ --text-muted: #9BA3B2;
527
+ --text-faint: #6B7382;
528
+ --accent: #7089FF;
529
+ --accent-soft: rgba(112, 137, 255, 0.14);
530
+ --accent-strong: #91A4FF;
531
+ --danger: #E28C8C;
532
+ --danger-soft: rgba(226, 140, 140, 0.12);
533
+ --good: #7BCBA4;
534
+ --warn: #D9A441;
535
+ --good-soft: rgba(123, 203, 164, 0.12);
536
+ --neutral-chip: #232935;
537
+ --shadow: rgba(0, 0, 0, 0.4);
538
+ }
539
+ }
540
+
541
+ :root[data-theme="dark"] {
542
+ --bg: #0E1116;
543
+ --surface: #161A21;
544
+ --surface-2: #1B2029;
545
+ --border: #2A303B;
546
+ --border-strong: #3A4150;
547
+ --text: #E8EAEE;
548
+ --text-muted: #9BA3B2;
549
+ --text-faint: #6B7382;
550
+ --accent: #7089FF;
551
+ --accent-soft: rgba(112, 137, 255, 0.14);
552
+ --accent-strong: #91A4FF;
553
+ --danger: #E28C8C;
554
+ --danger-soft: rgba(226, 140, 140, 0.12);
555
+ --good: #7BCBA4;
556
+ --good-soft: rgba(123, 203, 164, 0.12);
557
+ --neutral-chip: #232935;
558
+ --shadow: rgba(0, 0, 0, 0.4);
559
+ }
560
+
561
+ * { box-sizing: border-box; }
562
+
563
+ body {
564
+ margin: 0;
565
+ background: var(--bg);
566
+ color: var(--text);
567
+ font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
568
+ font-size: 15px;
569
+ line-height: 1.55;
570
+ -webkit-font-smoothing: antialiased;
571
+ }
572
+
573
+ .page {
574
+ max-width: 900px;
575
+ margin: 0 auto;
576
+ padding: 40px 24px 4px;
577
+ }
578
+
579
+ .eyebrow {
580
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
581
+ font-size: 11.5px;
582
+ letter-spacing: 0.09em;
583
+ text-transform: uppercase;
584
+ color: var(--text-faint);
585
+ margin-bottom: 10px;
586
+ }
587
+
588
+ h1 {
589
+ font-family: "Archivo", ui-sans-serif, sans-serif;
590
+ font-weight: 800;
591
+ font-size: clamp(26px, 4vw, 34px);
592
+ letter-spacing: -0.01em;
593
+ text-wrap: balance;
594
+ margin: 0 0 12px;
595
+ }
596
+
597
+ .lede {
598
+ color: var(--text-muted);
599
+ max-width: 62ch;
600
+ font-size: 15.5px;
601
+ margin: 0;
602
+ }
603
+
604
+ .summary-strip {
605
+ display: flex;
606
+ gap: 0;
607
+ margin-top: 24px;
608
+ border: 1px solid var(--border);
609
+ border-radius: 10px;
610
+ overflow: hidden;
611
+ background: var(--surface);
612
+ }
613
+
614
+ .summary-cell {
615
+ flex: 1;
616
+ padding: 14px 18px;
617
+ border-right: 1px solid var(--border);
618
+ }
619
+ .summary-cell:last-child { border-right: none; }
620
+
621
+ .summary-num {
622
+ font-family: "Archivo", sans-serif;
623
+ font-weight: 800;
624
+ font-size: 22px;
625
+ font-variant-numeric: tabular-nums;
626
+ line-height: 1;
627
+ margin-bottom: 4px;
628
+ }
629
+
630
+ .summary-num.accent { color: var(--accent); }
631
+
632
+ .summary-label {
633
+ font-size: 12px;
634
+ color: var(--text-muted);
635
+ }
636
+
637
+ .queue {
638
+ display: flex;
639
+ flex-direction: column;
640
+ gap: 18px;
641
+ margin-top: 24px;
642
+ }
643
+
644
+ .card {
645
+ background: var(--surface);
646
+ border: 1px solid var(--border);
647
+ border-radius: 12px;
648
+ padding: 22px 24px 24px;
649
+ }
650
+
651
+ .card-head {
652
+ display: flex;
653
+ justify-content: space-between;
654
+ align-items: flex-start;
655
+ gap: 16px;
656
+ margin-bottom: 6px;
657
+ }
658
+
659
+ .card-num {
660
+ font-family: "IBM Plex Mono", monospace;
661
+ font-size: 12px;
662
+ color: var(--text-faint);
663
+ margin-bottom: 6px;
664
+ }
665
+
666
+ .card-title {
667
+ font-family: "Archivo", sans-serif;
668
+ font-weight: 700;
669
+ font-size: 18px;
670
+ letter-spacing: -0.005em;
671
+ text-wrap: balance;
672
+ margin: 0;
673
+ }
674
+
675
+ .meta-row {
676
+ display: flex;
677
+ flex-wrap: wrap;
678
+ gap: 8px;
679
+ margin-top: 10px;
680
+ align-items: center;
681
+ }
682
+
683
+ .chip {
684
+ font-family: "IBM Plex Mono", monospace;
685
+ font-size: 11px;
686
+ padding: 3px 8px;
687
+ border-radius: 5px;
688
+ background: var(--neutral-chip);
689
+ color: var(--text-muted);
690
+ white-space: nowrap;
691
+ }
692
+
693
+ .chip.class-production { background: var(--danger-soft); color: var(--danger); }
694
+ .chip.class-safe { background: var(--good-soft); color: var(--good); }
695
+
696
+ .chip.repo {
697
+ max-width: 320px;
698
+ overflow: hidden;
699
+ text-overflow: ellipsis;
700
+ }
701
+
702
+ .status-chip {
703
+ font-family: "IBM Plex Mono", monospace;
704
+ font-size: 11px;
705
+ padding: 3px 8px;
706
+ border-radius: 5px;
707
+ white-space: nowrap;
708
+ }
709
+ .status-chip.saved { background: var(--good-soft); color: var(--good); }
710
+ .status-chip.pending { background: var(--neutral-chip); color: var(--text-faint); }
711
+ /* Status carries in text colour, not a filled badge (Signal / S:N rule). */
712
+ .chip.stale { background: none; padding-left: 0; color: var(--warn); font-weight: 600; }
713
+ /* Options are a radio group: fieldset+legend give screen readers the question
714
+ that the choices belong to. Reset the UA chrome so it still looks flat. */
715
+ .options-group { border: 0; margin: 0; padding: 0; min-width: 0; }
716
+ .options-group > legend { padding: 0; }
717
+ body.filter-unanswered .card.answered { display: none; }
718
+ .filter-toggle {
719
+ display: inline-flex; align-items: center; gap: 8px;
720
+ font-size: 12px; color: var(--text-faint); cursor: pointer;
721
+ min-height: 44px;
722
+ }
723
+ .filter-toggle input { width: 16px; height: 16px; accent-color: var(--accent); }
724
+ .draft-note { color: var(--warn); }
725
+
726
+ .section {
727
+ margin-top: 16px;
728
+ padding-top: 16px;
729
+ border-top: 1px solid var(--border);
730
+ }
731
+ .card-head + .section { margin-top: 18px; }
732
+
733
+ .section-label {
734
+ font-family: "IBM Plex Mono", monospace;
735
+ font-size: 11px;
736
+ letter-spacing: 0.08em;
737
+ text-transform: uppercase;
738
+ color: var(--text-faint);
739
+ margin-bottom: 6px;
740
+ }
741
+
742
+ .section p { margin: 0; color: var(--text); }
743
+ .section p + p { margin-top: 8px; }
744
+ .section p.muted { color: var(--text-muted); }
745
+
746
+ .options {
747
+ list-style: none;
748
+ margin: 4px 0 0;
749
+ padding: 0;
750
+ display: flex;
751
+ flex-direction: column;
752
+ gap: 8px;
753
+ }
754
+
755
+ .opt-label {
756
+ display: flex;
757
+ gap: 10px;
758
+ padding: 10px 12px;
759
+ background: var(--surface-2);
760
+ border: 1px solid var(--border);
761
+ border-radius: 8px;
762
+ font-size: 14.5px;
763
+ cursor: pointer;
764
+ align-items: flex-start;
765
+ transition: border-color 120ms ease, background 120ms ease;
766
+ }
767
+ .opt-label:hover { border-color: var(--border-strong); }
768
+ .opt-label.checked { border-color: var(--accent); background: var(--accent-soft); }
769
+
770
+ .opt-radio {
771
+ margin-top: 3px;
772
+ accent-color: var(--accent);
773
+ flex-shrink: 0;
774
+ width: 15px;
775
+ height: 15px;
776
+ cursor: pointer;
777
+ }
778
+ .opt-radio:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
779
+
780
+ .opt-letter {
781
+ font-family: "IBM Plex Mono", monospace;
782
+ font-weight: 500;
783
+ font-size: 13px;
784
+ color: var(--text-faint);
785
+ margin-right: 2px;
786
+ }
787
+
788
+ .comment-field {
789
+ margin-top: 10px;
790
+ }
791
+
792
+ .comment-field label {
793
+ display: block;
794
+ margin-bottom: 6px;
795
+ }
796
+
797
+ textarea.comment-input {
798
+ width: 100%;
799
+ min-height: 60px;
800
+ resize: vertical;
801
+ font-family: "IBM Plex Sans", sans-serif;
802
+ font-size: 14px;
803
+ line-height: 1.5;
804
+ padding: 10px 12px;
805
+ border: 1px solid var(--border);
806
+ border-radius: 8px;
807
+ background: var(--surface-2);
808
+ color: var(--text);
809
+ }
810
+ textarea.comment-input::placeholder { color: var(--text-faint); }
811
+ textarea.comment-input:focus-visible {
812
+ outline: 2px solid var(--accent);
813
+ outline-offset: 1px;
814
+ border-color: var(--accent);
815
+ }
816
+
817
+ .recommendation {
818
+ margin-top: 16px;
819
+ padding: 14px 16px;
820
+ background: var(--accent-soft);
821
+ border: 1px solid var(--accent);
822
+ border-radius: 8px;
823
+ }
824
+ .recommendation .section-label { color: var(--accent-strong); }
825
+ .recommendation p { color: var(--text); }
826
+
827
+ .inferred-note {
828
+ font-size: 12px;
829
+ color: var(--text-faint);
830
+ margin-top: 4px;
831
+ }
832
+
833
+ footer {
834
+ margin: 32px 0 24px;
835
+ padding-top: 20px;
836
+ border-top: 1px solid var(--border);
837
+ font-size: 12.5px;
838
+ color: var(--text-faint);
839
+ }
840
+
841
+ .save-bar-shell {
842
+ position: sticky;
843
+ bottom: 16px;
844
+ max-width: 900px;
845
+ margin: 0 auto;
846
+ padding: 0 24px;
847
+ z-index: 10;
848
+ }
849
+
850
+ .save-bar {
851
+ display: flex;
852
+ align-items: center;
853
+ justify-content: space-between;
854
+ gap: 16px;
855
+ flex-wrap: wrap;
856
+ padding: 14px 18px;
857
+ background: var(--surface);
858
+ border: 1px solid var(--border-strong);
859
+ border-radius: 12px;
860
+ box-shadow: 0 8px 24px var(--shadow);
861
+ }
862
+
863
+ .save-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
864
+
865
+ .save-count {
866
+ font-family: "IBM Plex Mono", monospace;
867
+ font-size: 12.5px;
868
+ color: var(--text-muted);
869
+ font-variant-numeric: tabular-nums;
870
+ }
871
+
872
+ .save-status { font-size: 12.5px; color: var(--text-faint); }
873
+ .save-status.error { color: var(--danger); }
874
+ .save-status.ok { color: var(--good); }
875
+
876
+ .save-btn {
877
+ font-family: "IBM Plex Sans", sans-serif;
878
+ font-weight: 600;
879
+ font-size: 14px;
880
+ padding: 10px 20px;
881
+ border-radius: 8px;
882
+ border: 1px solid var(--accent);
883
+ background: var(--accent);
884
+ color: #fff;
885
+ cursor: pointer;
886
+ white-space: nowrap;
887
+ flex-shrink: 0;
888
+ }
889
+ .save-btn:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
890
+ .save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
891
+ .save-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
892
+
893
+ @media (max-width: 560px) {
894
+ .summary-strip { flex-direction: column; }
895
+ .summary-cell { border-right: none; border-bottom: 1px solid var(--border); }
896
+ .summary-cell:last-child { border-bottom: none; }
897
+ .save-bar { flex-direction: column; align-items: stretch; }
898
+ .save-btn { width: 100%; }
899
+ }
900
+ </style>`;
901
+ var SAVE_BAR_HTML = `<div class="save-bar-shell" id="save-bar-shell">
902
+ <div class="save-bar" id="save-bar">
903
+ <div class="save-info">
904
+ <div class="save-count" id="save-count">0 answered</div>
905
+ <div class="save-status" id="save-status" role="status" aria-live="polite">Select a response on any card, then save.</div>
906
+ </div>
907
+ <label class="filter-toggle" for="filter-unanswered">
908
+ <input type="checkbox" id="filter-unanswered"> Show unanswered only
909
+ </label>
910
+ <button class="save-btn" id="save-btn" type="button">Save responses</button>
911
+ </div>
912
+ </div>`;
913
+
914
+ var artifactNS = null;
915
+ var readOnly = false;
916
+ var pendingWrites = false;
917
+
918
+ function escapeHtml(s) {
919
+ return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
920
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
921
+ });
922
+ }
923
+
924
+ // Mark a string as already-HTML so h`` leaves it alone. Everything NOT
925
+ // wrapped in raw() is escaped. This is the whole point: the default is safe,
926
+ // and bypassing it is a visible, greppable act.
927
+ function raw(html) {
928
+ return { __html: String(html == null ? "" : html) };
929
+ }
930
+
931
+ // Tagged template that escapes every interpolation. Card and header markup is
932
+ // built from item fields written by agents (Operations Center tasks, backlog
933
+ // items, peer-authored rally records), so a title of
934
+ // <img src=x onerror=alert(1)>
935
+ // must render as text, not run. Before this, escapeHtml() was applied at 5 of
936
+ // ~23 interpolation sites and every newly added field defaulted to unescaped.
937
+ function h(strings) {
938
+ var out = strings[0];
939
+ for (var i = 1; i < arguments.length; i++) {
940
+ var value = arguments[i];
941
+ out += (value && value.__html !== undefined ? value.__html : escapeHtml(value)) + strings[i];
942
+ }
943
+ return out;
944
+ }
945
+
946
+ // JSON.stringify does not escape "<", so a field containing "</script>" ends
947
+ // the data element early and everything after it parses as markup. Escaping
948
+ // the three markup-significant characters plus the two JS line terminators
949
+ // keeps the payload inert and still valid JSON.
950
+ function safeJsonForScript(value) {
951
+ return JSON.stringify(value)
952
+ .replace(/</g, "\\u003c")
953
+ .replace(/>/g, "\\u003e")
954
+ .replace(/&/g, "\\u0026")
955
+ .replace(/\u2028/g, "\\u2028")
956
+ .replace(/\u2029/g, "\\u2029");
957
+ }
958
+
959
+ function formatDate(iso) {
960
+ if (!iso) return "";
961
+ try { return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); }
962
+ catch (e) { return iso; }
963
+ }
964
+
965
+ // Days since an ISO date, or null when the field is not a parseable date.
966
+ // Queues rot: a decision nobody could answer for weeks is usually not the
967
+ // same decision any more, and the page should say so rather than present it
968
+ // as fresh.
969
+ var STALE_AFTER_DAYS = 14;
970
+ function daysSince(iso) {
971
+ if (!iso) return null;
972
+ var t = Date.parse(iso);
973
+ if (isNaN(t)) return null;
974
+ return Math.floor((Date.now() - t) / 86400000);
975
+ }
976
+
977
+ // Every queue needs a way to say "this stopped being a question" without
978
+ // pretending one of the real options was chosen. Appended here rather than
979
+ // asked of the author so it is present on every card by construction.
980
+ var MOOT_LETTER = "\u00D7";
981
+ function optionsFor(item) {
982
+ return (item.options || []).concat([
983
+ { letter: MOOT_LETTER, text: "No longer relevant — close without a decision." }
984
+ ]);
985
+ }
986
+
987
+ function optionHtml(item, opt) {
988
+ var checked = item.selected === opt.letter;
989
+ return h`<label class="opt-label${checked ? " checked" : ""}" data-item="${item.id}" data-letter="${opt.letter}">`
990
+ + h`<input type="radio" class="opt-radio" name="resp-${item.id}" value="${opt.letter}"${raw(checked ? " checked" : "")}>`
991
+ + h`<span><span class="opt-letter">${opt.letter}</span>${opt.text}</span>`
992
+ + "</label>";
993
+ }
994
+
995
+ function cardHtml(item) {
996
+ var statusChip = item.selected
997
+ ? h`<span class="status-chip saved">Answered · ${item.selected}${item.respondedAt ? " · " + formatDate(item.respondedAt) : ""}</span>`
998
+ : '<span class="status-chip pending">No response yet</span>';
999
+
1000
+ var inferredNote = item.whyInferred
1001
+ ? '<p class="inferred-note">Inferred — the source record’s own “why” field is unset for this item.</p>'
1002
+ : "";
1003
+
1004
+ var age = daysSince(item.touched);
1005
+ var staleChip = age !== null && age >= STALE_AFTER_DAYS
1006
+ ? h`<span class="chip stale">Untouched ${age} days</span>`
1007
+ : "";
1008
+
1009
+ return h`<div class="card${item.selected ? " answered" : ""}" id="card-${item.id}">`
1010
+ + '<div class="card-head"><div>'
1011
+ + h`<div class="card-num">${item.num} · opened ${item.opened} · last touched ${item.touched}</div>`
1012
+ + h`<h2 class="card-title">${item.title}</h2>`
1013
+ + "</div></div>"
1014
+ + '<div class="meta-row">'
1015
+ + h`<span class="chip repo">${item.repo}</span>`
1016
+ + h`<span class="chip${item.classChip ? " class-" + item.classChip : ""}">${item.typeLabel}</span>`
1017
+ + h`<span class="chip">${item.priority}</span>`
1018
+ + raw(staleChip).__html
1019
+ + raw(statusChip).__html
1020
+ + "</div>"
1021
+ + h`<div class="section"><div class="section-label">The decision</div><p>${item.decision}</p></div>`
1022
+ + h`<div class="section"><div class="section-label">Why it needs you</div><p>${item.why}</p>${raw(inferredNote)}</div>`
1023
+ + h`<div class="section"><div class="section-label">Impact</div><p>${item.impact}</p></div>`
1024
+ + '<div class="section">'
1025
+ + h`<fieldset class="options-group"><legend class="section-label">Your response — ${item.title}</legend>`
1026
+ + h`<div class="options">${raw(optionsFor(item).map(function (o) { return optionHtml(item, o); }).join(""))}</div>`
1027
+ + "</fieldset>"
1028
+ + '<div class="comment-field">'
1029
+ + h`<label class="section-label" for="comment-${item.id}">Comment (optional)</label>`
1030
+ + h`<textarea class="comment-input" id="comment-${item.id}" data-item="${item.id}" placeholder="Add context, a counter-proposal, or a condition on approval…">${item.comment}</textarea>`
1031
+ + "</div>"
1032
+ + "</div>"
1033
+ + h`<div class="recommendation"><div class="section-label">Recommendation</div><p>${item.recommendation}</p></div>`
1034
+ + "</div>";
1035
+ }
1036
+
1037
+ function renderBody(meta, items) {
1038
+ var answered = items.filter(function (i) { return i.selected; }).length;
1039
+ var cells = meta.summaryCells.map(function (c) {
1040
+ return h`<div class="summary-cell"><div class="summary-num">${c.num}</div><div class="summary-label">${c.label}</div></div>`;
1041
+ }).join("");
1042
+ cells += h`<div class="summary-cell"><div class="summary-num accent">${answered} / ${items.length}</div><div class="summary-label">Answered</div></div>`;
1043
+ return "<header>"
1044
+ + h`<div class="eyebrow">${meta.eyebrow}</div>`
1045
+ + h`<h1>${meta.title}</h1>`
1046
+ + h`<p class="lede">${meta.lede}</p>`
1047
+ + h`<div class="summary-strip">${raw(cells)}</div>`
1048
+ + "</header>"
1049
+ + h`<div class="queue">${raw(items.map(cardHtml).join(""))}</div>`
1050
+ + h`<footer>${meta.footer}</footer>`;
1051
+ }
1052
+
1053
+ function paint() {
1054
+ // renderBody() builds every node through the h`` tagged template, which escapes
1055
+ // each interpolation unless it is explicitly wrapped in raw(), so the value
1056
+ // reaching innerHTML carries no unescaped caller data. Proven, not asserted:
1057
+ // scripts/test_decision_queue_template_escaping.py renders a hostile item through
1058
+ // this exact file and fails if anything executes -- 4 of its 7 assertions fail
1059
+ // against the pre-fix template. Re-check that test before trusting this line.
1060
+ document.getElementById("root").innerHTML = renderBody(meta, items); // nosec: escaped by construction in renderBody; see test_decision_queue_template_escaping.py
1061
+ wireEvents();
1062
+ }
1063
+
1064
+ function wireEvents() {
1065
+ var radios = document.querySelectorAll(".opt-radio");
1066
+ for (var i = 0; i < radios.length; i++) {
1067
+ radios[i].addEventListener("change", onOptionChange);
1068
+ }
1069
+ var boxes = document.querySelectorAll(".comment-input");
1070
+ for (var j = 0; j < boxes.length; j++) {
1071
+ boxes[j].addEventListener("input", onCommentInput);
1072
+ }
1073
+ }
1074
+
1075
+ // Unsaved edits live only in memory until Save, so closing the tab loses
1076
+ // everything typed. Mirror them into per-viewer browser storage. Every access
1077
+ // is guarded: the artifact viewer can throw on storage in a thumbnail capture
1078
+ // or a browser set to block site data, and a draft is never worth a dead page.
1079
+ var DRAFT_KEY = "decision-queue-draft-v1";
1080
+ function saveDraft() {
1081
+ try {
1082
+ var d = {};
1083
+ items.forEach(function (it) {
1084
+ if (it.selected || (it.comment && it.comment.trim())) {
1085
+ d[it.id] = { selected: it.selected, comment: it.comment };
1086
+ }
1087
+ });
1088
+ window.localStorage.setItem(DRAFT_KEY, JSON.stringify(d));
1089
+ } catch (e) { /* storage unavailable — drafts are a convenience, not state */ }
1090
+ }
1091
+ function clearDraft() {
1092
+ try { window.localStorage.removeItem(DRAFT_KEY); } catch (e) {}
1093
+ }
1094
+ // Returns how many cards were restored, so the UI can say so rather than
1095
+ // silently resurrecting text the reader does not remember typing.
1096
+ function restoreDraft() {
1097
+ var d;
1098
+ try { d = JSON.parse(window.localStorage.getItem(DRAFT_KEY) || "null"); }
1099
+ catch (e) { return 0; }
1100
+ if (!d) return 0;
1101
+ var n = 0;
1102
+ items.forEach(function (it) {
1103
+ var saved = d[it.id];
1104
+ if (!saved) return;
1105
+ // The published page is the source of truth: only fill fields the saved
1106
+ // version left empty, so a draft can never overwrite an answer that
1107
+ // already round-tripped through publish().
1108
+ var changed = false;
1109
+ if (!it.selected && saved.selected) { it.selected = saved.selected; changed = true; }
1110
+ if (!it.comment && saved.comment) { it.comment = saved.comment; changed = true; }
1111
+ if (changed) n++;
1112
+ });
1113
+ return n;
1114
+ }
1115
+
1116
+ function findItem(id) {
1117
+ for (var i = 0; i < items.length; i++) { if (items[i].id === id) return items[i]; }
1118
+ return null;
1119
+ }
1120
+
1121
+ function onOptionChange(e) {
1122
+ var name = e.target.name; // "resp-<id>"
1123
+ var id = name.slice(5);
1124
+ var item = findItem(id);
1125
+ if (!item) return;
1126
+ item.selected = e.target.value;
1127
+ pendingWrites = true;
1128
+ var group = document.querySelectorAll('.opt-label[data-item="' + id + '"]');
1129
+ for (var i = 0; i < group.length; i++) {
1130
+ group[i].classList.toggle("checked", group[i].getAttribute("data-letter") === item.selected);
1131
+ }
1132
+ var card = document.getElementById("card-" + id);
1133
+ if (card) card.classList.toggle("answered", !!item.selected);
1134
+ saveDraft();
1135
+ updateSaveBar();
1136
+ }
1137
+
1138
+ function onCommentInput(e) {
1139
+ var id = e.target.getAttribute("data-item");
1140
+ var item = findItem(id);
1141
+ if (!item) return;
1142
+ item.comment = e.target.value;
1143
+ pendingWrites = true;
1144
+ saveDraft();
1145
+ updateSaveBar();
1146
+ }
1147
+
1148
+ function updateSaveBar() {
1149
+ var answered = items.filter(function (i) { return i.selected; }).length;
1150
+ var commented = items.filter(function (i) { return i.comment && i.comment.trim(); }).length;
1151
+ var countEl = document.getElementById("save-count");
1152
+ if (countEl) countEl.textContent = answered + " of " + items.length + " answered" + (commented ? " · " + commented + " with comments" : "");
1153
+ }
1154
+
1155
+ function setStatus(text, kind) {
1156
+ var el = document.getElementById("save-status");
1157
+ if (!el) return;
1158
+ el.textContent = text;
1159
+ el.className = "save-status" + (kind ? " " + kind : "");
1160
+ }
1161
+
1162
+ function buildDocument(meta, items) {
1163
+ var dataScript = '<script id="app-data">window.__META__ = ' + safeJsonForScript(meta) + ";\nwindow.__ITEMS__ = " + safeJsonForScript(items) + ';<\/script>';
1164
+ var appScriptTag = document.getElementById("app-script").outerHTML;
1165
+ return (
1166
+ "<!doctype html><html><head><meta charset=\"utf-8\">" + HEAD_HTML + "</head><body>" +
1167
+ '<div class="page" id="root">' + renderBody(meta, items) + "</div>" +
1168
+ SAVE_BAR_HTML +
1169
+ dataScript +
1170
+ appScriptTag +
1171
+ "</body></html>"
1172
+ );
1173
+ }
1174
+
1175
+ function onSaveClick() {
1176
+ if (readOnly) return;
1177
+ if (!artifactNS) { setStatus("Saving isn’t available in this view.", "error"); return; }
1178
+ var btn = document.getElementById("save-btn");
1179
+ btn.disabled = true;
1180
+ setStatus("Saving…");
1181
+ var now = new Date().toISOString();
1182
+ items.forEach(function (it) {
1183
+ if (it.selected || (it.comment && it.comment.trim())) it.respondedAt = now;
1184
+ });
1185
+ artifactNS.publish(buildDocument(meta, items)).then(
1186
+ function () {
1187
+ // successful publish reloads this view automatically
1188
+ clearDraft();
1189
+ },
1190
+ function (err) {
1191
+ btn.disabled = false;
1192
+ var code = err && err.code;
1193
+ if (code === "not_writer" || code === "not_granted" || code === "not_declared" || code === "capability_disabled") {
1194
+ readOnly = true;
1195
+ setStatus("This view is read-only — your response wasn’t saved.", "error");
1196
+ btn.textContent = "Read-only";
1197
+ btn.disabled = true;
1198
+ } else if (code === "conflict") {
1199
+ setStatus("Newer version found — reloading…");
1200
+ } else {
1201
+ setStatus("Save failed" + (code ? " (" + code + ")" : "") + " — try again.", "error");
1202
+ }
1203
+ }
1204
+ );
1205
+ }
1206
+
1207
+ var restored = restoreDraft();
1208
+ paint();
1209
+ updateSaveBar();
1210
+ if (restored) {
1211
+ pendingWrites = true;
1212
+ setStatus("Restored unsaved edits on " + restored + (restored === 1 ? " card" : " cards") + " — save to publish them.", "draft-note");
1213
+ }
1214
+
1215
+ // Filter is a view concern only: it hides answered cards, never mutates them,
1216
+ // so toggling it can't lose a response.
1217
+ var filterBox = document.getElementById("filter-unanswered");
1218
+ if (filterBox) {
1219
+ filterBox.addEventListener("change", function () {
1220
+ document.body.classList.toggle("filter-unanswered", filterBox.checked);
1221
+ });
1222
+ }
1223
+
1224
+ var saveBtn = document.getElementById("save-btn");
1225
+ saveBtn.addEventListener("click", onSaveClick);
1226
+
1227
+ if (window.claude && window.claude.use) {
1228
+ window.claude.use("artifact").then(function (ns) {
1229
+ artifactNS = ns;
1230
+ if (!ns) {
1231
+ readOnly = true;
1232
+ setStatus("Saving isn’t available in this view — responses stay local to this tab.", "error");
1233
+ saveBtn.disabled = true;
1234
+ }
1235
+ });
1236
+ } else {
1237
+ readOnly = true;
1238
+ setStatus("Saving isn’t available outside the Artifact viewer.", "error");
1239
+ saveBtn.disabled = true;
1240
+ }
1241
+ })();
1242
+ </script>