@forwardimpact/libwiki 0.1.3 → 0.2.0

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
@@ -7,16 +7,156 @@ import { runMemoCommand } from "../src/commands/memo.js";
7
7
  import { runRefreshCommand } from "../src/commands/refresh.js";
8
8
  import { runInitCommand } from "../src/commands/init.js";
9
9
  import { runPushCommand, runPullCommand } from "../src/commands/sync.js";
10
+ import { runBootCommand } from "../src/commands/boot.js";
11
+ import { runLogCommand } from "../src/commands/log.js";
12
+ import { runClaimCommand, runReleaseCommand } from "../src/commands/claim.js";
13
+ import { runInboxCommand } from "../src/commands/inbox.js";
14
+ import { runRotateCommand } from "../src/commands/rotate.js";
15
+ import { runAuditCommand } from "../src/commands/audit.js";
10
16
 
11
17
  const { version: VERSION } = JSON.parse(
12
18
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
13
19
  );
14
20
 
21
+ const wikiRootOpt = {
22
+ "wiki-root": {
23
+ type: "string",
24
+ description: "Override wiki root directory (default: wiki)",
25
+ },
26
+ };
27
+
28
+ const agentOpt = {
29
+ agent: {
30
+ type: "string",
31
+ description: "Agent name (falls back to LIBEVAL_AGENT_PROFILE env var)",
32
+ },
33
+ };
34
+
35
+ const todayOpt = {
36
+ today: {
37
+ type: "string",
38
+ description: "Override today's ISO date (testing)",
39
+ },
40
+ };
41
+
15
42
  const definition = {
16
43
  name: "fit-wiki",
17
44
  version: VERSION,
18
45
  description: "Wiki lifecycle management for the Kata agent system",
19
46
  commands: [
47
+ {
48
+ name: "boot",
49
+ description:
50
+ "Print on-boot digest (priorities, claims, storyboard items) as JSON",
51
+ options: {
52
+ ...agentOpt,
53
+ ...wikiRootOpt,
54
+ ...todayOpt,
55
+ format: {
56
+ type: "string",
57
+ description: "Output format: json (default) or markdown",
58
+ },
59
+ },
60
+ },
61
+ {
62
+ name: "log",
63
+ description:
64
+ "Append a decision/note/done entry to the current weekly log",
65
+ args: "[subcommand]",
66
+ options: {
67
+ ...agentOpt,
68
+ ...wikiRootOpt,
69
+ ...todayOpt,
70
+ surveyed: {
71
+ type: "string",
72
+ description: "Decision: routing levels surveyed",
73
+ },
74
+ chosen: { type: "string", description: "Decision: chosen action" },
75
+ rationale: { type: "string", description: "Decision: rationale" },
76
+ alternatives: { type: "string", description: "Decision: alternatives" },
77
+ field: { type: "string", description: "Note: field heading" },
78
+ body: { type: "string", description: "Note: field body" },
79
+ },
80
+ },
81
+ {
82
+ name: "claim",
83
+ description:
84
+ "Claim a target in MEMORY.md ## Active Claims (refuses duplicates)",
85
+ options: {
86
+ ...agentOpt,
87
+ ...wikiRootOpt,
88
+ ...todayOpt,
89
+ target: {
90
+ type: "string",
91
+ description: "What is being claimed (spec id, PR id, etc.)",
92
+ },
93
+ branch: { type: "string", description: "Branch carrying the work" },
94
+ pr: { type: "string", description: "Optional PR id" },
95
+ "expires-at": {
96
+ type: "string",
97
+ description: "Override expiry ISO date (default claim+7d)",
98
+ },
99
+ },
100
+ },
101
+ {
102
+ name: "release",
103
+ description: "Release a claim (or all expired claims with --expired)",
104
+ options: {
105
+ ...agentOpt,
106
+ ...wikiRootOpt,
107
+ ...todayOpt,
108
+ target: { type: "string", description: "Target to release" },
109
+ expired: {
110
+ type: "boolean",
111
+ description: "Release every row past expires_at",
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: "inbox",
117
+ description: "Triage the agent's Message Inbox (list/ack/promote/drop)",
118
+ args: "[subcommand]",
119
+ options: {
120
+ ...agentOpt,
121
+ ...wikiRootOpt,
122
+ ...todayOpt,
123
+ index: {
124
+ type: "string",
125
+ description: "Bullet index (0-based) for ack/promote/drop",
126
+ },
127
+ owner: {
128
+ type: "string",
129
+ description: "Owner field when promoting (default: --agent)",
130
+ },
131
+ },
132
+ },
133
+ {
134
+ name: "rotate",
135
+ description: "Force-rotate the current weekly log to a sealed part",
136
+ options: {
137
+ ...agentOpt,
138
+ ...wikiRootOpt,
139
+ ...todayOpt,
140
+ },
141
+ },
142
+ {
143
+ name: "audit",
144
+ description:
145
+ "Audit the wiki against the memory-protocol contract (500-line cap; cutover 2026-W23)",
146
+ options: {
147
+ ...wikiRootOpt,
148
+ ...todayOpt,
149
+ format: {
150
+ type: "string",
151
+ description: "Output format: text (default) or json",
152
+ },
153
+ "legacy-only": {
154
+ type: "boolean",
155
+ description:
156
+ "Run only the checks the legacy wiki-audit.sh carried (parity mode)",
157
+ },
158
+ },
159
+ },
20
160
  {
21
161
  name: "memo",
22
162
  description: "Send a cross-team memo into a teammate's Message Inbox",
@@ -35,26 +175,27 @@ const definition = {
35
175
  type: "string",
36
176
  description: "Memo text",
37
177
  },
38
- "wiki-root": {
39
- type: "string",
40
- description: "Override wiki root directory (default: auto-detected)",
41
- },
178
+ ...wikiRootOpt,
42
179
  },
43
180
  },
44
181
  {
45
182
  name: "refresh",
46
183
  description:
47
- "Regenerate XmR chart blocks inside a storyboard markdown file",
184
+ "Regenerate XmR and obstacle/experiment marker blocks in a storyboard",
48
185
  args: "[storyboard-path]",
186
+ options: {
187
+ format: {
188
+ type: "string",
189
+ description: "Output format: (default off) or json",
190
+ },
191
+ },
49
192
  },
50
193
  {
51
194
  name: "init",
52
- description: "Bootstrap a wiki working tree for a Kata installation",
195
+ description:
196
+ "Bootstrap a wiki working tree, scaffold Active Claims, install audit Stop-hook",
53
197
  options: {
54
- "wiki-root": {
55
- type: "string",
56
- description: "Override wiki root directory (default: wiki)",
57
- },
198
+ ...wikiRootOpt,
58
199
  "skills-dir": {
59
200
  type: "string",
60
201
  description: "Override skills directory (default: .claude/skills)",
@@ -64,22 +205,12 @@ const definition = {
64
205
  {
65
206
  name: "push",
66
207
  description: "Commit and push local wiki changes to the remote",
67
- options: {
68
- "wiki-root": {
69
- type: "string",
70
- description: "Override wiki root directory (default: wiki)",
71
- },
72
- },
208
+ options: { ...wikiRootOpt },
73
209
  },
74
210
  {
75
211
  name: "pull",
76
212
  description: "Pull remote wiki changes into the local working tree",
77
- options: {
78
- "wiki-root": {
79
- type: "string",
80
- description: "Override wiki root directory (default: wiki)",
81
- },
82
- },
213
+ options: { ...wikiRootOpt },
83
214
  },
84
215
  ],
85
216
  globalOptions: {
@@ -91,10 +222,15 @@ const definition = {
91
222
  },
92
223
  },
93
224
  examples: [
225
+ "fit-wiki boot --agent staff-engineer",
226
+ 'fit-wiki log decision --agent staff-engineer --surveyed "..." --chosen "..." --rationale "..."',
227
+ "fit-wiki claim --agent staff-engineer --target spec-1060 --branch claude/...",
228
+ "fit-wiki release --agent staff-engineer --target spec-1060",
229
+ "fit-wiki inbox list --agent staff-engineer",
230
+ "fit-wiki rotate --agent staff-engineer",
231
+ "fit-wiki audit",
94
232
  'fit-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
95
- 'fit-wiki memo --from technical-writer --to all --message "new XmR baseline"',
96
233
  "fit-wiki refresh",
97
- "fit-wiki refresh wiki/storyboard-2026-M05.md",
98
234
  "fit-wiki init",
99
235
  "fit-wiki push",
100
236
  "fit-wiki pull",
@@ -118,6 +254,13 @@ const definition = {
118
254
  const cli = createCli(definition);
119
255
 
120
256
  const COMMANDS = {
257
+ boot: runBootCommand,
258
+ log: runLogCommand,
259
+ claim: runClaimCommand,
260
+ release: runReleaseCommand,
261
+ inbox: runInboxCommand,
262
+ rotate: runRotateCommand,
263
+ audit: runAuditCommand,
121
264
  memo: runMemoCommand,
122
265
  refresh: runRefreshCommand,
123
266
  init: runInitCommand,
@@ -125,7 +268,7 @@ const COMMANDS = {
125
268
  pull: runPullCommand,
126
269
  };
127
270
 
128
- function main() {
271
+ async function main() {
129
272
  const parsed = cli.parse(process.argv.slice(2));
130
273
  if (!parsed) process.exit(0);
131
274
 
@@ -144,7 +287,7 @@ function main() {
144
287
  process.exit(2);
145
288
  }
146
289
 
147
- handler(values, args, cli);
290
+ await handler(values, args, cli);
148
291
  }
149
292
 
150
293
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@forwardimpact/libcli": "^0.1.0",
49
+ "@forwardimpact/libconfig": "^0.1.77",
49
50
  "@forwardimpact/libutil": "^0.1.0",
50
51
  "@forwardimpact/libxmr": "^1.1.0"
51
52
  },
@@ -0,0 +1,196 @@
1
+ import {
2
+ ACTIVE_CLAIMS_HEADING,
3
+ ACTIVE_CLAIMS_TABLE_HEADER,
4
+ ACTIVE_CLAIMS_TABLE_SEPARATOR,
5
+ } from "./constants.js";
6
+
7
+ const HEADER_RE =
8
+ /^\|\s*agent\s*\|\s*target\s*\|\s*branch\s*\|\s*pr\s*\|\s*claimed_at\s*\|\s*expires_at\s*\|\s*$/;
9
+ const SEPARATOR_RE = /^\|\s*---\s*\|/;
10
+ const ROW_RE =
11
+ /^\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*$/;
12
+
13
+ function findSection(lines) {
14
+ for (let i = 0; i < lines.length; i++) {
15
+ if (lines[i].trim() === ACTIVE_CLAIMS_HEADING) return i;
16
+ }
17
+ return -1;
18
+ }
19
+
20
+ function findNextH2(lines, start) {
21
+ for (let i = start; i < lines.length; i++) {
22
+ if (/^## /.test(lines[i])) return i;
23
+ }
24
+ return lines.length;
25
+ }
26
+
27
+ function isEmptyStateCell(cell) {
28
+ return cell === "*None*" || cell === "—";
29
+ }
30
+
31
+ function rowFromMatch(match) {
32
+ const [, agent, target, branch, pr, claimed_at, expires_at] = match;
33
+ if (isEmptyStateCell(agent.trim())) return null;
34
+ return {
35
+ agent: agent.trim(),
36
+ target: target.trim(),
37
+ branch: branch.trim(),
38
+ pr: pr.trim() === "—" || pr.trim() === "" ? null : pr.trim(),
39
+ claimed_at: claimed_at.trim(),
40
+ expires_at: expires_at.trim(),
41
+ };
42
+ }
43
+
44
+ function scanRowsBetween(lines, start, end) {
45
+ const claims = [];
46
+ let inTable = false;
47
+ let seenSeparator = false;
48
+ for (let i = start; i < end; i++) {
49
+ const line = lines[i];
50
+ if (HEADER_RE.test(line)) {
51
+ inTable = true;
52
+ continue;
53
+ }
54
+ if (inTable && SEPARATOR_RE.test(line)) {
55
+ seenSeparator = true;
56
+ continue;
57
+ }
58
+ if (!(inTable && seenSeparator && line.startsWith("|"))) continue;
59
+ const match = line.match(ROW_RE);
60
+ if (!match) continue;
61
+ const row = rowFromMatch(match);
62
+ if (row) claims.push(row);
63
+ }
64
+ return claims;
65
+ }
66
+
67
+ /** Parse the `## Active Claims` table from MEMORY.md text. Returns [] if absent. */
68
+ export function parseClaims(memoryText) {
69
+ if (typeof memoryText !== "string") return [];
70
+ const lines = memoryText.split("\n");
71
+ const heading = findSection(lines);
72
+ if (heading === -1) return [];
73
+ const sectionEnd = findNextH2(lines, heading + 1);
74
+ return scanRowsBetween(lines, heading + 1, sectionEnd);
75
+ }
76
+
77
+ function formatRow({ agent, target, branch, pr, claimed_at, expires_at }) {
78
+ const prCell = pr == null || pr === "" ? "—" : pr;
79
+ return `| ${agent} | ${target} | ${branch} | ${prCell} | ${claimed_at} | ${expires_at} |`;
80
+ }
81
+
82
+ function appendNewSection(memoryText, claim) {
83
+ const block = [
84
+ "",
85
+ ACTIVE_CLAIMS_HEADING,
86
+ "",
87
+ ACTIVE_CLAIMS_TABLE_HEADER,
88
+ ACTIVE_CLAIMS_TABLE_SEPARATOR,
89
+ formatRow(claim),
90
+ "",
91
+ ];
92
+ return memoryText.replace(/\n*$/, "") + "\n" + block.join("\n");
93
+ }
94
+
95
+ function findTableIndices(lines, heading, sectionEnd) {
96
+ let headerIdx = -1;
97
+ let separatorIdx = -1;
98
+ for (let i = heading + 1; i < sectionEnd; i++) {
99
+ if (HEADER_RE.test(lines[i])) headerIdx = i;
100
+ if (headerIdx !== -1 && SEPARATOR_RE.test(lines[i])) {
101
+ separatorIdx = i;
102
+ break;
103
+ }
104
+ }
105
+ return { headerIdx, separatorIdx };
106
+ }
107
+
108
+ function insertTableWithRow(lines, heading, sectionEnd, claim) {
109
+ let insertAt = heading + 1;
110
+ while (insertAt < sectionEnd && lines[insertAt].trim() === "") insertAt++;
111
+ while (insertAt < sectionEnd && lines[insertAt].trim() !== "") insertAt++;
112
+ const toInsert = [
113
+ "",
114
+ ACTIVE_CLAIMS_TABLE_HEADER,
115
+ ACTIVE_CLAIMS_TABLE_SEPARATOR,
116
+ formatRow(claim),
117
+ ];
118
+ lines.splice(insertAt, 0, ...toInsert);
119
+ return lines.join("\n");
120
+ }
121
+
122
+ function appendRowAfterTable(lines, sectionEnd, separatorIdx, claim) {
123
+ let lastRowIdx = separatorIdx;
124
+ for (let i = separatorIdx + 1; i < sectionEnd; i++) {
125
+ if (!lines[i].startsWith("|")) break;
126
+ lastRowIdx = i;
127
+ }
128
+ if (lastRowIdx > separatorIdx) {
129
+ const match = lines[lastRowIdx].match(ROW_RE);
130
+ if (match && isEmptyStateCell(match[1].trim())) {
131
+ lines[lastRowIdx] = formatRow(claim);
132
+ return lines.join("\n");
133
+ }
134
+ }
135
+ lines.splice(lastRowIdx + 1, 0, formatRow(claim));
136
+ return lines.join("\n");
137
+ }
138
+
139
+ /** Append a claim row to MEMORY.md text. Refuses if (agent, target) already present. */
140
+ export function appendClaim(memoryText, claim, _today) {
141
+ const existing = parseClaims(memoryText);
142
+ if (
143
+ existing.some((c) => c.agent === claim.agent && c.target === claim.target)
144
+ ) {
145
+ return { text: memoryText, inserted: false, reason: "duplicate" };
146
+ }
147
+ const lines = memoryText.split("\n");
148
+ const heading = findSection(lines);
149
+ if (heading === -1) {
150
+ return { text: appendNewSection(memoryText, claim), inserted: true };
151
+ }
152
+ const sectionEnd = findNextH2(lines, heading + 1);
153
+ const { headerIdx, separatorIdx } = findTableIndices(
154
+ lines,
155
+ heading,
156
+ sectionEnd,
157
+ );
158
+ if (headerIdx === -1 || separatorIdx === -1) {
159
+ return {
160
+ text: insertTableWithRow(lines, heading, sectionEnd, claim),
161
+ inserted: true,
162
+ };
163
+ }
164
+ return {
165
+ text: appendRowAfterTable(lines, sectionEnd, separatorIdx, claim),
166
+ inserted: true,
167
+ };
168
+ }
169
+
170
+ /** Remove the claim row matching (agent, target). Idempotent. */
171
+ export function removeClaim(memoryText, { agent, target }) {
172
+ const lines = memoryText.split("\n");
173
+ const heading = findSection(lines);
174
+ if (heading === -1) return { text: memoryText, removed: false };
175
+ const sectionEnd = findNextH2(lines, heading + 1);
176
+ for (let i = heading + 1; i < sectionEnd; i++) {
177
+ const match = lines[i].match(ROW_RE);
178
+ if (!match) continue;
179
+ if (match[1].trim() === agent && match[2].trim() === target) {
180
+ lines.splice(i, 1);
181
+ return { text: lines.join("\n"), removed: true };
182
+ }
183
+ }
184
+ return { text: memoryText, removed: false };
185
+ }
186
+
187
+ /** Split claims into active vs expired based on `expires_at >= today`. */
188
+ export function filterExpired(claims, today) {
189
+ const active = [];
190
+ const expired = [];
191
+ for (const c of claims) {
192
+ if (!c.expires_at || c.expires_at >= today) active.push(c);
193
+ else expired.push(c);
194
+ }
195
+ return { active, expired };
196
+ }
package/src/boot.js ADDED
@@ -0,0 +1,179 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseClaims, filterExpired } from "./active-claims.js";
4
+ import { MEMO_INBOX_MARKER, PRIORITY_INDEX_HEADING } from "./constants.js";
5
+
6
+ function readIfExists(filePath) {
7
+ if (!existsSync(filePath)) return null;
8
+ return readFileSync(filePath, "utf-8");
9
+ }
10
+
11
+ function currentStoryboardPath(wikiRoot, date) {
12
+ const yyyy = date.getUTCFullYear();
13
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
14
+ return path.join(wikiRoot, `storyboard-${yyyy}-M${mm}.md`);
15
+ }
16
+
17
+ function extractSummary(text) {
18
+ if (!text) return "";
19
+ const lines = text.split("\n");
20
+ let i = 0;
21
+ while (i < lines.length && lines[i].startsWith("#")) i++;
22
+ while (i < lines.length && lines[i].trim() === "") i++;
23
+ const paragraph = [];
24
+ while (i < lines.length && lines[i].trim() !== "") {
25
+ paragraph.push(lines[i]);
26
+ i++;
27
+ }
28
+ return paragraph.join(" ").trim();
29
+ }
30
+
31
+ function parsePriorityRow(line) {
32
+ const cells = line
33
+ .split("|")
34
+ .slice(1, -1)
35
+ .map((c) => c.trim());
36
+ if (cells.length < 5) return null;
37
+ const [item, agents, owner, status, added] = cells;
38
+ if (item === "*None*") return null;
39
+ return { item, agents, owner, status, added, link: null };
40
+ }
41
+
42
+ function findHeading(lines, heading) {
43
+ for (let i = 0; i < lines.length; i++) {
44
+ if (lines[i].trim() === heading) return i;
45
+ }
46
+ return -1;
47
+ }
48
+
49
+ function findSectionEnd(lines, start) {
50
+ for (let i = start; i < lines.length; i++) {
51
+ if (/^## /.test(lines[i])) return i;
52
+ }
53
+ return lines.length;
54
+ }
55
+
56
+ function parsePriorityTable(text) {
57
+ if (!text) return [];
58
+ const lines = text.split("\n");
59
+ const start = findHeading(lines, PRIORITY_INDEX_HEADING);
60
+ if (start === -1) return [];
61
+ const end = findSectionEnd(lines, start + 1);
62
+ const rows = [];
63
+ let inTable = false;
64
+ let seenSep = false;
65
+ for (let i = start + 1; i < end; i++) {
66
+ const line = lines[i];
67
+ if (/^\|\s*Item\s*\|/.test(line)) {
68
+ inTable = true;
69
+ continue;
70
+ }
71
+ if (inTable && /^\|\s*---/.test(line)) {
72
+ seenSep = true;
73
+ continue;
74
+ }
75
+ if (!(inTable && seenSep && line.startsWith("|"))) continue;
76
+ const row = parsePriorityRow(line);
77
+ if (row) rows.push(row);
78
+ }
79
+ return rows;
80
+ }
81
+
82
+ function splitPriorities(rows, agent) {
83
+ const owned = [];
84
+ const cross = [];
85
+ for (const r of rows) {
86
+ if (r.owner === agent) owned.push(r);
87
+ else cross.push(r);
88
+ }
89
+ return { owned, cross };
90
+ }
91
+
92
+ function parseStoryboardItems(text, agent) {
93
+ if (!text) return [];
94
+ const lines = text.split("\n");
95
+ const items = [];
96
+ let inAgent = false;
97
+ for (const line of lines) {
98
+ const h3Match = line.match(/^### (.+)$/);
99
+ if (h3Match) {
100
+ inAgent = h3Match[1].toLowerCase().startsWith(agent.toLowerCase());
101
+ continue;
102
+ }
103
+ if (!inAgent) continue;
104
+ const bullet = line.match(/^[-*]\s+(.+)$/);
105
+ if (bullet) {
106
+ items.push({
107
+ dim: agent,
108
+ threshold: bullet[1],
109
+ status: "open",
110
+ link: null,
111
+ });
112
+ }
113
+ }
114
+ return items;
115
+ }
116
+
117
+ function countInbox(text) {
118
+ if (!text) return 0;
119
+ const lines = text.split("\n");
120
+ const markerIdx = lines.findIndex((l) => l.trim() === MEMO_INBOX_MARKER);
121
+ if (markerIdx === -1) return 0;
122
+ let n = 0;
123
+ for (let i = markerIdx + 1; i < lines.length; i++) {
124
+ const line = lines[i];
125
+ if (line.trim() === "") continue;
126
+ if (/^##\s/.test(line)) break;
127
+ if (!line.startsWith("-")) continue;
128
+ if (/\*No new messages\.\*/.test(line)) continue;
129
+ n++;
130
+ }
131
+ return n;
132
+ }
133
+
134
+ function mapPriority(r) {
135
+ return { item: r.item, status: r.status, added: r.added, link: r.link };
136
+ }
137
+
138
+ function mapClaim(c) {
139
+ return {
140
+ agent: c.agent,
141
+ target: c.target,
142
+ branch: c.branch,
143
+ pr: c.pr,
144
+ claimed_at: c.claimed_at,
145
+ expires_at: c.expires_at,
146
+ };
147
+ }
148
+
149
+ /** Build the boot digest JSON object. */
150
+ export function buildDigest({ wikiRoot, agent, today, _fs, _gh }) {
151
+ const date = today instanceof Date ? today : new Date(today);
152
+ const todayStr = date.toISOString().slice(0, 10);
153
+
154
+ const summaryPath = path.join(wikiRoot, `${agent}.md`);
155
+ const memoryPath = path.join(wikiRoot, "MEMORY.md");
156
+ const storyboardPath = currentStoryboardPath(wikiRoot, date);
157
+
158
+ const summaryText = readIfExists(summaryPath);
159
+ const memoryText = readIfExists(memoryPath);
160
+ const storyboardText = readIfExists(storyboardPath);
161
+
162
+ const { active } = filterExpired(parseClaims(memoryText ?? ""), todayStr);
163
+ const { owned, cross } = splitPriorities(
164
+ parsePriorityTable(memoryText ?? ""),
165
+ agent,
166
+ );
167
+
168
+ return {
169
+ summary: extractSummary(summaryText),
170
+ owned_priorities: owned.map(mapPriority),
171
+ cross_cutting: cross.map(mapPriority),
172
+ claims: active.map(mapClaim),
173
+ storyboard_items: parseStoryboardItems(storyboardText ?? "", agent),
174
+ inbox_count: countInbox(summaryText),
175
+ storyboard_path: existsSync(storyboardPath)
176
+ ? path.relative(path.dirname(wikiRoot) || ".", storyboardPath)
177
+ : "",
178
+ };
179
+ }