@forwardimpact/libwiki 0.2.12 → 0.2.14
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/bin/fit-wiki.js +5 -8
- package/package.json +1 -1
- package/src/audit/rules.js +5 -0
- package/src/cli-definition.js +7 -8
- package/src/commands/fix.js +157 -37
- package/src/commands/refresh.js +17 -1
- package/src/util/wiki-dir.js +1 -1
package/bin/fit-wiki.js
CHANGED
|
@@ -16,14 +16,11 @@ const NEEDS_WIKI_SYNC = new Set(["claim", "release", "push", "pull", "init"]);
|
|
|
16
16
|
|
|
17
17
|
async function main() {
|
|
18
18
|
const runtime = createDefaultRuntime();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
25
|
-
const definition = createDefinition(runtime.proc.env, version);
|
|
26
|
-
const cli = createCli(definition, { runtime });
|
|
19
|
+
const definition = createDefinition(runtime.proc.env);
|
|
20
|
+
const cli = createCli(definition, {
|
|
21
|
+
runtime,
|
|
22
|
+
packageJsonUrl: new URL("../package.json", import.meta.url),
|
|
23
|
+
});
|
|
27
24
|
|
|
28
25
|
const parsed = cli.parse(runtime.proc.argv.slice(2));
|
|
29
26
|
if (!parsed) return runtime.proc.exit(0); // --help / --version already printed
|
package/package.json
CHANGED
package/src/audit/rules.js
CHANGED
|
@@ -257,6 +257,7 @@ export const RULES = [
|
|
|
257
257
|
id: "weekly-log.line-budget",
|
|
258
258
|
scope: "weekly-log-main",
|
|
259
259
|
severity: "fail",
|
|
260
|
+
remediation: "rotate",
|
|
260
261
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
261
262
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
262
263
|
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
@@ -265,6 +266,7 @@ export const RULES = [
|
|
|
265
266
|
id: "weekly-log.word-budget",
|
|
266
267
|
scope: "weekly-log-main",
|
|
267
268
|
severity: "fail",
|
|
269
|
+
remediation: "rotate",
|
|
268
270
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
269
271
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
270
272
|
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
@@ -282,6 +284,7 @@ export const RULES = [
|
|
|
282
284
|
id: "decision-block.heading-within-5",
|
|
283
285
|
scope: "weekly-log-main",
|
|
284
286
|
severity: "fail",
|
|
287
|
+
remediation: "flag",
|
|
285
288
|
check: decisionWithin5({
|
|
286
289
|
entryRe: /^## \d{4}-\d{2}-\d{2}(?:[\s(].*)?$/,
|
|
287
290
|
requiredLine: DECISION_HEADING,
|
|
@@ -305,6 +308,7 @@ export const RULES = [
|
|
|
305
308
|
id: "weekly-log-part.line-budget",
|
|
306
309
|
scope: "weekly-log-part",
|
|
307
310
|
severity: "fail",
|
|
311
|
+
remediation: "flag",
|
|
308
312
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
309
313
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
310
314
|
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
|
@@ -313,6 +317,7 @@ export const RULES = [
|
|
|
313
317
|
id: "weekly-log-part.word-budget",
|
|
314
318
|
scope: "weekly-log-part",
|
|
315
319
|
severity: "fail",
|
|
320
|
+
remediation: "flag",
|
|
316
321
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
317
322
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
318
323
|
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
package/src/cli-definition.js
CHANGED
|
@@ -13,16 +13,16 @@ import { runFixCommand } from "./commands/fix.js";
|
|
|
13
13
|
/**
|
|
14
14
|
* Build the `fit-wiki` libcli definition. The agent/sender defaults read the
|
|
15
15
|
* injected `env` rather than the ambient `process.env` so this module carries
|
|
16
|
-
* no ambient dependency; the bin shim passes `runtime.proc.env
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* per-command handler
|
|
16
|
+
* no ambient dependency; the bin shim passes `runtime.proc.env`. The version is
|
|
17
|
+
* resolved by libcli's `createCli` from the bin's `packageJsonUrl`. Each
|
|
18
|
+
* subcommand carries a `handler` and (for subcommand-bearing commands)
|
|
19
|
+
* `args`/`argsUsage` so `cli.dispatch` can route to the per-command handler
|
|
20
|
+
* with a frozen `ctx`.
|
|
20
21
|
*
|
|
21
22
|
* @param {Record<string, string>} env - The process env (`runtime.proc.env`).
|
|
22
|
-
* @param {string} version - The package version string.
|
|
23
23
|
* @returns {object} The libcli definition.
|
|
24
24
|
*/
|
|
25
|
-
export function createDefinition(env
|
|
25
|
+
export function createDefinition(env) {
|
|
26
26
|
const wikiRootOpt = {
|
|
27
27
|
"wiki-root": {
|
|
28
28
|
type: "string",
|
|
@@ -48,7 +48,6 @@ export function createDefinition(env, version) {
|
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
50
|
name: "fit-wiki",
|
|
51
|
-
version,
|
|
52
51
|
description: "Wiki lifecycle management for the Kata agent system",
|
|
53
52
|
commands: [
|
|
54
53
|
{
|
|
@@ -174,7 +173,7 @@ export function createDefinition(env, version) {
|
|
|
174
173
|
{
|
|
175
174
|
name: "fix",
|
|
176
175
|
description:
|
|
177
|
-
"Auto-fix wiki audit findings
|
|
176
|
+
"Auto-fix wiki audit findings: rotate weekly logs, fix the rest with an AI agent (technical-writer, Haiku), flag the unresolvable",
|
|
178
177
|
handler: runFixCommand,
|
|
179
178
|
options: {
|
|
180
179
|
...wikiRootOpt,
|
package/src/commands/fix.js
CHANGED
|
@@ -8,14 +8,25 @@ import {
|
|
|
8
8
|
} from "@forwardimpact/libeval";
|
|
9
9
|
import { RULES } from "../audit/rules.js";
|
|
10
10
|
import { buildContext, resolveScope } from "../audit/scopes.js";
|
|
11
|
+
import { rotateIfOverBudget, weeklyLogPath } from "../weekly-log.js";
|
|
11
12
|
import { currentDayIso } from "../util/clock.js";
|
|
12
13
|
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Pipeline: audit → deterministic rotation (the one fix needing a file seal the
|
|
16
|
+
// agent can't do) → re-audit → Haiku agent on the prose-judgment residual →
|
|
17
|
+
// flag what neither should touch. MAX_ROUNDS still caps the agent loop so an
|
|
18
|
+
// unresolvable agent-class finding fails loudly rather than spinning forever.
|
|
17
19
|
const MAX_ROUNDS = 3;
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A finding's remediation class, from the declarative rule. Rules without a
|
|
23
|
+
* `remediation` field default to `"agent"`: the Haiku agent handles all
|
|
24
|
+
* prose-judgment fixes (summary trims, section order, MEMORY.md prose).
|
|
25
|
+
*/
|
|
26
|
+
function classOf(finding) {
|
|
27
|
+
return RULES.find((r) => r.id === finding.id)?.remediation ?? "agent";
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* Every rule governing a scope with an open finding, as `id — hint` lines.
|
|
21
32
|
* Handing the agent the full contract for the files it edits — not just the
|
|
@@ -63,6 +74,56 @@ function composeFollowup(findings, projectRoot) {
|
|
|
63
74
|
].join("\n");
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Deterministic pre-pass: seal every over-budget current-week weekly-log main
|
|
79
|
+
* file via `rotateIfOverBudget`. The agent name comes from the audit's own
|
|
80
|
+
* subjects (keyed by path) — no filename parsing. `force: true` rotates even a
|
|
81
|
+
* word-over/line-under file.
|
|
82
|
+
*
|
|
83
|
+
* `rotateIfOverBudget` always seals the agent's *current-week* log, so we only
|
|
84
|
+
* call it when the finding IS that file. A prior-week over-budget main is left
|
|
85
|
+
* untouched (rotating it would force-seal a healthy current-week log instead);
|
|
86
|
+
* it survives the re-audit and is flagged for a human.
|
|
87
|
+
*/
|
|
88
|
+
function rotateOverBudgetMainLogs(
|
|
89
|
+
findings,
|
|
90
|
+
{ wikiRoot, today, projectRoot, fs, out },
|
|
91
|
+
) {
|
|
92
|
+
const subjects = buildContext({ wikiRoot, today, fs }).subjects[
|
|
93
|
+
"weekly-log-main"
|
|
94
|
+
];
|
|
95
|
+
const agentByPath = new Map(subjects.map((s) => [s.path, s.agentPrefix]));
|
|
96
|
+
for (const f of findings) {
|
|
97
|
+
if (classOf(f) !== "rotate") continue;
|
|
98
|
+
const agent = agentByPath.get(f.path);
|
|
99
|
+
if (!agent) continue;
|
|
100
|
+
if (weeklyLogPath(wikiRoot, agent, today) !== f.path) continue;
|
|
101
|
+
const res = rotateIfOverBudget(
|
|
102
|
+
wikiRoot,
|
|
103
|
+
agent,
|
|
104
|
+
today,
|
|
105
|
+
0,
|
|
106
|
+
{ force: true },
|
|
107
|
+
fs,
|
|
108
|
+
);
|
|
109
|
+
if (res.rotated) {
|
|
110
|
+
out(
|
|
111
|
+
`rotated ${path.relative(projectRoot, res.fromPath)} -> ` +
|
|
112
|
+
`${path.relative(projectRoot, res.toPath)}\n`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Report findings that need human judgment — never auto-fixed. */
|
|
119
|
+
function reportFlags(err, flagFindings, projectRoot) {
|
|
120
|
+
err(
|
|
121
|
+
`fit-wiki fix: ${flagFindings.length} finding(s) need human judgment ` +
|
|
122
|
+
`(not auto-fixable):\n` +
|
|
123
|
+
emitFindingsText(flagFindings, { cwd: projectRoot }),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
66
127
|
/**
|
|
67
128
|
* Surface a round's agent error, if any. Returns true when it is fatal: a
|
|
68
129
|
* missing sessionId means the process never started (e.g. the SDK refused
|
|
@@ -80,64 +141,123 @@ function isFatalError(result, round, err) {
|
|
|
80
141
|
return false;
|
|
81
142
|
}
|
|
82
143
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
const { runtime } = ctx.deps;
|
|
86
|
-
const projectRoot = resolveProjectRoot(runtime);
|
|
87
|
-
const wikiRoot = ctx.options["wiki-root"] || path.join(projectRoot, "wiki");
|
|
88
|
-
const today = ctx.options.today || currentDayIso(runtime);
|
|
89
|
-
const out = (s) => runtime.proc.stdout.write(s);
|
|
90
|
-
const err = (s) => runtime.proc.stderr.write(s);
|
|
91
|
-
|
|
92
|
-
// The agent's edits change the result, so re-read and re-audit each round.
|
|
93
|
-
const audit = () =>
|
|
94
|
-
runRules(RULES, buildContext({ wikiRoot, today, fs: runtime.fsSync }), {
|
|
95
|
-
resolveScope,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
let findings = audit();
|
|
99
|
-
if (findings.length === 0) {
|
|
100
|
-
out("nothing to fix\n");
|
|
101
|
-
return { ok: true };
|
|
102
|
-
}
|
|
103
|
-
|
|
144
|
+
/** Build the Haiku technical-writer runner for prose-judgment fixes. */
|
|
145
|
+
async function buildFixRunner(ctx, projectRoot, runtime) {
|
|
104
146
|
const query =
|
|
105
147
|
ctx.deps.query ?? (await import("@anthropic-ai/claude-agent-sdk")).query;
|
|
106
|
-
|
|
148
|
+
return createAgentRunner({
|
|
107
149
|
cwd: projectRoot,
|
|
108
150
|
query,
|
|
109
151
|
output: new Writable({ write: (_c, _e, cb) => cb() }),
|
|
110
152
|
model: "claude-haiku-4-5-20251001",
|
|
111
153
|
maxTurns: 30,
|
|
112
|
-
allowedTools: ["Read", "Write", "Edit"],
|
|
154
|
+
allowedTools: ["Read", "Glob", "Write", "Edit"],
|
|
113
155
|
settingSources: ["project"],
|
|
114
156
|
systemPrompt: composeProfilePrompt("technical-writer", {
|
|
115
157
|
profilesDir: path.resolve(projectRoot, ".claude/agents"),
|
|
158
|
+
runtime,
|
|
116
159
|
}),
|
|
117
|
-
redactor: createRedactor(),
|
|
160
|
+
redactor: createRedactor({ runtime }),
|
|
118
161
|
});
|
|
162
|
+
}
|
|
119
163
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Run the agent on the prose-judgment findings, re-auditing each round until
|
|
166
|
+
* clean, flag-only, or MAX_ROUNDS is exhausted. The audit is the verdict, not
|
|
167
|
+
* the agent's self-report; resuming extends the turn budget for a trim too
|
|
168
|
+
* large for one round.
|
|
169
|
+
*/
|
|
170
|
+
async function runAgentRounds(runner, agentFindings, deps) {
|
|
171
|
+
const { wikiRoot, projectRoot, audit, partition, out, err } = deps;
|
|
172
|
+
let task = composeTask(agentFindings, wikiRoot, projectRoot);
|
|
173
|
+
let flagFindings = [];
|
|
124
174
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
125
175
|
const result =
|
|
126
176
|
round === 0 ? await runner.run(task) : await runner.resume(task);
|
|
127
177
|
if (result.text) out(result.text + "\n");
|
|
128
178
|
if (isFatalError(result, round, err)) return { ok: false, code: 1 };
|
|
129
179
|
|
|
180
|
+
({ agentFindings, flagFindings } = partition(audit()));
|
|
181
|
+
if (agentFindings.length === 0) {
|
|
182
|
+
if (flagFindings.length === 0) {
|
|
183
|
+
out("fixed: wiki audit is clean\n");
|
|
184
|
+
return { ok: true, code: 0 };
|
|
185
|
+
}
|
|
186
|
+
reportFlags(err, flagFindings, projectRoot);
|
|
187
|
+
return { ok: false, code: 2 };
|
|
188
|
+
}
|
|
189
|
+
task = composeFollowup(agentFindings, projectRoot);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
err(
|
|
193
|
+
`fit-wiki fix: ${agentFindings.length} finding(s) remain after ` +
|
|
194
|
+
`${MAX_ROUNDS} round(s):\n` +
|
|
195
|
+
emitFindingsText(agentFindings, { cwd: projectRoot }),
|
|
196
|
+
);
|
|
197
|
+
if (flagFindings.length > 0) reportFlags(err, flagFindings, projectRoot);
|
|
198
|
+
return { ok: false, code: 1 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Run the wiki audit and auto-fix findings: rotate, then agent, then flag. */
|
|
202
|
+
export async function runFixCommand(ctx) {
|
|
203
|
+
const { runtime } = ctx.deps;
|
|
204
|
+
const fs = runtime.fsSync;
|
|
205
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
206
|
+
const wikiRoot = ctx.options["wiki-root"] || path.join(projectRoot, "wiki");
|
|
207
|
+
const today = ctx.options.today || currentDayIso(runtime);
|
|
208
|
+
const out = (s) => runtime.proc.stdout.write(s);
|
|
209
|
+
const err = (s) => runtime.proc.stderr.write(s);
|
|
210
|
+
|
|
211
|
+
// The agent's edits change the result, so re-read and re-audit each round.
|
|
212
|
+
const audit = () =>
|
|
213
|
+
runRules(RULES, buildContext({ wikiRoot, today, fs }), { resolveScope });
|
|
214
|
+
// The agent only ever gets prose-judgment (`agent`-class) findings. A
|
|
215
|
+
// `rotate` finding that survived the pre-pass (e.g. a prior-week log) is
|
|
216
|
+
// unfixable by the agent — and trimming append-only history to satisfy a
|
|
217
|
+
// budget would corrupt it — so it joins the flag set for a human.
|
|
218
|
+
const partition = (found) => ({
|
|
219
|
+
agentFindings: found.filter((f) => classOf(f) === "agent"),
|
|
220
|
+
flagFindings: found.filter((f) => classOf(f) !== "agent"),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
let findings = audit();
|
|
224
|
+
if (findings.length === 0) {
|
|
225
|
+
out("nothing to fix\n");
|
|
226
|
+
return { ok: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Deterministic layer: weekly-log rotation only.
|
|
230
|
+
if (findings.some((f) => classOf(f) === "rotate")) {
|
|
231
|
+
rotateOverBudgetMainLogs(findings, {
|
|
232
|
+
wikiRoot,
|
|
233
|
+
today,
|
|
234
|
+
projectRoot,
|
|
235
|
+
fs,
|
|
236
|
+
out,
|
|
237
|
+
});
|
|
130
238
|
findings = audit();
|
|
131
239
|
if (findings.length === 0) {
|
|
132
240
|
out("fixed: wiki audit is clean\n");
|
|
133
241
|
return { ok: true, code: 0 };
|
|
134
242
|
}
|
|
135
|
-
task = composeFollowup(findings, projectRoot);
|
|
136
243
|
}
|
|
137
244
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
|
|
245
|
+
// Residual: agent-class goes to the writer; everything else (flag, plus any
|
|
246
|
+
// rotate finding the deterministic pass could not handle) needs a human.
|
|
247
|
+
const { agentFindings, flagFindings } = partition(findings);
|
|
248
|
+
if (agentFindings.length === 0) {
|
|
249
|
+
reportFlags(err, flagFindings, projectRoot);
|
|
250
|
+
return { ok: false, code: 2 };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Constructed only now, so a rotation-only or flag-only run never spawns it.
|
|
254
|
+
const runner = await buildFixRunner(ctx, projectRoot, runtime);
|
|
255
|
+
return runAgentRounds(runner, agentFindings, {
|
|
256
|
+
wikiRoot,
|
|
257
|
+
projectRoot,
|
|
258
|
+
audit,
|
|
259
|
+
partition,
|
|
260
|
+
out,
|
|
261
|
+
err,
|
|
262
|
+
});
|
|
143
263
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -53,6 +53,18 @@ function spliceBlock(lines, block, rendered) {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// A missing current-month storyboard (e.g. a coaching run early in the month,
|
|
57
|
+
// before the storyboard meeting created it) is non-fatal: return null so the
|
|
58
|
+
// deterministic refresh step exits cleanly instead of failing the job.
|
|
59
|
+
function readStoryboardOrNull(runtime, storyboardPath) {
|
|
60
|
+
try {
|
|
61
|
+
return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code !== "ENOENT") throw err;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
/** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
|
|
57
69
|
export async function runRefreshCommand(ctx) {
|
|
58
70
|
const { runtime, gitClient } = ctx.deps;
|
|
@@ -63,7 +75,11 @@ export async function runRefreshCommand(ctx) {
|
|
|
63
75
|
projectRoot,
|
|
64
76
|
ctx.args["storyboard-path"] || currentStoryboardRelPath(runtime),
|
|
65
77
|
);
|
|
66
|
-
const text = runtime
|
|
78
|
+
const text = readStoryboardOrNull(runtime, storyboardPath);
|
|
79
|
+
if (text === null) {
|
|
80
|
+
runtime.proc.stderr.write(`refresh: no storyboard at ${storyboardPath}\n`);
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
67
83
|
const blocks = scanMarkers(text, {
|
|
68
84
|
warn: (message) => runtime.proc.stderr.write(message),
|
|
69
85
|
});
|
package/src/util/wiki-dir.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
/**
|
|
4
4
|
* Find the project root by upward `package.json` discovery from the current
|
|
5
5
|
* working directory, using the injected `runtime.finder` (the one canonical
|
|
6
|
-
* Finder
|
|
6
|
+
* Finder, constructed only inside libutil).
|
|
7
7
|
* @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
|
|
8
8
|
* @returns {string}
|
|
9
9
|
*/
|