@forwardimpact/libwiki 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +53 -33
  2. package/package.json +1 -1
  3. package/src/active-claims.js +5 -5
  4. package/src/agent-roster.js +2 -2
  5. package/src/audit/admission.js +14 -11
  6. package/src/audit/conflict-markers-rule.js +9 -9
  7. package/src/audit/grammar.js +21 -18
  8. package/src/audit/rule-builders.js +20 -19
  9. package/src/audit/rules.js +30 -27
  10. package/src/audit/scopes.js +34 -33
  11. package/src/audit/status-row.js +14 -15
  12. package/src/block-renderer.js +5 -4
  13. package/src/boot.js +10 -8
  14. package/src/budget-gate.js +39 -36
  15. package/src/budget.js +3 -3
  16. package/src/cli-definition.js +23 -20
  17. package/src/commands/audit.js +3 -3
  18. package/src/commands/boot.js +1 -1
  19. package/src/commands/claim.js +44 -38
  20. package/src/commands/curate.js +34 -31
  21. package/src/commands/fix.js +68 -64
  22. package/src/commands/inbox.js +1 -1
  23. package/src/commands/init.js +12 -7
  24. package/src/commands/ledger.js +11 -11
  25. package/src/commands/log.js +19 -17
  26. package/src/commands/memo.js +4 -1
  27. package/src/commands/product-mix.js +16 -15
  28. package/src/commands/refresh.js +25 -22
  29. package/src/commands/rotate.js +9 -8
  30. package/src/commands/sync.js +26 -17
  31. package/src/conflict-markers.js +21 -21
  32. package/src/constants.js +37 -33
  33. package/src/gitattributes.js +10 -9
  34. package/src/integrity.js +29 -27
  35. package/src/issue-list-renderer.js +24 -16
  36. package/src/lane-files.js +11 -10
  37. package/src/ledger/anchor.js +6 -6
  38. package/src/ledger/projection.js +35 -32
  39. package/src/ledger/reader.js +4 -4
  40. package/src/marker-scanner.js +3 -2
  41. package/src/sanitize.js +12 -11
  42. package/src/secret-gate.js +41 -40
  43. package/src/status.js +12 -11
  44. package/src/storyboard-skeleton.js +20 -18
  45. package/src/util/agent-flag.js +8 -8
  46. package/src/util/clock.js +1 -1
  47. package/src/util/wiki-dir.js +7 -7
  48. package/src/weekly-log.js +115 -101
  49. package/src/wiki-sync.js +393 -361
@@ -18,15 +18,15 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
18
18
  import { FAST_MODEL } from "@forwardimpact/libutil/models";
19
19
  import { createLogger } from "@forwardimpact/libtelemetry";
20
20
 
21
- // Pipeline: audit → deterministic rotation (the one fix needing a file seal the
22
- // agent can't do) → re-audit → Haiku agent on the prose-judgment residual →
23
- // flag what neither should touch. MAX_ROUNDS still caps the agent loop so an
24
- // unresolvable agent-class finding fails loudly rather than spinning forever.
21
+ // Pipeline: audit → deterministic rotation (the one fix that needs a file seal
22
+ // the agent cannot do) → re-audit → Haiku agent on the prose-judgment residual
23
+ // → flag what neither should touch. MAX_ROUNDS still caps the agent loop. An
24
+ // unresolvable agent-class finding fails loudly and does not loop forever.
25
25
  const MAX_ROUNDS = 3;
26
26
 
27
27
  /**
28
28
  * A finding's remediation class, from the declarative rule. Rules without a
29
- * `remediation` field default to `"agent"`: the Haiku agent handles all
29
+ * `remediation` field default to `"agent"`. The Haiku agent handles all
30
30
  * prose-judgment fixes (summary trims, section order, MEMORY.md prose).
31
31
  */
