@forwardimpact/libwiki 0.2.30 → 0.2.32
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 +5 -2
- package/package.json +4 -4
- package/src/cli-definition.js +34 -1
- package/src/commands/audit.js +16 -3
- package/src/commands/curate.js +185 -0
- package/src/commands/fix.js +1 -1
- package/src/wiki-sync.js +41 -2
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- BEGIN:description — Do not edit. Generated from package.json. -->
|
|
4
4
|
|
|
5
|
-
Wiki lifecycle
|
|
6
|
-
|
|
5
|
+
Wiki lifecycle for agent teams — persistent memory, declarative integrity
|
|
6
|
+
audits, and a collision ledger so coordination survives across sessions and
|
|
7
|
+
parallel work.
|
|
7
8
|
|
|
8
9
|
<!-- END:description -->
|
|
9
10
|
|
|
@@ -157,3 +158,5 @@ import {
|
|
|
157
158
|
|
|
158
159
|
- [Operate a Predictable Agent Team](https://www.forwardimpact.team/docs/libraries/predictable-team/index.md)
|
|
159
160
|
- [Send a Memo or Update a Storyboard](https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md)
|
|
161
|
+
- [Audit and Auto-Fix the Wiki](https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-integrity/index.md)
|
|
162
|
+
- [Allocate Collision-Ledger Entries for Parallel Work](https://www.forwardimpact.team/docs/libraries/predictable-team/collision-ledger/index.md)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libwiki",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Wiki lifecycle
|
|
3
|
+
"version": "0.2.32",
|
|
4
|
+
"description": "Wiki lifecycle for agent teams — persistent memory, declarative integrity audits, and a collision ledger so coordination survives across sessions and parallel work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
7
7
|
"memo",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"goal": "Operate a Predictable Agent Team",
|
|
24
24
|
"trigger": "An agent finishes a session and its findings vanish because there is no shared memory to write them to.",
|
|
25
25
|
"bigHire": "give agent teams stable memory that persists across sessions.",
|
|
26
|
-
"littleHire": "send a memo or
|
|
26
|
+
"littleHire": "send a memo, run an integrity audit, or refresh a storyboard without managing the wiki infrastructure.",
|
|
27
27
|
"competesWith": "git commit messages as memory; ephemeral conversation context; starting every session from scratch"
|
|
28
28
|
}
|
|
29
29
|
],
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@forwardimpact/libcli": "^0.1.0",
|
|
49
49
|
"@forwardimpact/libconfig": "^0.1.77",
|
|
50
|
-
"@forwardimpact/
|
|
50
|
+
"@forwardimpact/libharness": "^1.0.0",
|
|
51
51
|
"@forwardimpact/libpreflight": "^0.1.0",
|
|
52
52
|
"@forwardimpact/libtelemetry": "^0.1.47",
|
|
53
53
|
"@forwardimpact/libutil": "^0.1.0",
|
package/src/cli-definition.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runClaimCommand, runReleaseCommand } from "./commands/claim.js";
|
|
|
9
9
|
import { runInboxCommand } from "./commands/inbox.js";
|
|
10
10
|
import { runRotateCommand } from "./commands/rotate.js";
|
|
11
11
|
import { runAuditCommand } from "./commands/audit.js";
|
|
12
|
+
import { runCurateCommand } from "./commands/curate.js";
|
|
12
13
|
import { runFixCommand } from "./commands/fix.js";
|
|
13
14
|
import { runLedgerCommand } from "./commands/ledger.js";
|
|
14
15
|
|
|
@@ -174,6 +175,25 @@ export function createDefinition() {
|
|
|
174
175
|
},
|
|
175
176
|
},
|
|
176
177
|
},
|
|
178
|
+
{
|
|
179
|
+
name: "curate",
|
|
180
|
+
description:
|
|
181
|
+
"Audit the shared wiki and route any findings to the single wiki-curation issue (create or comment), addressed to the technical-writer",
|
|
182
|
+
handler: runCurateCommand,
|
|
183
|
+
options: {
|
|
184
|
+
...wikiRootOpt,
|
|
185
|
+
...todayOpt,
|
|
186
|
+
repo: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "owner/repo slug (default: origin remote)",
|
|
189
|
+
},
|
|
190
|
+
"dry-run": {
|
|
191
|
+
type: "boolean",
|
|
192
|
+
description:
|
|
193
|
+
"Print the issue body and intended action without calling gh",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
177
197
|
{
|
|
178
198
|
name: "fix",
|
|
179
199
|
description:
|
|
@@ -335,6 +355,7 @@ export function createDefinition() {
|
|
|
335
355
|
"fit-wiki inbox list --agent staff-engineer",
|
|
336
356
|
"fit-wiki rotate --agent staff-engineer",
|
|
337
357
|
"fit-wiki audit",
|
|
358
|
+
"fit-wiki curate",
|
|
338
359
|
"fit-wiki fix",
|
|
339
360
|
'fit-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
|
|
340
361
|
"fit-wiki refresh",
|
|
@@ -354,7 +375,19 @@ export function createDefinition() {
|
|
|
354
375
|
title: "Send a Memo or Update a Storyboard",
|
|
355
376
|
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md",
|
|
356
377
|
description:
|
|
357
|
-
"Send cross-team memos, refresh storyboard charts, and
|
|
378
|
+
"Send cross-team memos, refresh storyboard charts, sync the wiki, and record the product-mix metric.",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
title: "Audit and Auto-Fix the Wiki",
|
|
382
|
+
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-integrity/index.md",
|
|
383
|
+
description:
|
|
384
|
+
"Check the wiki against the rule catalogue, auto-fix what is safe, and flag the rest for a human.",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
title: "Allocate Collision-Ledger Entries for Parallel Work",
|
|
388
|
+
url: "https://www.forwardimpact.team/docs/libraries/predictable-team/collision-ledger/index.md",
|
|
389
|
+
description:
|
|
390
|
+
"Assign stable, collision-free ids to parallel work and rebuild the ledger projections.",
|
|
358
391
|
},
|
|
359
392
|
],
|
|
360
393
|
};
|
package/src/commands/audit.js
CHANGED
|
@@ -9,8 +9,14 @@ import { buildContext, resolveScope } from "../audit/scopes.js";
|
|
|
9
9
|
import { currentDayIso } from "../util/clock.js";
|
|
10
10
|
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
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.
|
|
16
|
+
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
17
|
+
* @returns {{ findings: object[], projectRoot: string }}
|
|
18
|
+
*/
|
|
19
|
+
export function auditWiki(ctx) {
|
|
14
20
|
const { runtime } = ctx.deps;
|
|
15
21
|
const options = ctx.options;
|
|
16
22
|
const projectRoot = resolveProjectRoot(runtime);
|
|
@@ -23,7 +29,14 @@ export function runAuditCommand(ctx) {
|
|
|
23
29
|
fs: runtime.fsSync,
|
|
24
30
|
subprocess: runtime.subprocess,
|
|
25
31
|
});
|
|
26
|
-
|
|
32
|
+
return { findings: runRules(RULES, auditCtx, { resolveScope }), projectRoot };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Run the wiki audit and emit findings. JSON via --format json. */
|
|
36
|
+
export function runAuditCommand(ctx) {
|
|
37
|
+
const { runtime } = ctx.deps;
|
|
38
|
+
const options = ctx.options;
|
|
39
|
+
const { findings, projectRoot } = auditWiki(ctx);
|
|
27
40
|
|
|
28
41
|
runtime.proc.stdout.write(
|
|
29
42
|
options.format === "json"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { emitFindingsJson } from "@forwardimpact/libutil";
|
|
3
|
+
import { createLogger } from "@forwardimpact/libtelemetry";
|
|
4
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
5
|
+
import { parseRepoSlug } from "../issue-list-renderer.js";
|
|
6
|
+
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
7
|
+
import { auditWiki } from "./audit.js";
|
|
8
|
+
|
|
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.
|
|
12
|
+
const LABEL = {
|
|
13
|
+
name: "wiki-curation",
|
|
14
|
+
color: "BFD4F2",
|
|
15
|
+
description: "Shared-wiki audit findings from scheduled curation",
|
|
16
|
+
};
|
|
17
|
+
const TITLE = "Wiki curation: shared-state audit findings";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compose the issue body from the audit's JSON findings. The findings ride a
|
|
21
|
+
* fenced ```json block; the body is passed to `gh` via `--body-file` (a temp
|
|
22
|
+
* file), never argv, so untrusted finding text cannot be misread as a flag.
|
|
23
|
+
* @param {string} findingsJson
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function buildBody(findingsJson) {
|
|
27
|
+
return [
|
|
28
|
+
"Scheduled `curate-wiki` audit found shared-wiki violations.",
|
|
29
|
+
"",
|
|
30
|
+
"Owner: **technical-writer** (service these via the curation shift; the per-PR `wiki` gate no longer reads shared wiki state).",
|
|
31
|
+
"",
|
|
32
|
+
"```json",
|
|
33
|
+
findingsJson,
|
|
34
|
+
"```",
|
|
35
|
+
"",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolve the monorepo's `owner/repo` slug the way refresh.js/product-mix.js
|
|
40
|
+
// do: an explicit FIT_GH_REPO override (sandbox proxy URLs), else the origin
|
|
41
|
+
// remote parsed via the injected git client. Null lets `gh` fall back to its
|
|
42
|
+
// own cwd resolution.
|
|
43
|
+
async function deriveRepo(gitClient, cwd, env) {
|
|
44
|
+
if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
|
|
45
|
+
if (!gitClient) return null;
|
|
46
|
+
try {
|
|
47
|
+
return parseRepoSlug(await gitClient.remoteGetUrl("origin", { cwd }));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A missing token is non-fatal: `gh` may still resolve ambient auth.
|
|
54
|
+
async function resolveToken() {
|
|
55
|
+
try {
|
|
56
|
+
return (await createScriptConfig("wiki")).ghToken();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Audit the shared wiki and, when it is dirty, route the findings to the
|
|
64
|
+
* single `wiki-curation` issue (create or comment) addressed to the
|
|
65
|
+
* technical-writer. This is the SOLE home of the shared-wiki audit verdict; the
|
|
66
|
+
* per-PR `wiki` gate no longer reads live wiki state. A clean wiki routes
|
|
67
|
+
* nothing. The label/search/create-or-comment logic lives here, not in the
|
|
68
|
+
* workflow, so the curation step is one CLI call.
|
|
69
|
+
*
|
|
70
|
+
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
71
|
+
* @returns {Promise<{ok: boolean}>}
|
|
72
|
+
*/
|
|
73
|
+
export async function runCurateCommand(ctx) {
|
|
74
|
+
const { runtime, gitClient } = ctx.deps;
|
|
75
|
+
const logger = createLogger("wiki", runtime);
|
|
76
|
+
const { findings } = auditWiki(ctx);
|
|
77
|
+
|
|
78
|
+
if (!findings.some((f) => f.level === "fail")) {
|
|
79
|
+
runtime.proc.stdout.write("wiki audit clean — no curation issue routed\n");
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = buildBody(emitFindingsJson(findings));
|
|
84
|
+
|
|
85
|
+
if (ctx.options["dry-run"]) {
|
|
86
|
+
runtime.proc.stdout.write(
|
|
87
|
+
`[dry-run] would route findings to issue "${TITLE}":\n\n${body}`,
|
|
88
|
+
);
|
|
89
|
+
return { ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cwd = resolveProjectRoot(runtime);
|
|
93
|
+
const repo =
|
|
94
|
+
ctx.options.repo || (await deriveRepo(gitClient, cwd, runtime.proc.env));
|
|
95
|
+
const token = await resolveToken();
|
|
96
|
+
const env = token
|
|
97
|
+
? { ...runtime.proc.env, GH_TOKEN: token }
|
|
98
|
+
: runtime.proc.env;
|
|
99
|
+
const repoArgs = repo ? ["--repo", repo] : [];
|
|
100
|
+
|
|
101
|
+
// Ensure the label exists; a re-create on an existing label exits non-zero,
|
|
102
|
+
// which is expected and ignored.
|
|
103
|
+
await runtime.subprocess.run(
|
|
104
|
+
"gh",
|
|
105
|
+
[
|
|
106
|
+
"label",
|
|
107
|
+
"create",
|
|
108
|
+
LABEL.name,
|
|
109
|
+
"--color",
|
|
110
|
+
LABEL.color,
|
|
111
|
+
"--description",
|
|
112
|
+
LABEL.description,
|
|
113
|
+
...repoArgs,
|
|
114
|
+
],
|
|
115
|
+
{ cwd, env },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const list = await runtime.subprocess.run(
|
|
119
|
+
"gh",
|
|
120
|
+
[
|
|
121
|
+
"issue",
|
|
122
|
+
"list",
|
|
123
|
+
"--search",
|
|
124
|
+
`${TITLE} in:title`,
|
|
125
|
+
"--state",
|
|
126
|
+
"open",
|
|
127
|
+
"--json",
|
|
128
|
+
"number",
|
|
129
|
+
...repoArgs,
|
|
130
|
+
],
|
|
131
|
+
{ cwd, env },
|
|
132
|
+
);
|
|
133
|
+
let number = null;
|
|
134
|
+
try {
|
|
135
|
+
number = JSON.parse(list.stdout || "[]")[0]?.number ?? null;
|
|
136
|
+
} catch {
|
|
137
|
+
number = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Pass the body through a temp file, not argv — robust to length and immune
|
|
141
|
+
// to finding text being read as a flag.
|
|
142
|
+
const tmp = runtime.proc.env.RUNNER_TEMP || runtime.proc.env.TMPDIR || "/tmp";
|
|
143
|
+
const bodyFile = path.join(tmp, "wiki-curation-body.md");
|
|
144
|
+
runtime.fsSync.writeFileSync(bodyFile, body);
|
|
145
|
+
|
|
146
|
+
const result = number
|
|
147
|
+
? await runtime.subprocess.run(
|
|
148
|
+
"gh",
|
|
149
|
+
[
|
|
150
|
+
"issue",
|
|
151
|
+
"comment",
|
|
152
|
+
String(number),
|
|
153
|
+
"--body-file",
|
|
154
|
+
bodyFile,
|
|
155
|
+
...repoArgs,
|
|
156
|
+
],
|
|
157
|
+
{ cwd, env },
|
|
158
|
+
)
|
|
159
|
+
: await runtime.subprocess.run(
|
|
160
|
+
"gh",
|
|
161
|
+
[
|
|
162
|
+
"issue",
|
|
163
|
+
"create",
|
|
164
|
+
"--title",
|
|
165
|
+
TITLE,
|
|
166
|
+
"--body-file",
|
|
167
|
+
bodyFile,
|
|
168
|
+
"--label",
|
|
169
|
+
LABEL.name,
|
|
170
|
+
...repoArgs,
|
|
171
|
+
],
|
|
172
|
+
{ cwd, env },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (result.exitCode !== 0) {
|
|
176
|
+
logger.warn("curate", `gh issue ${number ? "comment" : "create"} failed`);
|
|
177
|
+
return { ok: false };
|
|
178
|
+
}
|
|
179
|
+
runtime.proc.stdout.write(
|
|
180
|
+
number
|
|
181
|
+
? `Commented curation findings on issue #${number}\n`
|
|
182
|
+
: "Opened a new wiki-curation issue\n",
|
|
183
|
+
);
|
|
184
|
+
return { ok: true };
|
|
185
|
+
}
|
package/src/commands/fix.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createAgentRunner,
|
|
6
6
|
composeProfilePrompt,
|
|
7
7
|
createRedactor,
|
|
8
|
-
} from "@forwardimpact/
|
|
8
|
+
} from "@forwardimpact/libharness";
|
|
9
9
|
import { RULES } from "../audit/rules.js";
|
|
10
10
|
import { buildContext, resolveScope } from "../audit/scopes.js";
|
|
11
11
|
import {
|
package/src/wiki-sync.js
CHANGED
|
@@ -7,6 +7,24 @@ import { scanPushWindow, appendOverrideRecord } from "./secret-gate.js";
|
|
|
7
7
|
import { runBudgetGate } from "./budget-gate.js";
|
|
8
8
|
import { currentDayIso } from "./util/clock.js";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A git `insteadOf` rule that maps a URL to itself. Some sandboxed
|
|
12
|
+
* environments install a broad global rewrite —
|
|
13
|
+
* `url.<local-proxy>.insteadOf = https://github.com/` — that diverts every
|
|
14
|
+
* github.com URL, the wiki included, to a proxy that serves only the main repo
|
|
15
|
+
* and returns 403 for the wiki. An identity rule keyed to the full wiki URL is
|
|
16
|
+
* a longer prefix match than the broad `https://github.com/` rule, so git wins
|
|
17
|
+
* it by longest-match and leaves the URL untouched; the request then reaches
|
|
18
|
+
* github.com over the ambient HTTPS proxy. Where no such broad rewrite exists,
|
|
19
|
+
* the rule is a harmless no-op. Applied inline on clone and persisted into the
|
|
20
|
+
* clone's local config for every later network op (see {@link WikiSync#pinTransport}).
|
|
21
|
+
* @param {string} url - The wiki clone URL.
|
|
22
|
+
* @returns {string} A `-c`-form `url.<url>.insteadOf=<url>` entry.
|
|
23
|
+
*/
|
|
24
|
+
function selfInsteadOf(url) {
|
|
25
|
+
return `url.${url}.insteadOf=${url}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
/** The branch the wiki clone publishes (hard-coded in fetch / rebase / push). */
|
|
11
29
|
const BRANCH = "master";
|
|
12
30
|
const REMOTE = "origin";
|
|
@@ -215,15 +233,36 @@ export class WikiSync {
|
|
|
215
233
|
|
|
216
234
|
/** Clone the wiki from `url` if it is not already cloned. */
|
|
217
235
|
async ensureCloned(url) {
|
|
218
|
-
if (this.isCloned())
|
|
236
|
+
if (this.isCloned()) {
|
|
237
|
+
await this.#pinTransport(url);
|
|
238
|
+
return { cloned: true, reason: "already-cloned" };
|
|
239
|
+
}
|
|
219
240
|
try {
|
|
220
|
-
await this.#authed().clone(url, this.#wikiDir
|
|
241
|
+
await this.#authed().clone(url, this.#wikiDir, {
|
|
242
|
+
config: [selfInsteadOf(url)],
|
|
243
|
+
});
|
|
244
|
+
await this.#pinTransport(url);
|
|
221
245
|
return { cloned: true, reason: "cloned" };
|
|
222
246
|
} catch (err) {
|
|
223
247
|
return { cloned: false, reason: err.stderr?.trim() || err.message };
|
|
224
248
|
}
|
|
225
249
|
}
|
|
226
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Persist the identity-`insteadOf` for the wiki's own URL into the clone's
|
|
253
|
+
* local config, so every later network op (fetch / push / ls-remote) that
|
|
254
|
+
* runs against the stored remote URL — and re-applies `insteadOf` at
|
|
255
|
+
* transport time — is covered without threading `-c` through each call. The
|
|
256
|
+
* clone command itself takes the same rule inline (the `.git/config` does not
|
|
257
|
+
* exist yet). Idempotent; safe to re-run on a resumed clone. See
|
|
258
|
+
* {@link selfInsteadOf} for why the rule is needed.
|
|
259
|
+
*/
|
|
260
|
+
async #pinTransport(url) {
|
|
261
|
+
await this.#git.configSet(`url.${url}.insteadOf`, url, {
|
|
262
|
+
cwd: this.#wikiDir,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
/** Copy git user.name and user.email from the parent repository into the wiki repository. */
|
|
228
267
|
async inheritIdentity() {
|
|
229
268
|
const name = await this.#git.configGet("user.name", {
|