@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.
- package/README.md +53 -33
- package/package.json +1 -1
- package/src/active-claims.js +5 -5
- package/src/agent-roster.js +2 -2
- package/src/audit/admission.js +14 -11
- package/src/audit/conflict-markers-rule.js +9 -9
- package/src/audit/grammar.js +21 -18
- package/src/audit/rule-builders.js +20 -19
- package/src/audit/rules.js +30 -27
- package/src/audit/scopes.js +34 -33
- package/src/audit/status-row.js +14 -15
- package/src/block-renderer.js +5 -4
- package/src/boot.js +10 -8
- package/src/budget-gate.js +39 -36
- package/src/budget.js +3 -3
- package/src/cli-definition.js +23 -20
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +1 -1
- package/src/commands/claim.js +44 -38
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +68 -64
- package/src/commands/inbox.js +1 -1
- package/src/commands/init.js +12 -7
- package/src/commands/ledger.js +11 -11
- package/src/commands/log.js +19 -17
- package/src/commands/memo.js +4 -1
- package/src/commands/product-mix.js +16 -15
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +9 -8
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +37 -33
- package/src/gitattributes.js +10 -9
- package/src/integrity.js +29 -27
- package/src/issue-list-renderer.js +24 -16
- package/src/lane-files.js +11 -10
- package/src/ledger/anchor.js +6 -6
- package/src/ledger/projection.js +35 -32
- package/src/ledger/reader.js +4 -4
- package/src/marker-scanner.js +3 -2
- package/src/sanitize.js +12 -11
- package/src/secret-gate.js +41 -40
- package/src/status.js +12 -11
- package/src/storyboard-skeleton.js +20 -18
- package/src/util/agent-flag.js +8 -8
- package/src/util/clock.js +1 -1
- package/src/util/wiki-dir.js +7 -7
- package/src/weekly-log.js +115 -101
- package/src/wiki-sync.js +393 -361
package/src/cli-definition.js
CHANGED
|
@@ -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.
|
|
18
|
-
* the environment
|
|
19
|
-
* (`--from` for `memo`)
|
|
20
|
-
* ambient agent identity.
|
|
21
|
-
* the bin's `packageJsonUrl`. Each subcommand carries a `handler
|
|
22
|
-
*
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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 (
|
|
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)
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
},
|
package/src/commands/audit.js
CHANGED
|
@@ -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
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
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;
|
package/src/commands/boot.js
CHANGED
|
@@ -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
|
|
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;
|
package/src/commands/claim.js
CHANGED
|
@@ -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)
|
|
28
|
-
// fires before the local write is publishable
|
|
29
|
-
// later whole-tree sweep
|
|
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
|
|
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
|
|
56
|
-
*
|
|
57
|
-
* discipline (the singleton merge discipline) and the secret/ancestry
|
|
58
|
-
*
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
86
|
-
// foreign uncommitted files from parallel writers out of the commit.
|
|
87
|
-
// `reapply` closure re-derives this row against the fresh tip if the
|
|
88
|
-
// contends (the singleton merge discipline), so
|
|
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
|
|
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
|
|
96
|
-
// left unsafe for a later sweep
|
|
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
|
|
104
|
+
// rejected / transport: the local row landed. Warn and keep zero exit.
|
|
102
105
|
runtime.proc.stderr.write(
|
|
103
|
-
`saved locally
|
|
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"
|
|
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
|
|
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
|
|
127
|
-
* not-published non-zero envelope
|
|
128
|
-
* envelope. The row is already
|
|
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
|
|
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`.
|
|
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
|
|
166
|
-
// this now" assertion
|
|
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
|
|
224
|
-
// read survives
|
|
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
|
-
//
|
|
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;
|
package/src/commands/curate.js
CHANGED
|
@@ -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
|
-
//
|
|
11
|
-
// dirty wiki appends to one issue
|
|
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
|
|
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
|
|
28
|
-
* file)
|
|
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
|
|
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
|
|
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
|
|
53
|
-
* under GitHub's body limit
|
|
54
|
-
* down proportionally to the overflow, so it converges in a couple of
|
|
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
|
|
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
|
|
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.
|
|
102
|
-
* parse failure or empty result
|
|
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:
|
|
133
|
-
* label, find the open issue by title, then comment on it or create it.
|
|
134
|
-
* body goes through a temp file
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
//
|
|
154
|
-
//
|
|
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
|
|
173
|
-
// to
|
|
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
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
|
230
|
+
runtime.proc.stdout.write("wiki audit clean, no curation issue routed\n");
|
|
228
231
|
return { ok: true };
|
|
229
232
|
}
|
|
230
233
|
|