32
32
  function classOf(finding) {
@@ -34,14 +34,16 @@ function classOf(finding) {
34
34
  }
35
35
 
36
36
  /**
37
- * Every rule governing a scope with an open finding, as `id — hint` lines.
38
- * Handing the agent the full contract for the files it edits — not just the
39
- * failing rules — stops it fixing one finding by breaking another (dropping
40
- * the `**Last run**:` line, appending a section after `## Open Blockers`, …).
37
+ * Every rule that governs a scope with an open finding, as `id — hint` lines.
38
+ * The agent gets the full contract for the files it edits. The contract covers
39
+ * more than the rules that fail. It stops the agent from breaking one
40
+ * invariant while it fixes another finding (a dropped `**Last run**:` line, a
41
+ * section appended after `## Open Blockers`, …).
41
42
  *
42
- * Only static-string hints are listed: a function hint is a per-finding
43
- * resolved command (a `rotate` remediation the agent never performs), not a
44
- * file invariant, so listing it would leak its source text into the prompt.
43
+ * This lists only static-string hints. A function hint is a per-finding
44
+ * resolved command (a `rotate` remediation the agent never performs). A
45
+ * function hint is not a file invariant, so an entry for it would leak its
46
+ * source text into the prompt.
45
47
  */
46
48
  function invariantContract(findings) {
47
49
  const scopes = new Set(
@@ -53,52 +55,53 @@ function invariantContract(findings) {
53
55
  }
54
56
 
55
57
  /**
56
- * The opening task: the findings, the invariant contract, and the things the
57
- * rule hints don't cover — where trimmed history goes (only existing
58
- * weekly-log files; rotation owns minting new ones), and to prefer a single
59
- * Write.
58
+ * The opening task. It carries the findings, the invariant contract, and the
59
+ * things the rule hints do not cover. It says where trimmed history goes (only
60
+ * existing weekly-log files, because rotation mints the new ones). It also
61
+ * says to prefer a single Write.
60
62
  */
61
63
  function composeTask(findings, wikiRoot, projectRoot) {
62
64
  return [
63
- `Fix these wiki audit findings by editing files under ${wikiRoot}.`,
65
+ `Fix these wiki audit findings. Edit files under ${wikiRoot}.`,
64
66
  ``,
65
67
  emitFindingsText(findings, { cwd: projectRoot }),
66
68
  ``,
67
- `All of these invariants must hold when you finish — never fix one finding`,
68
- `by breaking another:`,
69
+ `All of these invariants must hold when you finish. Never fix one finding`,
70
+ `and break another:`,
69
71
  ...invariantContract(findings),
70
72
  ``,
71
73
  `Move history out of an over-budget summary into the agent's existing`,
72
- `weekly-log file or its current part (wiki/<agent>-YYYY-Www[-partN].md) —`,
73
- `never a new summary section, and never a new file: rotation tooling owns`,
74
- `when part files are created, so do not mint filenames yourself. If the`,
75
- `trimmed narrative already exists in the weekly log, replace it in the`,
76
- `summary with a pointer to that file instead of copying it anywhere.`,
74
+ `weekly-log file or its current part (wiki/<agent>-YYYY-Www[-partN].md).`,
75
+ `Never write a new summary section, and never a new file. The rotation`,
76
+ `tool owns when it creates part files, so do not mint filenames yourself.`,
77
+ `If the trimmed narrative already exists in the weekly log, replace it in`,
78
+ `the summary with a pointer to that file instead of copying it anywhere.`,
77
79
  `Prefer a single Write over many Edits.`,
78
80
  ].join("\n");
79
81
  }
80
82
 
81
- /** The resume task: the findings that survived the last edit. */
83
+ /** The resume task. It carries the findings that survived the last edit. */
82
84
  function composeFollowup(findings, projectRoot) {
83
85
  return [
84
- `The wiki still fails the audit. Remaining findings:`,
86
+ `The wiki still fails the audit. The findings that remain:`,
85
87
  ``,
86
88
  emitFindingsText(findings, { cwd: projectRoot }),
87
89
  ``,
88
- `Fix every one without breaking any invariant listed earlier.`,
90
+ `Fix every one. Do not break any invariant listed earlier.`,
89
91
  ].join("\n");
90
92
  }
91
93
 
92
94
  /**
93
- * Deterministic pre-pass: seal every over-budget current-week weekly-log main
94
- * file via `rotateIfOverBudget`. The agent name comes from the audit's own
95
- * subjects (keyed by path) — no filename parsing. `force: true` rotates even a
96
- * word-over/line-under file.
95
+ * Deterministic pre-pass. It seals every over-budget current-week main log
96
+ * with `rotateIfOverBudget`. The agent name comes from the audit's own
97
+ * subjects (keyed by path), so nothing parses the filename. `force: true`
98
+ * rotates even a word-over/line-under file.
97
99
  *
98
- * `rotateIfOverBudget` always seals the agent's *current-week* log, so we only
99
- * call it when the finding IS that file. A prior-week over-budget main is left
100
- * untouched (rotating it would force-seal a healthy current-week log instead);
101
- * it survives the re-audit and is flagged for a human.
100
+ * `rotateIfOverBudget` always seals the agent's *current-week* log, so we call
101
+ * it only when the finding IS that file. A prior-week over-budget main log
102
+ * stays untouched. A rotation would force-seal a healthy current-week log
103
+ * instead. The over-budget main survives the re-audit, and the run flags it
104
+ * for a human.
102
105
  */
103
106
  /**
104
107
  * Seal one over-budget current-week main log. A failed seal leaves the source
@@ -133,8 +136,8 @@ function rotateOverBudgetMainLogs(findings, deps) {
133
136
  ];
134
137
  const agentByPath = new Map(subjects.map((s) => [s.path, s.agentPrefix]));
135
138
  // A log over BOTH budgets yields two `rotate` findings with the same path
136
- // (different rule ids); seal each path once, or the second force call would
137
- // bisect the freshly-written fresh-main into a spurious near-empty part.
139
+ // (different rule ids). Seal each path once. A second force call would
140
+ // bisect the newly written main file into a spurious near-empty part.
138
141
  const sealed = new Set();
139
142
  for (const f of findings) {
140
143
  if (classOf(f) !== "rotate") continue;
@@ -147,9 +150,9 @@ function rotateOverBudgetMainLogs(findings, deps) {
147
150
  }
148
151
 
149
152
  /**
150
- * Re-bisect one over-budget sealed part, logging each new sibling slot it
151
- * produces (the reused source slot is not a new file). A failed reseal leaves
152
- * the source intact (the writer rolled back), so the re-audit re-flags it.
153
+ * Re-bisect one over-budget sealed part. Log each new sibling slot it produces
154
+ * (the reused source slot is not a new file). A failed reseal leaves the
155
+ * source intact (the writer rolled back), so the re-audit re-flags it.
153
156
  */
154
157
  function resealPart(partPath, { fs, projectRoot, out, err }) {
155
158
  try {
@@ -171,15 +174,15 @@ function resealPart(partPath, { fs, projectRoot, out, err }) {
171
174
  }
172
175
 
173
176
  /**
174
- * Deterministic pass for over-budget sealed parts: re-bisect each at its
175
- * day-section seams. A part with no splittable seam is left byte-identical (the
176
- * re-audit re-flags it for a human). Agent and week come from the part filename,
177
- * so no subject lookup is needed. Part findings are disjoint from the main-log
178
- * findings `rotateOverBudgetMainLogs` handles.
177
+ * Deterministic pass for over-budget sealed parts. It re-bisects each part at
178
+ * its day-section seams. A part with no splittable seam stays byte-identical
179
+ * (the re-audit re-flags it for a human). Agent and week come from the part
180
+ * filename, so the pass needs no subject lookup. Part findings are disjoint
181
+ * from the main-log findings `rotateOverBudgetMainLogs` handles.
179
182
  */
180
183
  function rebisectOverBudgetParts(findings, deps) {
181
- // A part over both budgets yields two findings with the same path; re-bisect
182
- // each path once.
184
+ // A part over both budgets yields two findings with the same path.
185
+ // Re-bisect each path once.
183
186
  const done = new Set();
184
187
  for (const f of findings) {
185
188
  if (classOf(f) !== "rotate") continue;
@@ -190,7 +193,7 @@ function rebisectOverBudgetParts(findings, deps) {
190
193
  }
191
194
  }
192
195
 
193
- /** Report findings that need human judgment — never auto-fixed. */
196
+ /** Report findings that need human judgment. Nothing auto-fixes them. */
194
197
  function reportFlags(err, flagFindings, projectRoot) {
195
198
  err(
196
199
  `gemba-wiki fix: ${flagFindings.length} finding(s) need human judgment ` +
@@ -200,11 +203,11 @@ function reportFlags(err, flagFindings, projectRoot) {
200
203
  }
201
204
 
202
205
  /**
203
- * Surface a round's agent error, if any. Returns true when it is fatal: a
204
- * missing sessionId means the process never started (e.g. the SDK refused
206
+ * Surface a round's agent error, if any. Returns true when the error is fatal.
207
+ * A missing sessionId means the process never started (e.g. the SDK refused
205
208
  * bypass-permissions as root), so there is nothing to resume. A turn-limit or
206
- * transient error keeps its session and may have made partial progress, so it
207
- * is noted but not fatal — the re-audit decides.
209
+ * transient error keeps its session and can make partial progress. The
210
+ * function notes it and does not treat it as fatal. The re-audit decides.
208
211
  */
209
212
  function isFatalError(result, round, err) {
210
213
  if (!result.error) return false;
@@ -237,10 +240,10 @@ async function buildFixRunner(ctx, projectRoot, runtime) {
237
240
  }
238
241
 
239
242
  /**
240
- * Run the agent on the prose-judgment findings, re-auditing each round until
241
- * clean, flag-only, or MAX_ROUNDS is exhausted. The audit is the verdict, not
242
- * the agent's self-report; resuming extends the turn budget for a trim too
243
- * large for one round.
243
+ * Run the agent on the prose-judgment findings. Re-audit each round until the
244
+ * wiki is clean, until only flags remain, or until MAX_ROUNDS is exhausted.
245
+ * The audit gives the verdict. The agent's self-report does not. A resume
246
+ * extends the turn budget for a trim too large for one round.
244
247
  */
245
248
  async function runAgentRounds(runner, agentFindings, deps) {
246
249
  const { wikiRoot, projectRoot, audit, partition, out, err } = deps;
@@ -291,10 +294,10 @@ export async function runFixCommand(ctx) {
291
294
  buildContext({ wikiRoot, today, fs, subprocess: runtime.subprocess }),
292
295
  { resolveScope },
293
296
  );
294
- // The agent only ever gets prose-judgment (`agent`-class) findings. A
295
- // `rotate` finding that survived the pre-pass (e.g. a prior-week log) is
296
- // unfixable by the agent — and trimming append-only history to satisfy a
297
- // budget would corrupt it — so it joins the flag set for a human.
297
+ // The agent only ever gets prose-judgment (`agent`-class) findings. The
298
+ // agent cannot fix a `rotate` finding that survived the pre-pass (e.g. a
299
+ // prior-week log). A trim of append-only history to satisfy a budget would
300
+ // corrupt the history. So the finding joins the flag set for a human.
298
301
  const partition = (found) => ({
299
302
  agentFindings: found.filter((f) => classOf(f) === "agent"),
300
303
  flagFindings: found.filter((f) => classOf(f) !== "agent"),
@@ -307,7 +310,7 @@ export async function runFixCommand(ctx) {
307
310
  }
308
311
 
309
312
  // Deterministic layer: seal over-budget main logs, then re-bisect over-budget
310
- // sealed parts. Both are content-preserving — no agent, no history rewrite.
313
+ // sealed parts. Both preserve content, with no agent and no history rewrite.
311
314
  if (findings.some((f) => classOf(f) === "rotate")) {
312
315
  const rotateDeps = { wikiRoot, today, projectRoot, fs, out, err };
313
316
  rotateOverBudgetMainLogs(findings, rotateDeps);
@@ -319,15 +322,16 @@ export async function runFixCommand(ctx) {
319
322
  }
320
323
  }
321
324
 
322
- // Residual: agent-class goes to the writer; everything else (flag, plus any
323
- // rotate finding the deterministic pass could not handle) needs a human.
325
+ // Residual: agent-class goes to the writer. Everything else needs a human
326
+ // (flag, plus any rotate finding the deterministic pass could not handle).
324
327
  const { agentFindings, flagFindings } = partition(findings);
325
328
  if (agentFindings.length === 0) {
326
329
  reportFlags(err, flagFindings, projectRoot);
327
330
  return { ok: false, code: 2 };
328
331
  }
329
332
 
330
- // Constructed only now, so a rotation-only or flag-only run never spawns it.
333
+ // Build the runner only now, so a rotation-only or flag-only run never
334
+ // spawns it.
331
335
  const runner = await buildFixRunner(ctx, projectRoot, runtime);
332
336
  return runAgentRounds(runner, agentFindings, {
333
337
  wikiRoot,
@@ -104,7 +104,7 @@ function appendPriorityRow(memoryText, { item, agents, owner, status, added }) {
104
104
  ];
105
105
  return memoryText.replace(/\n*$/, "") + "\n" + block.join("\n");
106
106
  }
107
- // Find last data row.
107
+ // Find the last data row.
108
108
  let sepIdx = -1;
109
109
  for (let i = headingIdx + 1; i < lines.length; i++) {
110
110
  if (/^\|\s*---/.test(lines[i])) {
@@ -9,7 +9,12 @@ import {
9
9
  ACTIVE_CLAIMS_TABLE_SEPARATOR,
10
10
  } from "../constants.js";
11
11
 
12
- /** Resolve the wiki clone URL. Honors the FIT_WIKI_URL env var as an explicit override (for sandboxed environments where `origin` is rewritten to a local proxy that does not serve wiki repos); otherwise derives the URL by appending `.wiki.git` to the parent repo's `origin` remote. */
12
+ /**
13
+ * Resolve the wiki clone URL. The FIT_WIKI_URL env var is an explicit
14
+ * override. Use it in a sandboxed environment where a local proxy replaces
15
+ * `origin` and does not serve wiki repos. Without that override, the function
16
+ * appends `.wiki.git` to the parent repo's `origin` remote.
17
+ */
13
18
  export async function deriveWikiUrl(gitClient, parentDir, env) {
14
19
  if (env.FIT_WIKI_URL) return env.FIT_WIKI_URL;
15
20
  try {
@@ -31,7 +36,7 @@ function scaffoldActiveClaims(runtime, memoryPath) {
31
36
  "",
32
37
  ACTIVE_CLAIMS_HEADING,
33
38
  "",
34
- "In-flight work claimed by an agent. Row present = active; row absent = settled.",
39
+ "In-flight work claimed by an agent. Row present = active. Row absent = settled.",
35
40
  "Writers: `gemba-wiki claim`, `gemba-wiki release`. Reader: `gemba-wiki boot`.",
36
41
  "",
37
42
  ACTIVE_CLAIMS_TABLE_HEADER,
@@ -65,14 +70,14 @@ async function maybeCloneWiki(wikiSync, gitClient, projectRoot, runtime) {
65
70
  if (cloneResult.cloned) {
66
71
  await wikiSync.inheritIdentity();
67
72
  } else {
68
- logger.warn(
69
- "init",
70
- "could not clone wiki, continuing with local-only steps",
71
- );
73
+ logger.warn("init", "could not clone wiki, so only the local steps run");
72
74
  }
73
75
  }
74
76
 
75
- /** Clone the wiki if not already present, scaffold Active Claims in MEMORY.md, and create per-skill metric directories. */
77
+ /**
78
+ * Clone the wiki if it is absent. Scaffold Active Claims in MEMORY.md. Create
79
+ * a metric directory for each skill.
80
+ */
76
81
  export async function runInitCommand(ctx) {
77
82
  const { runtime, wikiSync, gitClient } = ctx.deps;
78
83
  const options = ctx.options;
@@ -38,7 +38,7 @@ function nextFreeIds(fold, kind, count) {
38
38
  return ids;
39
39
  }
40
40
 
41
- /** Read the ordered anchor sequence and fold it, resolving the repo slug. */
41
+ /** Read the ordered anchor sequence, fold it, and resolve the repo slug. */
42
42
  async function loadFold({ gitClient, ghClient, wikiDir, issue }) {
43
43
  const url = await gitClient.remoteGetUrl("origin", { cwd: wikiDir });
44
44
  const { owner, repo } = parseOwnerRepo(url);
@@ -70,10 +70,10 @@ async function allocate(env, options) {
70
70
  };
71
71
  }
72
72
  const { fold, owner, repo } = await loadFold(env);
73
- // Backfill registers an anchor for ids that predate the anchor surface, named
74
- // explicitly via --ids; their event keys already exist in history. A plain
75
- // allocate mints the next free ids of the kind. The conflict detector at
76
- // rebuild guards against double-registering an id that already has an anchor.
73
+ // Backfill registers an anchor for ids that predate the anchor surface. The
74
+ // --ids flag names them, and their event keys already exist in history. A
75
+ // plain allocate mints the next free ids of the kind. The conflict detector
76
+ // at rebuild guards against an id that already has an anchor.
77
77
  let ids;
78
78
  if (options.ids) {
79
79
  ids = options.ids
@@ -93,8 +93,8 @@ async function allocate(env, options) {
93
93
  ids = nextFreeIds(fold, kind, count);
94
94
  }
95
95
  const body = renderAnchorBody({ kind, ids, event, note: options.note ?? "" });
96
- // The anchor publication is the allocation; no projection is written here.
97
- // The printed ids are provisional — a rebuild over the published sequence is
96
+ // The anchor publication is the allocation. This code writes no projection.
97
+ // The printed ids are provisional. A rebuild over the published sequence is
98
98
  // authoritative and resolves any concurrent interleave first-published-wins.
99
99
  await ghClient.apiPost(
100
100
  `repos/${owner}/${repo}/issues/${env.issue}/comments`,
@@ -118,7 +118,7 @@ function readLedgerPage(runtime, wikiDir) {
118
118
  : "";
119
119
  }
120
120
 
121
- /** Project the anchor record onto the ledger-page body, preserving cited prose. */
121
+ /** Project the anchor record onto the ledger-page body. Keep cited prose. */
122
122
  async function project(env, options) {
123
123
  const { runtime, wikiDir } = env;
124
124
  const labelMode = options.gapped ? "gapped" : "renumber";
@@ -183,9 +183,9 @@ const SUBS = { allocate, rebuild, verify };
183
183
  /**
184
184
  * `gemba-wiki ledger <allocate|rebuild|verify>` — the allocation procedure that
185
185
  * keeps identity off the merge-contested page. Allocation publishes an anchor
186
- * comment to the obstacle issue with no projection write at allocation time;
187
- * rebuild and verify project the anchor record onto the ledger page and MEMORY
188
- * row, preserving anchor-cited prose.
186
+ * comment to the obstacle issue and writes no projection at allocation time.
187
+ * Rebuild and verify project the anchor record onto the ledger page and MEMORY
188
+ * row. Both keep anchor-cited prose.
189
189
  */
190
190
  export async function runLedgerCommand(ctx) {
191
191
  const { runtime, gitClient, ghClient } = ctx.deps;
@@ -28,8 +28,8 @@ function commonContext(runtime, options) {
28
28
  }
29
29
 
30
30
  function lastDateHeading(text) {
31
- // Match `## YYYY-MM-DD` at the start of a line, optionally followed by
32
- // suffix text (e.g. `## 2026-05-19 (third activation)`).
31
+ // Match `## YYYY-MM-DD` at the start of a line. Suffix text can follow
32
+ // (e.g. `## 2026-05-19 (third activation)`).
33
33
  const re = new RegExp(WEEKLY_LOG_SEAM_RE.source, "gm");
34
34
  let last = null;
35
35
  let match;
@@ -38,12 +38,13 @@ function lastDateHeading(text) {
38
38
  }
39
39
 
40
40
  /**
41
- * Rotate before an append, never blocking it. A bisecting seal may now produce
42
- * multiple parts; the append still proceeds against the fresh current file. An
43
- * `incomplete` residue (a lone over-cap day-section sealed as its own part —
44
- * never the live file) is surfaced to stderr but does not block the append. A
45
- * thrown fs error is reported and swallowed: the writer rolled back, so the
46
- * (intact) current file still receives the new entry.
41
+ * Rotate before an append. The rotation never blocks the append. A seal that
42
+ * bisects can now produce multiple parts, and the append still proceeds
43
+ * against the fresh current file. An `incomplete` residue is a lone over-cap
44
+ * day-section sealed as its own part, never the live file. The function
45
+ * reports that residue to stderr, and it does not block the append. The
46
+ * function reports a thrown fs error and swallows it. The writer rolled back,
47
+ * so the (intact) current file still receives the new entry.
47
48
  */
48
49
  function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
49
50
  try {
@@ -60,7 +61,7 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
60
61
  createLogger("wiki", runtime).warn(
61
62
  "log",
62
63
  `day-section ${res.residue.section} alone exceeds the budget ` +
63
- `(${res.residue.lines} lines, ${res.residue.words} words); ` +
64
+ `(${res.residue.lines} lines, ${res.residue.words} words), ` +
64
65
  `sealed as ${res.residue.path} for manual recovery`,
65
66
  );
66
67
  }
@@ -70,10 +71,11 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
70
71
  }
71
72
 
72
73
  /**
73
- * Report the just-written weekly log's budget state — value, cap, and remaining
74
- * headroom for both the line and word budget — so a writer sees the ceiling
75
- * before composing the next entry rather than discovering it via a red gate.
76
- * Runs after every successful append, on the rotated and non-rotated path alike.
74
+ * Report the budget state of the weekly log just written. The report gives the
75
+ * value, the cap, and the headroom left for both the line budget and the word
76
+ * budget. A writer then sees the ceiling before the next entry, and does not
77
+ * find it at a red gate. This runs after every successful append, on the
78
+ * rotated path and the non-rotated path alike.
77
79
  */
78
80
  function reportBudget(target, runtime) {
79
81
  const text = runtime.fsSync.existsSync(target)
@@ -131,13 +133,13 @@ function runNote(runtime, options) {
131
133
  return { ok: false, code: 2 };
132
134
  }
133
135
  const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
134
- // Conservative budget: assume we'll prepend a date heading. Rotate on the
135
- // larger `withHeading` body so the word/line projection never under-counts.
136
+ // Conservative budget: assume a prepended date heading. Rotate on the larger
137
+ // `withHeading` body so the word/line projection never under-counts.
136
138
  const withHeading = `## ${today}\n\n${fieldBlock}`;
137
139
  rotateBeforeAppend(wikiRoot, agent, today, withHeading, runtime);
138
140
  const target = weeklyLogPath(wikiRoot, agent, today);
139
- // Append under the open entry if the file's last `## YYYY-MM-DD` is today;
140
- // otherwise open a new entry by prepending a date heading.
141
+ // Append under the open entry if the file's last `## YYYY-MM-DD` is today.
142
+ // If it is not, prepend a date heading to open a new entry.
141
143
  const existing = runtime.fsSync.existsSync(target)
142
144
  ? runtime.fsSync.readFileSync(target, "utf-8")
143
145
  : "";
@@ -64,7 +64,10 @@ function writeBroadcast(
64
64
  return { ok: true };
65
65
  }
66
66
 
67
- /** Write a memo to a target agent's summary file (or broadcast to all except the sender); the sender is the required --from flag. */
67
+ /**
68
+ * Write a memo to a target agent's summary file. A broadcast reaches every
69
+ * agent except the sender. The required --from flag names the sender.
70
+ */
68
71
  export function runMemoCommand(ctx) {
69
72
  const { runtime } = ctx.deps;
70
73
  const options = ctx.options;
@@ -5,10 +5,10 @@ import { parseRepoSlug } from "../issue-list-renderer.js";
5
5
  import { currentDayIso } from "../util/clock.js";
6
6
  import { resolveProjectRoot } from "../util/wiki-dir.js";
7
7
 
8
- // Resolve the monorepo's `owner/repo` slug the way `refresh.js` does: an
9
- // explicit `FIT_GH_REPO` env override (sandbox proxy URLs), else the origin
10
- // remote parsed via the injected git client. Returns null when nothing
11
- // parseable is found, in which case `gh` falls back to its own cwd resolution.
8
+ // Resolve the monorepo's `owner/repo` slug the way `refresh.js` does. An
9
+ // explicit `FIT_GH_REPO` env override wins (sandbox proxy URLs). Otherwise the
10
+ // injected git client parses the origin remote. Returns null when nothing
11
+ // parses. `gh` then falls back to its own cwd resolution.
12
12
  async function deriveRepo(gitClient, cwd, env) {
13
13
  if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
14
14
  if (!gitClient) return null;
@@ -20,8 +20,8 @@ async function deriveRepo(gitClient, cwd, env) {
20
20
  }
21
21
  }
22
22
 
23
- // A missing token is non-fatal: `gh` may still resolve ambient auth, and a
24
- // hard fetch failure downstream collapses to a logged warning and no row.
23
+ // A missing token is non-fatal. `gh` can still resolve ambient auth. A hard
24
+ // fetch failure downstream collapses to a logged warning and no row.
25
25
  async function resolveToken() {
26
26
  try {
27
27
  return (await createScriptConfig("wiki")).ghToken();
@@ -30,8 +30,8 @@ async function resolveToken() {
30
30
  }
31
31
  }
32
32
 
33
- // `gh pr list` returns at most this many PRs; a window that hits the cap is
34
- // truncated, so the caller warns rather than silently undercounting.
33
+ // `gh pr list` returns at most this many PRs. A window that hits the cap is
34
+ // truncated, so the caller warns and does not undercount in silence.
35
35
  const FETCH_LIMIT = 200;
36
36
 
37
37
  // Fetch merged PRs in `[since, until]` and return their parsed JSON, or null on
@@ -60,7 +60,7 @@ async function fetchMergedPrs({ runtime, cwd, repo, since, until, token }) {
60
60
  }
61
61
 
62
62
  // Tally merged PRs by their classification label. A PR with neither label is
63
- // unlabeled; `product` wins if both are somehow present.
63
+ // unlabeled. `product` wins if both labels are somehow present.
64
64
  function countByLabel(prs) {
65
65
  const counts = { product: 0, internal: 0, unlabeled: 0 };
66
66
  for (const pr of prs) {
@@ -75,10 +75,11 @@ function countByLabel(prs) {
75
75
  /**
76
76
  * Emit the product-vs-internal mix of merged PRs as a `product_share` metric
77
77
  * row. Counts PRs merged in `[since, until]` by their `product` / `internal`
78
- * label and appends `product_share = round(product / (product + internal) *
79
- * 100)` to `wiki/metrics/product-mix/<YYYY>.csv` via the `gemba-xmr record` write
80
- * path. Deterministic — re-running over the same merged PRs yields the same
81
- * value. A window with no labeled merged PRs emits no row (avoids a 0/0 ratio).
78
+ * label. Appends `product_share = round(product / (product + internal) * 100)`
79
+ * to `wiki/metrics/product-mix/<YYYY>.csv` through the `gemba-xmr record`
80
+ * write path. The result is deterministic. A second run over the same merged
81
+ * PRs yields the same value. A window with no labeled merged PRs emits no row
82
+ * (it avoids a 0/0 ratio).
82
83
  */
83
84
  export async function runProductMixCommand(ctx) {
84
85
  const { runtime, gitClient } = ctx.deps;
@@ -101,7 +102,7 @@ export async function runProductMixCommand(ctx) {
101
102
  if (prs.length >= FETCH_LIMIT) {
102
103
  logger.warn(
103
104
  "product-mix",
104
- `window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap; product_share may undercount`,
105
+ `window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap, so product_share may undercount`,
105
106
  );
106
107
  }
107
108
 
@@ -110,7 +111,7 @@ export async function runProductMixCommand(ctx) {
110
111
  if (total === 0) {
111
112
  logger.info(
112
113
  "product-mix",
113
- `no labeled merged PRs in ${since}..${until}; emitting no row`,
114
+ `no labeled merged PRs in ${since}..${until}, so this run emits no row`,
114
115
  );
115
116
  return { ok: true };
116
117
  }
@@ -30,11 +30,12 @@ async function deriveParentRepo(gitClient, parentDir, env) {
30
30
  }
31
31
 
32
32
  // Compose the agent-experiments block body. On a successful tracker query the
33
- // body is a fresh last-successful-sync stamp followed by freshly rendered,
34
- // label-re-checked, sanitized item lines. On a tracker failure the previously
35
- // materialized body (stamp + items) is preserved verbatim so boot keeps serving
36
- // the last good routing surface instead of an empty one, and the timestamp is
37
- // not advanced, so staleness stays auditable from the stamp.
33
+ // body is a fresh last-successful-sync stamp, then freshly rendered,
34
+ // label-re-checked, sanitized item lines. On a tracker failure the function
35
+ // keeps the body it materialized before (stamp + items) verbatim. Boot then
36
+ // still serves the last good routing surface. It does not serve an empty one.
37
+ // The function does not advance the timestamp, so the stamp keeps
38
+ // staleness auditable.
38
39
  async function renderAgentExperimentsBlock(block, lines, ghContext, runtime) {
39
40
  const priorBody = lines.slice(block.openLine + 1, block.closeLine);
40
41
  try {
@@ -91,8 +92,8 @@ function spliceBlock(lines, block, rendered) {
91
92
  );
92
93
  }
93
94
 
94
- // A missing current-month storyboard is non-fatal: return null so the caller
95
- // can create it (see createStoryboardSkeleton) rather than fail the job.
95
+ // A missing current-month storyboard is non-fatal. Return null so the caller
96
+ // can create it (see createStoryboardSkeleton) and does not fail the job.
96
97
  function readStoryboardOrNull(runtime, storyboardPath) {
97
98
  try {
98
99
  return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
@@ -104,10 +105,11 @@ function readStoryboardOrNull(runtime, storyboardPath) {
104
105
 
105
106
  // Create the current-month storyboard from the minimal skeleton when it does
106
107
  // not exist. Refresh is the deterministic "freshen the wiki" step and runs
107
- // before the session (kata-agent pre-run), so creating here guarantees the file
108
- // is on disk before participants look for it — without any lead having a write
109
- // tool. The skeleton carries the section structure and the generic issue-list
110
- // markers; the render pass below fills them and participants seed metric blocks.
108
+ // before the session (kata-agent pre-run). Creation here guarantees the file
109
+ // is on disk before participants look for it, and no lead needs a write tool.
110
+ // The skeleton carries the section structure and the generic issue-list
111
+ // markers. The render pass below fills them, and participants seed metric
112
+ // blocks.
111
113
  function createStoryboardSkeleton(runtime, storyboardPath, logger) {
112
114
  const skeleton = renderStoryboardSkeleton(currentDayIso(runtime));
113
115
  runtime.fsSync.mkdirSync(path.dirname(storyboardPath), { recursive: true });
@@ -116,12 +118,12 @@ function createStoryboardSkeleton(runtime, storyboardPath, logger) {
116
118
  return skeleton;
117
119
  }
118
120
 
119
- // Drop every MEMORY.md `## Active Claims` row past its `expires_at`, writing the
121
+ // Drop every MEMORY.md `## Active Claims` row past its `expires_at`. Write the
120
122
  // trimmed table back in place. Refresh is the deterministic "freshen the wiki"
121
- // step, so clearing lapsed claims belongs here alongside the storyboard render;
122
- // it runs whether or not the storyboard has marker blocks to regenerate. The
123
- // write is local, mirroring the storyboard splice — the caller's push publishes
124
- // it. A missing wiki or claims table is a clean no-op.
123
+ // step, so a sweep of lapsed claims belongs here beside the storyboard render.
124
+ // It runs whether or not the storyboard has marker blocks to regenerate. The
125
+ // write is local and mirrors the storyboard splice. The caller's push
126
+ // publishes it. A missing wiki or claims table is a clean no-op.
125
127
  function clearExpiredClaims(runtime, options, today, logger) {
126
128
  const memPath = path.join(resolveWikiRoot(runtime, options), "MEMORY.md");
127
129
  if (!runtime.fsSync.existsSync(memPath)) return;
@@ -148,8 +150,9 @@ export async function runRefreshCommand(ctx) {
148
150
  const logger = createLogger("wiki", runtime);
149
151
  const projectRoot = resolveProjectRoot(runtime);
150
152
 
151
- // Independent of the storyboard render below (and its early returns), so a
152
- // wiki with no storyboard or no marker blocks still gets its claims swept.
153
+ // This is independent of the storyboard render below (and its early
154
+ // returns). The sweep still runs on a wiki with no storyboard and on a wiki
155
+ // with no marker blocks.
153
156
  clearExpiredClaims(runtime, options, currentDayIso(runtime), logger);
154
157
 
155
158
  const storyboardPath = path.resolve(
@@ -171,14 +174,14 @@ export async function runRefreshCommand(ctx) {
171
174
  try {
172
175
  token = config.ghToken();
173
176
  } catch {
174
- // Missing token is non-fatal; issue-list renders will fail with a stderr
175
- // warning and the block will collapse to the notice line.
177
+ // A missing token is non-fatal. An issue-list render then fails with a
178
+ // stderr warning, and the block collapses to the notice line.
176
179
  }
177
180
  // Spawn `gh` from the project root so it resolves the monorepo's origin
178
181
  // instead of whatever git context the caller's cwd happens to be in (the
179
182
  // wiki sibling repo, a subagent worktree, a service dir, etc.). Also
180
- // resolve an explicit owner/repo slug so `gh` works when origin has been
181
- // rewritten to a proxy URL (sandbox environments) — `FIT_GH_REPO` env
183
+ // resolve an explicit owner/repo slug so `gh` works when a proxy URL
184
+ // replaced origin (sandbox environments). The `FIT_GH_REPO` env var
182
185
  // overrides the parsed origin.
183
186
  const ghContext = {
184
187
  cwd: projectRoot,