@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/src/commands/init.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fsAsync from "node:fs/promises";
|
|
5
5
|
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
6
7
|
import { WikiRepo } from "../wiki-repo.js";
|
|
7
8
|
import { listSkills } from "../skill-roster.js";
|
|
9
|
+
import {
|
|
10
|
+
ACTIVE_CLAIMS_HEADING,
|
|
11
|
+
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
12
|
+
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
13
|
+
} from "../constants.js";
|
|
14
|
+
|
|
15
|
+
/** Resolve the wiki clone URL. Honors the FIT_WIKI_URL env var as an explicit override (for sandboxed environments where `origin` is rewritten to a local proxy that does not serve wiki repos); otherwise derives the URL by appending `.wiki.git` to the parent repo's `origin` remote. */
|
|
16
|
+
export function deriveWikiUrl(parentDir) {
|
|
17
|
+
if (process.env.FIT_WIKI_URL) return process.env.FIT_WIKI_URL;
|
|
8
18
|
|
|
9
|
-
function deriveWikiUrl(parentDir) {
|
|
10
19
|
const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
|
|
11
20
|
encoding: "utf-8",
|
|
12
21
|
stdio: "pipe",
|
|
@@ -17,18 +26,96 @@ function deriveWikiUrl(parentDir) {
|
|
|
17
26
|
return base + ".wiki.git";
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
29
|
+
function scaffoldActiveClaims(memoryPath) {
|
|
30
|
+
if (!existsSync(memoryPath)) return false;
|
|
31
|
+
const text = readFileSync(memoryPath, "utf-8");
|
|
32
|
+
if (new RegExp(`^${ACTIVE_CLAIMS_HEADING}$`, "m").test(text)) return false;
|
|
25
33
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
const block = [
|
|
35
|
+
"",
|
|
36
|
+
ACTIVE_CLAIMS_HEADING,
|
|
37
|
+
"",
|
|
38
|
+
"In-flight work claimed by an agent. Row present = active; row absent = settled.",
|
|
39
|
+
"Writers: `fit-wiki claim`, `fit-wiki release`. Reader: `fit-wiki boot`.",
|
|
40
|
+
"",
|
|
41
|
+
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
42
|
+
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
43
|
+
"| *None* | — | — | — | — | — |",
|
|
44
|
+
"",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
const storyboardIdx = lines.findIndex((l) => l.trim() === "## Storyboard");
|
|
49
|
+
if (storyboardIdx === -1) {
|
|
50
|
+
writeFileSync(memoryPath, text.replace(/\n*$/, "") + "\n" + block + "\n");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
lines.splice(storyboardIdx, 0, ...block.split("\n"), "");
|
|
54
|
+
writeFileSync(memoryPath, lines.join("\n"));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const AUDIT_HOOK_COMMAND = "bunx fit-wiki audit";
|
|
31
59
|
|
|
60
|
+
function readSettings(settingsPath) {
|
|
61
|
+
if (!existsSync(settingsPath)) return {};
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
64
|
+
} catch {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`init: ${settingsPath} is not valid JSON; skipping stop-hook install\n`,
|
|
67
|
+
);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasAuditHook(settings) {
|
|
73
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.Stop)) return false;
|
|
74
|
+
for (const group of settings.hooks.Stop) {
|
|
75
|
+
if (!group || !Array.isArray(group.hooks)) continue;
|
|
76
|
+
if (
|
|
77
|
+
group.hooks.some(
|
|
78
|
+
(h) =>
|
|
79
|
+
h &&
|
|
80
|
+
typeof h.command === "string" &&
|
|
81
|
+
h.command.includes("fit-wiki audit"),
|
|
82
|
+
)
|
|
83
|
+
) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function addAuditHook(settings) {
|
|
91
|
+
if (!settings.hooks) settings.hooks = {};
|
|
92
|
+
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
|
|
93
|
+
if (settings.hooks.Stop.length === 0) {
|
|
94
|
+
settings.hooks.Stop.push({
|
|
95
|
+
hooks: [{ type: "command", command: AUDIT_HOOK_COMMAND }],
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(settings.hooks.Stop[0].hooks)) {
|
|
100
|
+
settings.hooks.Stop[0].hooks = [];
|
|
101
|
+
}
|
|
102
|
+
settings.hooks.Stop[0].hooks.push({
|
|
103
|
+
type: "command",
|
|
104
|
+
command: AUDIT_HOOK_COMMAND,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function installStopHook(settingsPath) {
|
|
109
|
+
const settings = readSettings(settingsPath);
|
|
110
|
+
if (settings === null) return false;
|
|
111
|
+
if (hasAuditHook(settings)) return false;
|
|
112
|
+
addAuditHook(settings);
|
|
113
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
114
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function maybeCloneWiki(projectRoot, wikiDir) {
|
|
32
119
|
const wikiUrl = deriveWikiUrl(projectRoot);
|
|
33
120
|
if (!wikiUrl) {
|
|
34
121
|
process.stderr.write(
|
|
@@ -36,20 +123,56 @@ export function runInitCommand(values, _args, cli) {
|
|
|
36
123
|
);
|
|
37
124
|
return;
|
|
38
125
|
}
|
|
126
|
+
const config = await createScriptConfig("wiki");
|
|
127
|
+
const repo = new WikiRepo({
|
|
128
|
+
wikiDir,
|
|
129
|
+
parentDir: projectRoot,
|
|
130
|
+
resolveToken: () => config.ghToken(),
|
|
131
|
+
});
|
|
132
|
+
const cloneResult = repo.ensureCloned(wikiUrl);
|
|
133
|
+
if (cloneResult.cloned) {
|
|
134
|
+
repo.inheritIdentity();
|
|
135
|
+
} else {
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
"init: could not clone wiki, continuing with local-only steps\n",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Clone the wiki if not already present, scaffold Active Claims in MEMORY.md, install the audit Stop-hook, and create per-skill metric directories. */
|
|
143
|
+
export async function runInitCommand(values, _args, _cli) {
|
|
144
|
+
const logger = { debug() {} };
|
|
145
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
146
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
147
|
+
|
|
148
|
+
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
149
|
+
const skillsDir = path.resolve(
|
|
150
|
+
projectRoot,
|
|
151
|
+
values["skills-dir"] ?? path.join(".claude", "skills"),
|
|
152
|
+
);
|
|
153
|
+
const settingsPath = path.resolve(projectRoot, ".claude", "settings.json");
|
|
39
154
|
|
|
40
|
-
|
|
155
|
+
await maybeCloneWiki(projectRoot, wikiDir);
|
|
41
156
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
157
|
+
if (existsSync(skillsDir)) {
|
|
158
|
+
for (const slug of listSkills({ skillsDir })) {
|
|
159
|
+
mkdirSync(path.join(wikiDir, "metrics", slug), { recursive: true });
|
|
160
|
+
}
|
|
46
161
|
}
|
|
47
162
|
|
|
48
|
-
|
|
163
|
+
if (existsSync(wikiDir)) {
|
|
164
|
+
const memoryPath = path.join(wikiDir, "MEMORY.md");
|
|
165
|
+
if (scaffoldActiveClaims(memoryPath)) {
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`init: scaffolded ${ACTIVE_CLAIMS_HEADING} in ${memoryPath}\n`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
49
171
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
172
|
+
if (installStopHook(settingsPath)) {
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
`init: installed Stop-hook audit entry in ${settingsPath}\n`,
|
|
175
|
+
);
|
|
53
176
|
}
|
|
54
177
|
|
|
55
178
|
process.stdout.write(`init: wiki ready at ${wikiDir}\n`);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fsAsync from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Finder } from "@forwardimpact/libutil";
|
|
4
|
+
import {
|
|
5
|
+
weeklyLogPath,
|
|
6
|
+
rotateIfOverBudget,
|
|
7
|
+
appendEntry,
|
|
8
|
+
} from "../weekly-log.js";
|
|
9
|
+
import { DECISION_HEADING } from "../constants.js";
|
|
10
|
+
|
|
11
|
+
function projectRootForCommand() {
|
|
12
|
+
const logger = { debug() {} };
|
|
13
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
14
|
+
return finder.findProjectRoot(process.cwd());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function commonContext(values) {
|
|
18
|
+
const agent = values.agent || process.env.LIBEVAL_AGENT_PROFILE;
|
|
19
|
+
if (!agent) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
"log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var\n",
|
|
22
|
+
);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
const projectRoot = projectRootForCommand();
|
|
26
|
+
const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
|
|
27
|
+
const today = values.today || new Date().toISOString().slice(0, 10);
|
|
28
|
+
return { agent, wikiRoot, today };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureDateHeading(body, today) {
|
|
32
|
+
const heading = `## ${today}`;
|
|
33
|
+
if (body.startsWith(heading) || body.startsWith("## ")) return body;
|
|
34
|
+
return `${heading}\n\n${body}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runDecision(values) {
|
|
38
|
+
const { agent, wikiRoot, today } = commonContext(values);
|
|
39
|
+
const surveyed = values.surveyed || "—";
|
|
40
|
+
const chosen = values.chosen || "—";
|
|
41
|
+
const rationale = values.rationale || "—";
|
|
42
|
+
const alternatives = values.alternatives || "—";
|
|
43
|
+
const body = [
|
|
44
|
+
`## ${today}`,
|
|
45
|
+
"",
|
|
46
|
+
DECISION_HEADING,
|
|
47
|
+
"",
|
|
48
|
+
`**Surveyed:** ${surveyed}`,
|
|
49
|
+
"",
|
|
50
|
+
`**Alternatives:** ${alternatives}`,
|
|
51
|
+
"",
|
|
52
|
+
`**Chosen:** ${chosen}`,
|
|
53
|
+
"",
|
|
54
|
+
`**Rationale:** ${rationale}`,
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
const lineCount = body.split("\n").length;
|
|
58
|
+
rotateIfOverBudget(wikiRoot, agent, today, lineCount);
|
|
59
|
+
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
60
|
+
appendEntry(target, body, agent, today);
|
|
61
|
+
process.stdout.write(`logged decision to ${target}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runNote(values) {
|
|
65
|
+
const { agent, wikiRoot, today } = commonContext(values);
|
|
66
|
+
if (!values.field || !values.body) {
|
|
67
|
+
process.stderr.write("log note requires --field and --body\n");
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
const body = ensureDateHeading(
|
|
71
|
+
`### ${values.field}\n\n${values.body}\n`,
|
|
72
|
+
today,
|
|
73
|
+
);
|
|
74
|
+
const lineCount = body.split("\n").length;
|
|
75
|
+
rotateIfOverBudget(wikiRoot, agent, today, lineCount);
|
|
76
|
+
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
77
|
+
appendEntry(target, body, agent, today);
|
|
78
|
+
process.stdout.write(`logged note to ${target}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runDone(values) {
|
|
82
|
+
const { agent, wikiRoot, today } = commonContext(values);
|
|
83
|
+
const body = `### Closed\n\nRun closed ${today}.\n`;
|
|
84
|
+
const lineCount = body.split("\n").length;
|
|
85
|
+
rotateIfOverBudget(wikiRoot, agent, today, lineCount);
|
|
86
|
+
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
87
|
+
appendEntry(target, body, agent, today);
|
|
88
|
+
process.stdout.write(`closed entry in ${target}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SUBS = { decision: runDecision, note: runNote, done: runDone };
|
|
92
|
+
|
|
93
|
+
/** Dispatch `log {decision|note|done}` to the matching sub-handler. */
|
|
94
|
+
export function runLogCommand(values, args, cli) {
|
|
95
|
+
const sub = args[0];
|
|
96
|
+
const handler = SUBS[sub];
|
|
97
|
+
if (!handler) {
|
|
98
|
+
cli.usageError("log requires subcommand: decision | note | done");
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
handler(values);
|
|
102
|
+
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -4,6 +4,7 @@ import fsAsync from "node:fs/promises";
|
|
|
4
4
|
import { Finder } from "@forwardimpact/libutil";
|
|
5
5
|
import { scanMarkers } from "../marker-scanner.js";
|
|
6
6
|
import { renderBlock, BlockRenderError } from "../block-renderer.js";
|
|
7
|
+
import { renderIssueList } from "../issue-list-renderer.js";
|
|
7
8
|
|
|
8
9
|
function currentStoryboardPath() {
|
|
9
10
|
const now = new Date();
|
|
@@ -12,8 +13,34 @@ function currentStoryboardPath() {
|
|
|
12
13
|
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
function renderForBlock(block, projectRoot) {
|
|
17
|
+
if (block.kind === "xmr") {
|
|
18
|
+
return renderBlock({
|
|
19
|
+
metric: block.metric,
|
|
20
|
+
csvPath: block.csvPath,
|
|
21
|
+
projectRoot,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (block.kind === "issue-list") {
|
|
25
|
+
return renderIssueList({
|
|
26
|
+
topic: block.topic,
|
|
27
|
+
state: block.state,
|
|
28
|
+
window: block.window,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function spliceBlock(lines, block, rendered) {
|
|
35
|
+
lines.splice(
|
|
36
|
+
block.openLine + 1,
|
|
37
|
+
block.closeLine - block.openLine - 1,
|
|
38
|
+
...rendered,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
|
|
43
|
+
export function runRefreshCommand(values, args, _cli) {
|
|
17
44
|
const logger = { debug() {} };
|
|
18
45
|
const finder = new Finder(fsAsync, logger, process);
|
|
19
46
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
@@ -24,7 +51,6 @@ export function runRefreshCommand(values, args, cli) {
|
|
|
24
51
|
);
|
|
25
52
|
const text = readFileSync(storyboardPath, "utf-8");
|
|
26
53
|
const blocks = scanMarkers(text);
|
|
27
|
-
|
|
28
54
|
if (blocks.length === 0) return;
|
|
29
55
|
|
|
30
56
|
const lines = text.split("\n");
|
|
@@ -33,16 +59,9 @@ export function runRefreshCommand(values, args, cli) {
|
|
|
33
59
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
34
60
|
const block = blocks[i];
|
|
35
61
|
try {
|
|
36
|
-
const rendered =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
projectRoot,
|
|
40
|
-
});
|
|
41
|
-
lines.splice(
|
|
42
|
-
block.openLine + 1,
|
|
43
|
-
block.closeLine - block.openLine - 1,
|
|
44
|
-
...rendered,
|
|
45
|
-
);
|
|
62
|
+
const rendered = renderForBlock(block, projectRoot);
|
|
63
|
+
if (!rendered) continue;
|
|
64
|
+
spliceBlock(lines, block, rendered);
|
|
46
65
|
spliced = true;
|
|
47
66
|
} catch (err) {
|
|
48
67
|
if (!(err instanceof BlockRenderError)) throw err;
|
|
@@ -53,4 +72,9 @@ export function runRefreshCommand(values, args, cli) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
if (spliced) writeFileSync(storyboardPath, lines.join("\n"));
|
|
75
|
+
if (values && values.format === "json") {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
JSON.stringify({ blocks: blocks.length, spliced }) + "\n",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
56
80
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fsAsync from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Finder } from "@forwardimpact/libutil";
|
|
4
|
+
import { rotateIfOverBudget } from "../weekly-log.js";
|
|
5
|
+
|
|
6
|
+
/** Force-rotate the current weekly log to a sealed part file. */
|
|
7
|
+
export function runRotateCommand(values, _args, cli) {
|
|
8
|
+
const agent = values.agent || process.env.LIBEVAL_AGENT_PROFILE;
|
|
9
|
+
if (!agent) {
|
|
10
|
+
cli.usageError("rotate requires --agent or LIBEVAL_AGENT_PROFILE");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
const logger = { debug() {} };
|
|
14
|
+
const finder = new Finder(fsAsync, logger, process);
|
|
15
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
16
|
+
const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
|
|
17
|
+
const today = values.today || new Date().toISOString().slice(0, 10);
|
|
18
|
+
|
|
19
|
+
const result = rotateIfOverBudget(wikiRoot, agent, today, 0, { force: true });
|
|
20
|
+
if (result.rotated) {
|
|
21
|
+
process.stdout.write(`rotated ${result.fromPath} → ${result.toPath}\n`);
|
|
22
|
+
} else {
|
|
23
|
+
process.stdout.write(`no rotation needed for ${agent}\n`);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/commands/sync.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fsAsync from "node:fs/promises";
|
|
3
3
|
import { Finder } from "@forwardimpact/libutil";
|
|
4
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
4
5
|
import { WikiRepo, WikiPullConflict } from "../wiki-repo.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
export function runPushCommand(values, _args, cli) {
|
|
7
|
+
async function buildRepo(values) {
|
|
8
8
|
const logger = { debug() {} };
|
|
9
9
|
const finder = new Finder(fsAsync, logger, process);
|
|
10
10
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
11
11
|
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const config = await createScriptConfig("wiki");
|
|
14
|
+
return new WikiRepo({
|
|
15
|
+
wikiDir,
|
|
16
|
+
parentDir: projectRoot,
|
|
17
|
+
resolveToken: () => config.ghToken(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Commit all wiki changes and push them to the remote wiki repository. */
|
|
22
|
+
export async function runPushCommand(values, _args, cli) {
|
|
23
|
+
const repo = await buildRepo(values);
|
|
14
24
|
repo.inheritIdentity();
|
|
15
25
|
|
|
16
26
|
const result = repo.commitAndPush("wiki: update from session");
|
|
@@ -22,13 +32,8 @@ export function runPushCommand(values, _args, cli) {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
/** Fetch and rebase the local wiki on origin/master; on rebase conflict, exit the process with code 1 and a message to resolve manually or push first. */
|
|
25
|
-
export function runPullCommand(values, _args, cli) {
|
|
26
|
-
const
|
|
27
|
-
const finder = new Finder(fsAsync, logger, process);
|
|
28
|
-
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
29
|
-
const wikiDir = path.resolve(projectRoot, values["wiki-root"] ?? "wiki");
|
|
30
|
-
|
|
31
|
-
const repo = new WikiRepo({ wikiDir, parentDir: projectRoot });
|
|
35
|
+
export async function runPullCommand(values, _args, cli) {
|
|
36
|
+
const repo = await buildRepo(values);
|
|
32
37
|
repo.inheritIdentity();
|
|
33
38
|
|
|
34
39
|
try {
|
package/src/constants.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
export const MEMO_INBOX_MARKER = "<!-- memo:inbox -->";
|
|
2
2
|
export const INBOX_HEADING = "## Message Inbox";
|
|
3
3
|
export const BROADCAST_TARGET = "all";
|
|
4
|
+
|
|
5
|
+
export const MEMORY_FILE = "MEMORY.md";
|
|
6
|
+
export const ACTIVE_CLAIMS_HEADING = "## Active Claims";
|
|
7
|
+
export const ACTIVE_CLAIMS_TABLE_HEADER =
|
|
8
|
+
"| agent | target | branch | pr | claimed_at | expires_at |";
|
|
9
|
+
export const ACTIVE_CLAIMS_TABLE_SEPARATOR =
|
|
10
|
+
"| --- | --- | --- | --- | --- | --- |";
|
|
11
|
+
export const PRIORITY_INDEX_HEADING = "## Cross-Cutting Priorities";
|
|
12
|
+
export const PRIORITY_INDEX_TABLE_HEADER =
|
|
13
|
+
"| Item | Agents | Owner | Status | Added |";
|
|
14
|
+
export const DECISION_HEADING = "### Decision";
|
|
15
|
+
|
|
16
|
+
// Cap derivation: ≤2.5% of a 1M-token context window = 25k tokens;
|
|
17
|
+
// ≈42 tokens/line empirical proxy → ~500 lines. See spec 1060
|
|
18
|
+
// design-a.md § Decision area 2 for the full anchor.
|
|
19
|
+
export const WEEKLY_LOG_LINE_BUDGET = 500;
|
|
20
|
+
export const SUMMARY_LINE_BUDGET = 80;
|
|
21
|
+
export const CUTOVER_ISO_WEEK = "2026-W23";
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,32 @@ export {
|
|
|
5
5
|
MEMO_INBOX_MARKER,
|
|
6
6
|
INBOX_HEADING,
|
|
7
7
|
BROADCAST_TARGET,
|
|
8
|
+
MEMORY_FILE,
|
|
9
|
+
ACTIVE_CLAIMS_HEADING,
|
|
10
|
+
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
11
|
+
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
12
|
+
PRIORITY_INDEX_HEADING,
|
|
13
|
+
PRIORITY_INDEX_TABLE_HEADER,
|
|
14
|
+
DECISION_HEADING,
|
|
15
|
+
WEEKLY_LOG_LINE_BUDGET,
|
|
16
|
+
SUMMARY_LINE_BUDGET,
|
|
17
|
+
CUTOVER_ISO_WEEK,
|
|
8
18
|
} from "./constants.js";
|
|
9
19
|
export { scanMarkers } from "./marker-scanner.js";
|
|
10
20
|
export { renderBlock } from "./block-renderer.js";
|
|
21
|
+
export { renderIssueList } from "./issue-list-renderer.js";
|
|
11
22
|
export { WikiRepo } from "./wiki-repo.js";
|
|
12
23
|
export { listSkills } from "./skill-roster.js";
|
|
24
|
+
export {
|
|
25
|
+
parseClaims,
|
|
26
|
+
appendClaim,
|
|
27
|
+
removeClaim,
|
|
28
|
+
filterExpired,
|
|
29
|
+
} from "./active-claims.js";
|
|
30
|
+
export {
|
|
31
|
+
isoWeek,
|
|
32
|
+
weeklyLogPath,
|
|
33
|
+
rotateIfOverBudget,
|
|
34
|
+
appendEntry,
|
|
35
|
+
} from "./weekly-log.js";
|
|
36
|
+
export { buildDigest } from "./boot.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function defaultGh(args) {
|
|
4
|
+
return spawnSync("gh", args, {
|
|
5
|
+
encoding: "utf-8",
|
|
6
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function daysAgo(today, n) {
|
|
11
|
+
const d = today instanceof Date ? new Date(today.getTime()) : new Date(today);
|
|
12
|
+
d.setUTCDate(d.getUTCDate() - n);
|
|
13
|
+
return d.toISOString().slice(0, 10);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Render an issue-list block for an obstacles/experiments marker. Returns markdown lines. */
|
|
17
|
+
export function renderIssueList({
|
|
18
|
+
topic,
|
|
19
|
+
state,
|
|
20
|
+
window,
|
|
21
|
+
today = new Date(),
|
|
22
|
+
gh = defaultGh,
|
|
23
|
+
}) {
|
|
24
|
+
const ghState = state === "closed" ? "closed" : "open";
|
|
25
|
+
const result = gh([
|
|
26
|
+
"issue",
|
|
27
|
+
"list",
|
|
28
|
+
"--label",
|
|
29
|
+
topic.replace(/s$/, ""),
|
|
30
|
+
"--state",
|
|
31
|
+
ghState,
|
|
32
|
+
"--json",
|
|
33
|
+
"number,title,labels,closedAt",
|
|
34
|
+
"--limit",
|
|
35
|
+
"100",
|
|
36
|
+
]);
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`refresh: gh issue list failed for ${topic}:${state}\n`,
|
|
40
|
+
);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
let issues;
|
|
44
|
+
try {
|
|
45
|
+
issues = JSON.parse(result.stdout || "[]");
|
|
46
|
+
} catch {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`refresh: gh issue list JSON parse failed for ${topic}:${state}\n`,
|
|
49
|
+
);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (state === "closed") {
|
|
54
|
+
const windowDays = window
|
|
55
|
+
? Number.parseInt(window.replace("d", ""), 10)
|
|
56
|
+
: 7;
|
|
57
|
+
const cutoff = daysAgo(today, windowDays);
|
|
58
|
+
issues = issues.filter(
|
|
59
|
+
(i) => i.closedAt && i.closedAt.slice(0, 10) >= cutoff,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = [];
|
|
64
|
+
for (const issue of issues) {
|
|
65
|
+
const tag = topic === "experiments" ? "Exp" : "Obs";
|
|
66
|
+
lines.push(`- **${tag} #${issue.number} — ${issue.title}**`);
|
|
67
|
+
}
|
|
68
|
+
return lines;
|
|
69
|
+
}
|