honestweek 0.2.0 → 0.3.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/README.md +252 -197
- package/SKILL.md +25 -74
- package/bin/honestweek.mjs +81 -24
- package/flows/client.md +19 -0
- package/flows/digest.md +11 -0
- package/flows/mine.md +18 -0
- package/flows/view.md +45 -0
- package/flows/weekly.md +29 -0
- package/lib/ask.mjs +1142 -0
- package/lib/build.mjs +12 -3
- package/lib/config-lookup.mjs +179 -0
- package/lib/config.mjs +20 -0
- package/lib/demo/week.mjs +3 -0
- package/lib/digest-carry.mjs +3 -2
- package/lib/digest-store.mjs +3 -2
- package/lib/digest.mjs +21 -14
- package/lib/discover.mjs +28 -22
- package/lib/emit/index.mjs +18 -6
- package/lib/harvest.mjs +19 -1
- package/lib/history.mjs +10 -2
- package/lib/init.mjs +241 -55
- package/lib/mine.mjs +18 -8
- package/lib/preview.mjs +66 -13
- package/lib/private-words.mjs +25 -5
- package/lib/problems/index.mjs +60 -7
- package/lib/prompt-lane.mjs +14 -13
- package/lib/prompt-store.mjs +2 -1
- package/lib/prompts.mjs +13 -7
- package/lib/replay/assemble.mjs +25 -8
- package/lib/replay/index.mjs +200 -12
- package/lib/replay/lookup.mjs +6 -3
- package/lib/replay/saved-sessions.mjs +315 -0
- package/lib/replay/views.mjs +12 -1
- package/lib/{view → replay}/word-index.mjs +3 -3
- package/lib/replay/words.mjs +122 -0
- package/lib/repo-identity.mjs +81 -13
- package/lib/saved/checks.mjs +381 -0
- package/lib/saved/history.mjs +235 -0
- package/lib/saved/saver.mjs +111 -0
- package/lib/saved/store.mjs +256 -0
- package/lib/status.mjs +288 -0
- package/lib/validate.mjs +12 -3
- package/lib/view/assets/common.css +2 -0
- package/lib/view/assets/common.js +20 -2
- package/lib/view/assets/problems.js +4 -3
- package/lib/view/assets/replay.js +3 -1
- package/lib/view/assets/search.js +1 -1
- package/lib/view/assets/sessions.js +1 -0
- package/lib/view/assets/settings.html +16 -0
- package/lib/view/assets/settings.js +108 -4
- package/lib/view/assets/setup.html +7 -0
- package/lib/view/assets/setup.js +9 -0
- package/lib/view/codex-judge.mjs +1 -1
- package/lib/view/data.mjs +160 -76
- package/lib/view/own-week.mjs +119 -0
- package/lib/view/page-link.mjs +84 -0
- package/lib/view/problems-route.mjs +29 -3
- package/lib/view/replay-export.mjs +1 -1
- package/lib/view/selftest/clickthrough.js +9 -5
- package/lib/view/server.mjs +3 -1
- package/lib/view/settings.mjs +102 -34
- package/lib/view/setup.mjs +27 -14
- package/lib/view/suggest-words.mjs +67 -0
- package/lib/view.mjs +82 -133
- package/lib/worktrees.mjs +31 -18
- package/package.json +2 -1
package/lib/validate.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
|
|
25
25
|
import { loadConfig } from './config.mjs';
|
|
26
|
+
import { commandConfig } from './config-lookup.mjs';
|
|
26
27
|
import { STATUSES } from './badges.mjs';
|
|
27
28
|
import { localDateInTimezone, resolveWeek } from './resolve-week.mjs';
|
|
28
29
|
import { loadValidatedPromptLane } from './prompt-lane.mjs';
|
|
@@ -172,14 +173,22 @@ export function validateItems(items, config, { noDashes = false } = {}) {
|
|
|
172
173
|
/**
|
|
173
174
|
* runValidate({ cwd, argv, io }) -> exit code (0 clean, 2 problems, 1 setup error).
|
|
174
175
|
*/
|
|
175
|
-
export async function runValidate({ cwd = process.cwd(), argv = [], now = new Date(), io = defaultIo(), promptLockHeld = false, transactionFs = {} } = {}) {
|
|
176
|
+
export async function runValidate({ cwd = process.cwd(), argv = [], now = new Date(), io = defaultIo(), promptLockHeld = false, transactionFs = {}, found } = {}) {
|
|
176
177
|
if (!promptLockHeld) {
|
|
178
|
+
// The config can sit elsewhere (--config, HONESTWEEK_CONFIG, the user-level file); every file
|
|
179
|
+
// validate reads or writes then sits beside it, never in an unrelated folder this ran in.
|
|
180
|
+
const opened = commandConfig({ command: 'validate', cwd, argv, err: io.err });
|
|
181
|
+
if (opened.error) {
|
|
182
|
+
io.err(`validate: ${opened.error}\n`);
|
|
183
|
+
return io.exit(1) ?? 1;
|
|
184
|
+
}
|
|
185
|
+
({ cwd, argv } = opened);
|
|
177
186
|
let code;
|
|
178
187
|
try {
|
|
179
188
|
assertNoDigestPending(cwd);
|
|
180
189
|
code = await withPromptLock(
|
|
181
190
|
cwd,
|
|
182
|
-
() => runValidate({ cwd, argv, now, io: { out: io.out, err: io.err, exit: (value) => value }, promptLockHeld: true, transactionFs }),
|
|
191
|
+
() => runValidate({ cwd, argv, now, io: { out: io.out, err: io.err, exit: (value) => value }, promptLockHeld: true, transactionFs, found: opened.found }),
|
|
183
192
|
{ ensureIgnored: false },
|
|
184
193
|
);
|
|
185
194
|
} catch (err) {
|
|
@@ -192,7 +201,7 @@ export async function runValidate({ cwd = process.cwd(), argv = [], now = new Da
|
|
|
192
201
|
|
|
193
202
|
let config;
|
|
194
203
|
try {
|
|
195
|
-
config = loadConfig(join(cwd, CONFIG_FILE));
|
|
204
|
+
config = loadConfig(found?.path ?? join(cwd, CONFIG_FILE));
|
|
196
205
|
} catch (err) {
|
|
197
206
|
io.err(`validate: ${err.message}\n`);
|
|
198
207
|
return io.exit(1) ?? 1;
|
|
@@ -732,6 +732,8 @@ details.legendbox > summary, details.catalog > summary { color: var(--ink-2); }
|
|
|
732
732
|
.setup { max-width: 760px; margin: 0 auto; padding: 24px 24px 8px; }
|
|
733
733
|
.setup h1 { font-size: 22px; font-weight: 600; margin: 0 0 8px; }
|
|
734
734
|
.setup .lead { color: var(--ink-2); margin: 0 0 20px; }
|
|
735
|
+
/* A folder path has no spaces to break at, so it breaks anywhere rather than run off a phone. */
|
|
736
|
+
.setup .fine { overflow-wrap: anywhere; }
|
|
735
737
|
.setup-part { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px 20px; margin: 0 0 16px; }
|
|
736
738
|
.setup-part h2 { font-size: 16px; font-weight: 600; margin: 0 0 8px; }
|
|
737
739
|
.setup-part .fine { font-size: 13px; color: var(--ink-2); margin: 6px 0; }
|
|
@@ -785,7 +785,7 @@
|
|
|
785
785
|
const line = r.ref?.line ?? r.line;
|
|
786
786
|
if (r.git || r.ref?.sha) return `<h3>${title}</h3><p>A git object: the engine checked it with git.</p>`;
|
|
787
787
|
const body = typeof r.body === 'string' ? r.body : r.record != null ? (typeof r.record === 'string' ? r.record : JSON.stringify(r.record, null, 2)) : '';
|
|
788
|
-
return `<h3>${title}</h3><p>${r.verified ? '<span class="verified">✓ re-read from disk, fingerprint matches</span>' : `<span class="notverified">✗ ${esc(r.note ?? 'not checked')}</span>`}${line ? ` · line ${esc(line)}` : ''}</p>${body ? `<pre>${esc(body)}</pre>` : ''}`;
|
|
788
|
+
return `<h3>${title}</h3><p>${r.verified ? '<span class="verified">✓ re-read from disk, fingerprint matches</span>' : r.saved ? `<span class="muted">${esc(r.note ?? 'saved')}</span>` : `<span class="notverified">✗ ${esc(r.note ?? 'not checked')}</span>`}${line ? ` · line ${esc(line)}` : ''}</p>${body ? `<pre>${esc(body)}</pre>` : ''}`;
|
|
789
789
|
};
|
|
790
790
|
const output = typeof a.output === 'string' ? a.output : typeof a.output?.text === 'string' ? a.output.text : null;
|
|
791
791
|
let html = '';
|
|
@@ -991,12 +991,30 @@
|
|
|
991
991
|
return `${own} ${help('What launched it?', why)}`;
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
/**
|
|
995
|
+
* A session that came back from saved results (honestweek.saved/): "Saved Oct 6, 2026, log gone",
|
|
996
|
+
* with when, by which version and what that means behind a "?" (or in a tooltip, `brief`).
|
|
997
|
+
* Nothing for any other session.
|
|
998
|
+
*/
|
|
999
|
+
function savedHtml(saved, { brief = false } = {}) {
|
|
1000
|
+
if (!saved || typeof saved !== 'object') return '';
|
|
1001
|
+
const t = toT(saved.at);
|
|
1002
|
+
const day = Number.isFinite(t) ? new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'an earlier run';
|
|
1003
|
+
const gone = saved.log === 'gone';
|
|
1004
|
+
const short = gone ? `Saved ${day}, log gone` : `Saved ${day}`;
|
|
1005
|
+
const by = typeof saved.by === 'string' ? ` by honestweek ${saved.by}` : '';
|
|
1006
|
+
const why = gone
|
|
1007
|
+
? `Saved on ${day}${by}, from a log that's no longer on disk. Its steps can't be checked against their log lines, and each keeps the evidence word it had then. Private text is never saved, so Show private text shows it redacted.`
|
|
1008
|
+
: `Saved on ${day}${by}.`;
|
|
1009
|
+
return brief ? `<span class="muted saved-tag" title="${esc(why)}">${esc(short)}</span>` : `<span class="muted saved-tag">${esc(short)}</span> ${help('About saved sessions', why)}`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
994
1012
|
/** Extra lines a page adds to a step's record panel, each a function of the step's id. */
|
|
995
1013
|
const stepNotes = [];
|
|
996
1014
|
const addStepNote = (fn) => stepNotes.push(fn);
|
|
997
1015
|
|
|
998
1016
|
window.HW = {
|
|
999
|
-
launchHref, sessionHref, startedByHtml, addStepNote, PROGRAM_START,
|
|
1017
|
+
launchHref, sessionHref, startedByHtml, savedHtml, addStepNote, PROGRAM_START,
|
|
1000
1018
|
esc, plural, pct, toT, num, lvl, time, dayOf, dur, windowText, windowShort, daySpan, friendlyDates, loadText, showLoad, showWindow, nothingIn, setTimezone, command,
|
|
1001
1019
|
shell, start, load, fail, fatal, setStatus, addNote, isId, ID, navCount, problemsSummary,
|
|
1002
1020
|
chip, chips: HWE.chips, sym: HWE.sym, ruleText, authorship, who, timeHow, timeMoved, memberLevel, kindLabel, lowerFirst, stripKind, summary, describeStep, describeStepPieces, clip, clipHtml, GROUP_LABEL,
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
const agentTag = (f) => {
|
|
127
127
|
const a = agentOf(f);
|
|
128
128
|
const repo = D.sessions?.[f.session]?.repo;
|
|
129
|
-
return `${a ? `<span class="agent">${esc(a)}</span>` : ''}${repo ? `<span class="repotag" title="Repository">${esc(repo)}</span>` : ''}`;
|
|
129
|
+
return `${a ? `<span class="agent">${esc(a)}</span>` : ''}${repo ? `<span class="repotag" title="Repository">${esc(repo)}</span>` : ''}${(HW.savedHtml ?? (() => ''))(D.sessions?.[f.session]?.saved, { brief: true })}`;
|
|
130
130
|
};
|
|
131
131
|
const timeHtml = (f) => (f.at ? `<time datetime="${esc(f.at)}">${esc(when(f.at))}</time>` : '<span class="muted">no time</span>');
|
|
132
132
|
// The plain past-tense headline says it happened, so it titles only a pattern found in the log;
|
|
@@ -206,6 +206,7 @@
|
|
|
206
206
|
: e?.skipped ? HW.friendlyDates(e.note ?? "The week before isn't loaded, so nothing is compared.")
|
|
207
207
|
: e?.partial || e?.loaded ? HW.friendlyDates(e.note ?? '')
|
|
208
208
|
: e && e.sessions === 0 ? `No logs in the ${plural(e.days ?? 0, 'day')} before, so no trend yet.`
|
|
209
|
+
: e?.saved ? 'Compared with the week before from its saved results.'
|
|
209
210
|
: '';
|
|
210
211
|
el.hidden = !text;
|
|
211
212
|
el.textContent = text;
|
|
@@ -595,7 +596,7 @@
|
|
|
595
596
|
<p><b>Measures.</b> ${(c.patterns ?? []).filter((id) => PAT.has(id)).map((id) => { const src = sourcesLink(PAT.get(id)); return `<a href="#${esc(id)}">${esc(titleOf(PAT.get(id)))}</a>${src ? ` <span class="nowrap">(${src})</span>` : ''}`; }).join('; ')}</p>
|
|
596
597
|
<p><b>How this is known.</b> ${esc(c.how)} Its findings are ${chip(c.evidence)} or stronger where marked.</p>
|
|
597
598
|
${numbersHtml(c)}
|
|
598
|
-
${c.ran ? `<p><b>Checked.</b> ${esc(c.checked)}</p>` : `<p><b>Not run.</b> ${esc(c.notRun ?? '')}</p>`}
|
|
599
|
+
${c.ran ? `<p><b>Checked.</b> ${esc(c.checked)}</p>` : c.saved ? `<p><b>From saved results.</b> ${esc(plural(num(c.saved.findings), 'finding'))} from when it checked the sessions that came back unread. It didn't run on the logs read this time.</p>` : `<p><b>Not run.</b> ${esc(c.notRun ?? '')}</p>`}
|
|
599
600
|
${c.stats?.length ? `<ul class="stgrid">${c.stats.map((s) => `<li><span class="sl">${esc(s.label)}</span> <span class="sv">${statVal(s)} ${s.evidence ? chip(s.evidence) : ''}</span></li>`).join('')}</ul>` : ''}
|
|
600
601
|
${used.length ? `<details class="rules"><summary>The ${used.length === 1 ? 'rule' : `${used.length} rules`} behind it</summary><dl>${used.map((id) => `<dt><code>${esc(id)}</code></dt><dd>${esc(rules.get(id))}${sourceOf.get(id) ? (sourceOf.get(id).covers ? ` For ${esc(sourceOf.get(id).covers)}, from ${sourceLink(sourceOf.get(id))}; the rest is ${OWN}.` : ` From ${sourceLink(sourceOf.get(id))}.`) : ''}</dd>`).join('')}</dl>${rulesBasis(used, sourceOf)}</details>` : ''}
|
|
601
602
|
</div>
|
|
@@ -612,7 +613,7 @@
|
|
|
612
613
|
<p><b>Which agent.</b> Each finding names the agent its session ran on. A card says "Partial on Codex" or "Not on Codex yet" (or the same for Claude Code) when its check reads that agent's logs only in part or not at all; with no such mark, the check runs on both.</p>
|
|
613
614
|
<p><b>The catalog.</b> ${esc(D.catalog?.about ?? '')} Its ${esc(full(D.catalog?.sources ?? 0))} sources were checked on ${esc(checked || 'the dates the catalog records')}.</p>
|
|
614
615
|
<p><b>Detection is from local logs only.</b> Nothing here calls out to the web or reads anything but the session files this page already read. "Not checked" means a pattern wasn't looked for, so it says nothing about whether it happened; "can't be checked from logs" means the catalog says a log can't show it.</p>
|
|
615
|
-
<p><b>What the counts cover.</b> ${esc(plural(num(cov.sessions), 'session'))} in your configured repositories with records in this window, ${esc(plural(num(cov.toolCalls), 'tool call'))}${cov.usageRecorded ? ` and ${esc(plural(num(cov.modelCalls), 'model call'))} with token counts (${tok(num(cov.tokens))} tokens in all)` : ', and no token counts, so the checks that measure tokens didn\'t run'}. Token estimates overlap between checks, so they don't add up to a total
|
|
616
|
+
<p><b>What the counts cover.</b> ${esc(plural(num(cov.sessions), 'session'))} in your configured repositories with records in this window, ${esc(plural(num(cov.toolCalls), 'tool call'))}${cov.usageRecorded ? ` and ${esc(plural(num(cov.modelCalls), 'model call'))} with token counts (${tok(num(cov.tokens))} tokens in all)` : ', and no token counts, so the checks that measure tokens didn\'t run'}. Token estimates overlap between checks, so they don't add up to a total.${num(cov.saved) > 0 ? ` Of these, ${esc(plural(num(cov.saved), 'session'))} came back from saved results unread, since their logs haven't changed or are gone: their findings are the ones saved with them, and what each check says it read counts only the sessions read this run.` : ''}</p>
|
|
616
617
|
<p><b>Fixes are for you to adopt.</b> Copy puts the catalog's draft on your clipboard. Nothing on this page changes your settings, hooks, skills or instructions.</p>`;
|
|
617
618
|
}
|
|
618
619
|
|
|
@@ -2023,7 +2023,9 @@
|
|
|
2023
2023
|
// What started the session shown, when a program did: the step that launched it, linked,
|
|
2024
2024
|
// or a "?" that says why none is named.
|
|
2025
2025
|
const shownKey = opened ? opened.a.session : multi ? null : sessions[0]?.key ?? null;
|
|
2026
|
-
|
|
2026
|
+
// A session shown from saved results says so, once, beside what started it.
|
|
2027
|
+
const savedRow = sessions.find((s) => s.saved && (!shownKey || s.key === shownKey)) ?? null;
|
|
2028
|
+
const started = [startedLine(shownKey, events, sessions), savedRow ? (HW.savedHtml ?? (() => ''))(savedRow.saved) : ''].filter(Boolean).join(' · ') || null;
|
|
2027
2029
|
$('startedBy').hidden = !started;
|
|
2028
2030
|
$('startedBy').innerHTML = started ?? '';
|
|
2029
2031
|
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
// ---- a reference: /api/lookup ------------------------------------------------------------
|
|
177
177
|
const PR_RE = /^(?:(?:[\w.-]+\/)?[\w.-]+#|pr\s*#?\s*|pull\/|#)(\d+)$/i;
|
|
178
178
|
const PR_URL = /github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)/i;
|
|
179
|
-
const isRef = (q) => PR_RE.test(q) || PR_URL.test(q) || /^[0-9a-f]{7,40}$/i.test(q) || /^(file|branch):/i.test(q) || (!/\s/.test(q) && (/[\\/]/.test(q) || /^[\w.-]+\.[a-z0-9]{1,6}$/i.test(q)));
|
|
179
|
+
const isRef = (q) => /^(pr|pull|commit|path):/i.test(q) || PR_RE.test(q) || PR_URL.test(q) || /^[0-9a-f]{7,40}$/i.test(q) || /^(file|branch):/i.test(q) || (!/\s/.test(q) && (/[\\/]/.test(q) || /^[\w.-]+\.[a-z0-9]{1,6}$/i.test(q)));
|
|
180
180
|
// A lookup answer: `kind`, the engine's reading of the reference in `parsed`, and on a restore by
|
|
181
181
|
// id the typed text in `query`.
|
|
182
182
|
function renderLookup(a) {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
esc(toolName(s.tool)),
|
|
36
36
|
s.repo ? esc(s.repo) : '',
|
|
37
37
|
GROUP_NOTE[s.group] ?? '',
|
|
38
|
+
(HW.savedHtml ?? (() => ''))(s.saved, { brief: true }),
|
|
38
39
|
opened ? HW.startedByHtml(opened, s.firstAt) : Number.isFinite(num(s.prompts)) ? `${plural(num(s.prompts), 'prompt')} ${sym(lvl(s.prompts, 'derived'))}` : '',
|
|
39
40
|
look,
|
|
40
41
|
].filter(Boolean).join(' · ');
|
|
@@ -68,6 +68,9 @@
|
|
|
68
68
|
<label for="terms" id="termsLabel"></label>
|
|
69
69
|
<input id="terms" type="text" autocomplete="off" spellcheck="false">
|
|
70
70
|
</div>
|
|
71
|
+
<button type="button" class="btn" id="suggestBtn">Suggest words from my sessions</button>
|
|
72
|
+
<p class="fine" id="suggestNote" role="status" hidden></p>
|
|
73
|
+
<ul class="repolist" id="suggestList" hidden></ul>
|
|
71
74
|
</details>
|
|
72
75
|
<section class="setup-part" aria-labelledby="goalsH">
|
|
73
76
|
<h2 id="goalsH"><label for="goals">Goal list</label> <span id="goalsHelp"></span></h2>
|
|
@@ -80,6 +83,19 @@
|
|
|
80
83
|
<h2 id="longH"><label for="longLimit">Long-session limit</label> <span id="longHelp"></span></h2>
|
|
81
84
|
<input class="setup-input" id="longLimit" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" placeholder="Off">
|
|
82
85
|
</section>
|
|
86
|
+
<section class="setup-part" aria-labelledby="saveH">
|
|
87
|
+
<h2 id="saveH"><label><input type="checkbox" id="saveOn" autocomplete="off"> Save results between runs</label> <span id="saveHelp"></span></h2>
|
|
88
|
+
<div class="setup-inline">
|
|
89
|
+
<label for="saveKeep">Keep saved days for</label>
|
|
90
|
+
<input id="saveKeep" type="number" min="1" max="36500" autocomplete="off">
|
|
91
|
+
<span>days</span>
|
|
92
|
+
</div>
|
|
93
|
+
<label class="fine"><input type="checkbox" id="saveHistory" autocomplete="off"> Each day's history too, for Replay</label>
|
|
94
|
+
<label class="fine"><input type="checkbox" id="saveOthers" autocomplete="off"> Also display-only and outside sessions' history</label>
|
|
95
|
+
<p class="fine muted" id="savedLine"></p>
|
|
96
|
+
<button type="button" class="btn" id="forgetBtn" hidden>Forget saved results</button>
|
|
97
|
+
<p class="fine" id="forgetNote" role="status" hidden></p>
|
|
98
|
+
</section>
|
|
83
99
|
<div class="setup-inline setup-actions">
|
|
84
100
|
<button type="button" class="btn" id="previewBtn">Review changes</button>
|
|
85
101
|
<button type="button" class="btn primary" id="saveBtn">Save</button>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The Settings page: change this folder's config later, without the terminal. How far back
|
|
2
2
|
// to look, the repositories and their roles, the emails, the private words, the goal list and
|
|
3
|
-
// whether the Problems page includes /insights,
|
|
3
|
+
// whether the Problems page includes /insights, its long-session limit, and whether results are
|
|
4
|
+
// saved between runs, for how long, with a button that deletes what's saved.
|
|
4
5
|
// "Review changes" asks the server what would change, in plain words; Save writes it and the
|
|
5
6
|
// same server reloads the week, so the next page shows the new settings.
|
|
6
7
|
//
|
|
@@ -29,8 +30,37 @@
|
|
|
29
30
|
goalsFile: $('goals').value.trim(),
|
|
30
31
|
insights: $('insights').checked,
|
|
31
32
|
longSessionTokens: tokensOf($('longLimit').value),
|
|
33
|
+
saveResults: { on: $('saveOn').checked, keepDays: daysOf($('saveKeep').value), history: $('saveHistory').checked, otherSessions: $('saveOthers').checked },
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
/** Days as typed: empty is the default (null), a whole number is itself, and anything else goes
|
|
37
|
+
* to the server as typed, which says what it takes. */
|
|
38
|
+
function daysOf(text) {
|
|
39
|
+
const t = String(text).trim();
|
|
40
|
+
if (!t) return null;
|
|
41
|
+
return /^\d+$/.test(t) ? Number(t) : t;
|
|
42
|
+
}
|
|
43
|
+
/** "12 days saved, 340 KB", or why nothing can be. */
|
|
44
|
+
function savedText(s) {
|
|
45
|
+
if (!s?.available) return s?.note ?? '';
|
|
46
|
+
if (!s.days && !s.bytes) return s.error ? `Nothing saved yet. The last save failed: ${s.error}.` : 'Nothing saved yet.';
|
|
47
|
+
const size = s.bytes >= 1048576 ? `${(s.bytes / 1048576).toFixed(1)} MB` : `${Math.max(1, Math.round(s.bytes / 1024))} KB`;
|
|
48
|
+
const line = `${s.days} ${s.days === 1 ? 'day' : 'days'} saved, ${size}.`;
|
|
49
|
+
return s.error ? `${line} The last save failed: ${s.error}.` : line;
|
|
50
|
+
}
|
|
51
|
+
/** "Forget saved results": one confirm, then the server deletes the whole folder. */
|
|
52
|
+
async function forget() {
|
|
53
|
+
if (!window.confirm("Delete every saved result? Days whose logs are gone can't be rebuilt.")) return;
|
|
54
|
+
$('forgetBtn').disabled = true;
|
|
55
|
+
const r = await F.call('POST', 'saved/forget', {});
|
|
56
|
+
$('forgetNote').hidden = false;
|
|
57
|
+
$('forgetNote').textContent = r.body?.message ?? `The server answered ${r.status}.`;
|
|
58
|
+
if (r.status === 200) {
|
|
59
|
+
$('savedLine').textContent = 'Nothing saved.';
|
|
60
|
+
$('forgetBtn').hidden = true;
|
|
61
|
+
}
|
|
62
|
+
$('forgetBtn').disabled = false;
|
|
63
|
+
}
|
|
34
64
|
/** A token count as typed: empty is off (null), "150k" and "150,000" are 150000, and anything
|
|
35
65
|
* else goes to the server as typed, which says what it takes. */
|
|
36
66
|
function tokensOf(text) {
|
|
@@ -64,7 +94,7 @@
|
|
|
64
94
|
}
|
|
65
95
|
const items = [...r.body.changes, ...(r.body.notes ?? [])];
|
|
66
96
|
if (!r.body.changes.length) {
|
|
67
|
-
list([]);
|
|
97
|
+
list(r.body.notes ?? []);
|
|
68
98
|
F.problem('Nothing changed.');
|
|
69
99
|
return;
|
|
70
100
|
}
|
|
@@ -76,7 +106,8 @@
|
|
|
76
106
|
F.problem('');
|
|
77
107
|
F.status('Saving.');
|
|
78
108
|
const r = await F.call('POST', 'settings/save', sent);
|
|
79
|
-
// A refusal, or nothing to change, writes
|
|
109
|
+
// A refusal, or nothing to change, writes no config (nothing to change may still add the
|
|
110
|
+
// config's .gitignore line): say so and leave Save on. A 409 (the file
|
|
80
111
|
// changed on disk, or this run can't edit it) needs a reload, so Save stays off.
|
|
81
112
|
if (r.status !== 200 || r.body.saved === false) {
|
|
82
113
|
F.status('');
|
|
@@ -86,9 +117,18 @@
|
|
|
86
117
|
}
|
|
87
118
|
if (r.body.restart) {
|
|
88
119
|
F.status(`Saved. ${r.body.restart}`, { bad: true });
|
|
120
|
+
if (r.body.warning) list([r.body.warning]);
|
|
89
121
|
document.body.dataset.ready = 'saved-restart';
|
|
90
122
|
return;
|
|
91
123
|
}
|
|
124
|
+
if (r.body.warning) {
|
|
125
|
+
// The config may be committed, or git couldn't be asked: say so before leaving the page.
|
|
126
|
+
F.status('Saved.');
|
|
127
|
+
list([r.body.warning]);
|
|
128
|
+
$('changes').append(F.el('li', {}, F.el('a', { href: 'problems.html', textContent: 'Load your week' })));
|
|
129
|
+
document.body.dataset.ready = 'saved-warning';
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
92
132
|
F.status('Saved. Loading your week.');
|
|
93
133
|
document.body.dataset.ready = 'saved';
|
|
94
134
|
location.href = 'problems.html';
|
|
@@ -131,6 +171,52 @@
|
|
|
131
171
|
say();
|
|
132
172
|
}
|
|
133
173
|
|
|
174
|
+
/** "Suggest words from my sessions": words in my prompts and session titles that look like
|
|
175
|
+
* names, each with buttons that add it to the names or the client words above. Nothing is
|
|
176
|
+
* saved until Save. */
|
|
177
|
+
async function suggest() {
|
|
178
|
+
const note = $('suggestNote');
|
|
179
|
+
const list = $('suggestList');
|
|
180
|
+
note.hidden = false;
|
|
181
|
+
note.textContent = 'Reading your prompts and session titles.';
|
|
182
|
+
list.hidden = true;
|
|
183
|
+
list.textContent = '';
|
|
184
|
+
const r = await F.call('GET', 'suggest-words');
|
|
185
|
+
if (r.status === 503) {
|
|
186
|
+
note.textContent = 'Your sessions are still loading. Try again in a moment.';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (r.status !== 200) {
|
|
190
|
+
note.textContent = r.body?.message ?? r.body?.error ?? `The server answered ${r.status}.`;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const typed = () => new Set([$('names').value, $('terms').value].join(',').split(',').map((w) => w.trim().toLowerCase()).filter(Boolean));
|
|
194
|
+
const words = r.body.words.filter((x) => !typed().has(x.word.toLowerCase()));
|
|
195
|
+
const left = () => list.querySelectorAll('li').length;
|
|
196
|
+
const say = () => {
|
|
197
|
+
note.textContent = left() ? 'Words in your prompts that look like names. Add the private ones, then save.' : 'No words to suggest.';
|
|
198
|
+
list.hidden = !left();
|
|
199
|
+
};
|
|
200
|
+
for (const x of words) {
|
|
201
|
+
const li = F.el('li', { className: 'reporow' });
|
|
202
|
+
const button = (text, id) => {
|
|
203
|
+
const b = F.el('button', { type: 'button', className: 'btn', textContent: text });
|
|
204
|
+
b.setAttribute('aria-label', `Add ${x.word} to ${text === 'Name' ? 'names' : 'client words'}`);
|
|
205
|
+
b.addEventListener('click', () => {
|
|
206
|
+
const box = $(id);
|
|
207
|
+
box.value = box.value.trim() ? `${box.value.trim().replace(/,$/, '')}, ${x.word}` : x.word;
|
|
208
|
+
box.dispatchEvent(new Event('input'));
|
|
209
|
+
li.remove();
|
|
210
|
+
say();
|
|
211
|
+
});
|
|
212
|
+
return b;
|
|
213
|
+
};
|
|
214
|
+
li.append(F.el('div', { className: 'repomain' }, F.el('span', { className: 'reponame', textContent: x.word }), F.el('span', { className: 'repowhen', textContent: `seen ${x.count} times` })), button('Name', 'names'), button('Client word', 'terms'));
|
|
215
|
+
list.append(li);
|
|
216
|
+
}
|
|
217
|
+
say();
|
|
218
|
+
}
|
|
219
|
+
|
|
134
220
|
async function start() {
|
|
135
221
|
const state = await HWP.ready;
|
|
136
222
|
const stop = (text) => {
|
|
@@ -152,6 +238,12 @@
|
|
|
152
238
|
F.help($('emailHelp'), 'emails', ['A commit counts as yours only when one of these addresses wrote it. Separate several with commas.']);
|
|
153
239
|
F.help($('goalsHelp'), 'the goal list', ['A JSON file of your goals and their changes. Leave it empty for none.']);
|
|
154
240
|
F.help($('insHelp'), '/insights', ["Shows what Claude Code's /insights wrote about your sessions on the Problems page, as its own AI-written group."]);
|
|
241
|
+
F.help($('saveHelp'), 'saved results', [
|
|
242
|
+
"Keeps what each run works out, with private words hidden, in honestweek.saved/ beside your config, readable only by you and git-ignored. Each run adds to it, so problems --session can still say whether a session was checked, and what was found, after Claude Code deletes its log (after 30 days, by default).",
|
|
243
|
+
"With each day's history too, a session whose log is gone stays on Replay and Problems, marked saved. Check results alone take far less room.",
|
|
244
|
+
"History is kept for your configured repositories' sessions. Sessions from display-only repositories and folders outside your config, which the page also lists, are kept only with the last box ticked.",
|
|
245
|
+
'Private text is never saved. Days older than the number you keep are deleted.',
|
|
246
|
+
]);
|
|
155
247
|
F.help($('longHelp'), 'the long-session limit', ['Problems flags an agent that kept going past this many tokens of context. Empty is off.', "No vendor recommends a number. The Claude API's own compaction starts at 150,000 unless you change it."]);
|
|
156
248
|
repos = shown.repos.map((x) => ({ ...x }));
|
|
157
249
|
const rl = F.repoList($('repos'), repos, changed, { newestFirst: true });
|
|
@@ -169,7 +261,19 @@
|
|
|
169
261
|
$('insights').checked = shown.insights === true;
|
|
170
262
|
$('insights').addEventListener('change', changed);
|
|
171
263
|
$('longLimit').value = shown.longSessionTokens == null ? '' : shown.longSessionTokens.toLocaleString('en-US');
|
|
172
|
-
|
|
264
|
+
$('saveOn').checked = shown.saveResults?.on === true;
|
|
265
|
+
$('saveKeep').placeholder = String(shown.saveResults?.defaultDays ?? 365);
|
|
266
|
+
$('saveKeep').value = shown.saveResults?.keepDays == null ? '' : String(shown.saveResults.keepDays);
|
|
267
|
+
$('saveHistory').checked = shown.saveResults?.history !== false;
|
|
268
|
+
$('saveOn').addEventListener('change', changed);
|
|
269
|
+
$('saveHistory').addEventListener('change', changed);
|
|
270
|
+
$('saveOthers').checked = shown.saveResults?.otherSessions === true;
|
|
271
|
+
$('saveOthers').addEventListener('change', changed);
|
|
272
|
+
$('savedLine').textContent = savedText(shown.saved);
|
|
273
|
+
$('forgetBtn').hidden = !(shown.saved?.available && (shown.saved.days || shown.saved.bytes));
|
|
274
|
+
$('forgetBtn').addEventListener('click', forget);
|
|
275
|
+
for (const id of ['emails', 'names', 'terms', 'goals', 'longLimit', 'saveKeep']) $(id).addEventListener('input', changed);
|
|
276
|
+
$('suggestBtn').addEventListener('click', suggest);
|
|
173
277
|
$('previewBtn').addEventListener('click', review);
|
|
174
278
|
$('saveBtn').addEventListener('click', save);
|
|
175
279
|
$('form').hidden = false;
|
|
@@ -73,6 +73,13 @@
|
|
|
73
73
|
<h2 id="goalsH"><label for="goals">Goal list</label> <span class="muted">(optional)</span> <span id="goalsHelp"></span></h2>
|
|
74
74
|
<input class="setup-input" id="goals" type="text" autocomplete="off" spellcheck="false" placeholder="/path/to/your/goals.json">
|
|
75
75
|
</section>
|
|
76
|
+
<section class="setup-part" aria-labelledby="saveToH" id="saveToPart" hidden>
|
|
77
|
+
<h2 id="saveToH"><label for="saveTo">Save it for</label> <span id="saveToHelp"></span></h2>
|
|
78
|
+
<select id="saveTo">
|
|
79
|
+
<option value="folder" id="saveToFolder">This folder</option>
|
|
80
|
+
<option value="user" id="saveToUser">Every folder</option>
|
|
81
|
+
</select>
|
|
82
|
+
</section>
|
|
76
83
|
<div class="setup-inline setup-actions">
|
|
77
84
|
<button type="button" class="btn" id="previewBtn">Preview</button>
|
|
78
85
|
<button type="button" class="btn primary" id="saveBtn">Save</button>
|
package/lib/view/assets/setup.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
names: $('names').value,
|
|
25
25
|
terms: $('terms').value,
|
|
26
26
|
...(goals ? { goalsFile: goals } : {}),
|
|
27
|
+
...($('saveToPart').hidden ? {} : { saveTo: $('saveTo').value }),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
/** Any change makes the shown preview out of date, so it's hidden. Save is always on: the
|
|
@@ -118,6 +119,14 @@
|
|
|
118
119
|
$('timezone').addEventListener('input', () => hc.plan());
|
|
119
120
|
$('namesLabel').textContent = info.privateWords.names;
|
|
120
121
|
$('termsLabel').textContent = info.privateWords.terms;
|
|
122
|
+
// Where to save: offered only when this run would find a user-level config from any folder.
|
|
123
|
+
if (info.userConfig) {
|
|
124
|
+
$('saveToFolder').textContent = `This folder (${info.folder})`;
|
|
125
|
+
$('saveToUser').textContent = `Every folder (${info.userConfig})`;
|
|
126
|
+
F.help($('saveToHelp'), 'where to save', ['honestweek reads the config in the folder it runs in first. A folder without one uses the every-folder config, so an agent working in any project finds it.']);
|
|
127
|
+
$('saveTo').addEventListener('change', changed);
|
|
128
|
+
$('saveToPart').hidden = false;
|
|
129
|
+
}
|
|
121
130
|
for (const id of ['emails', 'timezone', 'names', 'terms', 'goals']) $(id).addEventListener('input', changed);
|
|
122
131
|
$('previewBtn').addEventListener('click', preview);
|
|
123
132
|
$('saveBtn').addEventListener('click', save);
|
package/lib/view/codex-judge.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { dirname, join } from 'node:path';
|
|
|
28
28
|
|
|
29
29
|
import { atomicWriteJson } from '../atomic-json.mjs';
|
|
30
30
|
import { ensureGitignore } from '../init.mjs';
|
|
31
|
-
import { cutRedacted } from '
|
|
31
|
+
import { cutRedacted } from '../replay/word-index.mjs';
|
|
32
32
|
import { findOnPath, keepEnv, stopTree, SYSTEM_ENV } from './insights.mjs';
|
|
33
33
|
import { systemProgram } from '../windows-root.mjs';
|
|
34
34
|
|