@forwardimpact/libwiki 0.2.24 → 0.2.25

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 CHANGED
@@ -6,8 +6,13 @@ import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
6
6
  import { GitClient } from "@forwardimpact/libutil/git-client";
7
7
  import { createScriptConfig } from "@forwardimpact/libconfig";
8
8
  import { createCli } from "@forwardimpact/libcli";
9
+ import { createLogger } from "@forwardimpact/libtelemetry";
9
10
  import { WikiSync } from "../src/wiki-sync.js";
10
- import { resolveProjectRoot, resolveWikiRoot } from "../src/util/wiki-dir.js";
11
+ import {
12
+ resolveProjectRoot,
13
+ resolveWikiRoot,
14
+ wikiExists,
15
+ } from "../src/util/wiki-dir.js";
11
16
  import { createDefinition } from "../src/cli-definition.js";
12
17
 
13
18
  // Commands that mutate or sync the remote wiki need a constructed WikiSync
@@ -37,6 +42,21 @@ async function main() {
37
42
  return runtime.proc.exit(2);
38
43
  }
39
44
 
45
+ // Every command except `init` operates on an existing wiki tree. When it is
46
+ // absent (e.g. a fresh worktree where bootstrap.sh never ran), degrade
47
+ // gracefully: warn and exit 0 so the session Stop hook and other callers do
48
+ // not fail loudly. `init` is exempt — it creates the tree.
49
+ if (command !== "init") {
50
+ const wikiDir = resolveWikiRoot(runtime, parsed.values);
51
+ if (!wikiExists(runtime, wikiDir)) {
52
+ createLogger("wiki", runtime).warn(
53
+ command,
54
+ `no wiki at ${wikiDir}; skipping (run \`fit-wiki init\` to create one)`,
55
+ );
56
+ return runtime.proc.exit(0);
57
+ }
58
+ }
59
+
40
60
  const gitClient = new GitClient({ runtime });
41
61
  let wikiSync;
42
62
  if (NEEDS_WIKI_SYNC.has(command)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -49,6 +49,7 @@
49
49
  "@forwardimpact/libconfig": "^0.1.77",
50
50
  "@forwardimpact/libeval": "^0.1.49",
51
51
  "@forwardimpact/libpreflight": "^0.1.0",
52
+ "@forwardimpact/libtelemetry": "^0.1.47",
52
53
  "@forwardimpact/libutil": "^0.1.0",
53
54
  "@forwardimpact/libxmr": "^2.0.0"
54
55
  },
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { addDays } from "@forwardimpact/libutil";
3
+ import { createLogger } from "@forwardimpact/libtelemetry";
3
4
  import {
4
5
  appendClaim,
5
6
  removeClaim,
@@ -28,7 +29,10 @@ async function pushWiki(wikiSync, runtime, message) {
28
29
  if (result.pushed)
29
30
  runtime.proc.stdout.write("push: committed and pushed\n");
30
31
  } catch (err) {
31
- runtime.proc.stderr.write(`push failed (saved locally): ${err.message}\n`);
32
+ createLogger("wiki", runtime).warn(
33
+ "claim",
34
+ `push failed (saved locally): ${err.message}`,
35
+ );
32
36
  }
33
37
  }
34
38
 
@@ -64,8 +68,9 @@ export async function runClaimCommand(ctx) {
64
68
  expires_at: expires,
65
69
  });
