@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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/build-loop-surface.mdc +12 -11
- package/.cursor/rules/skill-index.mdc +33 -0
- package/AGENTS.md +213 -34
- package/README.md +99 -31
- package/agents/advisor.md +4 -4
- package/agents/alignment-checker.md +2 -2
- package/agents/architecture-scout.md +4 -4
- package/agents/build-orchestrator.md +38 -36
- package/agents/database-assessor.md +11 -5
- package/agents/design-contract-specialist.md +8 -8
- package/agents/fact-checker.md +13 -3
- package/agents/fix-critique.md +2 -2
- package/agents/independent-auditor.md +60 -7
- package/agents/leak-scanner.md +82 -0
- package/agents/overfitting-reviewer.md +2 -2
- package/agents/plan-critic.md +1 -1
- package/agents/promotion-reviewer.md +5 -5
- package/agents/retrospective-synthesizer.md +138 -35
- package/agents/scope-auditor.md +82 -11
- package/agents/security-reviewer.md +56 -2
- package/agents/self-improvement-architect.md +17 -3
- package/agents/transcript-pattern-miner.md +5 -5
- package/agents/ui-validator.md +1 -1
- package/bin/build-loop-debugger.js +143 -0
- package/bin/build-loop-install.js +1 -4
- package/bin/build-loop-load-probe.js +345 -0
- package/codex-skills/build-loop/SKILL.md +28 -6
- package/commands/feedback.md +37 -0
- package/dist/src/interactive-verifier.d.ts +1 -14
- package/dist/src/interactive-verifier.d.ts.map +1 -1
- package/dist/src/interactive-verifier.js +6 -113
- package/dist/src/interactive-verifier.js.map +1 -1
- package/dist/src/quality.d.ts +5 -0
- package/dist/src/quality.d.ts.map +1 -0
- package/dist/src/quality.js +81 -0
- package/dist/src/quality.js.map +1 -0
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/storage.js +37 -3
- package/dist/src/storage.js.map +1 -1
- package/docs/agent-surface-policy.md +35 -31
- package/docs/memory-setup.md +19 -0
- package/hooks/git/pre-push +65 -4
- package/hooks/hooks.json +95 -38
- package/hooks/pre-commit +20 -1
- package/hooks/pre-edit-rally-point.sh +10 -3
- package/hooks/session-start-codex-hook-trust.sh +30 -0
- package/hooks/session-start-git-hooks.sh +3 -1
- package/hooks/session-start-rally-point.sh +52 -4
- package/hooks/session-start-worktree-gc.sh +47 -94
- package/hooks/stop-transcript-sweep.sh +173 -0
- package/hooks/test_closeout.sh +14 -2
- package/package.json +8 -7
- package/scripts/README.md +1 -1
- package/scripts/_paths.py +65 -0
- package/scripts/groundwork_exchange.py +1012 -0
- package/scripts/install_memory.py +33 -1
- package/scripts/lessons_index/ingest.py +13 -2
- package/scripts/lessons_index/query.py +36 -13
- package/scripts/memory_context/__init__.py +108 -14
- package/scripts/memory_graph/__init__.py +5 -1
- package/scripts/project_resolver.py +42 -36
- package/scripts/sync_plugin_cache.py +37 -2
- package/skills/agent-rally-point/SKILL.md +46 -0
- package/skills/api-registry-bridge/SKILL.md +1 -1
- package/skills/architecture/dead/SKILL.md +1 -1
- package/skills/architecture/impact/SKILL.md +1 -1
- package/skills/architecture/review/SKILL.md +1 -1
- package/skills/architecture/rules/SKILL.md +3 -3
- package/skills/architecture/scan/SKILL.md +1 -1
- package/skills/architecture/trace/SKILL.md +1 -1
- package/skills/attribution-standard/SKILL.md +6 -6
- package/skills/auto-decision-capture/SKILL.md +31 -2
- package/skills/auto-finding-capture/SKILL.md +28 -1
- package/skills/build-loop/SKILL.md +131 -23
- package/skills/build-loop/fallbacks.md +16 -21
- package/skills/build-loop/phases/ui-validation.md +2 -2
- package/skills/build-loop/references/advisor-dispatch-ladder.md +1 -1
- package/skills/build-loop/references/apple-native-planning.md +1 -1
- package/skills/build-loop/references/autonomous-and-per-commit-modes.md +11 -5
- package/skills/build-loop/references/autonomy-dashboard.md +115 -0
- package/skills/build-loop/references/capability-routing.md +24 -2
- package/skills/build-loop/references/coordination.md +24 -6
- package/skills/build-loop/references/experiment-results-template.md +15 -3
- package/skills/build-loop/references/leadership.md +1 -1
- package/skills/build-loop/references/memory.md +14 -3
- package/skills/build-loop/references/modular-systems-pack.md +8 -0
- package/skills/build-loop/references/output-style.md +86 -0
- package/skills/build-loop/references/phase-1-assess.md +102 -2
- package/skills/build-loop/references/phase-2-plan.md +9 -1
- package/skills/build-loop/references/phase-3-execute.md +5 -2
- package/skills/build-loop/references/phase-4-review.md +85 -8
- package/skills/build-loop/references/phase-5-iterate.md +76 -8
- package/skills/build-loop/references/phase-6-learn.md +10 -17
- package/skills/build-loop/references/privileged-request-broker.md +254 -0
- package/skills/build-loop/references/resource-aware-execution.md +183 -0
- package/skills/build-loop/references/self-recursive-dev.md +2 -2
- package/skills/build-loop/references/status-output-format.md +207 -0
- package/skills/build-loop/references/verify-dispatch.md +56 -2
- package/skills/building-with-deepagents/SKILL.md +1 -1
- package/skills/claim-scope/SKILL.md +185 -0
- package/skills/color-engine/SKILL.md +103 -0
- package/skills/color-engine/_core.py +464 -0
- package/skills/color-engine/color_engine.py +175 -0
- package/skills/cost-rca/SKILL.md +61 -0
- package/skills/data-plane-worktrees/SKILL.md +139 -0
- package/skills/data-plane-worktrees/agents/openai.yaml +4 -0
- package/skills/database-practice/SKILL.md +200 -0
- package/skills/database-practice/references/diagnostic-queries.sql +126 -0
- package/skills/database-practice/references/vector-and-graph-tuning.md +208 -0
- package/skills/database-practice/scripts/db_table_map.py +1244 -0
- package/skills/database-practice/scripts/test_db_table_map.py +514 -0
- package/skills/debug-loop/SKILL.md +36 -6
- package/skills/debugging-memory/SKILL.md +32 -430
- package/skills/debugging-memory/references/pattern-extraction.md +4 -4
- package/skills/debugging-memory/references/search.md +32 -120
- package/skills/debugging-memory/references/store.md +32 -126
- package/skills/debugging-memory/references/subagent-integration.md +1 -1
- package/skills/decision-queue/SKILL.md +251 -0
- package/skills/decision-queue/assets/template.html +1242 -0
- package/skills/decision-queue/references/example-large-queue-batching.md +164 -0
- package/skills/decision-queue/scripts/regen_template_constants.py +160 -0
- package/skills/defenseclaw-bridge/SKILL.md +2 -2
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +2 -9
- package/skills/drain-proposals/SKILL.md +53 -0
- package/skills/focused-loop-builder/SKILL.md +31 -0
- package/skills/focused-loop-builder/references/spec-format.md +27 -0
- package/skills/handoff/SKILL.md +169 -8
- package/skills/ibr-bridge/SKILL.md +4 -1
- package/skills/knowledge/SKILL.md +26 -14
- package/skills/knowledge/references/review-mode.md +2 -3
- package/skills/knowledge/templates/madr-minimal.md +1 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/model-bakeoff/SKILL.md +48 -10
- package/skills/model-tiering/SKILL.md +92 -31
- package/skills/native-ax-driver/SKILL.md +38 -5
- package/skills/native-ax-driver/scripts/native_driver.py +278 -22
- package/skills/native-ax-driver/scripts/test_native_driver.py +227 -0
- package/skills/optimize/SKILL.md +1 -1
- package/skills/plugin-builder/SKILL.md +48 -1
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +3 -4
- package/skills/plugin-builder/references/distribution.md +13 -2
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +2 -2
- package/skills/plugin-tests/SKILL.md +2 -2
- package/skills/recursive-retrospective/SKILL.md +1 -1
- package/skills/repo-closeout/SKILL.md +17 -0
- package/skills/repo-closeout/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/SKILL.md +179 -0
- package/skills/repo-maintenance/agents/openai.yaml +4 -0
- package/skills/repo-maintenance/references/pre-public-hygiene.md +134 -0
- package/skills/repo-maintenance/references/repository-taxonomy.md +161 -0
- package/skills/repo-maintenance/references/safety-protocol.md +106 -0
- package/skills/repo-maintenance/references/stack-profiles.md +138 -0
- package/skills/repo-maintenance/scripts/audit_repo_maintenance.py +1198 -0
- package/skills/repo-maintenance/scripts/test_audit_repo_maintenance.py +506 -0
- package/skills/repository-intelligence/SKILL.md +189 -0
- package/skills/repository-intelligence/agents/openai.yaml +4 -0
- package/skills/repository-intelligence/references/assessment-rubric.md +88 -0
- package/skills/repository-intelligence/scripts/repository_inventory.py +347 -0
- package/skills/research/SKILL.md +12 -2
- package/skills/root-cause-analysis/SKILL.md +1 -1
- package/skills/runtime-parity-verification/SKILL.md +36 -1
- package/skills/security-methodology/SKILL.md +23 -10
- package/skills/security-methodology/references/agentic-handoff-templates.md +220 -0
- package/skills/security-methodology/references/cross-source-matrix.md +1 -1
- package/skills/security-methodology/references/owasp-agentic-top-10.md +1 -1
- package/skills/security-scan/SKILL.md +55 -15
- package/skills/self-improve/SKILL.md +70 -50
- package/skills/silent-assumptions/SKILL.md +341 -0
- package/skills/silent-assumptions/references/elicitation-detectors.md +342 -0
- package/skills/spec-writing/SKILL.md +128 -24
- package/skills/spec-writing/scripts/check_checklist.py +114 -15
- package/skills/ui-design/SKILL.md +6 -4
- package/skills/ui-design/references/color-engine.md +132 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +8 -8
- package/skills/ui-design/references/ui-guidance-sources.md +1 -1
- package/skills/ui-design/references/universal-design-principles.alt.md +2 -2
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +0 -41
- package/plugin-artifacts/codex/AGENTS.md +0 -560
- package/plugin-artifacts/codex/BUILD-ARTIFACT.md +0 -5
- package/plugin-artifacts/codex/LICENSE +0 -202
- package/plugin-artifacts/codex/README.md +0 -313
- package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
- package/plugin-artifacts/codex/docs/agent-surface-policy.md +0 -63
- package/plugin-artifacts/codex/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/references/agent-role-taxonomy.md +0 -135
- package/plugin-artifacts/codex/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/references/autonomy-config.md +0 -231
- package/plugin-artifacts/codex/references/backlog-system.md +0 -285
- package/plugin-artifacts/codex/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/references/coordination-file-template.md +0 -181
- package/plugin-artifacts/codex/references/coordination-rules.md +0 -552
- package/plugin-artifacts/codex/references/dogfood-reload-checkpoint.md +0 -112
- package/plugin-artifacts/codex/references/halt-and-ask-protocol.md +0 -102
- package/plugin-artifacts/codex/references/implementer-envelope-schema.md +0 -302
- package/plugin-artifacts/codex/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/references/leadership.md +0 -72
- package/plugin-artifacts/codex/references/memory-systems.md +0 -261
- package/plugin-artifacts/codex/references/memory.md +0 -313
- package/plugin-artifacts/codex/references/model-tier-mapping.md +0 -296
- package/plugin-artifacts/codex/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/references/research-trigger-policy.md +0 -140
- package/plugin-artifacts/codex/references/runtime-smoke-triggers.md +0 -42
- package/plugin-artifacts/codex/references/self-review.md +0 -234
- package/plugin-artifacts/codex/references/single-writer-commit-protocol.md +0 -90
- package/plugin-artifacts/codex/references/task-capture-policy.md +0 -68
- package/plugin-artifacts/codex/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/references/ui-spotcheck-protocol.md +0 -65
- package/plugin-artifacts/codex/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +0 -381
- package/plugin-artifacts/codex/skills/build-loop/detect-plugins.mjs +0 -82
- package/plugin-artifacts/codex/skills/build-loop/eval-guide.md +0 -65
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +0 -549
- package/plugin-artifacts/codex/skills/build-loop/phases/fact-check.md +0 -42
- package/plugin-artifacts/codex/skills/build-loop/phases/ui-validation.md +0 -267
- package/plugin-artifacts/codex/skills/build-loop/references/advisor-dispatch-ladder.md +0 -62
- package/plugin-artifacts/codex/skills/build-loop/references/apple-native-planning.md +0 -439
- package/plugin-artifacts/codex/skills/build-loop/references/autonomous-and-per-commit-modes.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +0 -231
- package/plugin-artifacts/codex/skills/build-loop/references/codex-subagents.md +0 -106
- package/plugin-artifacts/codex/skills/build-loop/references/coordination.md +0 -161
- package/plugin-artifacts/codex/skills/build-loop/references/correction-aware-capture.md +0 -177
- package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +0 -101
- package/plugin-artifacts/codex/skills/build-loop/references/independent-auditor.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/intent-capability-pack.md +0 -257
- package/plugin-artifacts/codex/skills/build-loop/references/intent-exploration-prompts.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/leadership.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +0 -313
- package/plugin-artifacts/codex/skills/build-loop/references/modular-systems-pack.md +0 -96
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +0 -222
- package/plugin-artifacts/codex/skills/build-loop/references/pay-it-forward-arch.md +0 -98
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +0 -249
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +0 -86
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +0 -49
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +0 -341
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +0 -72
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +0 -58
- package/plugin-artifacts/codex/skills/build-loop/references/recent-design-structures.md +0 -274
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/ASSESSMENT.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +0 -149
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +0 -32
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +0 -48
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +0 -60
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +0 -51
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +0 -71
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +0 -52
- package/plugin-artifacts/codex/skills/build-loop/references/refactor-history/trace-comparison.md +0 -202
- package/plugin-artifacts/codex/skills/build-loop/references/self-recursive-dev.md +0 -77
- package/plugin-artifacts/codex/skills/build-loop/references/self-review.md +0 -234
- package/plugin-artifacts/codex/skills/build-loop/references/ui-io-contract.md +0 -116
- package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +0 -85
- package/plugin-artifacts/codex/skills/build-loop/scanners/audit-design-rules.mjs +0 -476
- package/plugin-artifacts/codex/skills/build-loop/scanners/require-visual-evidence.mjs +0 -239
- package/plugin-artifacts/codex/skills/build-loop/templates/backlog-item.md +0 -35
- package/plugin-artifacts/codex/skills/build-loop/templates/codex-worker-prompt.md +0 -100
- package/plugin-artifacts/codex/skills/build-loop/templates/ui-subagent-prompt.md +0 -179
- package/plugin-artifacts/codex/skills/build-loop/templates/ux-fix-plan.md +0 -40
- 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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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>
|