@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
@@ -14,13 +14,13 @@ import { runFixCommand } from "./commands/fix.js";
14
14
  import { runLedgerCommand } from "./commands/ledger.js";
15
15
 
16
16
  /**
17
- * Build the `gemba-wiki` libcli definition. Agent identity is never resolved from
18
- * the environment: agent-scoped subcommands require an explicit `--agent`
19
- * (`--from` for `memo`) and fail closed without it, so this module carries no
20
- * ambient agent identity. The version is resolved by libcli's `createCli` from
21
- * the bin's `packageJsonUrl`. Each subcommand carries a `handler` and (for
22
- * subcommand-bearing commands) `args`/`argsUsage` so `cli.dispatch` can route to
23
- * the per-command handler with a frozen `ctx`.
17
+ * Build the `gemba-wiki` libcli definition. This module never resolves agent
18
+ * identity from the environment. Agent-scoped subcommands require an explicit
19
+ * `--agent` (`--from` for `memo`). They fail closed without it, so this module
20
+ * carries no ambient agent identity. libcli's `createCli` resolves the version
21
+ * from the bin's `packageJsonUrl`. Each subcommand carries a `handler`. A
22
+ * command that takes subcommands also carries `args`/`argsUsage`, so
23
+ * `cli.dispatch` can route to the per-command handler with a frozen `ctx`.
24
24
  *
25
25
  * @returns {object} The libcli definition.
26
26
  */
@@ -48,12 +48,12 @@ export function createDefinition() {
48
48
 
49
49
  return {
50
50
  name: "gemba-wiki",
51
- description: "Wiki lifecycle management for the Kata agent system",
51
+ description: "Manage the wiki lifecycle for the Kata agent system",
52
52
  commands: [
53
53
  {
54
54
  name: "boot",
55
55
  description:
56
- "Print on-boot digest (priorities, claims, storyboard items) as JSON",
56
+ "Print the on-boot digest (priorities, claims, storyboard items) as JSON",
57
57
  handler: runBootCommand,
58
58
  options: {
59
59
  ...agentOpt,
@@ -101,9 +101,12 @@ export function createDefinition() {
101
101
  ...todayOpt,
102
102
  target: {
103
103
  type: "string",
104
- description: "What is being claimed (spec id, PR id, etc.)",
104
+ description: "Target to claim (spec id, PR id, etc.)",
105
+ },
106
+ branch: {
107
+ type: "string",
108
+ description: "Branch that carries the work",
105
109
  },
106
- branch: { type: "string", description: "Branch carrying the work" },
107
110
  pr: { type: "string", description: "Optional PR id" },
108
111
  "expires-at": {
109
112
  type: "string",
@@ -142,7 +145,7 @@ export function createDefinition() {
142
145
  },
143
146
  owner: {
144
147
  type: "string",
145
- description: "Owner field when promoting (default: --agent)",
148
+ description: "Owner field for promote (default: --agent)",
146
149
  },
147
150
  },
148
151
  },
@@ -190,7 +193,7 @@ export function createDefinition() {
190
193
  "dry-run": {
191
194
  type: "boolean",
192
195
  description:
193
- "Print the issue body and intended action without calling gh",
196
+ "Print the issue body and intended action. Do not call gh",
194
197
  },
195
198
  },
196
199
  },
@@ -217,7 +220,7 @@ export function createDefinition() {
217
220
  to: {
218
221
  type: "string",
219
222
  description:
220
- 'Target agent name, or "all" to broadcast (sender is skipped)',
223
+ 'Target agent name, or "all" to broadcast (skips the sender)',
221
224
  },
222
225
  message: {
223
226
  type: "string",
@@ -287,7 +290,7 @@ export function createDefinition() {
287
290
  type: "string",
288
291
  multiple: true,
289
292
  description:
290
- "Pathspec(s) limiting the write-set; omit to land the session's dirty set",
293
+ "Pathspec(s) that limit the write-set. Omit to land the session's dirty set",
291
294
  },
292
295
  },
293
296
  },
@@ -330,7 +333,7 @@ export function createDefinition() {
330
333
  gapped: {
331
334
  type: "boolean",
332
335
  description:
333
- "Render double-allocation losers as a gap, not a renumber",
336
+ "Render double-allocation losers as a gap instead of a renumber",
334
337
  },
335
338
  issue: {
336
339
  type: "string",
@@ -367,25 +370,25 @@ export function createDefinition() {
367
370
  documentation: [
368
371
  {
369
372
  title: "Operate a Predictable Agent Team",
370
- url: "https://www.forwardimpact.team/docs/libraries/predictable-team/index.md",
373
+ url: "https://www.gemba.team/docs/predictable-team/index.md",
371
374
  description:
372
375
  "End-to-end guide to wiki memory, XmR charts, and team coordination.",
373
376
  },
374
377
  {
375
378
  title: "Send a Memo or Update a Storyboard",
376
- url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md",
379
+ url: "https://www.gemba.team/docs/predictable-team/wiki-operations/index.md",
377
380
  description:
378
381
  "Send cross-team memos, refresh storyboard charts, sync the wiki, and record the product-mix metric.",
379
382
  },
380
383
  {
381
384
  title: "Audit and Auto-Fix the Wiki",
382
- url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-integrity/index.md",
385
+ url: "https://www.gemba.team/docs/predictable-team/wiki-integrity/index.md",
383
386
  description:
384
387
  "Check the wiki against the rule catalogue, auto-fix what is safe, and flag the rest for a human.",
385
388
  },
386
389
  {
387
390
  title: "Allocate Collision-Ledger Entries for Parallel Work",
388
- url: "https://www.forwardimpact.team/docs/libraries/predictable-team/collision-ledger/index.md",
391
+ url: "https://www.gemba.team/docs/predictable-team/collision-ledger/index.md",
389
392
  description:
390
393
  "Assign stable, collision-free ids to parallel work and rebuild the ledger projections.",
391
394
  },
@@ -11,8 +11,8 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
11
11
 
12
12
  /**
13
13
  * Run the wiki audit and return its findings plus the resolved project root.
14
- * Shared by `runAuditCommand` (emits them) and `runCurateCommand` (routes
15
- * them to an issue) so the two cannot drift.
14
+ * `runAuditCommand` emits the findings. `runCurateCommand` routes them to an
15
+ * issue. Both share this function, so the two cannot drift.
16
16
  * @param {import("@forwardimpact/libcli").InvocationContext} ctx
17
17
  * @returns {{ findings: object[], projectRoot: string }}
18
18
  */
@@ -32,7 +32,7 @@ export function auditWiki(ctx) {
32
32
  return { findings: runRules(RULES, auditCtx, { resolveScope }), projectRoot };
33
33
  }
34
34
 
35
- /** Run the wiki audit and emit findings. JSON via --format json. */
35
+ /** Run the wiki audit and emit findings. Use --format json for JSON. */
36
36
  export function runAuditCommand(ctx) {
37
37
  const { runtime } = ctx.deps;
38
38
  const options = ctx.options;
@@ -40,7 +40,7 @@ function renderMarkdown(digest) {
40
40
  return lines.join("\n");
41
41
  }
42
42
 
43
- /** Print the on-boot digest for the calling agent. JSON by default; --format markdown renders prose. */
43
+ /** Print the on-boot digest for the agent that runs it. JSON by default. --format markdown renders prose. */
44
44
  export function runBootCommand(ctx) {
45
45
  const { runtime } = ctx.deps;
46
46
  const options = ctx.options;
@@ -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