66
70
  if (!result.inserted) {
67
- runtime.proc.stderr.write(
68
- `claim already exists for ${agent}/${options.target}\n`,
71
+ createLogger("wiki", runtime).warn(
72
+ "claim",
73
+ `claim already exists for ${agent}/${options.target}`,
69
74
  );
70
75
  return { ok: false, code: 2 };
71
76
  }
@@ -16,6 +16,7 @@ import {
16
16
  import { currentDayIso } from "../util/clock.js";
17
17
  import { resolveProjectRoot } from "../util/wiki-dir.js";
18
18
  import { FAST_MODEL } from "@forwardimpact/libutil/models";
19
+ import { createLogger } from "@forwardimpact/libtelemetry";
19
20
 
20
21
  // Pipeline: audit → deterministic rotation (the one fix needing a file seal the
21
22
  // agent can't do) → re-audit → Haiku agent on the prose-judgment residual →
@@ -276,7 +277,8 @@ export async function runFixCommand(ctx) {
276
277
  const wikiRoot = ctx.options["wiki-root"] || path.join(projectRoot, "wiki");
277
278
  const today = ctx.options.today || currentDayIso(runtime);
278
279
  const out = (s) => runtime.proc.stdout.write(s);
279
- const err = (s) => runtime.proc.stderr.write(s);
280
+ const logger = createLogger("wiki", runtime);
281
+ const err = (s) => logger.warn("fix", String(s).replace(/\n+$/, ""));
280
282
 
281
283
  // The agent's edits change the result, so re-read and re-audit each round.
282
284
  const audit = () =>
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { createLogger } from "@forwardimpact/libtelemetry";
2
3
  import {
3
4
  MEMO_INBOX_MARKER,
4
5
  PRIORITY_INDEX_HEADING,
@@ -11,8 +12,9 @@ function paths(runtime, options) {
11
12
  const wikiRoot = resolveWikiRoot(runtime, options);
12
13
  const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
13
14
  if (!agent) {
14
- runtime.proc.stderr.write(
15
- "inbox requires --agent or LIBEVAL_AGENT_PROFILE\n",
15
+ createLogger("wiki", runtime).warn(
16
+ "inbox",
17
+ "inbox requires --agent or LIBEVAL_AGENT_PROFILE",
16
18
  );
17
19
  return { error: { ok: false, code: 2 } };
18
20
  }
@@ -67,13 +69,13 @@ function ackOrDropCmd(runtime, options) {
67
69
  const { summaryPath } = p;
68
70
  const idx = Number.parseInt(options.index ?? "", 10);
69
71
  if (!Number.isInteger(idx) || idx < 0) {
70
- runtime.proc.stderr.write("inbox requires --index <n>\n");
72
+ createLogger("wiki", runtime).warn("inbox", "inbox requires --index <n>");
71
73
  return { ok: false, code: 2 };
72
74
  }
73
75
  const text = runtime.fsSync.readFileSync(summaryPath, "utf-8");
74
76
  const { lines, bulletIdxs } = readInboxBullets(text);
75
77
  if (idx >= bulletIdxs.length) {
76
- runtime.proc.stderr.write(`no bullet at index ${idx}\n`);
78
+ createLogger("wiki", runtime).warn("inbox", `no bullet at index ${idx}`);
77
79
  return { ok: false, code: 2 };
78
80
  }
79
81
  removeBulletAt(lines, bulletIdxs[idx]);
@@ -134,13 +136,16 @@ function promoteCmd(runtime, options) {
134
136
  const { summaryPath, memoryPath, agent } = p;
135
137
  const idx = Number.parseInt(options.index ?? "", 10);
136
138
  if (!Number.isInteger(idx) || idx < 0) {
137
- runtime.proc.stderr.write("inbox promote requires --index <n>\n");
139
+ createLogger("wiki", runtime).warn(
140
+ "inbox",
141
+ "inbox promote requires --index <n>",
142
+ );
138
143
  return { ok: false, code: 2 };
139
144
  }
140
145
  const text = runtime.fsSync.readFileSync(summaryPath, "utf-8");
141
146
  const { lines, bullets, bulletIdxs } = readInboxBullets(text);
142
147
  if (idx >= bullets.length) {
143
- runtime.proc.stderr.write(`no bullet at index ${idx}\n`);
148
+ createLogger("wiki", runtime).warn("inbox", `no bullet at index ${idx}`);
144
149
  return { ok: false, code: 2 };
145
150
  }
146
151
  const bulletText = bullets[idx].replace(/^[-*]\s+/, "");
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { createLogger } from "@forwardimpact/libtelemetry";
2
3
  import { listSkills } from "../skill-roster.js";
3
4
  import { resolveProjectRoot, resolveWikiRoot } from "../util/wiki-dir.js";
4
5
  import {
@@ -53,19 +54,19 @@ function scaffoldActiveClaims(runtime, memoryPath) {
53
54
  }
54
55
 
55
56
  async function maybeCloneWiki(wikiSync, gitClient, projectRoot, runtime) {
57
+ const logger = createLogger("wiki", runtime);
56
58
  const wikiUrl = await deriveWikiUrl(gitClient, projectRoot, runtime.proc.env);
57
59
  if (!wikiUrl) {
58
- runtime.proc.stderr.write(
59
- "init: could not determine wiki URL from origin remote\n",
60
- );
60
+ logger.warn("init", "could not determine wiki URL from origin remote");
61
61
  return;
62
62
  }
63
63
  const cloneResult = await wikiSync.ensureCloned(wikiUrl);
64
64
  if (cloneResult.cloned) {
65
65
  await wikiSync.inheritIdentity();
66
66
  } else {
67
- runtime.proc.stderr.write(
68
- "init: could not clone wiki, continuing with local-only steps\n",
67
+ logger.warn(
68
+ "init",
69
+ "could not clone wiki, continuing with local-only steps",
69
70
  );
70
71
  }
71
72
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@forwardimpact/libtelemetry";
1
2
  import {
2
3
  weeklyLogPath,
3
4
  rotateIfOverBudget,
@@ -10,8 +11,9 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
10
11
  function commonContext(runtime, options) {
11
12
  const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
12
13
  if (!agent) {
13
- runtime.proc.stderr.write(
14
- "log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var\n",
14
+ createLogger("wiki", runtime).warn(
15
+ "log",
16
+ "log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var",
15
17
  );
16
18
  return { error: { ok: false, code: 2 } };
17
19
  }
@@ -49,14 +51,15 @@ function rotateBeforeAppend(wikiRoot, agent, today, appendLines, runtime) {
49
51
  runtime.fsSync,
50
52
  );
51
53
  if (res.status === "incomplete") {
52
- runtime.proc.stderr.write(
53
- `note: day-section ${res.residue.section} alone exceeds the budget ` +
54
+ createLogger("wiki", runtime).warn(
55
+ "log",
56
+ `day-section ${res.residue.section} alone exceeds the budget ` +
54
57
  `(${res.residue.lines} lines, ${res.residue.words} words); ` +
55
- `sealed as ${res.residue.path} for manual recovery\n`,
58
+ `sealed as ${res.residue.path} for manual recovery`,
56
59
  );
57
60
  }
58
61
  } catch (e) {
59
- runtime.proc.stderr.write(`note: rotation failed: ${e.message}\n`);
62
+ createLogger("wiki", runtime).warn("log", `rotation failed: ${e.message}`);
60
63
  }
61
64
  }
62
65
 
@@ -95,7 +98,10 @@ function runNote(runtime, options) {
95
98
  if (ctx.error) return ctx.error;
96
99
  const { agent, wikiRoot, today } = ctx;
97
100
  if (!options.field || !options.body) {
98
- runtime.proc.stderr.write("log note requires --field and --body\n");
101
+ createLogger("wiki", runtime).warn(
102
+ "log",
103
+ "log note requires --field and --body",
104
+ );
99
105
  return { ok: false, code: 2 };
100
106
  }
101
107
  const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { createLogger } from "@forwardimpact/libtelemetry";
2
3
  import { writeMemo } from "../memo-writer.js";
3
4
  import { listAgents } from "../agent-roster.js";
4
5
  import { BROADCAST_TARGET } from "../constants.js";
@@ -11,8 +12,9 @@ function writeAndCheck(runtime, summaryPath, sender, message, today) {
11
12
  runtime.fsSync,
12
13
  );
13
14
  if (!result.written) {
14
- runtime.proc.stderr.write(
15
- `summary lacks memo:inbox marker: ${result.path}\n`,
15
+ createLogger("wiki", runtime).warn(
16
+ "memo",
17
+ `summary lacks memo:inbox marker: ${result.path}`,
16
18
  );
17
19
  return { ok: false, code: 2 };
18
20
  }
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { yearMonth } from "@forwardimpact/libutil";
3
+ import { createLogger } from "@forwardimpact/libtelemetry";
3
4
  import { createScriptConfig } from "@forwardimpact/libconfig";
4
5
  import { scanMarkers } from "../marker-scanner.js";
5
6
  import { renderBlock, BlockRenderError } from "../block-renderer.js";
@@ -69,6 +70,7 @@ function readStoryboardOrNull(runtime, storyboardPath) {
69
70
  export async function runRefreshCommand(ctx) {
70
71
  const { runtime, gitClient } = ctx.deps;
71
72
  const options = ctx.options;
73
+ const logger = createLogger("wiki", runtime);
72
74
  const projectRoot = resolveProjectRoot(runtime);
73
75
 
74
76
  const storyboardPath = path.resolve(
@@ -77,11 +79,11 @@ export async function runRefreshCommand(ctx) {
77
79
  );
78
80
  const text = readStoryboardOrNull(runtime, storyboardPath);
79
81
  if (text === null) {
80
- runtime.proc.stderr.write(`refresh: no storyboard at ${storyboardPath}\n`);
82
+ logger.warn("refresh", `no storyboard at ${storyboardPath}`);
81
83
  return { ok: true };
82
84
  }
83
85
  const blocks = scanMarkers(text, {
84
- warn: (message) => runtime.proc.stderr.write(message),
86
+ warn: (message) => logger.warn("refresh", message),
85
87
  });
86
88
  if (blocks.length === 0) return { ok: true };
87
89
 
@@ -122,8 +124,9 @@ export async function runRefreshCommand(ctx) {
122
124
  spliced = true;
123
125
  } catch (err) {
124
126
  if (!(err instanceof BlockRenderError)) throw err;
125
- runtime.proc.stderr.write(
126
- `refresh-error ${storyboardPath}:${block.openLine + 1} ${err.message}\n`,
127
+ logger.error(
128
+ "refresh",
129
+ `refresh-error ${storyboardPath}:${block.openLine + 1} ${err.message}`,
127
130
  );
128
131
  }
129
132
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@forwardimpact/libtelemetry";
1
2
  import { rotateIfOverBudget, weeklyLogPath } from "../weekly-log.js";
2
3
  import { currentDayIso } from "../util/clock.js";
3
4
  import { resolveWikiRoot } from "../util/wiki-dir.js";
@@ -5,6 +6,7 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
5
6
  /** Force-rotate the current weekly log to a sealed part file. */
6
7
  export function runRotateCommand(ctx) {
7
8
  const { runtime } = ctx.deps;
9
+ const logger = createLogger("wiki", runtime);
8
10
  const options = ctx.options;
9
11
  const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
10
12
  if (!agent) {
@@ -34,7 +36,7 @@ export function runRotateCommand(ctx) {
34
36
  runtime.fsSync,
35
37
  );
36
38
  } catch (e) {
37
- runtime.proc.stderr.write(`rotate failed: ${e.message}\n`);
39
+ logger.error("rotate", `rotate failed: ${e.message}`);
38
40
  return { ok: false, code: 1 };
39
41
  }
40
42
  switch (result.status) {
@@ -51,12 +53,13 @@ export function runRotateCommand(ctx) {
51
53
  runtime.proc.stdout.write(`sealed → ${part}\n`);
52
54
  }
53
55
  const { section, lines, words, path: residuePath } = result.residue;
54
- runtime.proc.stderr.write(
56
+ logger.error(
57
+ "rotate",
55
58
  `day-section ${section} alone exceeds the budget ` +
56
59
  `(${lines} lines, ${words} words) and cannot be split at a day ` +
57
60
  `seam: ${residuePath}\n` +
58
61
  `recover it by hand — bisect the section at a finer seam ` +
59
- `(see the memory protocol's manual-recovery convention)\n`,
62
+ `(see the memory protocol's manual-recovery convention)`,
60
63
  );
61
64
  return { ok: false, code: 1 };
62
65
  }
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "@forwardimpact/libtelemetry";
1
2
  import { WikiPullConflict } from "../wiki-sync.js";
2
3
 
3
4
  /** Commit all wiki changes and push them to the remote wiki repository. */
@@ -25,8 +26,9 @@ export async function runPullCommand(ctx) {
25
26
  return { ok: true };
26
27
  } catch (err) {
27
28
  if (err instanceof WikiPullConflict) {
28
- runtime.proc.stderr.write(
29
- "fit-wiki pull: rebase conflict — local divergence detected; resolve manually or push first\n",
29
+ createLogger("wiki", runtime).error(
30
+ "pull",
31
+ "rebase conflict — local divergence detected; resolve manually or push first",
30
32
  );
31
33
  return { ok: false, code: 1 };
32
34
  }
@@ -1,4 +1,5 @@
1
1
  import { addDays } from "@forwardimpact/libutil";
2
+ import { createLogger } from "@forwardimpact/libtelemetry";
2
3
 
3
4
  /** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
4
5
  export function parseRepoSlug(originUrl) {
@@ -54,8 +55,9 @@ export async function renderIssueList({
54
55
  const env = token ? { ...runtime.proc.env, GH_TOKEN: token } : undefined;
55
56
  const result = await runtime.subprocess.run("gh", args, { cwd, env });
56
57
  if (result.exitCode !== 0) {
57
- runtime.proc.stderr.write(
58
- `refresh: gh issue list failed for ${topic}:${state}\n`,
58
+ createLogger("wiki", runtime).warn(
59
+ "refresh",
60
+ `gh issue list failed for ${topic}:${state}`,
59
61
  );
60
62
  return [];
61
63
  }
@@ -63,8 +65,9 @@ export async function renderIssueList({
63
65
  try {
64
66
  issues = JSON.parse(result.stdout || "[]");
65
67
  } catch {
66
- runtime.proc.stderr.write(
67
- `refresh: gh issue list JSON parse failed for ${topic}:${state}\n`,
68
+ createLogger("wiki", runtime).warn(
69
+ "refresh",
70
+ `gh issue list JSON parse failed for ${topic}:${state}`,
68
71
  );
69
72
  return [];
70
73
  }
@@ -22,3 +22,16 @@ export function resolveProjectRoot(runtime) {
22
22
  export function resolveWikiRoot(runtime, options = {}) {
23
23
  return options["wiki-root"] || path.join(resolveProjectRoot(runtime), "wiki");
24
24
  }
25
+
26
+ /**
27
+ * Report whether the resolved wiki root exists on disk. Commands that read or
28
+ * sync an existing wiki use this to degrade gracefully (warn and exit 0) when
29
+ * the tree was never bootstrapped — e.g. a fresh worktree where
30
+ * `scripts/bootstrap.sh` did not run.
31
+ * @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
32
+ * @param {string} wikiDir - The resolved wiki root.
33
+ * @returns {boolean}
34
+ */
35
+ export function wikiExists(runtime, wikiDir) {
36
+ return runtime.fsSync.existsSync(wikiDir);
37
+ }