@forwardimpact/libwiki 0.2.13 → 0.2.15
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 -12
- package/src/audit/scopes.js +5 -6
- package/src/cli-definition.js +7 -8
- package/src/commands/fix.js +156 -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
|
@@ -77,7 +77,6 @@ const columnCount = (expected) => (s) =>
|
|
|
77
77
|
|
|
78
78
|
const exists = (s) => (s.exists ? null : {});
|
|
79
79
|
const expired = (s, ctx) => (s.expires_at < ctx.today ? {} : null);
|
|
80
|
-
const always = () => ({});
|
|
81
80
|
|
|
82
81
|
function entryHasDecision(lines, startIdx, requiredLine, stopRe) {
|
|
83
82
|
let seen = 0;
|
|
@@ -257,6 +256,7 @@ export const RULES = [
|
|
|
257
256
|
id: "weekly-log.line-budget",
|
|
258
257
|
scope: "weekly-log-main",
|
|
259
258
|
severity: "fail",
|
|
259
|
+
remediation: "rotate",
|
|
260
260
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
261
261
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
262
262
|
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
@@ -265,6 +265,7 @@ export const RULES = [
|
|
|
265
265
|
id: "weekly-log.word-budget",
|
|
266
266
|
scope: "weekly-log-main",
|
|
267
267
|
severity: "fail",
|
|
268
|
+
remediation: "rotate",
|
|
268
269
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
269
270
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
270
271
|
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
@@ -282,6 +283,7 @@ export const RULES = [
|
|
|
282
283
|
id: "decision-block.heading-within-5",
|
|
283
284
|
scope: "weekly-log-main",
|
|
284
285
|
severity: "fail",
|
|
286
|
+
remediation: "flag",
|
|
285
287
|
check: decisionWithin5({
|
|
286
288
|
entryRe: /^## \d{4}-\d{2}-\d{2}(?:[\s(].*)?$/,
|
|
287
289
|
requiredLine: DECISION_HEADING,
|
|
@@ -305,6 +307,7 @@ export const RULES = [
|
|
|
305
307
|
id: "weekly-log-part.line-budget",
|
|
306
308
|
scope: "weekly-log-part",
|
|
307
309
|
severity: "fail",
|
|
310
|
+
remediation: "flag",
|
|
308
311
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
309
312
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
310
313
|
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
|
@@ -313,6 +316,7 @@ export const RULES = [
|
|
|
313
316
|
id: "weekly-log-part.word-budget",
|
|
314
317
|
scope: "weekly-log-part",
|
|
315
318
|
severity: "fail",
|
|
319
|
+
remediation: "flag",
|
|
316
320
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
317
321
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
318
322
|
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
|
@@ -487,15 +491,4 @@ export const RULES = [
|
|
|
487
491
|
// -- STATUS.md rows (per-migration-unit sub-row schema) --
|
|
488
492
|
|
|
489
493
|
...STATUS_ROW_RULES,
|
|
490
|
-
|
|
491
|
-
// -- Stray files --
|
|
492
|
-
|
|
493
|
-
{
|
|
494
|
-
id: "wiki.stray-file",
|
|
495
|
-
scope: "stray-file",
|
|
496
|
-
severity: "fail",
|
|
497
|
-
check: always,
|
|
498
|
-
message: () => "Does not match any known scope",
|
|
499
|
-
hint: "rename to a recognized scope (summary, weekly log, weekly-log part) or remove the file",
|
|
500
|
-
},
|
|
501
494
|
];
|
package/src/audit/scopes.js
CHANGED
|
@@ -74,8 +74,7 @@ function classifyFile(filePath, fs) {
|
|
|
74
74
|
const base = path.basename(filePath);
|
|
75
75
|
if (EXCLUDED_BASES.has(base)) return null;
|
|
76
76
|
// STATUS.md is loaded separately (loadStatus) and audited via the dedicated
|
|
77
|
-
// `status-row` scope — skip the per-file classification
|
|
78
|
-
// as a stray file.
|
|
77
|
+
// `status-row` scope — skip the per-file classification.
|
|
79
78
|
if (base === "STATUS.md") return null;
|
|
80
79
|
if (NON_SUMMARY_PREFIXES.some((p) => base.startsWith(p))) return null;
|
|
81
80
|
if (WEEKLY_LOG_NAME_RE.test(base)) {
|
|
@@ -85,8 +84,10 @@ function classifyFile(filePath, fs) {
|
|
|
85
84
|
return { kind: "weekly-log-part", subject: loadFile(filePath, fs) };
|
|
86
85
|
}
|
|
87
86
|
const subject = loadFile(filePath, fs);
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Files that do not match a summary or weekly-log shape are left
|
|
88
|
+
// unclassified: stray files are not audited.
|
|
89
|
+
if (!SUMMARY_H1_RE.test(subject.firstLine)) return null;
|
|
90
|
+
return { kind: "summary", subject };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function loadMemory(wikiRoot, fs) {
|
|
@@ -216,7 +217,6 @@ const SCOPE_RESOLVERS = {
|
|
|
216
217
|
...r,
|
|
217
218
|
path: ctx.status.path,
|
|
218
219
|
})),
|
|
219
|
-
"stray-file": (ctx) => ctx.subjects.stray,
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
/** Resolve a scope key into the list of subjects the engine should iterate. */
|
|
@@ -236,7 +236,6 @@ export function buildContext({ wikiRoot, today, fs }) {
|
|
|
236
236
|
summary: [],
|
|
237
237
|
"weekly-log-main": [],
|
|
238
238
|
"weekly-log-part": [],
|
|
239
|
-
stray: [],
|
|
240
239
|
};
|
|
241
240
|
for (const file of listMdFiles(wikiRoot, fs)) {
|
|
242
241
|
const classified = classifyFile(file, fs);
|
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,65 +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"),
|
|
116
158
|
runtime,
|
|
117
159
|
}),
|
|
118
|
-
redactor: createRedactor(),
|
|
160
|
+
redactor: createRedactor({ runtime }),
|
|
119
161
|
});
|
|
162
|
+
}
|
|
120
163
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 = [];
|
|
125
174
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
126
175
|
const result =
|
|
127
176
|
round === 0 ? await runner.run(task) : await runner.resume(task);
|
|
128
177
|
if (result.text) out(result.text + "\n");
|
|
129
178
|
if (isFatalError(result, round, err)) return { ok: false, code: 1 };
|
|
130
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
|
+
});
|
|
131
238
|
findings = audit();
|
|
132
239
|
if (findings.length === 0) {
|
|
133
240
|
out("fixed: wiki audit is clean\n");
|
|
134
241
|
return { ok: true, code: 0 };
|
|
135
242
|
}
|
|
136
|
-
task = composeFollowup(findings, projectRoot);
|
|
137
243
|
}
|
|
138
244
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
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
|
+
});
|
|
144
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
|
*/
|