@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 +169 -26
- package/package.json +2 -1
- package/src/active-claims.js +196 -0
- package/src/boot.js +179 -0
- package/src/commands/audit.js +354 -0
- package/src/commands/boot.js +66 -0
- package/src/commands/claim.js +107 -0
- package/src/commands/inbox.js +180 -0
- package/src/commands/init.js +144 -21
- package/src/commands/log.js +102 -0
- package/src/commands/refresh.js +37 -13
- package/src/commands/rotate.js +25 -0
- package/src/commands/sync.js +15 -10
- package/src/constants.js +18 -0
- package/src/index.js +24 -0
- package/src/issue-list-renderer.js +69 -0
- package/src/marker-scanner.js +75 -28
- package/src/weekly-log.js +91 -0
- package/src/wiki-repo.js +45 -11
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
|
-
|
|
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
|
|
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:
|
|
195
|
+
description:
|
|
196
|
+
"Bootstrap a wiki working tree, scaffold Active Claims, install audit Stop-hook",
|
|
53
197
|
options: {
|
|
54
|
-
|
|
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.
|
|
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
|
+
}
|