entropy-machines 0.1.1
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/LICENSE +93 -0
- package/README.md +68 -0
- package/agents/isolated-worker.md +128 -0
- package/agents/verifier.md +158 -0
- package/bin/dispatch +700 -0
- package/bin/doclint +460 -0
- package/bin/drain +507 -0
- package/bin/drain-pick.py +168 -0
- package/bin/drain-prompt.md +67 -0
- package/bin/drain-run.sh +342 -0
- package/bin/entropy-machines-init +285 -0
- package/bin/handoff +1151 -0
- package/bin/init +232 -0
- package/bin/post-fold-audit +377 -0
- package/bin/serve +724 -0
- package/bin/status +208 -0
- package/bin/tracker +153 -0
- package/docs/AGENT-QUICKSTART.md +86 -0
- package/docs/CONFIG.md +68 -0
- package/docs/NPM.md +91 -0
- package/docs/SERVE.md +74 -0
- package/docs/TRACKER-ADAPTER.md +66 -0
- package/doctrine/HANDOFF-PROMPT.md +63 -0
- package/doctrine/README.md +62 -0
- package/doctrine/ROLES.md +27 -0
- package/doctrine/WORKFLOW.md +87 -0
- package/hooks/commit-msg +24 -0
- package/hooks/post-checkout +354 -0
- package/hooks/pre-commit +33 -0
- package/lib/PRD-001-orientation.html +1180 -0
- package/lib/REPORT-TEMPLATE.html +413 -0
- package/lib/changelog-collate.mjs +328 -0
- package/lib/changelog-guard.sh +157 -0
- package/lib/changelog-new.mjs +70 -0
- package/lib/config.mjs +283 -0
- package/lib/config.py +317 -0
- package/lib/doc-template.html +807 -0
- package/lib/entropy-drain.plist.in +59 -0
- package/lib/entropy-drain.service.in +53 -0
- package/lib/entropy-drain.timer.in +36 -0
- package/lib/fail-first.mjs +901 -0
- package/lib/handoff-guard.sh +623 -0
- package/lib/install-hooks.sh +169 -0
- package/lib/notes.py +675 -0
- package/lib/preflight-tree.mjs +82 -0
- package/lib/roots.sh +212 -0
- package/lib/themes/daylight.css +84 -0
- package/lib/themes/high-contrast.css +36 -0
- package/lib/tracker-file +333 -0
- package/lib/tracker-view.py +784 -0
- package/package.json +38 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- ===================== entropy-machines dialogue doc =====================
|
|
3
|
+
A PRD/plan/report the owner ANSWERS. bin/serve renders it, rewires the save
|
|
4
|
+
button to POST /__save so answers are written back into THIS file on disk,
|
|
5
|
+
and live-reloads the open tab when an agent writes a reply.
|
|
6
|
+
|
|
7
|
+
CONTRACT — bin/serve and the agents both depend on these:
|
|
8
|
+
* every question lives in a div with class `response` and a unique,
|
|
9
|
+
stable `data-resp` attribute, containing a label, a div with class
|
|
10
|
+
`discuss`, and one textarea.
|
|
11
|
+
* data-resp keys are unique per doc and STABLE — they are the identity an
|
|
12
|
+
agent uses to read and write a specific answer. Renaming one orphans it.
|
|
13
|
+
* a script element of type application/json with id `responses-data`,
|
|
14
|
+
near the end of the body, is where saved answers are mirrored.
|
|
15
|
+
* pages are section elements with class `page` and id `p0`, `p1`, ...,
|
|
16
|
+
each with a matching nav anchor carrying the same id in `data-page`.
|
|
17
|
+
|
|
18
|
+
A QUESTION WITHOUT A .response BOX IS INVISIBLE. Only a data-resp is saved,
|
|
19
|
+
counted, or reported as outstanding — a question written as plain prose
|
|
20
|
+
renders with nowhere to answer it and the doc reports itself as answered.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
NOTE: this comment deliberately describes the markup in prose instead of
|
|
24
|
+
showing it. It used to contain literal examples, and every regex-based tool
|
|
25
|
+
that reads these docs -- the answer counter, the session-start nag, ad-hoc
|
|
26
|
+
checks -- matched the DOCUMENTATION as though it were a real question. Each
|
|
27
|
+
doc then reported one phantom unanswered question that nobody could answer,
|
|
28
|
+
so "fully answered" was unreachable. The readers strip comments now too, but
|
|
29
|
+
the fix that matters is not putting a live example in a comment.
|
|
30
|
+
|
|
31
|
+
Visual standard (VS Code hc-black / hc-light):
|
|
32
|
+
1. Separate with BORDERS, never background tints.
|
|
33
|
+
2. Focus/hover is the focus hue as a ring, never a fill.
|
|
34
|
+
3. State is never encoded in opacity — weight, case or colour+word.
|
|
35
|
+
4. Four type steps only (14/17/21/30); mono gets its own step.
|
|
36
|
+
========================================================================= -->
|
|
37
|
+
<html lang="en"><head>
|
|
38
|
+
<meta charset="utf-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40
|
+
<title>PRD-001 — Orient the factory to this repo</title>
|
|
41
|
+
<style>
|
|
42
|
+
/* High-contrast stylesheet. Palette is VS Code's hc-black / hc-light from
|
|
43
|
+
src/vs/platform/theme/common/colors/baseColors.ts — not hand-picked, so it
|
|
44
|
+
carries their AAA work rather than an invented scheme.
|
|
45
|
+
|
|
46
|
+
Three rules this file exists to hold:
|
|
47
|
+
1. Separate with BORDERS, never background tints. Backgrounds stay pure
|
|
48
|
+
#000 / #FFF. No grey panel layers, no tinted cards, no filled badges.
|
|
49
|
+
2. Focus / active / hover is the focus hue as an inset ring, never a
|
|
50
|
+
background change. :focus-visible is global and never suppressed.
|
|
51
|
+
3. State is never encoded in opacity. No faded "done", no opacity:.6 for
|
|
52
|
+
secondary text — weight, uppercase, or an underline instead. */
|
|
53
|
+
|
|
54
|
+
:root{
|
|
55
|
+
--bg:#FFFFFF; --fg:#292929;
|
|
56
|
+
--border:#0F4A85; /* contrastBorder */
|
|
57
|
+
--focus:#006BBD; /* focusBorder */
|
|
58
|
+
--link:#0F4A85;
|
|
59
|
+
--negative:#A81C0B; --positive:#0A5C21; --caution:#7A4A00; --accent:#6B21A8;
|
|
60
|
+
|
|
61
|
+
/* Four steps. Nothing below 14px, no half-pixels, no em ratios that compute
|
|
62
|
+
to fractions. Monospace gets its OWN step rather than a .9em of body,
|
|
63
|
+
because mono reads larger than sans at equal px.
|
|
64
|
+
|
|
65
|
+
REVISED 2026-08-06 after the first doc shipped and read as flat. The old
|
|
66
|
+
scale was 12/14/16/20: body BELOW the browser's 16px default, and h2 only
|
|
67
|
+
1.14x body, so a page of prose + inline code came out as one grey size
|
|
68
|
+
with noise in it. The steps are now ~1.25x apart with a bigger jump to
|
|
69
|
+
the title, which is what makes a heading read as a heading. */
|
|
70
|
+
--fs-sm:14px; /* meta, ids, mono, chips, eyebrow labels, badges */
|
|
71
|
+
--fs-base:17px; /* body, list rows, controls */
|
|
72
|
+
--fs-head:21px; /* section headings */
|
|
73
|
+
--fs-title:30px; /* page / panel title */
|
|
74
|
+
|
|
75
|
+
/* Prose measure. Tables and code want the full column; running text does
|
|
76
|
+
not — 17px across a 1040px main is ~105 characters, well past the point
|
|
77
|
+
where the eye loses the next line. */
|
|
78
|
+
--measure:70ch;
|
|
79
|
+
|
|
80
|
+
--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
81
|
+
--mono:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
|
|
82
|
+
--radius:0;
|
|
83
|
+
}
|
|
84
|
+
@media (prefers-color-scheme:dark){
|
|
85
|
+
:root{
|
|
86
|
+
--bg:#000000; --fg:#FFFFFF;
|
|
87
|
+
--border:#6FC3DF; --focus:#F38518; --link:#21A6FF;
|
|
88
|
+
--negative:#F48771; --positive:#23D18B; --caution:#F5F543; --accent:#D670D6;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/* The toggle stamps data-theme on the root; it must win over the media query
|
|
92
|
+
in BOTH directions, so each theme is restated rather than only the dark one. */
|
|
93
|
+
:root[data-theme="light"]{
|
|
94
|
+
--bg:#FFFFFF; --fg:#292929; --border:#0F4A85; --focus:#006BBD; --link:#0F4A85;
|
|
95
|
+
--negative:#A81C0B; --positive:#0A5C21; --caution:#7A4A00; --accent:#6B21A8;
|
|
96
|
+
}
|
|
97
|
+
:root[data-theme="dark"]{
|
|
98
|
+
--bg:#000000; --fg:#FFFFFF; --border:#6FC3DF; --focus:#F38518; --link:#21A6FF;
|
|
99
|
+
--negative:#F48771; --positive:#23D18B; --caution:#F5F543; --accent:#D670D6;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
*{box-sizing:border-box}
|
|
103
|
+
html{font-size:16px;}
|
|
104
|
+
body{font-family:var(--font); font-size:var(--fs-base); line-height:1.65;
|
|
105
|
+
color:var(--fg); background:var(--bg); margin:0; display:flex; min-height:100vh;}
|
|
106
|
+
|
|
107
|
+
/* Rule 2, globally. Never suppressed anywhere below. */
|
|
108
|
+
:focus-visible{outline:2px solid var(--focus); outline-offset:1px;}
|
|
109
|
+
|
|
110
|
+
code{font-family:var(--mono); font-size:var(--fs-sm); background:none;
|
|
111
|
+
border:1px solid var(--border); padding:0 .25em;}
|
|
112
|
+
pre{background:none; border:1px solid var(--border); padding:12px 14px; overflow-x:auto;
|
|
113
|
+
font-family:var(--mono); font-size:var(--fs-sm); line-height:1.55;}
|
|
114
|
+
pre code{border:none; padding:0;}
|
|
115
|
+
|
|
116
|
+
nav{width:296px; flex:none; border-right:1px solid var(--border); background:var(--bg);
|
|
117
|
+
padding:22px 14px; position:sticky; top:0; height:100vh; overflow-y:auto;}
|
|
118
|
+
nav .brand{display:flex; align-items:center; gap:8px; font-weight:700;
|
|
119
|
+
font-size:var(--fs-base); margin-bottom:16px; padding:0 6px;}
|
|
120
|
+
nav .brand .logo{width:18px; height:18px; border:1px solid var(--border); background:none;}
|
|
121
|
+
nav a{display:block; padding:6px 10px; color:var(--fg); text-decoration:none;
|
|
122
|
+
font-size:var(--fs-base); margin-bottom:2px; border:1px solid transparent;}
|
|
123
|
+
nav a:hover{box-shadow:inset 0 0 0 2px var(--focus);}
|
|
124
|
+
/* Current page: a border and weight, not a fill. */
|
|
125
|
+
nav a.cur{border-color:var(--border); font-weight:700;}
|
|
126
|
+
nav .grp{font-size:var(--fs-sm); font-weight:700; text-transform:uppercase;
|
|
127
|
+
letter-spacing:.06em; color:var(--fg); margin:16px 6px 6px;}
|
|
128
|
+
nav .foot{font-size:var(--fs-sm); color:var(--fg); margin-top:20px; padding:0 6px;}
|
|
129
|
+
|
|
130
|
+
/* serve.py injects #__navmark-css late in <body>, so it wins on source order
|
|
131
|
+
at equal specificity. These selectors carry one extra type each to beat it.
|
|
132
|
+
Its defaults are off-standard three ways: .72rem (11.52px, a fifth size),
|
|
133
|
+
a hardcoded #fdf6e3 fill, and pill radii — a filled badge is a background
|
|
134
|
+
tint, which rule 1 forbids. It also references --good/--warn-deep/--muted/
|
|
135
|
+
--code-bg, which this palette does not define, so its colours fall back to
|
|
136
|
+
inherit. Do not delete its block from the file: it is re-injected on serve. */
|
|
137
|
+
nav a span.navmark{font-size:var(--fs-sm); font-weight:700; line-height:1.4;
|
|
138
|
+
background:none; border-radius:0; padding:0 .35em; margin-left:.4em;
|
|
139
|
+
border:1px solid currentColor; color:var(--caution);}
|
|
140
|
+
nav a span.navmark.done{color:var(--positive); border-color:var(--positive);}
|
|
141
|
+
nav a span.navmark.todo{color:var(--caution); border-color:var(--caution); background:none;}
|
|
142
|
+
nav div.navsummary{font-size:var(--fs-sm); font-weight:700; color:var(--fg);
|
|
143
|
+
background:none; border:1px solid var(--border); border-radius:0;
|
|
144
|
+
margin:12px 6px 4px; padding:5px 9px;}
|
|
145
|
+
nav div.navsummary .all-done{color:var(--positive);}
|
|
146
|
+
|
|
147
|
+
main{flex:1; min-width:0; padding:38px 42px 120px; max-width:1060px;}
|
|
148
|
+
.page{display:none;} .page.cur{display:block;}
|
|
149
|
+
|
|
150
|
+
h1{font-size:var(--fs-title); font-weight:700; letter-spacing:-.015em;
|
|
151
|
+
line-height:1.2; margin:2px 0 8px;}
|
|
152
|
+
h2{font-size:var(--fs-head); font-weight:700; line-height:1.3; margin:34px 0 8px;}
|
|
153
|
+
/* Same step as h2, distinguished by case rather than a fifth size. */
|
|
154
|
+
h3{font-size:var(--fs-head); font-weight:700; text-transform:uppercase;
|
|
155
|
+
letter-spacing:.03em; line-height:1.3; margin:24px 0 6px;}
|
|
156
|
+
h1 code,h2 code,h3 code{font-size:var(--fs-head);}
|
|
157
|
+
.sub{font-size:var(--fs-base); margin:0 0 20px;}
|
|
158
|
+
p{margin:11px 0;}
|
|
159
|
+
main a{color:var(--link); text-decoration:underline;}
|
|
160
|
+
|
|
161
|
+
/* Measure. Tables, code and the response boxes keep the full column; running
|
|
162
|
+
text is capped so the eye finds the next line. Applied to the block, not
|
|
163
|
+
to `main`, so a wide table is never squeezed to prose width. */
|
|
164
|
+
.page > p, .page > ul, .page > ol, .page > .sub, .callout, blockquote{
|
|
165
|
+
max-width:var(--measure);}
|
|
166
|
+
|
|
167
|
+
/* Rule 1: every variant is the same pure background plus a border. The kind
|
|
168
|
+
is carried by the tag's colour and the left border, not by a wash. */
|
|
169
|
+
.callout{padding:12px 16px; margin:16px 0; border:1px solid var(--border); background:none;}
|
|
170
|
+
.callout.key{border-left:3px solid var(--accent);}
|
|
171
|
+
.callout.good{border-left:3px solid var(--positive);}
|
|
172
|
+
.callout.warn{border-left:3px solid var(--caution);}
|
|
173
|
+
.callout.bad{border-left:3px solid var(--negative);}
|
|
174
|
+
.callout .tag{font-size:var(--fs-sm); font-weight:700; text-transform:uppercase;
|
|
175
|
+
letter-spacing:.05em; display:block; margin-bottom:4px;}
|
|
176
|
+
.callout.key .tag{color:var(--accent);} .callout.good .tag{color:var(--positive);}
|
|
177
|
+
.callout.warn .tag{color:var(--caution);} .callout.bad .tag{color:var(--negative);}
|
|
178
|
+
|
|
179
|
+
.tbl-wrap{overflow-x:auto; margin:18px 0;}
|
|
180
|
+
table{border-collapse:collapse; width:100%; font-size:var(--fs-base);}
|
|
181
|
+
th,td{padding:10px 12px; text-align:left; border:1px solid var(--border); vertical-align:top;}
|
|
182
|
+
thead th{font-size:var(--fs-sm); font-weight:700; text-transform:uppercase;
|
|
183
|
+
letter-spacing:.04em; color:var(--fg); white-space:nowrap;}
|
|
184
|
+
td code{font-size:var(--fs-sm);}
|
|
185
|
+
|
|
186
|
+
ul,ol{margin:10px 0; padding-left:24px;} li{margin:7px 0;}
|
|
187
|
+
.foot{font-size:var(--fs-sm);}
|
|
188
|
+
|
|
189
|
+
.rec{border:1px solid var(--border); border-left:3px solid var(--positive);
|
|
190
|
+
padding:8px 13px; margin:13px 0;}
|
|
191
|
+
.rec .lbl{font-size:var(--fs-sm); font-weight:700; text-transform:uppercase;
|
|
192
|
+
letter-spacing:.05em; color:var(--positive); display:block; margin-bottom:3px;}
|
|
193
|
+
|
|
194
|
+
/* Chips are outlined, never filled — a filled badge is a background tint. */
|
|
195
|
+
.pill{display:inline-block; font-size:var(--fs-sm); font-weight:700; padding:1px 8px;
|
|
196
|
+
border:1px solid var(--border); background:none; color:var(--fg); vertical-align:middle;}
|
|
197
|
+
.pill.dec{color:var(--caution); border-color:var(--caution);}
|
|
198
|
+
.pill.red{color:var(--negative); border-color:var(--negative);}
|
|
199
|
+
.pill.grn{color:var(--positive); border-color:var(--positive);}
|
|
200
|
+
|
|
201
|
+
blockquote{border-left:3px solid var(--border); margin:10px 0; padding-left:13px;
|
|
202
|
+
font-size:var(--fs-base);}
|
|
203
|
+
|
|
204
|
+
.sev{font-weight:700;}
|
|
205
|
+
.sev.r{color:var(--negative);} .sev.o{color:var(--caution);} .sev.y{color:var(--caution);}
|
|
206
|
+
.verdict{font-weight:700; white-space:nowrap;}
|
|
207
|
+
.verdict.clean{color:var(--positive);} .verdict.tangled{color:var(--caution);}
|
|
208
|
+
.verdict.bound{color:var(--negative);}
|
|
209
|
+
|
|
210
|
+
/* ---- response boxes: page-level ----
|
|
211
|
+
min-height:52px and overflow:hidden are load-bearing for the skill's
|
|
212
|
+
auto-grow (it measures scrollHeight); do not "tidy" them away. */
|
|
213
|
+
.response{margin:16px 0 6px; border:1px solid var(--border);
|
|
214
|
+
border-left:3px solid var(--caution); background:none; padding:9px 13px;}
|
|
215
|
+
.response label{display:block; font-size:var(--fs-sm); font-weight:700;
|
|
216
|
+
text-transform:uppercase; letter-spacing:.05em; color:var(--caution); margin-bottom:5px;}
|
|
217
|
+
.response .discuss{margin:0 0 10px; padding:9px 12px; border:1px solid var(--border);
|
|
218
|
+
border-left:2px solid var(--accent); background:none;
|
|
219
|
+
font-size:var(--fs-base); line-height:1.55; color:var(--fg);}
|
|
220
|
+
.response .discuss code{font-size:var(--fs-sm);}
|
|
221
|
+
.response textarea{width:100%; min-height:52px; resize:vertical; font-family:var(--font);
|
|
222
|
+
font-size:var(--fs-base); color:var(--fg); background:var(--bg);
|
|
223
|
+
border:1px solid var(--border); padding:7px 10px; line-height:1.5; overflow:hidden;}
|
|
224
|
+
.response textarea:focus{outline:none; box-shadow:inset 0 0 0 2px var(--focus);}
|
|
225
|
+
/* Answered state: colour + weight on the label, never a fade or a fill. */
|
|
226
|
+
.response.filled{border-left-color:var(--positive);}
|
|
227
|
+
.response.filled label{color:var(--positive);}
|
|
228
|
+
|
|
229
|
+
/* ---- response boxes: row-level (compact) ----
|
|
230
|
+
1.9em height is the skill's explicit override of the 52px above; keep it. */
|
|
231
|
+
tr.row-note td{padding:3px 9px 9px; border:1px solid var(--border);}
|
|
232
|
+
tr.row-note .response.mini{margin:0; border:none; background:none; padding:0;}
|
|
233
|
+
tr.row-note .response.mini textarea{display:block; width:100%; min-height:0; height:1.9em;
|
|
234
|
+
resize:none; overflow:hidden; font-family:var(--font); font-size:var(--fs-base);
|
|
235
|
+
line-height:1.4; color:var(--fg); background:var(--bg);
|
|
236
|
+
border:1px solid var(--border); padding:4px 9px;}
|
|
237
|
+
tr.row-note .response.mini textarea:focus{color:var(--fg);
|
|
238
|
+
box-shadow:inset 0 0 0 2px var(--focus); border-color:var(--focus);}
|
|
239
|
+
tr.row-note .response.mini.filled textarea{border-color:var(--positive);}
|
|
240
|
+
|
|
241
|
+
.savebar{position:fixed; bottom:14px; right:19px; display:flex; align-items:center;
|
|
242
|
+
gap:11px; z-index:50; background:var(--bg); border:1px solid var(--border);
|
|
243
|
+
padding:7px 9px 7px 16px;}
|
|
244
|
+
.savebar .stat{font-size:var(--fs-sm); color:var(--fg);}
|
|
245
|
+
.savebar button{font-family:var(--font); font-size:var(--fs-base); font-weight:700;
|
|
246
|
+
cursor:pointer; border:1px solid var(--border); background:none; color:var(--fg);
|
|
247
|
+
padding:6px 14px;}
|
|
248
|
+
.savebar button:hover{box-shadow:inset 0 0 0 2px var(--focus);}
|
|
249
|
+
.savebar button.ghost{border-color:var(--border); color:var(--fg); padding:6px 10px;}
|
|
250
|
+
</style>
|
|
251
|
+
</head><body>
|
|
252
|
+
<nav>
|
|
253
|
+
<div class="brand"><span class="logo"></span> PRD-001</div>
|
|
254
|
+
<div class="grp">Orientation</div>
|
|
255
|
+
<a href="#p0" data-page="p0" class="cur">0 · How this factory works</a>
|
|
256
|
+
<div class="grp">Your repo</div>
|
|
257
|
+
<a href="#p1" data-page="p1">1 · What we found</a>
|
|
258
|
+
<div class="grp">Open questions</div>
|
|
259
|
+
<a href="#p2" data-page="p2">2 · Which suites gate a landing</a>
|
|
260
|
+
<a href="#p3" data-page="p3">3 · What's protected</a>
|
|
261
|
+
<a href="#p4" data-page="p4">4 · Tracker backend</a>
|
|
262
|
+
<a href="#p5" data-page="p5">5 · Landing policy</a>
|
|
263
|
+
<a href="#p6" data-page="p6">6 · First real work</a>
|
|
264
|
+
<div class="grp">Output</div>
|
|
265
|
+
<a href="#p7" data-page="p7">7 · Issues this PRD creates</a>
|
|
266
|
+
<div class="foot">This is the upstream artifact: filling it in creates
|
|
267
|
+
your project's first tracker issues, not the other way around. 💾 Save
|
|
268
|
+
writes your answers back into this file. New sections may get appended
|
|
269
|
+
below over time — this one won't move or get renamed.</div>
|
|
270
|
+
</nav>
|
|
271
|
+
|
|
272
|
+
<main>
|
|
273
|
+
|
|
274
|
+
<section class="page cur" id="p0">
|
|
275
|
+
<h1>PRD-001 — Orient the factory to this repo</h1>
|
|
276
|
+
<p class="sub">The first document you fill in here. Answering it files your project's first real issues.</p>
|
|
277
|
+
|
|
278
|
+
<p><code>entropy-machines</code> is a workflow for running coding agents on
|
|
279
|
+
a real codebase without letting them corrupt each other's work or land
|
|
280
|
+
things nobody proved. It is four roles and a handful of gates, each one
|
|
281
|
+
built because something specific went wrong once.</p>
|
|
282
|
+
|
|
283
|
+
<div class="callout key"><span class="tag">upstream, not downstream</span>
|
|
284
|
+
A PRD is the artifact that <strong>creates</strong> work here — work does
|
|
285
|
+
not create a PRD. Filling in the questions on the following pages is what
|
|
286
|
+
files your first tracker issues; a one-off "discovery" task is never the
|
|
287
|
+
entry point, it's one issue that might fall out of an answer below.
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<h2 data-informational>The cycle</h2>
|
|
291
|
+
<pre><code>ISSUES ──▶ WORKED ──▶ VERIFIED ──▶ FOLDED ──▶ SPRINT REPORT ──▶ ISSUES</code></pre>
|
|
292
|
+
<p>Full stage-by-stage version, including the incidents each stage's rules
|
|
293
|
+
were built to stop recurring: <code>doctrine/WORKFLOW.md</code>. You should
|
|
294
|
+
not need to open it to follow this page.</p>
|
|
295
|
+
|
|
296
|
+
<h2 data-informational>Four roles</h2>
|
|
297
|
+
<div class="tbl-wrap">
|
|
298
|
+
<table>
|
|
299
|
+
<thead><tr><th>Role</th><th>Does</th><th>Never</th></tr></thead>
|
|
300
|
+
<tbody>
|
|
301
|
+
<tr><td><strong>Owner</strong> (you)</td>
|
|
302
|
+
<td>Rules on open questions like the ones ahead, says when held work
|
|
303
|
+
may merge, ticks the sprint closed.</td>
|
|
304
|
+
<td>Delegates the sprint-close tick or a decision an issue is gated
|
|
305
|
+
on — there is no code path to either.</td></tr>
|
|
306
|
+
<tr><td>Worker</td>
|
|
307
|
+
<td>Implements one scoped issue, in its own git worktree, leaves a
|
|
308
|
+
<code>HANDOFF.md</code>.</td>
|
|
309
|
+
<td>Commits, pushes, merges, or edits outside its declared file
|
|
310
|
+
scope.</td></tr>
|
|
311
|
+
<tr><td>Verifier</td>
|
|
312
|
+
<td>Sweeps <em>all</em> finished work for the sprint, once, on a
|
|
313
|
+
clean tree; reports a verdict.</td>
|
|
314
|
+
<td>Fixes, lands, or repeats a number it did not personally
|
|
315
|
+
re-run.</td></tr>
|
|
316
|
+
<tr><td>Orchestrator</td>
|
|
317
|
+
<td>Dispatches, folds verified work into <code>main</code> (the only
|
|
318
|
+
committer), turns rulings into issues.</td>
|
|
319
|
+
<td>Lands held work without your go-ahead, or relays a worker's own
|
|
320
|
+
claim as if it had re-run it.</td></tr>
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
</div>
|
|
324
|
+
<p>Full definitions, including what each role must <em>not</em> do and why:
|
|
325
|
+
<code>doctrine/ROLES.md</code>.</p>
|
|
326
|
+
|
|
327
|
+
<h2 data-informational>What the gates protect you from</h2>
|
|
328
|
+
<ul>
|
|
329
|
+
<li><code>bin/dispatch</code> refuses to run from anywhere but the repo
|
|
330
|
+
root, refuses uncommitted edits already sitting in the files it's about
|
|
331
|
+
to hand out, and refuses to overlap a file another live dispatch
|
|
332
|
+
already holds — a worker never starts from a wrong or dirty base.</li>
|
|
333
|
+
<li>The commit-msg hook refuses a commit that lands a dispatched issue
|
|
334
|
+
with no handoff record for it — a worker's
|
|
335
|
+
<code>changed</code>/<code>found</code>/<code>assumed</code>/<code>next</code>
|
|
336
|
+
is never lost to a fast commit.</li>
|
|
337
|
+
<li>The pre-commit hook refuses a commit touching a watched path with no
|
|
338
|
+
<code>changelog.d/</code> fragment — nobody has to reconstruct what
|
|
339
|
+
changed by reading the diff (off entirely if
|
|
340
|
+
<code>changelog.enabled</code> is false).</li>
|
|
341
|
+
<li><code>bin/handoff</code> refuses <code>--verified</code> text that
|
|
342
|
+
just relays what the worker claimed — "tests pass" is not evidence,
|
|
343
|
+
you have to say what <em>you</em> re-ran.</li>
|
|
344
|
+
<li>The tracker refuses <code>status=held</code> outright — held-ness
|
|
345
|
+
lives in <code>heldWhy</code>, gated-ness in <code>gate</code>, and
|
|
346
|
+
neither is a status value, so a hold with no reason attached cannot be
|
|
347
|
+
created.</li>
|
|
348
|
+
</ul>
|
|
349
|
+
<p>That is everything you need to read the rest of this document. The next
|
|
350
|
+
page is what an agent found looking at your actual repo; the pages after
|
|
351
|
+
that are yours to rule on.</p>
|
|
352
|
+
</section>
|
|
353
|
+
|
|
354
|
+
<section class="page" id="p1">
|
|
355
|
+
<h1>What we found in your repo</h1>
|
|
356
|
+
<p class="sub">Observations, not settled fact. Correct anything wrong
|
|
357
|
+
before answering the questions that follow — they build on this.</p>
|
|
358
|
+
|
|
359
|
+
<div class="callout warn"><span class="tag">not yet filled in</span>
|
|
360
|
+
This page ships empty. It gets filled in by an agent as part of bringing
|
|
361
|
+
this factory into your project — if you are reading this straight out of
|
|
362
|
+
the harness repo, everything below the line is a placeholder waiting for
|
|
363
|
+
that pass.
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<!-- AGENT FILLING THIS IN: run the commands below, do not read them off a
|
|
367
|
+
README and repeat its claims back. An unverified suite command is
|
|
368
|
+
exactly the kind of claim this harness distrusts on principle: see
|
|
369
|
+
docs/CONFIG.md, bin/init leaves config.json's `suites` array empty
|
|
370
|
+
rather than guess at one, for the same reason. Report only what you
|
|
371
|
+
personally ran and watched pass or fail, with the actual command and
|
|
372
|
+
its actual result (pass, fail, or count).
|
|
373
|
+
|
|
374
|
+
Fill in, under each heading below, as short prose or a list:
|
|
375
|
+
|
|
376
|
+
Language and toolchain — what is actually in use: runtime, package
|
|
377
|
+
manager, framework. Read it off lockfiles and manifests, not off the
|
|
378
|
+
directory name or a guess.
|
|
379
|
+
|
|
380
|
+
Test and typecheck and build — the exact command you ran for each
|
|
381
|
+
(they may be the same command, or may not all exist) and what
|
|
382
|
+
happened. "Ran `npm test`, 41 passed, 0 failed" is the right shape.
|
|
383
|
+
"Probably `npm test`, per the README" is not — leave it unverified
|
|
384
|
+
and say so, do not put it in this section.
|
|
385
|
+
|
|
386
|
+
Codegen — is there a step that writes committed files from source
|
|
387
|
+
(a generate script, a codegen target)? What does it emit? Did you
|
|
388
|
+
run it and confirm the output matches what is already committed?
|
|
389
|
+
|
|
390
|
+
Worktree symlinks — what does a completely fresh worktree of this
|
|
391
|
+
repo need linked in before anything above will even run — the usual
|
|
392
|
+
one is a package manager's local cache directory, but check for a
|
|
393
|
+
virtualenv or vendored dependencies too. Prove it, don't guess it:
|
|
394
|
+
add a worktree, try the test command unlinked, note what actually
|
|
395
|
+
breaks.
|
|
396
|
+
|
|
397
|
+
Changelog convention — does one already exist (a CHANGELOG file, a
|
|
398
|
+
fragments directory, a commit message convention)? Quote what you
|
|
399
|
+
found. Do not invent a convention that is not there.
|
|
400
|
+
|
|
401
|
+
Leave the open questions on the pages after this one alone. Those are
|
|
402
|
+
owner calls (doctrine/ROLES.md) — not something to infer from code. -->
|
|
403
|
+
|
|
404
|
+
<h2 data-informational>Language & toolchain</h2>
|
|
405
|
+
<p><em>Not yet filled in.</em></p>
|
|
406
|
+
|
|
407
|
+
<h2 data-informational>Test / typecheck / build — commands actually run, and what happened</h2>
|
|
408
|
+
<p><em>Not yet filled in.</em></p>
|
|
409
|
+
|
|
410
|
+
<h2 data-informational>Codegen</h2>
|
|
411
|
+
<p><em>Not yet filled in.</em></p>
|
|
412
|
+
|
|
413
|
+
<h2 data-informational>Worktree symlinks needed</h2>
|
|
414
|
+
<p><em>Not yet filled in.</em></p>
|
|
415
|
+
|
|
416
|
+
<h2 data-informational>Changelog convention</h2>
|
|
417
|
+
<p><em>Not yet filled in.</em></p>
|
|
418
|
+
</section>
|
|
419
|
+
|
|
420
|
+
<section class="page" id="p2">
|
|
421
|
+
<h1>Which suites gate a landing, and which are advisory?</h1>
|
|
422
|
+
<p class="sub">This becomes <code>config.json</code>'s <code>suites</code> array.</p>
|
|
423
|
+
|
|
424
|
+
<p>Every entry under <code>suites</code> runs in the verifier's sweep and in
|
|
425
|
+
<code>bin/post-fold-audit</code>. An entry tagged <code>"slow"</code> can be
|
|
426
|
+
skipped with <code>--skip slow</code> — that is the built-in way to say
|
|
427
|
+
"don't block every single fold on this one," which in practice is what
|
|
428
|
+
makes a suite advisory rather than gating (<code>docs/CONFIG.md</code>).</p>
|
|
429
|
+
|
|
430
|
+
<div class="response" data-resp="prd001-q1-suites">
|
|
431
|
+
<label>Suites: gate vs. advisory</label>
|
|
432
|
+
<div class="discuss">
|
|
433
|
+
<strong>Option A</strong> — a small, fast set gates every fold (say,
|
|
434
|
+
typecheck + unit); anything slow or flake-prone (e2e, integration) is
|
|
435
|
+
tagged <code>"slow"</code> and only required once per sprint via the
|
|
436
|
+
verifier, not on every single dispatch. Cost: a red slow suite can sit
|
|
437
|
+
underneath a landed fix for a while, surfacing on your own review
|
|
438
|
+
cadence instead of blocking immediately.<br>
|
|
439
|
+
<strong>Option B</strong> — everything in <code>suites</code> gates
|
|
440
|
+
every fold, nothing tagged slow. Cost: every dispatch cycle pays the
|
|
441
|
+
full suite's wall time and flake rate, even for a one-line fix.<br>
|
|
442
|
+
I'd take <strong>A</strong> — start with typecheck + unit gating,
|
|
443
|
+
tag anything slow or historically flaky, and widen the gate later once
|
|
444
|
+
you trust it. Which suites do you have (or plan to have), and which
|
|
445
|
+
of them gate vs. which are advisory?
|
|
446
|
+
</div>
|
|
447
|
+
<textarea placeholder="Type your answer…"></textarea>
|
|
448
|
+
</div>
|
|
449
|
+
</section>
|
|
450
|
+
|
|
451
|
+
<section class="page" id="p3">
|
|
452
|
+
<h1>What belongs in <code>project.protectedPaths</code>?</h1>
|
|
453
|
+
<p class="sub">Paths no dispatched agent may edit on its own initiative.</p>
|
|
454
|
+
|
|
455
|
+
<p>Candidates: an append-only interface, a generated bundle, a cross-writer
|
|
456
|
+
contract, anything that needs a human holding both ends. A worker (and the
|
|
457
|
+
unattended drainer, if you ever arm it) refuses to touch a listed path;
|
|
458
|
+
the drainer's runner also diffs finished work against the list before
|
|
459
|
+
pushing and quarantines the whole run on a match.</p>
|
|
460
|
+
|
|
461
|
+
<div class="response" data-resp="prd001-q2-protected-paths">
|
|
462
|
+
<label>protectedPaths list</label>
|
|
463
|
+
<div class="discuss">
|
|
464
|
+
<strong>Option A</strong> — start narrow: only what you can name right
|
|
465
|
+
now (a generated bundle, a public API surface, a schema migration you
|
|
466
|
+
don't want an agent improvising on). Cost: something you forgot is
|
|
467
|
+
editable by a worker until you notice and add it.<br>
|
|
468
|
+
<strong>Option B</strong> — start wide: anything that took real design
|
|
469
|
+
thought, docs included. Cost: workers stall asking to touch ordinary
|
|
470
|
+
files, and "held, needs owner" piles up for things that never needed
|
|
471
|
+
protecting.<br>
|
|
472
|
+
I'd take <strong>A</strong> — the list only blocks automated edits, not
|
|
473
|
+
yours; you can always add to it the first time a worker touches
|
|
474
|
+
something you regret. What paths go in it?
|
|
475
|
+
</div>
|
|
476
|
+
<textarea placeholder="Type your answer…"></textarea>
|
|
477
|
+
</div>
|
|
478
|
+
</section>
|
|
479
|
+
|
|
480
|
+
<section class="page" id="p4">
|
|
481
|
+
<h1>Keep the built-in file tracker, or point at one you already use?</h1>
|
|
482
|
+
<p class="sub">This becomes <code>tracker.backend</code>.</p>
|
|
483
|
+
|
|
484
|
+
<p>The harness needs exactly six operations from a tracker —
|
|
485
|
+
<code>show</code>, <code>notes</code>, <code>remember</code>,
|
|
486
|
+
<code>claim</code>, <code>ready</code>, <code>set</code> (no
|
|
487
|
+
<code>add</code>; an issue is created with <code>set</code>, which
|
|
488
|
+
auto-vivifies). <code>"file"</code> is the built-in backend, storing
|
|
489
|
+
<code>.entropy-machines/issues.json</code> locally. <code>"command"</code> shells
|
|
490
|
+
out to any executable implementing the same six ops
|
|
491
|
+
(<code>docs/TRACKER-ADAPTER.md</code>) — and that adapter has to be able to
|
|
492
|
+
express <em>held</em> (a <code>heldWhy</code> field) and <em>gated</em>
|
|
493
|
+
(a <code>gate</code> field) correctly, or <code>ready</code> will
|
|
494
|
+
over-report what's actually claimable.</p>
|
|
495
|
+
|
|
496
|
+
<div class="response" data-resp="prd001-q3-tracker-backend">
|
|
497
|
+
<label>tracker.backend</label>
|
|
498
|
+
<div class="discuss">
|
|
499
|
+
<strong>Option A</strong> — keep <code>"file"</code>. Zero setup,
|
|
500
|
+
works today, per-checkout state under <code>.entropy-machines/</code>
|
|
501
|
+
(gitignored). Cost: not shared across machines, no UI beyond
|
|
502
|
+
<code>bin/tracker show</code>.<br>
|
|
503
|
+
<strong>Option B</strong> — point <code>tracker.command.bin</code> at
|
|
504
|
+
an adapter for a tracker you already run. Cost: someone has to write
|
|
505
|
+
and maintain that adapter, and get held/gated right, or claimable work
|
|
506
|
+
is reported wrong.<br>
|
|
507
|
+
I'd take <strong>A</strong> to start — nothing about the workflow
|
|
508
|
+
depends on which backend answers <code>bin/tracker ready</code>, so
|
|
509
|
+
migrating later costs nothing you wouldn't pay anyway. File-backed, or
|
|
510
|
+
point at something else?
|
|
511
|
+
</div>
|
|
512
|
+
<textarea placeholder="Type your answer…"></textarea>
|
|
513
|
+
</div>
|
|
514
|
+
</section>
|
|
515
|
+
|
|
516
|
+
<section class="page" id="p5">
|
|
517
|
+
<h1>Does verified work land on your say-so every time, or on the verifier's word?</h1>
|
|
518
|
+
<p class="sub">Not a config key — a policy for how the orchestrator behaves.</p>
|
|
519
|
+
|
|
520
|
+
<p>The two doctrine docs phrase this slightly differently.
|
|
521
|
+
<code>doctrine/WORKFLOW.md</code> ties your say-so specifically to
|
|
522
|
+
<em>held</em> work: "Nothing is folded while work is HELD. The owner says
|
|
523
|
+
when." <code>README.md</code>'s summary states it more broadly: "Nothing
|
|
524
|
+
lands eagerly. Finished work is held until the owner says merge." Read
|
|
525
|
+
literally, the first lets the orchestrator fold anything verified and not
|
|
526
|
+
explicitly held, on its own judgment; the second reads as every fold
|
|
527
|
+
waiting on you. Worth ruling on explicitly rather than each orchestrator
|
|
528
|
+
session guessing which reading you meant.</p>
|
|
529
|
+
|
|
530
|
+
<div class="response" data-resp="prd001-q4-landing-policy">
|
|
531
|
+
<label>Landing policy</label>
|
|
532
|
+
<div class="discuss">
|
|
533
|
+
<strong>Option A</strong> — owner-gated: the orchestrator checks in
|
|
534
|
+
with you before every fold, even something verified and not held.
|
|
535
|
+
Cost: a check-in step per issue or per sprint; slower cadence, but
|
|
536
|
+
nothing lands without you seeing it first.<br>
|
|
537
|
+
<strong>Option B</strong> — verifier-gated: the orchestrator folds
|
|
538
|
+
automatically once the verifier reports healthy and the issue isn't
|
|
539
|
+
held; you brake specific issues with <code>heldWhy</code>. Cost:
|
|
540
|
+
faster, but you only get asked when the orchestrator is unsure —
|
|
541
|
+
you have to actually use <code>heldWhy</code> proactively.<br>
|
|
542
|
+
I'd take <strong>A</strong> for a new project — until you've seen a
|
|
543
|
+
few sprints go cleanly and trust the verifier's sweep and your own
|
|
544
|
+
review habits, then reassess toward B. Which do you want?
|
|
545
|
+
</div>
|
|
546
|
+
<textarea placeholder="Type your answer…"></textarea>
|
|
547
|
+
</div>
|
|
548
|
+
</section>
|
|
549
|
+
|
|
550
|
+
<section class="page" id="p6">
|
|
551
|
+
<h1>What turns this from orientation into a project?</h1>
|
|
552
|
+
<p class="sub">The one question above that isn't config at all.</p>
|
|
553
|
+
|
|
554
|
+
<p>Everything on the previous four pages configures the harness. None of
|
|
555
|
+
it is a real feature or bug fixed yet. This is the question whose answer
|
|
556
|
+
becomes the first issue dispatched to an actual worker.</p>
|
|
557
|
+
|
|
558
|
+
<div class="response" data-resp="prd001-q5-first-work">
|
|
559
|
+
<label>First real issue</label>
|
|
560
|
+
<div class="discuss">
|
|
561
|
+
<strong>Option A</strong> — dispatch a verification issue first:
|
|
562
|
+
confirm the "What we found" page for real and finish
|
|
563
|
+
<code>config.json</code> (suites, generate, worktree links). Cost:
|
|
564
|
+
one dispatch cycle spent on bookkeeping before touching a real feature
|
|
565
|
+
or bug.<br>
|
|
566
|
+
<strong>Option B</strong> — skip straight to a real feature or bug you
|
|
567
|
+
already know needs doing, and patch <code>config.json</code>'s gaps
|
|
568
|
+
opportunistically as a worker or verifier hits them. Cost: your first
|
|
569
|
+
few dispatches run against unverified suites and paths, so a failure
|
|
570
|
+
might be the harness's config rather than the actual work.<br>
|
|
571
|
+
I'd take <strong>A</strong> if anything on the "What we found" page is
|
|
572
|
+
marked unverified; <strong>B</strong> if you already trust your own
|
|
573
|
+
toolchain commands and just want to see the loop move. What's the
|
|
574
|
+
first real issue, and which order?
|
|
575
|
+
</div>
|
|
576
|
+
<textarea placeholder="Type your answer…"></textarea>
|
|
577
|
+
</div>
|
|
578
|
+
</section>
|
|
579
|
+
|
|
580
|
+
<section class="page" id="p7">
|
|
581
|
+
<h1>Issues this PRD creates</h1>
|
|
582
|
+
<p class="sub">Your five answers above, filed. Literal commands — the
|
|
583
|
+
tracker has six operations, no <code>add</code>; an issue is created
|
|
584
|
+
with <code>set</code>, which auto-vivifies
|
|
585
|
+
(<code>docs/TRACKER-ADAPTER.md</code>).</p>
|
|
586
|
+
|
|
587
|
+
<pre><code>bin/tracker set i-suites-policy \
|
|
588
|
+
status=notstarted \
|
|
589
|
+
title="Configure config.json suites per PRD-001 Q1" \
|
|
590
|
+
effort=S
|
|
591
|
+
|
|
592
|
+
bin/tracker set i-protected-paths \
|
|
593
|
+
status=notstarted \
|
|
594
|
+
title="Set project.protectedPaths per PRD-001 Q2" \
|
|
595
|
+
effort=S
|
|
596
|
+
|
|
597
|
+
# Q3 answered above but not yet DONE until you (or the worker) actually
|
|
598
|
+
# write it into config.json -- filed the same as any other issue.
|
|
599
|
+
bin/tracker set i-tracker-backend \
|
|
600
|
+
status=notstarted \
|
|
601
|
+
title="Configure tracker.backend per PRD-001 Q3" \
|
|
602
|
+
effort=S
|
|
603
|
+
|
|
604
|
+
bin/tracker set i-landing-policy \
|
|
605
|
+
status=notstarted \
|
|
606
|
+
title="Record landing policy per PRD-001 Q4" \
|
|
607
|
+
effort=S
|
|
608
|
+
|
|
609
|
+
# Q5's answer names your actual first issue -- write its own `set` once
|
|
610
|
+
# you've picked it; there's no generic id to hand you here.
|
|
611
|
+
|
|
612
|
+
bin/tracker remember --issue q-1 \
|
|
613
|
+
"Filed i-suites-policy, i-protected-paths, i-tracker-backend, i-landing-policy from PRD-001-orientation."
|
|
614
|
+
bin/tracker set q-1 status=done</code></pre>
|
|
615
|
+
|
|
616
|
+
<p>If instead you're still genuinely undecided on one of these — not
|
|
617
|
+
"haven't gotten to it," actually undecided — file it <em>gated</em>
|
|
618
|
+
instead of plain <code>notstarted</code>, so <code>ready</code> holds it
|
|
619
|
+
back until you clear the gate:</p>
|
|
620
|
+
|
|
621
|
+
<pre><code>bin/tracker set i-tracker-backend \
|
|
622
|
+
status=notstarted \
|
|
623
|
+
title="Decide tracker.backend per PRD-001 Q3" \
|
|
624
|
+
gate="PRD-001#q3"</code></pre>
|
|
625
|
+
|
|
626
|
+
<p>Both forms run for real, against a throwaway project, while this page
|
|
627
|
+
was written:</p>
|
|
628
|
+
|
|
629
|
+
<pre><code>$ bin/tracker ready
|
|
630
|
+
{"blockedBy": [], "effort": "S", "id": "i-landing-policy", "status": "notstarted", "title": "..."}
|
|
631
|
+
{"blockedBy": [], "effort": "S", "id": "i-protected-paths", "status": "notstarted", "title": "..."}
|
|
632
|
+
{"blockedBy": [], "effort": "S", "id": "i-suites-policy", "status": "notstarted", "title": "..."}
|
|
633
|
+
# i-tracker-backend (filed gated) does not appear. q-1 (filed done) does not
|
|
634
|
+
# appear either -- ready means claimable, not "exists."
|
|
635
|
+
|
|
636
|
+
$ bin/tracker set i-tracker-backend status=held
|
|
637
|
+
tracker-file: REFUSED — unknown status 'held'. Known: done, notstarted, progress.
|
|
638
|
+
held and gated are not status values — set heldWhy=<reason> or gate=<handle> instead.</code></pre>
|
|
639
|
+
|
|
640
|
+
<div class="callout good"><span class="tag">already done for you</span>
|
|
641
|
+
<code>q-1</code>, "Orient the factory to this repo," is already filed
|
|
642
|
+
and was claimable before you opened this page —
|
|
643
|
+
<code>bin/tracker ready</code> shows it from the moment
|
|
644
|
+
<code>bin/init</code> ran. Marking it <code>status=done</code> above is
|
|
645
|
+
what closes the loop this PRD opened: a PRD creates issues, and one of
|
|
646
|
+
them was always going to be "answer this PRD."
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<p>Next: <code>bin/dispatch <id> --files "…" --brief "…"</code> on
|
|
650
|
+
whichever of the above is both <code>ready</code> and next in the order you
|
|
651
|
+
gave in Q5 — see <code>docs/AGENT-QUICKSTART.md</code> for one full walkthrough of
|
|
652
|
+
dispatch through handoff, start to finish, on a real example.</p>
|
|
653
|
+
</section>
|
|
654
|
+
|
|
655
|
+
</main>
|
|
656
|
+
|
|
657
|
+
<div class="savebar">
|
|
658
|
+
<span class="stat" id="saveStat">Unsaved changes…</span>
|
|
659
|
+
<button class="ghost" id="clearBtn" title="Clear all responses">✕</button>
|
|
660
|
+
<button id="saveBtn">💾 Save</button>
|
|
661
|
+
</div>
|
|
662
|
+
<script type="application/json" id="responses-data">{}</script>
|
|
663
|
+
<script>
|
|
664
|
+
(function(){
|
|
665
|
+
var links=[].slice.call(document.querySelectorAll('nav a[data-page]'));
|
|
666
|
+
function show(id){
|
|
667
|
+
document.querySelectorAll('.page').forEach(function(p){ p.classList.toggle('cur', p.id===id); });
|
|
668
|
+
links.forEach(function(l){ l.classList.toggle('cur', l.dataset.page===id); });
|
|
669
|
+
try{ history.replaceState(null,'','#'+id); }catch(e){}
|
|
670
|
+
window.scrollTo(0,0);
|
|
671
|
+
document.querySelectorAll('#'+id+' .response textarea').forEach(function(t){ autoGrow(t); });
|
|
672
|
+
}
|
|
673
|
+
links.forEach(function(l){ l.addEventListener('click', function(ev){ ev.preventDefault(); show(l.dataset.page); }); });
|
|
674
|
+
if (location.hash && document.getElementById(location.hash.slice(1))) show(location.hash.slice(1));
|
|
675
|
+
// Required: without this, a deep link (#p2) does nothing when the tab is ALREADY
|
|
676
|
+
// open — the URL changes but the reader stays on page 0. show() uses
|
|
677
|
+
// history.replaceState, which does not fire hashchange, so there's no loop.
|
|
678
|
+
window.addEventListener('hashchange', function(){ var h=location.hash.slice(1); if(h && document.getElementById(h)) show(h); });
|
|
679
|
+
var KEY='PRD-001-orientation'; // CUSTOMIZE: unique per doc so localStorage never collides across docs
|
|
680
|
+
var boxes=[].slice.call(document.querySelectorAll('.response'));
|
|
681
|
+
var stat=document.getElementById('saveStat');
|
|
682
|
+
var dataEl=document.getElementById('responses-data');
|
|
683
|
+
var fileHandle=null;
|
|
684
|
+
function ta(b){return b.querySelector('textarea');}
|
|
685
|
+
function key(b){return b.getAttribute('data-resp');}
|
|
686
|
+
function markFilled(b){ if(ta(b).value.trim()) b.classList.add('filled'); else b.classList.remove('filled'); }
|
|
687
|
+
function autoGrow(el){ el.style.height='auto'; el.style.height=el.scrollHeight+'px'; }
|
|
688
|
+
function load(){
|
|
689
|
+
var saved={};
|
|
690
|
+
try{ saved=JSON.parse(dataEl.textContent||'{}')||{}; }catch(e){}
|
|
691
|
+
try{ var ls=JSON.parse(localStorage.getItem(KEY)||'{}'); Object.keys(ls).forEach(function(k){ if(ls[k]&&ls[k].trim()) saved[k]=ls[k]; }); }catch(e){}
|
|
692
|
+
boxes.forEach(function(b){ var v=saved[key(b)]; if(typeof v==='string') ta(b).value=v; markFilled(b); autoGrow(ta(b)); });
|
|
693
|
+
}
|
|
694
|
+
function collect(){ var o={}; boxes.forEach(function(b){ o[key(b)]=ta(b).value; }); return o; }
|
|
695
|
+
function autosave(){ try{ localStorage.setItem(KEY,JSON.stringify(collect())); }catch(e){} }
|
|
696
|
+
document.getElementById('saveBtn').addEventListener('click',async function(){
|
|
697
|
+
dataEl.textContent=JSON.stringify(collect(),null,2);
|
|
698
|
+
boxes.forEach(function(b){ ta(b).textContent=ta(b).value; });
|
|
699
|
+
var html='<!doctype html>\n'+document.documentElement.outerHTML;
|
|
700
|
+
if(window.showSaveFilePicker){
|
|
701
|
+
try{
|
|
702
|
+
if(!fileHandle) fileHandle=await window.showSaveFilePicker({suggestedName:'PRD-001-orientation.html',types:[{description:'HTML',accept:{'text/html':['.html']}}]});
|
|
703
|
+
var w=await fileHandle.createWritable(); await w.write(html); await w.close();
|
|
704
|
+
stat.textContent='Saved to file'; return;
|
|
705
|
+
}catch(err){ if(err&&err.name==='AbortError'){stat.textContent='Save cancelled';return;} }
|
|
706
|
+
}
|
|
707
|
+
var blob=new Blob([html],{type:'text/html'}); var a=document.createElement('a');
|
|
708
|
+
a.href=URL.createObjectURL(blob); a.download='PRD-001-orientation.html';
|
|
709
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
710
|
+
stat.textContent='Downloaded — overwrite the original';
|
|
711
|
+
});
|
|
712
|
+
document.getElementById('clearBtn').addEventListener('click',function(){
|
|
713
|
+
if(!confirm('Clear all responses?'))return;
|
|
714
|
+
boxes.forEach(function(b){ ta(b).value=''; markFilled(b); autoGrow(ta(b)); });
|
|
715
|
+
try{ localStorage.removeItem(KEY); }catch(e){}
|
|
716
|
+
dataEl.textContent='{}'; stat.textContent='Cleared';
|
|
717
|
+
});
|
|
718
|
+
boxes.forEach(function(b){ ta(b).addEventListener('input',function(){ markFilled(b); autoGrow(ta(b)); autosave(); stat.textContent='Unsaved changes…'; }); });
|
|
719
|
+
load();
|
|
720
|
+
window.addEventListener('resize',function(){ boxes.forEach(function(b){ autoGrow(ta(b)); }); });
|
|
721
|
+
})();
|
|
722
|
+
</script>
|
|
723
|
+
<script id="__disk-save-patch" data-ds="2">
|
|
724
|
+
(function(){
|
|
725
|
+
var btn=document.getElementById('saveBtn');
|
|
726
|
+
var stat=document.getElementById('saveStat');
|
|
727
|
+
if(!btn) return;
|
|
728
|
+
var b=btn.cloneNode(true); btn.parentNode.replaceChild(b, btn);
|
|
729
|
+
function collect(){ var o={}; document.querySelectorAll('.response[data-resp]').forEach(function(el){ var t=el.querySelector('textarea'); if(t) o[el.getAttribute('data-resp')]=t.value; }); return o; }
|
|
730
|
+
var file=(location.pathname.split('/').pop()||'').split('?')[0];
|
|
731
|
+
b.addEventListener('click', async function(){
|
|
732
|
+
var data=collect();
|
|
733
|
+
if(stat) stat.textContent='Saving to disk…';
|
|
734
|
+
try{
|
|
735
|
+
var res=await fetch('/__save?file='+encodeURIComponent(file),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
|
|
736
|
+
if(res.ok){
|
|
737
|
+
if(stat) stat.textContent='Saved to disk ✓';
|
|
738
|
+
// Re-baseline the page's idea of what is on disk. Without this,
|
|
739
|
+
// responses-data still holds what the doc was SERVED with, so anything
|
|
740
|
+
// comparing the boxes against it reads every saved answer as an unsaved
|
|
741
|
+
// edit for the rest of the session — which made the live-reload patch
|
|
742
|
+
// refuse to auto-reload and tell the reader to 💾 something they had
|
|
743
|
+
// just saved.
|
|
744
|
+
try{
|
|
745
|
+
var el=document.getElementById('responses-data');
|
|
746
|
+
if(el) el.textContent=JSON.stringify(data,null,2);
|
|
747
|
+
}catch(_){}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if(stat) stat.textContent='Save failed ('+res.status+')';
|
|
751
|
+
}catch(e){
|
|
752
|
+
if(stat) stat.textContent='Server offline — downloaded a copy (start serve.py to save in place)';
|
|
753
|
+
try{
|
|
754
|
+
var dataEl=document.getElementById('responses-data');
|
|
755
|
+
if(dataEl) dataEl.textContent=JSON.stringify(data,null,2);
|
|
756
|
+
document.querySelectorAll('.response[data-resp] textarea').forEach(function(t){ t.textContent=t.value; });
|
|
757
|
+
var htmlOut='<!doctype html>\n'+document.documentElement.outerHTML;
|
|
758
|
+
var a=document.createElement('a');
|
|
759
|
+
a.href=URL.createObjectURL(new Blob([htmlOut],{type:'text/html'}));
|
|
760
|
+
a.download=file; document.body.appendChild(a); a.click(); a.remove();
|
|
761
|
+
}catch(_){}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
})();
|
|
765
|
+
</script>
|
|
766
|
+
<style id="__todobanner-css">
|
|
767
|
+
/* Palette-aware. The first version hardcoded #fdf6e3 and pill radii, which
|
|
768
|
+
the 2026-08-07 doc standard forbids (rule 1: separate with BORDERS, never
|
|
769
|
+
background tints; no filled badges). Vars fall back for docs still on the
|
|
770
|
+
old sheet. */
|
|
771
|
+
#__todobar{ position:sticky; top:0; z-index:40; margin:0 0 1.1rem;
|
|
772
|
+
border:2px solid var(--caution,#a16207); background:var(--bg,#fff);
|
|
773
|
+
color:var(--caution,#a16207); padding:.6rem .85rem; font-size:var(--fs-base,.9rem); }
|
|
774
|
+
#__todobar[hidden]{ display:none; }
|
|
775
|
+
#__todobar .hd{ font-weight:700; text-transform:uppercase; letter-spacing:.04em;
|
|
776
|
+
font-size:var(--fs-sm,.72rem); display:block; margin-bottom:.35rem; }
|
|
777
|
+
#__todobar a{ display:inline-block; font-weight:700; color:var(--caution,#a16207);
|
|
778
|
+
background:none; border:1px solid currentColor; padding:.14rem .6rem;
|
|
779
|
+
margin:.16rem .3rem .16rem 0; text-decoration:none; }
|
|
780
|
+
#__todobar a:hover{ outline:2px solid var(--focus,#006BBD); outline-offset:1px; }
|
|
781
|
+
</style>
|
|
782
|
+
<script id="__todobanner-patch" data-tb="2">
|
|
783
|
+
// An in-page "these need you" bar. Same source of truth as the nav marks —
|
|
784
|
+
// __janusBoxes.pendingIn (see __boxstate-patch). Clicking a chip switches to
|
|
785
|
+
// that page via the existing nav link, so it works with the doc's own router.
|
|
786
|
+
//
|
|
787
|
+
// v2: a row-note and a follow-up the conversation ran past are no longer
|
|
788
|
+
// "answers needed". v1 counted every empty textarea, so a doc with per-row
|
|
789
|
+
// note slots showed a permanent demand the owner could never clear.
|
|
790
|
+
(function(){
|
|
791
|
+
var main=document.querySelector('main'); if(!main) return;
|
|
792
|
+
var bar=document.createElement('div'); bar.id='__todobar'; bar.hidden=true;
|
|
793
|
+
main.insertBefore(bar, main.firstChild);
|
|
794
|
+
function label(link){
|
|
795
|
+
// Clone and drop the navmark counter first — reading link.textContent raw
|
|
796
|
+
// concatenates "0/1" onto the title and no amount of regex untangles
|
|
797
|
+
// "0/10 · What is actually open" reliably.
|
|
798
|
+
var c=link.cloneNode(true);
|
|
799
|
+
var m=c.querySelector('.navmark'); if(m) m.remove();
|
|
800
|
+
var t=(c.textContent||'').replace(/\s+/g,' ').trim();
|
|
801
|
+
return t.replace(/^\d+\s*·\s*/,'').replace(/\s*⚑\s*$/,'').trim() || link.dataset.page;
|
|
802
|
+
}
|
|
803
|
+
function refresh(){
|
|
804
|
+
var pending=[];
|
|
805
|
+
[].slice.call(document.querySelectorAll('nav a[data-page]')).forEach(function(link){
|
|
806
|
+
var page=document.getElementById(link.dataset.page); if(!page) return;
|
|
807
|
+
if(!window.__janusBoxes) return; // helper not loaded yet — deferred pass runs
|
|
808
|
+
var n=window.__janusBoxes.pendingIn(page).length;
|
|
809
|
+
if(n) pending.push({id:link.dataset.page, n:n, text:label(link)});
|
|
810
|
+
});
|
|
811
|
+
if(!pending.length){ bar.hidden=true; bar.innerHTML=''; return; }
|
|
812
|
+
var total=pending.reduce(function(a,p){ return a+p.n; },0);
|
|
813
|
+
bar.hidden=false;
|
|
814
|
+
bar.innerHTML='<span class="hd">'+total+(total===1?' answer needed':' answers needed')+'</span>'+
|
|
815
|
+
pending.map(function(p){
|
|
816
|
+
return '<a href="#'+p.id+'" data-goto="'+p.id+'">'+p.text+(p.n>1?' ('+p.n+')':'')+'</a>';
|
|
817
|
+
}).join('');
|
|
818
|
+
}
|
|
819
|
+
bar.addEventListener('click', function(e){
|
|
820
|
+
var a=e.target.closest('a[data-goto]'); if(!a) return;
|
|
821
|
+
e.preventDefault();
|
|
822
|
+
var nav=document.querySelector('nav a[data-page="'+a.dataset.goto+'"]');
|
|
823
|
+
if(nav) nav.click();
|
|
824
|
+
// Focus the first box that actually WANTS an answer — not merely the first
|
|
825
|
+
// empty one, which on a doc with row-notes is a note slot nobody owes.
|
|
826
|
+
var page=document.getElementById(a.dataset.goto);
|
|
827
|
+
var want=(page && window.__janusBoxes) ? window.__janusBoxes.pendingIn(page) : [];
|
|
828
|
+
var target=(want[0] && want[0].querySelector('textarea')) ||
|
|
829
|
+
document.querySelector('#'+a.dataset.goto+' .response[data-resp] textarea');
|
|
830
|
+
if(target){ target.scrollIntoView({block:'center'}); target.focus(); }
|
|
831
|
+
});
|
|
832
|
+
document.querySelectorAll('.response[data-resp] textarea').forEach(function(t){
|
|
833
|
+
t.addEventListener('input', refresh);
|
|
834
|
+
});
|
|
835
|
+
document.addEventListener('click', function(e){
|
|
836
|
+
var id=e.target && e.target.id;
|
|
837
|
+
if(id==='saveBtn'||id==='clearBtn') setTimeout(refresh,60);
|
|
838
|
+
});
|
|
839
|
+
refresh();
|
|
840
|
+
// See the same two hooks in __navmark-patch: on an already-patched doc the
|
|
841
|
+
// boxstate helper is appended AFTER this block, so the parse-time pass above
|
|
842
|
+
// finds nothing and the deferred passes do the work.
|
|
843
|
+
document.addEventListener('DOMContentLoaded', refresh);
|
|
844
|
+
setTimeout(refresh, 0);
|
|
845
|
+
})();
|
|
846
|
+
</script>
|
|
847
|
+
<style id="__navmark-css">
|
|
848
|
+
nav a[data-page]{ overflow:hidden; }
|
|
849
|
+
nav a .navmark{ float:right; font-size:var(--fs-sm,12px); font-weight:700; line-height:1.5; margin-left:.4rem; }
|
|
850
|
+
nav a .navmark.done{ color:var(--good); }
|
|
851
|
+
nav a .navmark.todo{ color:var(--warn-deep); background:#fdf6e3; padding:.02rem .34rem; border-radius:99px; }
|
|
852
|
+
@media (prefers-color-scheme:dark){ nav a .navmark.todo{ background:#241d10; } }
|
|
853
|
+
nav .navsummary{ font-size:var(--fs-sm,12px); font-weight:700; color:var(--muted); margin:.5rem .4rem .2rem; padding:.3rem .55rem; border-radius:6px; background:var(--code-bg); }
|
|
854
|
+
nav .navsummary .all-done{ color:var(--good); }
|
|
855
|
+
</style>
|
|
856
|
+
<script id="__navmark-patch" data-nm="2">
|
|
857
|
+
// Live per-section review state in the sidebar. Independent of the main doc
|
|
858
|
+
// script — listens to input/save/clear.
|
|
859
|
+
//
|
|
860
|
+
// v2: counts come from __janusBoxes (see __boxstate-patch), so a row-note and
|
|
861
|
+
// a follow-up the conversation ran past no longer read as an unanswered
|
|
862
|
+
// question. v1 counted every empty textarea and told the owner a finished doc
|
|
863
|
+
// still needed him.
|
|
864
|
+
(function(){
|
|
865
|
+
var links=[].slice.call(document.querySelectorAll('nav a[data-page]'));
|
|
866
|
+
function ensure(link){ var m=link.querySelector('.navmark'); if(!m){ m=document.createElement('span'); m.className='navmark'; link.insertBefore(m, link.firstChild); } return m; }
|
|
867
|
+
var summary=null;
|
|
868
|
+
var foot=document.querySelector('nav .foot');
|
|
869
|
+
if(foot){ summary=document.createElement('div'); summary.className='navsummary'; foot.parentNode.insertBefore(summary, foot); }
|
|
870
|
+
function refresh(){
|
|
871
|
+
var done=0, withBoxes=0;
|
|
872
|
+
links.forEach(function(link){
|
|
873
|
+
var page=document.getElementById(link.dataset.page);
|
|
874
|
+
if(!page) return;
|
|
875
|
+
var c=(window.__janusBoxes ? window.__janusBoxes.countsIn(page) : null);
|
|
876
|
+
if(!c){ return; } // helper not loaded yet — the deferred pass will run
|
|
877
|
+
var m=link.querySelector('.navmark');
|
|
878
|
+
if(c.total===0){ if(m) m.parentNode.removeChild(m); return; }
|
|
879
|
+
withBoxes++;
|
|
880
|
+
m=ensure(link);
|
|
881
|
+
// A page with nothing PENDING is done even when a tail follow-up sits
|
|
882
|
+
// empty — that box is answered-past, not owed.
|
|
883
|
+
if(c.pending===0){ m.textContent='✓'; m.className='navmark done'; done++; }
|
|
884
|
+
else { m.textContent=c.filled+'/'+c.total; m.className='navmark todo'; }
|
|
885
|
+
});
|
|
886
|
+
if(summary){
|
|
887
|
+
if(!withBoxes){ summary.textContent=''; }
|
|
888
|
+
else if(done===withBoxes){ summary.innerHTML='<span class="all-done">✓ all '+withBoxes+' sections answered</span>'; }
|
|
889
|
+
else { summary.textContent='Answered '+done+'/'+withBoxes+' sections'; }
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
document.querySelectorAll('.response[data-resp] textarea').forEach(function(t){ t.addEventListener('input', refresh); });
|
|
893
|
+
document.addEventListener('click', function(e){ var id=e.target && e.target.id; if(id==='saveBtn'||id==='clearBtn'){ setTimeout(refresh,60); } });
|
|
894
|
+
refresh();
|
|
895
|
+
// An ALREADY-PATCHED doc gets __boxstate-patch appended after this block, so
|
|
896
|
+
// the helper does not exist during the parse-time pass above. Both hooks are
|
|
897
|
+
// cheap and idempotent.
|
|
898
|
+
document.addEventListener('DOMContentLoaded', refresh);
|
|
899
|
+
setTimeout(refresh, 0);
|
|
900
|
+
})();
|
|
901
|
+
</script>
|
|
902
|
+
<style id="__trackernav-css">
|
|
903
|
+
/* Borders, never tints (2026-08-07 doc standard). Vars fall back for docs
|
|
904
|
+
still on the old sheet. */
|
|
905
|
+
nav .tracknav{ display:flex; flex-wrap:wrap; gap:.35rem; margin:.45rem .4rem .6rem; }
|
|
906
|
+
nav .tracknav a{ font-size:var(--fs-sm,12px); font-weight:700; text-decoration:none;
|
|
907
|
+
color:var(--muted,#71717a); border:1px solid var(--border,#ddd6fe);
|
|
908
|
+
padding:.1rem .5rem; }
|
|
909
|
+
nav .tracknav a:hover{ color:var(--accent,#7c3aed); border-color:currentColor; }
|
|
910
|
+
</style>
|
|
911
|
+
<script id="__trackernav-patch">
|
|
912
|
+
// A top-of-sidebar link to the issue tracker. ONE link, because TRACKER.html
|
|
913
|
+
// is the only generated page this harness has: `bin/tracker render` writes it
|
|
914
|
+
// into the docs directory next to this file, and bin/serve serves it from
|
|
915
|
+
// there. It used to carry four (DOCS / INDEX / WORKSTREAMS as well), inherited
|
|
916
|
+
// from the project this template came from, and the other three 404'd in every
|
|
917
|
+
// project ever initialised here — nothing in this harness generates them, and
|
|
918
|
+
// workstreams are not even a concept in its issue model. Do not add a link
|
|
919
|
+
// back until something writes the page it points at.
|
|
920
|
+
(function(){
|
|
921
|
+
var brand=document.querySelector('nav .brand'); if(!brand) return;
|
|
922
|
+
var row=document.createElement('div'); row.className='tracknav';
|
|
923
|
+
row.innerHTML='<a href="TRACKER.html">issues</a>';
|
|
924
|
+
brand.parentNode.insertBefore(row, brand.nextSibling);
|
|
925
|
+
})();
|
|
926
|
+
</script>
|
|
927
|
+
<style id="__livereload-css">
|
|
928
|
+
/* Borders, never tints (2026-08-07 doc standard). */
|
|
929
|
+
#__replybar{ position:fixed; left:50%; transform:translateX(-50%); bottom:14px; z-index:60;
|
|
930
|
+
display:flex; align-items:center; gap:.7rem; background:var(--bg,#fff);
|
|
931
|
+
border:2px solid var(--positive,#0A5C21); color:var(--positive,#0A5C21);
|
|
932
|
+
padding:.5rem .8rem; font-size:var(--fs-base,.95rem); font-weight:700; }
|
|
933
|
+
#__replybar[hidden]{ display:none; }
|
|
934
|
+
#__replybar button{ font: inherit; font-weight:700; cursor:pointer; color:inherit;
|
|
935
|
+
background:none; border:1px solid currentColor; padding:.2rem .7rem; }
|
|
936
|
+
#__replybar button:hover{ outline:2px solid var(--focus,#006BBD); outline-offset:1px; }
|
|
937
|
+
#__replybar .dismiss{ border:none; font-weight:400; }
|
|
938
|
+
</style>
|
|
939
|
+
<script id="__livereload-patch" data-lr="2">
|
|
940
|
+
// "I replied" signal. Without it the reader has no way to learn that a fold
|
|
941
|
+
// landed except being told in chat — which was the last manual link in the
|
|
942
|
+
// loop (owner, 2026-08-09: "otherwise I won't know that you've responded").
|
|
943
|
+
//
|
|
944
|
+
// Polls /__docversion for THIS file and watches the REVIEW SIGNATURE — a hash
|
|
945
|
+
// of the folded <aside class="review"> blocks — not the version.
|
|
946
|
+
//
|
|
947
|
+
// It watched the version first, and that was wrong in the most annoying
|
|
948
|
+
// possible way: a 💾 bumps the version, so saving your own answer announced
|
|
949
|
+
// "New reply — reload to read it" and the reload showed nothing new, because
|
|
950
|
+
// nothing was. Reported by the owner 2026-08-11 ("I never actually see replies
|
|
951
|
+
// upon reload"). The signature moves when a reply is added or rewritten and
|
|
952
|
+
// stays put for anything the reader does to their own boxes, which is the
|
|
953
|
+
// question the banner claims to answer.
|
|
954
|
+
//
|
|
955
|
+
// It will not reload over unsaved typing. Clean is defined as: every textarea
|
|
956
|
+
// matches the responses-data baked into the page at load. Clean -> reload on
|
|
957
|
+
// its own. Dirty -> show the bar and let the reader choose, so a half-typed
|
|
958
|
+
// answer is never destroyed by my fold landing at the wrong moment.
|
|
959
|
+
(function(){
|
|
960
|
+
var file=(location.pathname.split('/').pop()||'').split('?')[0];
|
|
961
|
+
if(!file) return;
|
|
962
|
+
var seen=null, stopped=false;
|
|
963
|
+
var bar=document.createElement('div');
|
|
964
|
+
bar.id='__replybar'; bar.hidden=true;
|
|
965
|
+
bar.innerHTML='<span id="__replytext">New reply — reload to read it</span>'+
|
|
966
|
+
'<button id="__replygo">Reload</button>'+
|
|
967
|
+
'<button class="dismiss" id="__replyx" title="Dismiss">✕</button>';
|
|
968
|
+
document.body.appendChild(bar);
|
|
969
|
+
bar.querySelector('#__replygo').addEventListener('click',function(){ location.reload(); });
|
|
970
|
+
bar.querySelector('#__replyx').addEventListener('click',function(){ bar.hidden=true; stopped=true; });
|
|
971
|
+
|
|
972
|
+
function baked(){
|
|
973
|
+
var el=document.getElementById('responses-data');
|
|
974
|
+
try{ return JSON.parse((el&&el.textContent)||'{}')||{}; }catch(e){ return {}; }
|
|
975
|
+
}
|
|
976
|
+
function dirty(){
|
|
977
|
+
var disk=baked(), out=false;
|
|
978
|
+
document.querySelectorAll('.response[data-resp]').forEach(function(el){
|
|
979
|
+
var t=el.querySelector('textarea'); if(!t) return;
|
|
980
|
+
if((t.value||'').trim() !== String(disk[el.getAttribute('data-resp')]||'').trim()) out=true;
|
|
981
|
+
});
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
function tick(){
|
|
985
|
+
if(stopped) return;
|
|
986
|
+
fetch('/__docversion?file='+encodeURIComponent(file),{cache:'no-store'})
|
|
987
|
+
.then(function(r){ return r.ok ? r.json() : null; })
|
|
988
|
+
.then(function(d){
|
|
989
|
+
if(!d || typeof d.reviews!=='string') return;
|
|
990
|
+
if(seen===null){ seen=d.reviews; return; }
|
|
991
|
+
if(d.reviews===seen) return;
|
|
992
|
+
seen=d.reviews; // announce a given reply once, not every 5s
|
|
993
|
+
// Reload only when nothing would be lost; otherwise hand the reader
|
|
994
|
+
// the choice. Either way the bar reports that a reply exists.
|
|
995
|
+
if(!dirty()){ location.reload(); return; }
|
|
996
|
+
document.getElementById('__replytext').textContent=
|
|
997
|
+
'New reply on disk — you have unsaved edits. 💾 first, then reload.';
|
|
998
|
+
bar.hidden=false;
|
|
999
|
+
})
|
|
1000
|
+
.catch(function(){ /* server gone: stay quiet, the doc still works */ });
|
|
1001
|
+
}
|
|
1002
|
+
setInterval(tick, 5000); tick();
|
|
1003
|
+
})();
|
|
1004
|
+
</script>
|
|
1005
|
+
<script id="__autosave-patch">
|
|
1006
|
+
// Typing survives a closed tab. It did not.
|
|
1007
|
+
//
|
|
1008
|
+
// The dialogue template autosaves to localStorage as you type;
|
|
1009
|
+
// REPORT-TEMPLATE.html never did, so a report collected answers only between
|
|
1010
|
+
// the moment you typed them and the moment you pressed 💾 — close the tab
|
|
1011
|
+
// first and they were gone with no trace anywhere. Injected rather than
|
|
1012
|
+
// templated so every doc already written gets it too.
|
|
1013
|
+
//
|
|
1014
|
+
// It only ever FILLS AN EMPTY BOX. Disk is the truth for anything saved
|
|
1015
|
+
// (__disk-wins-patch), and a stale browser copy overwriting a saved answer is
|
|
1016
|
+
// a failure this folder has already had once — four answers on 2026-08-05 came
|
|
1017
|
+
// back holding the text of four other boxes.
|
|
1018
|
+
(function(){
|
|
1019
|
+
var boxes=[].slice.call(document.querySelectorAll('.response[data-resp] textarea'));
|
|
1020
|
+
if(!boxes.length) return;
|
|
1021
|
+
var file=(location.pathname.split('/').pop()||'doc').split('?')[0];
|
|
1022
|
+
// "janus-" prefix is load-bearing: __disk-wins-patch heals the stored map by
|
|
1023
|
+
// looking up the first key with it.
|
|
1024
|
+
var KEY='janus-'+file;
|
|
1025
|
+
function keyOf(t){ return t.closest('.response').getAttribute('data-resp'); }
|
|
1026
|
+
try{
|
|
1027
|
+
var saved=JSON.parse(localStorage.getItem(KEY)||'{}')||{};
|
|
1028
|
+
boxes.forEach(function(t){
|
|
1029
|
+
var v=saved[keyOf(t)];
|
|
1030
|
+
if(!t.value.trim() && typeof v==='string' && v.trim()){
|
|
1031
|
+
t.value=v;
|
|
1032
|
+
t.closest('.response').classList.add('filled');
|
|
1033
|
+
t.style.height='auto'; t.style.height=t.scrollHeight+'px';
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
}catch(e){}
|
|
1037
|
+
function dump(){
|
|
1038
|
+
var o={}; boxes.forEach(function(t){ o[keyOf(t)]=t.value; });
|
|
1039
|
+
try{ localStorage.setItem(KEY, JSON.stringify(o)); }catch(e){}
|
|
1040
|
+
}
|
|
1041
|
+
boxes.forEach(function(t){ t.addEventListener('input', dump); });
|
|
1042
|
+
document.addEventListener('click', function(e){
|
|
1043
|
+
if(e.target && e.target.id==='clearBtn') setTimeout(dump, 60);
|
|
1044
|
+
});
|
|
1045
|
+
})();
|
|
1046
|
+
</script>
|
|
1047
|
+
<script id="__disk-wins-patch">
|
|
1048
|
+
/* Disk beats a stale localStorage for answers that are already SAVED.
|
|
1049
|
+
*
|
|
1050
|
+
* The template's load() does the opposite: it merges localStorage OVER the
|
|
1051
|
+
* on-disk responses-data, unconditionally. That cost us four answers on
|
|
1052
|
+
* 2026-08-05 — a1-goal-followup, a7-tools, a8-toolset and a9-render came back
|
|
1053
|
+
* holding the text of the LAST four boxes, and the next save wrote the shift
|
|
1054
|
+
* to disk as truth. Recovered from history/v015; the browser's localStorage
|
|
1055
|
+
* was later confirmed to still hold the corrupted values, so the tab stayed
|
|
1056
|
+
* armed to redo the damage on every subsequent save.
|
|
1057
|
+
*
|
|
1058
|
+
* localStorage exists to protect typing that has NOT been saved yet, and that
|
|
1059
|
+
* is all it should win. So: a key with a non-empty value on disk is restored
|
|
1060
|
+
* from disk; a key that is empty on disk keeps whatever the browser holds.
|
|
1061
|
+
* Then localStorage is rewritten from the corrected DOM, which HEALS the drift
|
|
1062
|
+
* instead of masking it — otherwise the bad map survives every reload.
|
|
1063
|
+
*
|
|
1064
|
+
* Runs after the template's own load() because it is appended at end-of-body.
|
|
1065
|
+
*/
|
|
1066
|
+
(function(){
|
|
1067
|
+
var dataEl=document.getElementById('responses-data');
|
|
1068
|
+
if(!dataEl) return;
|
|
1069
|
+
var disk={};
|
|
1070
|
+
try{ disk=JSON.parse(dataEl.textContent||'{}')||{}; }catch(e){ return; }
|
|
1071
|
+
var healed=[];
|
|
1072
|
+
document.querySelectorAll('.response[data-resp]').forEach(function(el){
|
|
1073
|
+
var t=el.querySelector('textarea'); if(!t) return;
|
|
1074
|
+
var k=el.getAttribute('data-resp'), v=disk[k];
|
|
1075
|
+
if(typeof v==='string' && v.trim() && t.value!==v){
|
|
1076
|
+
healed.push(k+' ('+t.value.length+'->'+v.length+' chars)');
|
|
1077
|
+
t.value=v;
|
|
1078
|
+
}
|
|
1079
|
+
if(t.value.trim()) el.classList.add('filled'); else el.classList.remove('filled');
|
|
1080
|
+
t.style.height='auto'; t.style.height=t.scrollHeight+'px';
|
|
1081
|
+
});
|
|
1082
|
+
if(healed.length){
|
|
1083
|
+
try{
|
|
1084
|
+
// Which stored map belongs to THIS doc. It used to be "the first key
|
|
1085
|
+
// starting with janus-", which was a guess: localStorage is per-ORIGIN,
|
|
1086
|
+
// so every doc served from 127.0.0.1:8787 shares one store, and the
|
|
1087
|
+
// first janus- key is usually some other document's answers — healing
|
|
1088
|
+
// would then overwrite THAT doc's unsaved typing with this doc's.
|
|
1089
|
+
// Ownership is now proved, not guessed: a map belongs to this doc if it
|
|
1090
|
+
// holds at least one of this doc's data-resp ids. __autosave-patch's own
|
|
1091
|
+
// key (janus-<file>) is always included.
|
|
1092
|
+
var mine={};
|
|
1093
|
+
document.querySelectorAll('.response[data-resp]').forEach(function(el){
|
|
1094
|
+
var t=el.querySelector('textarea');
|
|
1095
|
+
if(t) mine[el.getAttribute('data-resp')]=t.value;
|
|
1096
|
+
});
|
|
1097
|
+
var file=(location.pathname.split('/').pop()||'doc').split('?')[0];
|
|
1098
|
+
var targets=['janus-'+file];
|
|
1099
|
+
for(var i=0;i<localStorage.length;i++){
|
|
1100
|
+
var k2=localStorage.key(i);
|
|
1101
|
+
if(!k2 || k2.indexOf('janus-')!==0 || targets.indexOf(k2)>=0) continue;
|
|
1102
|
+
try{
|
|
1103
|
+
var m=JSON.parse(localStorage.getItem(k2)||'{}')||{};
|
|
1104
|
+
if(Object.keys(m).some(function(kk){ return kk in mine; })) targets.push(k2);
|
|
1105
|
+
}catch(_e){}
|
|
1106
|
+
}
|
|
1107
|
+
targets.forEach(function(k3){
|
|
1108
|
+
// Only rewrite ids the map already held, plus this doc's own key in
|
|
1109
|
+
// full — a shared-prefix map from another doc keeps its other entries.
|
|
1110
|
+
var out;
|
|
1111
|
+
if(k3==='janus-'+file){ out=mine; }
|
|
1112
|
+
else {
|
|
1113
|
+
try{ out=JSON.parse(localStorage.getItem(k3)||'{}')||{}; }catch(_e){ return; }
|
|
1114
|
+
Object.keys(out).forEach(function(kk){ if(kk in mine) out[kk]=mine[kk]; });
|
|
1115
|
+
}
|
|
1116
|
+
localStorage.setItem(k3,JSON.stringify(out));
|
|
1117
|
+
});
|
|
1118
|
+
}catch(e){}
|
|
1119
|
+
console.warn('[dialogue-doc] restored '+healed.length+
|
|
1120
|
+
' answer(s) from disk over a stale localStorage: '+healed.join(', '));
|
|
1121
|
+
}
|
|
1122
|
+
})();
|
|
1123
|
+
</script>
|
|
1124
|
+
<script id="__boxstate-patch" data-bs="1">
|
|
1125
|
+
// WHICH BOXES ACTUALLY WANT THE READER. One definition, used by the nav marks
|
|
1126
|
+
// and the todo bar, because they used to each count "every .response[data-resp]
|
|
1127
|
+
// with an empty textarea" and that count lied twice over:
|
|
1128
|
+
//
|
|
1129
|
+
// 1. A .mini row-note ("note on this row") is an INVITATION, never a demand.
|
|
1130
|
+
// Counting them left pages reading 0/7 forever.
|
|
1131
|
+
// 2. A follow-up box the conversation already ran PAST is not pending. Chains
|
|
1132
|
+
// read question -> answer -> my reply + follow-up -> answer -> ...; a gap
|
|
1133
|
+
// in the middle means the reader kept going, a gap at the END means they
|
|
1134
|
+
// have not answered yet.
|
|
1135
|
+
//
|
|
1136
|
+
// Both were reported by the owner against a doc that claimed four open
|
|
1137
|
+
// questions when every real question in it was answered ("uh.. everything is
|
|
1138
|
+
// checked off. what is open here?", 2026-08-23). Same rule as doctracker.py's
|
|
1139
|
+
// qstate/mark_superseded — if you change one, change the other.
|
|
1140
|
+
(function(){
|
|
1141
|
+
function chainOf(key){
|
|
1142
|
+
var stem=key, m;
|
|
1143
|
+
while((m=/^(.*)-followup\d*$/.exec(stem))) stem=m[1];
|
|
1144
|
+
return stem;
|
|
1145
|
+
}
|
|
1146
|
+
function all(){
|
|
1147
|
+
return [].slice.call(document.querySelectorAll('.response[data-resp]'));
|
|
1148
|
+
}
|
|
1149
|
+
// A box counts at all only if it is a real question box, not a row-note.
|
|
1150
|
+
function countable(box){ return !box.classList.contains('mini'); }
|
|
1151
|
+
function filled(box){
|
|
1152
|
+
var t=box.querySelector('textarea');
|
|
1153
|
+
return !!(t && t.value.trim());
|
|
1154
|
+
}
|
|
1155
|
+
// Answered later in the SAME chain, anywhere in the doc, in document order.
|
|
1156
|
+
function supersededMap(){
|
|
1157
|
+
var boxes=all(), seen={}, out={};
|
|
1158
|
+
for(var i=boxes.length-1;i>=0;i--){
|
|
1159
|
+
var key=boxes[i].getAttribute('data-resp'), ch=chainOf(key);
|
|
1160
|
+
out[key]=!!seen[ch];
|
|
1161
|
+
if(filled(boxes[i])) seen[ch]=true;
|
|
1162
|
+
}
|
|
1163
|
+
return out;
|
|
1164
|
+
}
|
|
1165
|
+
function pendingIn(page){
|
|
1166
|
+
var sup=supersededMap();
|
|
1167
|
+
return [].slice.call(page.querySelectorAll('.response[data-resp]'))
|
|
1168
|
+
.filter(function(b){
|
|
1169
|
+
return countable(b) && !filled(b) && !sup[b.getAttribute('data-resp')];
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
function countsIn(page){
|
|
1173
|
+
var boxes=[].slice.call(page.querySelectorAll('.response[data-resp]')).filter(countable);
|
|
1174
|
+
return {total:boxes.length, filled:boxes.filter(filled).length,
|
|
1175
|
+
pending:pendingIn(page).length};
|
|
1176
|
+
}
|
|
1177
|
+
window.__janusBoxes={chainOf:chainOf, pendingIn:pendingIn, countsIn:countsIn};
|
|
1178
|
+
})();
|
|
1179
|
+
</script>
|
|
1180
|
+
</body></html>
|