@forwardimpact/libwiki 0.2.33 → 0.2.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
4
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",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@forwardimpact/libcli": "^0.1.0",
49
49
  "@forwardimpact/libconfig": "^0.1.77",
50
- "@forwardimpact/libharness": "^1.0.0",
50
+ "@forwardimpact/libharness": "^2.0.0",
51
51
  "@forwardimpact/libpreflight": "^0.1.0",
52
52
  "@forwardimpact/libtelemetry": "^0.1.47",
53
53
  "@forwardimpact/libutil": "^0.1.0",
@@ -11,6 +11,7 @@ import {
11
11
  parseRepoSlug,
12
12
  } from "../issue-list-renderer.js";
13
13
  import { parseClaims, filterExpired, removeClaim } from "../active-claims.js";
14
+ import { renderStoryboardSkeleton } from "../storyboard-skeleton.js";
14
15
  import { currentDayIso } from "../util/clock.js";
15
16
  import { resolveProjectRoot, resolveWikiRoot } from "../util/wiki-dir.js";
16
17
 
@@ -90,9 +91,8 @@ function spliceBlock(lines, block, rendered) {
90
91
  );
91
92
  }
92
93
 
93
- // A missing current-month storyboard (e.g. a coaching run early in the month,
94
- // before the storyboard meeting created it) is non-fatal: return null so the
95
- // deterministic refresh step exits cleanly instead of failing the job.
94
+ // A missing current-month storyboard is non-fatal: return null so the caller
95
+ // can create it (see createStoryboardSkeleton) rather than fail the job.
96
96
  function readStoryboardOrNull(runtime, storyboardPath) {
97
97
  try {
98
98
  return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
@@ -102,6 +102,20 @@ function readStoryboardOrNull(runtime, storyboardPath) {
102
102
  }
103
103
  }
104
104
 
105
+ // Create the current-month storyboard from the minimal skeleton when it does
106
+ // not exist. Refresh is the deterministic "freshen the wiki" step and runs
107
+ // before the session (kata-agent pre-run), so creating here guarantees the file
108
+ // is on disk before participants look for it — without any lead having a write
109
+ // tool. The skeleton carries the section structure and the generic issue-list
110
+ // markers; the render pass below fills them and participants seed metric blocks.
111
+ function createStoryboardSkeleton(runtime, storyboardPath, logger) {
112
+ const skeleton = renderStoryboardSkeleton(currentDayIso(runtime));
113
+ runtime.fsSync.mkdirSync(path.dirname(storyboardPath), { recursive: true });
114
+ runtime.fsSync.writeFileSync(storyboardPath, skeleton);
115
+ logger.info("refresh", `created storyboard at ${storyboardPath}`);
116
+ return skeleton;
117
+ }
118
+
105
119
  // Drop every MEMORY.md `## Active Claims` row past its `expires_at`, writing the
106
120
  // trimmed table back in place. Refresh is the deterministic "freshen the wiki"
107
121
  // step, so clearing lapsed claims belongs here alongside the storyboard render;
@@ -142,11 +156,11 @@ export async function runRefreshCommand(ctx) {
142
156
  projectRoot,
143
157
  ctx.args["storyboard-path"] || currentStoryboardRelPath(runtime),
144
158
  );
145
- const text = readStoryboardOrNull(runtime, storyboardPath);
146
- if (text === null) {
147
- logger.warn("refresh", `no storyboard at ${storyboardPath}`);
148
- return { ok: true };
149
- }
159
+ const existing = readStoryboardOrNull(runtime, storyboardPath);
160
+ const created = existing === null;
161
+ const text = created
162
+ ? createStoryboardSkeleton(runtime, storyboardPath, logger)
163
+ : existing;
150
164
  const blocks = scanMarkers(text, {
151
165
  warn: (message) => logger.warn("refresh", message),
152
166
  });
@@ -200,7 +214,7 @@ export async function runRefreshCommand(ctx) {
200
214
  if (spliced) runtime.fsSync.writeFileSync(storyboardPath, lines.join("\n"));
201
215
  if (options.format === "json") {
202
216
  runtime.proc.stdout.write(
203
- JSON.stringify({ blocks: blocks.length, spliced }) + "\n",
217
+ JSON.stringify({ blocks: blocks.length, spliced, created }) + "\n",
204
218
  );
205
219
  }
206
220
  return { ok: true };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Storyboard skeleton — the minimal, valid storyboard file `fit-wiki refresh`
3
+ * writes when the current-month board does not yet exist. It carries only the
4
+ * structural surface libwiki owns: the five Toyota Kata sections and the
5
+ * generic `obstacles`/`experiments` issue-list marker blocks that refresh
6
+ * renders from tracker state.
7
+ *
8
+ * Deliberately *not* here: the per-agent `#### {metric}` XmR blocks. Their
9
+ * agent→metric grouping is per-installation curation libwiki cannot infer, so a
10
+ * participant seeds each missing marker pair (see the kata-session skill) and a
11
+ * later refresh renders it. Section budgets and authoring prose ("write the
12
+ * challenge here") stay in the skill's `storyboard-template.md`, the L4
13
+ * authoring layer — this skeleton is content-free scaffolding.
14
+ */
15
+
16
+ const MONTH_NAMES = [
17
+ "January",
18
+ "February",
19
+ "March",
20
+ "April",
21
+ "May",
22
+ "June",
23
+ "July",
24
+ "August",
25
+ "September",
26
+ "October",
27
+ "November",
28
+ "December",
29
+ ];
30
+
31
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
32
+
33
+ /** Whether `year` is a leap year under the Gregorian rule. */
34
+ function isLeapYear(year) {
35
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
36
+ }
37
+
38
+ /**
39
+ * Last calendar day of the month containing `todayIso` (ISO `YYYY-MM-DD`).
40
+ * Pure integer/calendar math — no `Date`, so the module stays free of ambient
41
+ * time deps.
42
+ */
43
+ function endOfMonthIso(todayIso) {
44
+ const [year, month] = todayIso.split("-").map(Number);
45
+ const lastDay =
46
+ month === 2 && isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1];
47
+ return `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
48
+ }
49
+
50
+ /**
51
+ * Render the minimal storyboard skeleton for the month containing `todayIso`.
52
+ * Pure — takes the day as an ISO string and returns markdown. The heading reads
53
+ * `# Storyboard — {YYYY} {Month}`; the marker blocks match the syntax the
54
+ * scanner (`marker-scanner.js`) and renderer (`commands/refresh.js`) expect.
55
+ *
56
+ * @param {string} todayIso - ISO date string (`YYYY-MM-DD`).
57
+ * @returns {string} The skeleton markdown, newline-terminated.
58
+ */
59
+ export function renderStoryboardSkeleton(todayIso) {
60
+ const [year, month] = todayIso.split("-").map(Number);
61
+ const monthName = MONTH_NAMES[month - 1];
62
+ return `# Storyboard — ${year} ${monthName}
63
+
64
+ ## Challenge
65
+
66
+ > [product-manager sets this in the planning meeting.]
67
+
68
+ ## Target Condition
69
+
70
+ **Due:** ${endOfMonthIso(todayIso)}
71
+
72
+ > [product-manager sets this in the planning meeting.]
73
+
74
+ ## Current Condition
75
+
76
+ **Last updated:** ${todayIso}
77
+
78
+ ### Headlines
79
+
80
+ None.
81
+
82
+ ## Obstacles
83
+
84
+ ### Active
85
+
86
+ <!-- obstacles:open Do not edit. Auto-generated. -->
87
+ <!-- /obstacles -->
88
+
89
+ ### Concluded (last 7 days)
90
+
91
+ <!-- obstacles:closed Do not edit. Auto-generated. -->
92
+ <!-- /obstacles -->
93
+
94
+ ## Experiments
95
+
96
+ ### Active
97
+
98
+ <!-- experiments:open Do not edit. Auto-generated. -->
99
+ <!-- /experiments -->
100
+
101
+ ### Concluded (last 7 days)
102
+
103
+ <!-- experiments:closed Do not edit. Auto-generated. -->
104
+ <!-- /experiments -->
105
+ `;
106
+ }