@forwardimpact/libwiki 0.2.11 → 0.2.12
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 +49 -286
- package/package.json +1 -1
- package/src/agent-roster.js +7 -6
- package/src/audit/rules.js +5 -0
- package/src/audit/scopes.js +81 -28
- package/src/audit/status-row.js +51 -0
- package/src/block-renderer.js +7 -8
- package/src/boot.js +19 -19
- package/src/cli-definition.js +284 -0
- package/src/commands/audit.js +14 -12
- package/src/commands/boot.js +13 -14
- package/src/commands/claim.js +76 -82
- package/src/commands/fix.js +116 -37
- package/src/commands/inbox.js +61 -54
- package/src/commands/init.js +47 -53
- package/src/commands/log.js +58 -52
- package/src/commands/memo.js +59 -50
- package/src/commands/refresh.js +40 -36
- package/src/commands/rotate.js +26 -15
- package/src/commands/sync.js +17 -16
- package/src/index.js +1 -1
- package/src/issue-list-renderer.js +29 -28
- package/src/marker-migrator.js +8 -7
- package/src/marker-scanner.js +13 -8
- package/src/memo-writer.js +7 -6
- package/src/skill-roster.js +7 -3
- package/src/status.js +19 -0
- package/src/util/clock.js +13 -0
- package/src/util/wiki-dir.js +24 -0
- package/src/weekly-log.js +37 -37
- package/src/wiki-sync.js +164 -0
- package/src/build-repo.js +0 -20
- package/src/io.js +0 -77
- package/src/wiki-repo.js +0 -167
package/src/commands/fix.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fsAsync from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { Writable } from "node:stream";
|
|
4
|
-
import {
|
|
3
|
+
import { emitFindingsText, runRules } from "@forwardimpact/libutil";
|
|
5
4
|
import {
|
|
6
5
|
createAgentRunner,
|
|
7
6
|
composeProfilePrompt,
|
|
@@ -9,56 +8,136 @@ import {
|
|
|
9
8
|
} from "@forwardimpact/libeval";
|
|
10
9
|
import { RULES } from "../audit/rules.js";
|
|
11
10
|
import { buildContext, resolveScope } from "../audit/scopes.js";
|
|
11
|
+
import { currentDayIso } from "../util/clock.js";
|
|
12
|
+
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
|
|
18
|
-
const today = values.today || new Date().toISOString().slice(0, 10);
|
|
14
|
+
// The agent edits, we re-audit, and resume on whatever still fails. Cap the
|
|
15
|
+
// rounds so a finding the agent cannot resolve (e.g. a budget needing
|
|
16
|
+
// `fit-wiki rotate`, which it has no Bash to run) fails loudly, not forever.
|
|
17
|
+
const MAX_ROUNDS = 3;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Every rule governing a scope with an open finding, as `id — hint` lines.
|
|
21
|
+
* Handing the agent the full contract for the files it edits — not just the
|
|
22
|
+
* failing rules — stops it fixing one finding by breaking another (dropping
|
|
23
|
+
* the `**Last run**:` line, appending a section after `## Open Blockers`, …).
|
|
24
|
+
*/
|
|
25
|
+
function invariantContract(findings) {
|
|
26
|
+
const scopes = new Set(
|
|
27
|
+
findings.map((f) => RULES.find((r) => r.id === f.id)?.scope),
|
|
28
|
+
);
|
|
29
|
+
return RULES.filter((r) => scopes.has(r.scope) && r.hint).map(
|
|
30
|
+
(r) => `- ${r.id} — ${r.hint}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The opening task: the findings, the invariant contract, and the two things
|
|
36
|
+
* the rule hints don't cover — where trimmed history goes, and to prefer a
|
|
37
|
+
* single Write.
|
|
38
|
+
*/
|
|
39
|
+
function composeTask(findings, wikiRoot, projectRoot) {
|
|
40
|
+
return [
|
|
41
|
+
`Fix these wiki audit findings by editing files under ${wikiRoot}.`,
|
|
42
|
+
``,
|
|
43
|
+
emitFindingsText(findings, { cwd: projectRoot }),
|
|
44
|
+
``,
|
|
45
|
+
`All of these invariants must hold when you finish — never fix one finding`,
|
|
46
|
+
`by breaking another:`,
|
|
47
|
+
...invariantContract(findings),
|
|
48
|
+
``,
|
|
49
|
+
`Move history out of an over-budget summary into the agent's weekly-log`,
|
|
50
|
+
`file (wiki/<agent>-YYYY-Www.md), never a new summary section. Prefer a`,
|
|
51
|
+
`single Write over many Edits.`,
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The resume task: the findings that survived the last edit. */
|
|
56
|
+
function composeFollowup(findings, projectRoot) {
|
|
57
|
+
return [
|
|
58
|
+
`The wiki still fails the audit. Remaining findings:`,
|
|
59
|
+
``,
|
|
60
|
+
emitFindingsText(findings, { cwd: projectRoot }),
|
|
61
|
+
``,
|
|
62
|
+
`Fix every one without breaking any invariant listed earlier.`,
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Surface a round's agent error, if any. Returns true when it is fatal: a
|
|
68
|
+
* missing sessionId means the process never started (e.g. the SDK refused
|
|
69
|
+
* bypass-permissions as root), so there is nothing to resume. A turn-limit or
|
|
70
|
+
* transient error keeps its session and may have made partial progress, so it
|
|
71
|
+
* is noted but not fatal — the re-audit decides.
|
|
72
|
+
*/
|
|
73
|
+
function isFatalError(result, round, err) {
|
|
74
|
+
if (!result.error) return false;
|
|
75
|
+
if (!result.sessionId) {
|
|
76
|
+
err(`fit-wiki fix: agent run failed: ${result.error.message}\n`);
|
|
77
|
+
return true;
|
|
26
78
|
}
|
|
79
|
+
err(`fit-wiki fix: round ${round} agent error: ${result.error.message}\n`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
27
82
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
83
|
+
/** Run the wiki audit and auto-fix findings via a Haiku-powered AgentRunner. */
|
|
84
|
+
export async function runFixCommand(ctx) {
|
|
85
|
+
const { runtime } = ctx.deps;
|
|
86
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
87
|
+
const wikiRoot = ctx.options["wiki-root"] || path.join(projectRoot, "wiki");
|
|
88
|
+
const today = ctx.options.today || currentDayIso(runtime);
|
|
89
|
+
const out = (s) => runtime.proc.stdout.write(s);
|
|
90
|
+
const err = (s) => runtime.proc.stderr.write(s);
|
|
35
91
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
92
|
+
// The agent's edits change the result, so re-read and re-audit each round.
|
|
93
|
+
const audit = () =>
|
|
94
|
+
runRules(RULES, buildContext({ wikiRoot, today, fs: runtime.fsSync }), {
|
|
95
|
+
resolveScope,
|
|
96
|
+
});
|
|
39
97
|
|
|
40
|
-
|
|
98
|
+
let findings = audit();
|
|
99
|
+
if (findings.length === 0) {
|
|
100
|
+
out("nothing to fix\n");
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
41
103
|
|
|
104
|
+
const query =
|
|
105
|
+
ctx.deps.query ?? (await import("@anthropic-ai/claude-agent-sdk")).query;
|
|
42
106
|
const runner = createAgentRunner({
|
|
43
107
|
cwd: projectRoot,
|
|
44
108
|
query,
|
|
45
|
-
output:
|
|
109
|
+
output: new Writable({ write: (_c, _e, cb) => cb() }),
|
|
46
110
|
model: "claude-haiku-4-5-20251001",
|
|
47
|
-
maxTurns:
|
|
111
|
+
maxTurns: 30,
|
|
48
112
|
allowedTools: ["Read", "Write", "Edit"],
|
|
49
113
|
settingSources: ["project"],
|
|
50
|
-
systemPrompt,
|
|
51
|
-
|
|
114
|
+
systemPrompt: composeProfilePrompt("technical-writer", {
|
|
115
|
+
profilesDir: path.resolve(projectRoot, ".claude/agents"),
|
|
116
|
+
}),
|
|
117
|
+
redactor: createRedactor(),
|
|
52
118
|
});
|
|
53
119
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
// The audit is the verdict, not the agent's self-report: run, re-audit, and
|
|
121
|
+
// resume the session on whatever still fails until clean or out of rounds.
|
|
122
|
+
// Resuming also extends the turn budget for a trim too large for one round.
|
|
123
|
+
let task = composeTask(findings, wikiRoot, projectRoot);
|
|
124
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
125
|
+
const result =
|
|
126
|
+
round === 0 ? await runner.run(task) : await runner.resume(task);
|
|
127
|
+
if (result.text) out(result.text + "\n");
|
|
128
|
+
if (isFatalError(result, round, err)) return { ok: false, code: 1 };
|
|
129
|
+
|
|
130
|
+
findings = audit();
|
|
131
|
+
if (findings.length === 0) {
|
|
132
|
+
out("fixed: wiki audit is clean\n");
|
|
133
|
+
return { ok: true, code: 0 };
|
|
134
|
+
}
|
|
135
|
+
task = composeFollowup(findings, projectRoot);
|
|
136
|
+
}
|
|
60
137
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
138
|
+
err(
|
|
139
|
+
`fit-wiki fix: ${findings.length} finding(s) remain after ${MAX_ROUNDS} round(s):\n` +
|
|
140
|
+
emitFindingsText(findings, { cwd: projectRoot }),
|
|
141
|
+
);
|
|
142
|
+
return { ok: false, code: 1 };
|
|
64
143
|
}
|
package/src/commands/inbox.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
-
import fsAsync from "node:fs/promises";
|
|
3
1
|
import path from "node:path";
|
|
4
|
-
import { Finder } from "@forwardimpact/libutil";
|
|
5
2
|
import {
|
|
6
3
|
MEMO_INBOX_MARKER,
|
|
7
4
|
PRIORITY_INDEX_HEADING,
|
|
8
5
|
PRIORITY_INDEX_TABLE_HEADER,
|
|
9
6
|
} from "../constants.js";
|
|
7
|
+
import { currentDayIso } from "../util/clock.js";
|
|
8
|
+
import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
10
9
|
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
return finder.findProjectRoot(process.cwd());
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function paths(values) {
|
|
18
|
-
const root = projectRoot();
|
|
19
|
-
const wikiRoot = values["wiki-root"] || path.join(root, "wiki");
|
|
20
|
-
const agent = values.agent || process.env.LIBEVAL_AGENT_PROFILE;
|
|
10
|
+
function paths(runtime, options) {
|
|
11
|
+
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
12
|
+
const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
|
|
21
13
|
if (!agent) {
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
runtime.proc.stderr.write(
|
|
15
|
+
"inbox requires --agent or LIBEVAL_AGENT_PROFILE\n",
|
|
16
|
+
);
|
|
17
|
+
return { error: { ok: false, code: 2 } };
|
|
24
18
|
}
|
|
25
19
|
return {
|
|
26
20
|
summaryPath: path.join(wikiRoot, `${agent}.md`),
|
|
@@ -53,33 +47,39 @@ function removeBulletAt(lines, idx) {
|
|
|
53
47
|
return lines;
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
function listCmd(
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
function listCmd(runtime, options) {
|
|
51
|
+
const p = paths(runtime, options);
|
|
52
|
+
if (p.error) return p.error;
|
|
53
|
+
const { summaryPath } = p;
|
|
54
|
+
if (!runtime.fsSync.existsSync(summaryPath)) {
|
|
55
|
+
runtime.proc.stdout.write(JSON.stringify({ bullets: [] }) + "\n");
|
|
56
|
+
return { ok: true };
|
|
61
57
|
}
|
|
62
|
-
const text = readFileSync(summaryPath, "utf-8");
|
|
58
|
+
const text = runtime.fsSync.readFileSync(summaryPath, "utf-8");
|
|
63
59
|
const { bullets } = readInboxBullets(text);
|
|
64
|
-
|
|
60
|
+
runtime.proc.stdout.write(JSON.stringify({ bullets }, null, 2) + "\n");
|
|
61
|
+
return { ok: true };
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
function ackOrDropCmd(
|
|
68
|
-
const
|
|
69
|
-
|
|
64
|
+
function ackOrDropCmd(runtime, options) {
|
|
65
|
+
const p = paths(runtime, options);
|
|
66
|
+
if (p.error) return p.error;
|
|
67
|
+
const { summaryPath } = p;
|
|
68
|
+
const idx = Number.parseInt(options.index ?? "", 10);
|
|
70
69
|
if (!Number.isInteger(idx) || idx < 0) {
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
runtime.proc.stderr.write("inbox requires --index <n>\n");
|
|
71
|
+
return { ok: false, code: 2 };
|
|
73
72
|
}
|
|
74
|
-
const text = readFileSync(summaryPath, "utf-8");
|
|
73
|
+
const text = runtime.fsSync.readFileSync(summaryPath, "utf-8");
|
|
75
74
|
const { lines, bulletIdxs } = readInboxBullets(text);
|
|
76
75
|
if (idx >= bulletIdxs.length) {
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
runtime.proc.stderr.write(`no bullet at index ${idx}\n`);
|
|
77
|
+
return { ok: false, code: 2 };
|
|
79
78
|
}
|
|
80
79
|
removeBulletAt(lines, bulletIdxs[idx]);
|
|
81
|
-
writeFileSync(summaryPath, lines.join("\n"));
|
|
82
|
-
|
|
80
|
+
runtime.fsSync.writeFileSync(summaryPath, lines.join("\n"));
|
|
81
|
+
runtime.proc.stdout.write(`removed inbox bullet ${idx}\n`);
|
|
82
|
+
return { ok: true };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function appendPriorityRow(memoryText, { item, agents, owner, status, added }) {
|
|
@@ -128,28 +128,30 @@ function appendPriorityRow(memoryText, { item, agents, owner, status, added }) {
|
|
|
128
128
|
return lines.join("\n");
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function promoteCmd(
|
|
132
|
-
const
|
|
133
|
-
|
|
131
|
+
function promoteCmd(runtime, options) {
|
|
132
|
+
const p = paths(runtime, options);
|
|
133
|
+
if (p.error) return p.error;
|
|
134
|
+
const { summaryPath, memoryPath, agent } = p;
|
|
135
|
+
const idx = Number.parseInt(options.index ?? "", 10);
|
|
134
136
|
if (!Number.isInteger(idx) || idx < 0) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
runtime.proc.stderr.write("inbox promote requires --index <n>\n");
|
|
138
|
+
return { ok: false, code: 2 };
|
|
137
139
|
}
|
|
138
|
-
const text = readFileSync(summaryPath, "utf-8");
|
|
140
|
+
const text = runtime.fsSync.readFileSync(summaryPath, "utf-8");
|
|
139
141
|
const { lines, bullets, bulletIdxs } = readInboxBullets(text);
|
|
140
142
|
if (idx >= bullets.length) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
runtime.proc.stderr.write(`no bullet at index ${idx}\n`);
|
|
144
|
+
return { ok: false, code: 2 };
|
|
143
145
|
}
|
|
144
146
|
const bulletText = bullets[idx].replace(/^[-*]\s+/, "");
|
|
145
147
|
removeBulletAt(lines, bulletIdxs[idx]);
|
|
146
|
-
writeFileSync(summaryPath, lines.join("\n"));
|
|
148
|
+
runtime.fsSync.writeFileSync(summaryPath, lines.join("\n"));
|
|
147
149
|
|
|
148
|
-
const memText = existsSync(memoryPath)
|
|
149
|
-
? readFileSync(memoryPath, "utf-8")
|
|
150
|
+
const memText = runtime.fsSync.existsSync(memoryPath)
|
|
151
|
+
? runtime.fsSync.readFileSync(memoryPath, "utf-8")
|
|
150
152
|
: "";
|
|
151
|
-
const today =
|
|
152
|
-
const owner =
|
|
153
|
+
const today = options.today || currentDayIso(runtime);
|
|
154
|
+
const owner = options.owner || agent;
|
|
153
155
|
const promoted = appendPriorityRow(memText, {
|
|
154
156
|
item: bulletText,
|
|
155
157
|
agents: agent,
|
|
@@ -157,24 +159,29 @@ function promoteCmd(values) {
|
|
|
157
159
|
status: "active",
|
|
158
160
|
added: today,
|
|
159
161
|
});
|
|
160
|
-
writeFileSync(memoryPath, promoted);
|
|
161
|
-
|
|
162
|
+
runtime.fsSync.writeFileSync(memoryPath, promoted);
|
|
163
|
+
runtime.proc.stdout.write(`promoted inbox bullet ${idx} to priorities\n`);
|
|
164
|
+
return { ok: true };
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const SUBS = {
|
|
165
168
|
list: listCmd,
|
|
166
|
-
ack:
|
|
167
|
-
drop:
|
|
169
|
+
ack: ackOrDropCmd,
|
|
170
|
+
drop: ackOrDropCmd,
|
|
168
171
|
promote: promoteCmd,
|
|
169
172
|
};
|
|
170
173
|
|
|
171
174
|
/** Dispatch `inbox {list|ack|promote|drop}` to the matching sub-handler. */
|
|
172
|
-
export function runInboxCommand(
|
|
173
|
-
const
|
|
175
|
+
export function runInboxCommand(ctx) {
|
|
176
|
+
const { runtime } = ctx.deps;
|
|
177
|
+
const sub = ctx.args.subcommand;
|
|
174
178
|
const handler = SUBS[sub];
|
|
175
179
|
if (!handler) {
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
code: 2,
|
|
183
|
+
error: "inbox requires subcommand: list | ack | promote | drop",
|
|
184
|
+
};
|
|
178
185
|
}
|
|
179
|
-
handler(
|
|
186
|
+
return handler(runtime, ctx.options);
|
|
180
187
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
1
|
import path from "node:path";
|
|
4
|
-
import fsAsync from "node:fs/promises";
|
|
5
|
-
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
-
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
7
|
-
import { WikiRepo } from "../wiki-repo.js";
|
|
8
2
|
import { listSkills } from "../skill-roster.js";
|
|
9
|
-
import {
|
|
3
|
+
import { resolveProjectRoot, resolveWikiRoot } from "../util/wiki-dir.js";
|
|
10
4
|
import {
|
|
11
5
|
ACTIVE_CLAIMS_HEADING,
|
|
12
6
|
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
@@ -14,22 +8,21 @@ import {
|
|
|
14
8
|
} from "../constants.js";
|
|
15
9
|
|
|
16
10
|
/** 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. */
|
|
17
|
-
export function deriveWikiUrl(parentDir, env
|
|
11
|
+
export async function deriveWikiUrl(gitClient, parentDir, env) {
|
|
18
12
|
if (env.FIT_WIKI_URL) return env.FIT_WIKI_URL;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return base + ".wiki.git";
|
|
13
|
+
try {
|
|
14
|
+
const origin = await gitClient.remoteGetUrl("origin", { cwd: parentDir });
|
|
15
|
+
if (!origin) return null;
|
|
16
|
+
const base = origin.replace(/\.git$/, "");
|
|
17
|
+
return base + ".wiki.git";
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
function scaffoldActiveClaims(memoryPath) {
|
|
31
|
-
if (!existsSync(memoryPath)) return false;
|
|
32
|
-
const text = readFileSync(memoryPath, "utf-8");
|
|
23
|
+
function scaffoldActiveClaims(runtime, memoryPath) {
|
|
24
|
+
if (!runtime.fsSync.existsSync(memoryPath)) return false;
|
|
25
|
+
const text = runtime.fsSync.readFileSync(memoryPath, "utf-8");
|
|
33
26
|
if (new RegExp(`^${ACTIVE_CLAIMS_HEADING}$`, "m").test(text)) return false;
|
|
34
27
|
|
|
35
28
|
const block = [
|
|
@@ -48,65 +41,66 @@ function scaffoldActiveClaims(memoryPath) {
|
|
|
48
41
|
const lines = text.split("\n");
|
|
49
42
|
const storyboardIdx = lines.findIndex((l) => l.trim() === "## Storyboard");
|
|
50
43
|
if (storyboardIdx === -1) {
|
|
51
|
-
writeFileSync(
|
|
44
|
+
runtime.fsSync.writeFileSync(
|
|
45
|
+
memoryPath,
|
|
46
|
+
text.replace(/\n*$/, "") + "\n" + block + "\n",
|
|
47
|
+
);
|
|
52
48
|
return true;
|
|
53
49
|
}
|
|
54
50
|
lines.splice(storyboardIdx, 0, ...block.split("\n"), "");
|
|
55
|
-
writeFileSync(memoryPath, lines.join("\n"));
|
|
51
|
+
runtime.fsSync.writeFileSync(memoryPath, lines.join("\n"));
|
|
56
52
|
return true;
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
async function maybeCloneWiki(
|
|
60
|
-
const wikiUrl = deriveWikiUrl(projectRoot,
|
|
55
|
+
async function maybeCloneWiki(wikiSync, gitClient, projectRoot, runtime) {
|
|
56
|
+
const wikiUrl = await deriveWikiUrl(gitClient, projectRoot, runtime.proc.env);
|
|
61
57
|
if (!wikiUrl) {
|
|
62
|
-
|
|
58
|
+
runtime.proc.stderr.write(
|
|
59
|
+
"init: could not determine wiki URL from origin remote\n",
|
|
60
|
+
);
|
|
63
61
|
return;
|
|
64
62
|
}
|
|
65
|
-
const
|
|
66
|
-
const repo = new WikiRepo({
|
|
67
|
-
wikiDir,
|
|
68
|
-
parentDir: projectRoot,
|
|
69
|
-
resolveToken: () => config.ghToken(),
|
|
70
|
-
});
|
|
71
|
-
const cloneResult = repo.ensureCloned(wikiUrl);
|
|
63
|
+
const cloneResult = await wikiSync.ensureCloned(wikiUrl);
|
|
72
64
|
if (cloneResult.cloned) {
|
|
73
|
-
|
|
65
|
+
await wikiSync.inheritIdentity();
|
|
74
66
|
} else {
|
|
75
|
-
|
|
67
|
+
runtime.proc.stderr.write(
|
|
68
|
+
"init: could not clone wiki, continuing with local-only steps\n",
|
|
69
|
+
);
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
/** Clone the wiki if not already present, scaffold Active Claims in MEMORY.md, and create per-skill metric directories. */
|
|
80
|
-
export async function runInitCommand(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
io = createDefaultIo(),
|
|
85
|
-
) {
|
|
86
|
-
const logger = { debug() {} };
|
|
87
|
-
const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
|
|
88
|
-
const projectRoot = finder.findProjectRoot(io.cwd());
|
|
74
|
+
export async function runInitCommand(ctx) {
|
|
75
|
+
const { runtime, wikiSync, gitClient } = ctx.deps;
|
|
76
|
+
const options = ctx.options;
|
|
77
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
89
78
|
|
|
90
|
-
const wikiDir =
|
|
79
|
+
const wikiDir = resolveWikiRoot(runtime, options);
|
|
91
80
|
const skillsDir = path.resolve(
|
|
92
81
|
projectRoot,
|
|
93
|
-
|
|
82
|
+
options["skills-dir"] ?? path.join(".claude", "skills"),
|
|
94
83
|
);
|
|
95
84
|
|
|
96
|
-
await maybeCloneWiki(
|
|
85
|
+
await maybeCloneWiki(wikiSync, gitClient, projectRoot, runtime);
|
|
97
86
|
|
|
98
|
-
if (existsSync(skillsDir)) {
|
|
99
|
-
for (const slug of listSkills({ skillsDir })) {
|
|
100
|
-
mkdirSync(path.join(wikiDir, "metrics", slug), {
|
|
87
|
+
if (runtime.fsSync.existsSync(skillsDir)) {
|
|
88
|
+
for (const slug of listSkills({ skillsDir }, runtime.fsSync)) {
|
|
89
|
+
runtime.fsSync.mkdirSync(path.join(wikiDir, "metrics", slug), {
|
|
90
|
+
recursive: true,
|
|
91
|
+
});
|
|
101
92
|
}
|
|
102
93
|
}
|
|
103
94
|
|
|
104
|
-
if (existsSync(wikiDir)) {
|
|
95
|
+
if (runtime.fsSync.existsSync(wikiDir)) {
|
|
105
96
|
const memoryPath = path.join(wikiDir, "MEMORY.md");
|
|
106
|
-
if (scaffoldActiveClaims(memoryPath)) {
|
|
107
|
-
|
|
97
|
+
if (scaffoldActiveClaims(runtime, memoryPath)) {
|
|
98
|
+
runtime.proc.stdout.write(
|
|
99
|
+
`init: scaffolded ${ACTIVE_CLAIMS_HEADING} in ${memoryPath}\n`,
|
|
100
|
+
);
|
|
108
101
|
}
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
|
|
104
|
+
runtime.proc.stdout.write(`init: wiki ready at ${wikiDir}\n`);
|
|
105
|
+
return { ok: true };
|
|
112
106
|
}
|
package/src/commands/log.js
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import fsAsync from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Finder } from "@forwardimpact/libutil";
|
|
5
1
|
import {
|
|
6
2
|
weeklyLogPath,
|
|
7
3
|
rotateIfOverBudget,
|
|
8
4
|
appendEntry,
|
|
9
5
|
} from "../weekly-log.js";
|
|
10
6
|
import { DECISION_HEADING } from "../constants.js";
|
|
11
|
-
import {
|
|
7
|
+
import { currentDayIso } from "../util/clock.js";
|
|
8
|
+
import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
12
9
|
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
|
|
16
|
-
return finder.findProjectRoot(io.cwd());
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function commonContext(values, io) {
|
|
20
|
-
const agent = values.agent || io.env.LIBEVAL_AGENT_PROFILE;
|
|
10
|
+
function commonContext(runtime, options) {
|
|
11
|
+
const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
|
|
21
12
|
if (!agent) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
runtime.proc.stderr.write(
|
|
14
|
+
"log requires --agent <name> or LIBEVAL_AGENT_PROFILE env var\n",
|
|
15
|
+
);
|
|
16
|
+
return { error: { ok: false, code: 2 } };
|
|
25
17
|
}
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const today = values.today || io.today();
|
|
18
|
+
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
19
|
+
const today = options.today || currentDayIso(runtime);
|
|
29
20
|
return { agent, wikiRoot, today };
|
|
30
21
|
}
|
|
31
22
|
|
|
@@ -39,14 +30,14 @@ function lastDateHeading(text) {
|
|
|
39
30
|
return last;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
function runDecision(
|
|
43
|
-
const ctx = commonContext(
|
|
44
|
-
if (
|
|
33
|
+
function runDecision(runtime, options) {
|
|
34
|
+
const ctx = commonContext(runtime, options);
|
|
35
|
+
if (ctx.error) return ctx.error;
|
|
45
36
|
const { agent, wikiRoot, today } = ctx;
|
|
46
|
-
const surveyed =
|
|
47
|
-
const chosen =
|
|
48
|
-
const rationale =
|
|
49
|
-
const alternatives =
|
|
37
|
+
const surveyed = options.surveyed || "—";
|
|
38
|
+
const chosen = options.chosen || "—";
|
|
39
|
+
const rationale = options.rationale || "—";
|
|
40
|
+
const alternatives = options.alternatives || "—";
|
|
50
41
|
const body = [
|
|
51
42
|
`## ${today}`,
|
|
52
43
|
"",
|
|
@@ -62,55 +53,70 @@ function runDecision(values, io) {
|
|
|
62
53
|
"",
|
|
63
54
|
].join("\n");
|
|
64
55
|
const lineCount = body.split("\n").length;
|
|
65
|
-
rotateIfOverBudget(wikiRoot, agent, today, lineCount);
|
|
56
|
+
rotateIfOverBudget(wikiRoot, agent, today, lineCount, {}, runtime.fsSync);
|
|
66
57
|
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
67
|
-
appendEntry(target, body, agent, today);
|
|
68
|
-
|
|
58
|
+
appendEntry(target, body, agent, today, runtime.fsSync);
|
|
59
|
+
runtime.proc.stdout.write(`logged decision to ${target}\n`);
|
|
60
|
+
return { ok: true };
|
|
69
61
|
}
|
|
70
62
|
|
|
71
|
-
function runNote(
|
|
72
|
-
const ctx = commonContext(
|
|
73
|
-
if (
|
|
63
|
+
function runNote(runtime, options) {
|
|
64
|
+
const ctx = commonContext(runtime, options);
|
|
65
|
+
if (ctx.error) return ctx.error;
|
|
74
66
|
const { agent, wikiRoot, today } = ctx;
|
|
75
|
-
if (!
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return;
|
|
67
|
+
if (!options.field || !options.body) {
|
|
68
|
+
runtime.proc.stderr.write("log note requires --field and --body\n");
|
|
69
|
+
return { ok: false, code: 2 };
|
|
79
70
|
}
|
|
80
|
-
const fieldBlock = `### ${
|
|
71
|
+
const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
|
|
81
72
|
// Conservative line budget: assume we'll prepend a date heading.
|
|
82
73
|
const withHeading = `## ${today}\n\n${fieldBlock}`;
|
|
83
|
-
rotateIfOverBudget(
|
|
74
|
+
rotateIfOverBudget(
|
|
75
|
+
wikiRoot,
|
|
76
|
+
agent,
|
|
77
|
+
today,
|
|
78
|
+
withHeading.split("\n").length,
|
|
79
|
+
{},
|
|
80
|
+
runtime.fsSync,
|
|
81
|
+
);
|
|
84
82
|
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
85
83
|
// Append under the open entry if the file's last `## YYYY-MM-DD` is today;
|
|
86
84
|
// otherwise open a new entry by prepending a date heading.
|
|
87
|
-
const existing = existsSync(target)
|
|
85
|
+
const existing = runtime.fsSync.existsSync(target)
|
|
86
|
+
? runtime.fsSync.readFileSync(target, "utf-8")
|
|
87
|
+
: "";
|
|
88
88
|
const body = lastDateHeading(existing) === today ? fieldBlock : withHeading;
|
|
89
|
-
appendEntry(target, body, agent, today);
|
|
90
|
-
|
|
89
|
+
appendEntry(target, body, agent, today, runtime.fsSync);
|
|
90
|
+
runtime.proc.stdout.write(`logged note to ${target}\n`);
|
|
91
|
+
return { ok: true };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function runDone(
|
|
94
|
-
const ctx = commonContext(
|
|
95
|
-
if (
|
|
94
|
+
function runDone(runtime, options) {
|
|
95
|
+
const ctx = commonContext(runtime, options);
|
|
96
|
+
if (ctx.error) return ctx.error;
|
|
96
97
|
const { agent, wikiRoot, today } = ctx;
|
|
97
98
|
const body = `### Closed\n\nRun closed ${today}.\n`;
|
|
98
99
|
const lineCount = body.split("\n").length;
|
|
99
|
-
rotateIfOverBudget(wikiRoot, agent, today, lineCount);
|
|
100
|
+
rotateIfOverBudget(wikiRoot, agent, today, lineCount, {}, runtime.fsSync);
|
|
100
101
|
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
101
|
-
appendEntry(target, body, agent, today);
|
|
102
|
-
|
|
102
|
+
appendEntry(target, body, agent, today, runtime.fsSync);
|
|
103
|
+
runtime.proc.stdout.write(`closed entry in ${target}\n`);
|
|
104
|
+
return { ok: true };
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
const SUBS = { decision: runDecision, note: runNote, done: runDone };
|
|
106
108
|
|
|
107
109
|
/** Dispatch `log {decision|note|done}` to the matching sub-handler. */
|
|
108
|
-
export function runLogCommand(
|
|
109
|
-
const
|
|
110
|
+
export function runLogCommand(ctx) {
|
|
111
|
+
const { runtime } = ctx.deps;
|
|
112
|
+
const sub = ctx.args.subcommand;
|
|
110
113
|
const handler = SUBS[sub];
|
|
111
114
|
if (!handler) {
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
code: 2,
|
|
118
|
+
error: "log requires subcommand: decision | note | done",
|
|
119
|
+
};
|
|
114
120
|
}
|
|
115
|
-
handler(
|
|
121
|
+
return handler(runtime, ctx.options);
|
|
116
122
|
}
|