@forwardimpact/libwiki 0.3.0 → 0.3.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.
Files changed (49) hide show
  1. package/README.md +30 -29
  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 +19 -16
  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
@@ -24,9 +24,9 @@ const NOT_PUBLISHED = {
24
24
  };
25
25
 
26
26
  // Failure reasons that, on the claim/release surfaces, are an unsafe-state
27
- // refusal (D7/D9 family) rather than a saved-locally success (D1): the refusal
28
- // fires before the local write is publishable, or leaves the tree unsafe for a
29
- // later whole-tree sweep, so the surface must exit non-zero.
27
+ // refusal (D7/D9 family) rather than a saved-locally success (D1). The refusal
28
+ // fires before the local write is publishable. Or it leaves the tree unsafe
29
+ // for a later whole-tree sweep. So the surface must exit non-zero.
30
30
  const UNSAFE_STATE_REASONS = new Set([
31
31
  PUSH_REASONS.PRECONDITION,
32
32
  PUSH_REASONS.RESIDUE_CONFLICT,
@@ -37,7 +37,7 @@ const UNSAFE_STATE_REASONS = new Set([
37
37
  function notPublishedMessage(err) {
38
38
  return (
39
39
  `${err.message}\n` +
40
- "The row was written to MEMORY.md but is NOT published — it remains an " +
40
+ "The row is in MEMORY.md but is NOT published. It remains an " +
41
41
  "uncommitted working-tree change.\n"
42
42
  );
43
43
  }
@@ -52,23 +52,25 @@ function memoryPath(runtime, options) {
52
52
  }
53
53
 
54
54
  /**
55
- * Push the claim/release MEMORY.md change and translate the honest outcome
56
- * (the honest-CLI contract) into a command envelope, composed with the singleton merge
57
- * discipline (the singleton merge discipline) and the secret/ancestry guards:
58
- * - landed (grounded or re-applied) ⇒ `{ ok: true }`, success message printed;
55
+ * Push the claim/release MEMORY.md change. Translate the honest outcome (the
56
+ * honest-CLI contract) into a command envelope. Compose it with the singleton
57
+ * merge discipline (the singleton merge discipline) and the secret/ancestry
58
+ * guards:
59
+ * - landed (grounded or re-applied) ⇒ `{ ok: true }` with a printed success
60
+ * message;
59
61
  * - `rejected`/`transport` ⇒ `{ ok: true }` with a saved-locally warning (the
60
- * landed-locally row is complete; the session-end push is its retry);
62
+ * landed-locally row is complete, and the session-end push is its retry);
61
63
  * - `precondition`/`residue-conflict`/`conservation` ⇒ `{ ok: false, code: 1 }`
62
- * (D7/D9 unsafe-state family — the row is not published and the tree may be
63
- * left unsafe for a later whole-tree sweep);
64
+ * (the D7/D9 unsafe-state family, where the row is not published and the
65
+ * tree may be left unsafe for a later whole-tree sweep);
64
66
  * - a secret-gate refusal ⇒ `{ ok: false, code: 1 }` ({@link refusalEnvelope});
65
- * - an {@link AncestryRefusal} is rethrown so `pushRowOrRefuse` maps it to the
66
- * not-published non-zero envelope;
67
+ * - this function rethrows an {@link AncestryRefusal} so `pushRowOrRefuse`
68
+ * maps it to the not-published non-zero envelope;
67
69
  * - any other thrown error is a network/credential failure that degrades to
68
70
  * "saved locally" (`{ ok: true }`).
69
71
  *
70
72
  * The `reapply` closure re-derives this row against the fresh tip if the
71
- * landing contends, so a parallel writer's row is never erased.
73
+ * landing contends, so this command never erases a parallel writer's row.
72
74
  *
73
75
  * @param {object} wikiSync - The WikiSync collaborator (may be absent in tests).
74
76
  * @param {object} runtime - The runtime bag (for stdout/stderr).
@@ -82,37 +84,38 @@ async function pushWiki(wikiSync, runtime, message, reapply) {
82
84
  let result;
83
85
  try {
84
86
  await wikiSync.inheritIdentity();
85
- // claim/release contract is a 1-line MEMORY.md change; the pathspec keeps
86
- // foreign uncommitted files from parallel writers out of the commit. The
87
- // `reapply` closure re-derives this row against the fresh tip if the landing
88
- // contends (the singleton merge discipline), so a parallel writer's row is never erased.
87
+ // The claim/release contract is a 1-line MEMORY.md change. The pathspec
88
+ // keeps foreign uncommitted files from parallel writers out of the commit.
89
+ // The `reapply` closure re-derives this row against the fresh tip if the
90
+ // landing contends (the singleton merge discipline), so this command never
91
+ // erases a parallel writer's row.
89
92
  result = await wikiSync.commitAndPush(message, ["MEMORY.md"], { reapply });
90
93
  } catch (err) {
91
- // An ancestry-guard refusal pierces the saved-locally degradation: rethrow
92
- // so pushRowOrRefuse maps it to the not-published non-zero envelope.
94
+ // An ancestry-guard refusal pierces the saved-locally degradation. Rethrow
95
+ // it so pushRowOrRefuse maps it to the not-published non-zero envelope.
93
96
  if (err instanceof AncestryRefusal) throw err;
94
97
  if (err instanceof WikiPushFailure) {
95
- // D7/D9 unsafe-state family: the row is not published and the tree may be
96
- // left unsafe for a later sweep — fail the command closed (non-zero).
98
+ // D7/D9 unsafe-state family. The row is not published, and the tree may
99
+ // be left unsafe for a later sweep. Fail the command closed (non-zero).
97
100
  if (UNSAFE_STATE_REASONS.has(err.reason)) {
98
101
  runtime.proc.stderr.write(`${err.message}\n`);
99
102
  return { ok: false, code: 1 };
100
103
  }
101
- // rejected / transport: the local row landed; warn and keep zero exit.
104
+ // rejected / transport: the local row landed. Warn and keep zero exit.
102
105
  runtime.proc.stderr.write(
103
- `saved locally — not yet visible to parallel sessions (${err.reason}): ${err.message}\n`,
106
+ `saved locally, not yet visible to parallel sessions (${err.reason}): ${err.message}\n`,
104
107
  );
105
108
  return { ok: true };
106
109
  }
107
- // Any other failure: preserve fire-and-forget "saved locally" — the change
108
- // is on disk and the command still succeeds.
110
+ // Any other failure: preserve fire-and-forget "saved locally". The change
111
+ // is on disk, and the command still succeeds.
109
112
  createLogger("wiki", runtime).warn(
110
113
  "claim",
111
114
  `push failed (saved locally): ${err.message}`,
112
115
  );
113
116
  return { ok: true };
114
117
  }
115
- // A secret-gate refusal fails the command closed; a grounded-landed or a
118
+ // A secret-gate refusal fails the command closed. A grounded-landed or a
116
119
  // re-applied push reports success.
117
120
  const refusal = refusalEnvelope(runtime, result);
118
121
  if (refusal) return refusal;
@@ -123,16 +126,17 @@ async function pushWiki(wikiSync, runtime, message, reapply) {
123
126
  }
124
127
 
125
128
  /**
126
- * Push a written claim/release row, mapping an ancestry-guard refusal to the
127
- * not-published non-zero envelope and any other outcome to `pushWiki`'s
128
- * envelope. The row is already written to MEMORY.md; on refusal it stays as an
129
+ * Push a written claim/release row. Map an ancestry-guard refusal to the
130
+ * not-published non-zero envelope. Map any other outcome to `pushWiki`'s
131
+ * envelope. The row is already in MEMORY.md. On a refusal it stays as an
129
132
  * uncommitted working-tree change. The `reapply` closure re-derives the same
130
133
  * row against the fresh tip when the landing contends.
131
134
  */
132
135
  async function pushRowOrRefuse(wikiSync, runtime, message, reapply) {
133
136
  try {
134
137
  // Propagate pushWiki's envelope so a secret-gate or unsafe-state refusal
135
- // ({ ok: false }) fails the command closed; a clean push returns { ok: true }.
138
+ // ({ ok: false }) fails the command closed. A clean push returns
139
+ // { ok: true }.
136
140
  return await pushWiki(wikiSync, runtime, message, reapply);
137
141
  } catch (err) {
138
142
  if (err instanceof AncestryRefusal) {
@@ -143,7 +147,7 @@ async function pushRowOrRefuse(wikiSync, runtime, message, reapply) {
143
147
  }
144
148
  }
145
149
 
146
- /** Insert a row into MEMORY.md `## Active Claims`. Refuses if (agent, target) already present. */
150
+ /** Insert a row into MEMORY.md `## Active Claims`. It refuses if (agent, target) is already present. */
147
151
  export async function runClaimCommand(ctx) {
148
152
  const { runtime, wikiSync } = ctx.deps;
149
153
  const options = ctx.options;
@@ -162,8 +166,9 @@ export async function runClaimCommand(ctx) {
162
166
  };
163
167
  }
164
168
  const today = options.today || currentDayIso(runtime);
165
- // Default expiry is claim+1 day: a claim is a short-lived "actively shipping
166
- // this now" assertion, not a long lease. A run that outlives one day re-claims.
169
+ // Default expiry is claim+1 day. A claim is a short-lived "actively shipping
170
+ // this now" assertion. It is not a long lease. A run that outlives one day
171
+ // re-claims.
167
172
  const expires = options["expires-at"] || addDays(today, 1);
168
173
  const memPath = memoryPath(runtime, options);
169
174
  const text = readMemory(runtime, memPath);
@@ -220,8 +225,8 @@ export async function runReleaseCommand(ctx) {
220
225
  }
221
226
  runtime.fsSync.writeFileSync(memPath, current);
222
227
  runtime.proc.stdout.write(`released ${count} expired claim(s)\n`);
223
- // Re-derive expiry against the fresh tip so a renewal landed since the stale
224
- // read survives; only still-expired rows are removed.
228
+ // Re-derive expiry against the fresh tip so a renewal landed since the
229
+ // stale read survives. Remove only the rows that are still expired.
225
230
  const reapply = (fresh) => {
226
231
  const freshExpired = filterExpired(parseClaims(fresh), today).expired;
227
232
  let next = fresh;
@@ -265,8 +270,9 @@ export async function runReleaseCommand(ctx) {
265
270
  return { ok: true };
266
271
  }
267
272
  runtime.proc.stdout.write(`released ${options.target}\n`);
268
- // Re-apply the same removal against the fresh tip if the landing contends;
269
- // re-removing an absent row is a no-op, so a re-release never resurrects it.
273
+ // Re-apply the same removal against the fresh tip if the landing contends.
274
+ // A second removal of an absent row is a no-op, so a re-release never
275
+ // resurrects it.
270
276
  const reapply = (fresh) => {
271
277
  const r = removeClaim(fresh, { agent, target: options.target });
272
278
  return r.removed ? r.text : null;
@@ -7,8 +7,8 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
7
7
  import { auditWiki } from "./audit.js";
8
8
 
9
9
  // The routing contract: a single open issue, addressed to the technical-writer,
10
- // holding the audit findings. The title is matched verbatim on every run so a
11
- // dirty wiki appends to one issue rather than opening a new one each day.
10
+ // that holds the audit findings. Every run matches the title verbatim, so a
11
+ // dirty wiki appends to one issue and does not open a new one each day.
12
12
  const LABEL = {
13
13
  name: "wiki-curation",
14
14
  color: "BFD4F2",
@@ -19,13 +19,14 @@ const TITLE = "Wiki curation: shared-state audit findings";
19
19
  // GitHub rejects an issue or comment body over 65536 characters. Keep the whole
20
20
  // body under a margin below that so the preamble, JSON fence, and truncation
21
21
  // notice always fit. When the findings overflow, the body carries the first N
22
- // that fit plus a count; the full list stays reproducible via `gemba-wiki audit`.
22
+ // that fit plus a count. `gemba-wiki audit` still reproduces the full list.
23
23
  const MAX_BODY = 65000;
24
24
 
25
25
  /**
26
26
  * Compose the issue body from the audit's JSON findings. The findings ride a
27
- * fenced ```json block; the body is passed to `gh` via `--body-file` (a temp
28
- * file), never argv, so untrusted finding text cannot be misread as a flag.
27
+ * fenced ```json block. The caller passes the body to `gh` through
28
+ * `--body-file` (a temp file) and never through argv. So `gh` cannot misread
29
+ * untrusted finding text as a flag.
29
30
  * @param {string} findingsJson
30
31
  * @param {{shown?: number, total?: number}} [trunc]
31
32
  * @returns {string}
@@ -34,12 +35,12 @@ function buildBody(findingsJson, { shown, total } = {}) {
34
35
  const lines = [
35
36
  "Scheduled `curate-wiki` audit found shared-wiki violations.",
36
37
  "",
37
- "Owner: **technical-writer** (service these via the curation shift; the per-PR `wiki` gate no longer reads shared wiki state).",
38
+ "Owner: **technical-writer**. Service these through the curation shift. The per-PR `wiki` gate no longer reads shared wiki state.",
38
39
  "",
39
40
  ];
40
41
  if (total != null && shown != null && shown < total) {
41
42
  lines.push(
42
- `Showing ${shown} of ${total} findings — the body was truncated to fit GitHub's comment limit. Run \`gemba-wiki audit\` for the full list.`,
43
+ `Showing ${shown} of ${total} findings. The body is truncated to fit GitHub's comment limit. Run \`gemba-wiki audit\` for the full list.`,
43
44
  "",
44
45
  );
45
46
  }
@@ -49,9 +50,10 @@ function buildBody(findingsJson, { shown, total } = {}) {
49
50
 
50
51
  /**
51
52
  * Build the largest postable body for the audit findings. The full findings
52
- * usually fit; when they don't, keep the first N `fail` findings that stay
53
- * under GitHub's body limit and label the body as truncated. The shrink steps
54
- * down proportionally to the overflow, so it converges in a couple of passes.
53
+ * usually fit. When they do not fit, keep the first N `fail` findings that
54
+ * stay under GitHub's body limit. Label the body as truncated. The shrink
55
+ * steps down proportionally to the overflow, so it converges in a couple of
56
+ * passes.
55
57
  * @param {{level: string}[]} findings
56
58
  * @returns {string}
57
59
  */
@@ -76,8 +78,8 @@ function fitBody(findings) {
76
78
 
77
79
  // Resolve the monorepo's `owner/repo` slug the way refresh.js/product-mix.js
78
80
  // do: an explicit FIT_GH_REPO override (sandbox proxy URLs), else the origin
79
- // remote parsed via the injected git client. Null lets `gh` fall back to its
80
- // own cwd resolution.
81
+ // remote parsed through the injected git client. Null lets `gh` fall back to
82
+ // its own cwd resolution.
81
83
  async function deriveRepo(gitClient, cwd, env) {
82
84
  if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
83
85
  if (!gitClient) return null;
@@ -88,7 +90,7 @@ async function deriveRepo(gitClient, cwd, env) {
88
90
  }
89
91
  }
90
92
 
91
- // A missing token is non-fatal: `gh` may still resolve ambient auth.
93
+ // A missing token is non-fatal. `gh` may still resolve ambient auth.
92
94
  async function resolveToken() {
93
95
  try {
94
96
  return (await createScriptConfig("wiki")).ghToken();
@@ -98,8 +100,9 @@ async function resolveToken() {
98
100
  }
99
101
 
100
102
  /**
101
- * Find the open `wiki-curation` issue by its verbatim title, or null. Any
102
- * parse failure or empty result is treated as "no issue" (create path).
103
+ * Find the open `wiki-curation` issue by its verbatim title, or null. This
104
+ * function treats any parse failure or empty result as "no issue" (create
105
+ * path).
103
106
  * @param {import("@forwardimpact/libcli").InvocationContext["deps"]["runtime"]} runtime
104
107
  * @param {string[]} repoArgs
105
108
  * @param {{cwd: string, env: object}} opts
@@ -129,11 +132,11 @@ async function findOpenIssue(runtime, repoArgs, opts) {
129
132
  }
130
133
 
131
134
  /**
132
- * Route the composed body to the single `wiki-curation` issue: ensure the
133
- * label, find the open issue by title, then comment on it or create it. The
134
- * body goes through a temp file (never argv) so untrusted finding text cannot
135
- * be read as a flag. On a `gh` failure the reason is logged and `ok:false`
136
- * returned so the caller exits non-zero.
135
+ * Route the composed body to the single `wiki-curation` issue: make sure the
136
+ * label exists, find the open issue by title, then comment on it or create it.
137
+ * The body goes through a temp file and never through argv. So `gh` cannot
138
+ * read untrusted finding text as a flag. On a `gh` failure this function logs
139
+ * the reason and returns `ok:false`, so the caller exits non-zero.
137
140
  * @param {import("@forwardimpact/libcli").InvocationContext} ctx
138
141
  * @param {string} body
139
142
  * @param {ReturnType<typeof createLogger>} logger
@@ -150,8 +153,8 @@ async function routeFindings(ctx, body, logger) {
150
153
  : runtime.proc.env;
151
154
  const repoArgs = repo ? ["--repo", repo] : [];
152
155
 
153
- // Ensure the label exists; a re-create on an existing label exits non-zero,
154
- // which is expected and ignored.
156
+ // Make sure the label exists. A re-create on an existing label exits
157
+ // non-zero. That is expected, and the code ignores it.
155
158
  await runtime.subprocess.run(
156
159
  "gh",
157
160
  [
@@ -169,8 +172,8 @@ async function routeFindings(ctx, body, logger) {
169
172
 
170
173
  const number = await findOpenIssue(runtime, repoArgs, { cwd, env });
171
174
 
172
- // Pass the body through a temp file, not argv — robust to length and immune
173
- // to finding text being read as a flag.
175
+ // Pass the body through a temp file instead of argv. A temp file is robust
176
+ // to length, and `gh` cannot read finding text as a flag.
174
177
  const tmp = runtime.proc.env.RUNNER_TEMP || runtime.proc.env.TMPDIR || "/tmp";
175
178
  const bodyFile = path.join(tmp, "wiki-curation-body.md");
176
179
  runtime.fsSync.writeFileSync(bodyFile, body);
@@ -208,12 +211,12 @@ async function routeFindings(ctx, body, logger) {
208
211
  }
209
212
 
210
213
  /**
211
- * Audit the shared wiki and, when it is dirty, route the findings to the
212
- * single `wiki-curation` issue (create or comment) addressed to the
213
- * technical-writer. This is the SOLE home of the shared-wiki audit verdict; the
214
- * per-PR `wiki` gate no longer reads live wiki state. A clean wiki routes
215
- * nothing. The label/search/create-or-comment logic lives here, not in the
216
- * workflow, so the curation step is one CLI call.
214
+ * Audit the shared wiki. When it is dirty, route the findings to the single
215
+ * `wiki-curation` issue (create or comment) addressed to the technical-writer.
216
+ * This is the SOLE home of the shared-wiki audit verdict. The per-PR `wiki`
217
+ * gate no longer reads live wiki state. A clean wiki routes nothing. The
218
+ * label/search/create-or-comment logic lives here and not in the workflow, so
219
+ * the curation step is one CLI call.
217
220
  *
218
221
  * @param {import("@forwardimpact/libcli").InvocationContext} ctx
219
222
  * @returns {Promise<{ok: boolean}>}
@@ -224,7 +227,7 @@ export async function runCurateCommand(ctx) {
224
227
  const { findings } = auditWiki(ctx);
225
228
 
226
229
  if (!findings.some((f) => f.level === "fail")) {
227
- runtime.proc.stdout.write("wiki audit clean — no curation issue routed\n");
230
+ runtime.proc.stdout.write("wiki audit clean, no curation issue routed\n");
228
231
  return { ok: true };
229
232
  }
230
233
 
@@ -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;