@forwardimpact/libwiki 0.2.11 → 0.2.13
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 +117 -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/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
|
}
|
package/src/commands/memo.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { 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 { writeMemo } from "../memo-writer.js";
|
|
6
3
|
import { listAgents } from "../agent-roster.js";
|
|
7
4
|
import { BROADCAST_TARGET } from "../constants.js";
|
|
5
|
+
import { currentDayIso } from "../util/clock.js";
|
|
6
|
+
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
8
7
|
|
|
9
|
-
function writeAndCheck(summaryPath, sender, message, today) {
|
|
10
|
-
const result = writeMemo(
|
|
8
|
+
function writeAndCheck(runtime, summaryPath, sender, message, today) {
|
|
9
|
+
const result = writeMemo(
|
|
10
|
+
{ summaryPath, sender, message, today },
|
|
11
|
+
runtime.fsSync,
|
|
12
|
+
);
|
|
11
13
|
if (!result.written) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
runtime.proc.stderr.write(
|
|
15
|
+
`summary lacks memo:inbox marker: ${result.path}\n`,
|
|
16
|
+
);
|
|
17
|
+
return { ok: false, code: 2 };
|
|
14
18
|
}
|
|
15
|
-
|
|
19
|
+
runtime.proc.stdout.write(`wrote ${result.path}\n`);
|
|
20
|
+
return { ok: true };
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
function resolveTargetPath(wikiRoot, target) {
|
|
@@ -25,72 +30,76 @@ function resolveTargetPath(wikiRoot, target) {
|
|
|
25
30
|
return { summaryPath, escapesRoot };
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
function writeSingleTarget(
|
|
33
|
+
function writeSingleTarget(
|
|
34
|
+
runtime,
|
|
35
|
+
{ wikiRoot, target, sender, message, today },
|
|
36
|
+
) {
|
|
29
37
|
const { summaryPath, escapesRoot } = resolveTargetPath(wikiRoot, target);
|
|
30
38
|
if (escapesRoot) {
|
|
31
|
-
|
|
32
|
-
process.exit(2);
|
|
39
|
+
return { ok: false, code: 2, error: `target escapes wiki root: ${target}` };
|
|
33
40
|
}
|
|
34
|
-
if (!existsSync(summaryPath)) {
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
if (!runtime.fsSync.existsSync(summaryPath)) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code: 2,
|
|
45
|
+
error: `target summary not found: ${summaryPath}`,
|
|
46
|
+
};
|
|
37
47
|
}
|
|
38
|
-
writeAndCheck(summaryPath, sender, message, today);
|
|
48
|
+
return writeAndCheck(runtime, summaryPath, sender, message, today);
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
function writeBroadcast(
|
|
42
|
-
|
|
51
|
+
function writeBroadcast(
|
|
52
|
+
runtime,
|
|
53
|
+
{ agentsDir, wikiRoot, sender, message, today },
|
|
54
|
+
) {
|
|
55
|
+
const agents = listAgents({ agentsDir, wikiRoot }, runtime.fsSync);
|
|
43
56
|
for (const { agent, summaryPath } of agents) {
|
|
44
57
|
if (agent === sender) continue;
|
|
45
|
-
writeAndCheck(summaryPath, sender, message, today);
|
|
58
|
+
const result = writeAndCheck(runtime, summaryPath, sender, message, today);
|
|
59
|
+
if (!result.ok) return result;
|
|
46
60
|
}
|
|
61
|
+
return { ok: true };
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
/** Write a memo to a target agent's summary file (or broadcast to all except the sender); sender is --from or LIBEVAL_AGENT_PROFILE env var. */
|
|
50
|
-
export function runMemoCommand(
|
|
51
|
-
const
|
|
65
|
+
export function runMemoCommand(ctx) {
|
|
66
|
+
const { runtime } = ctx.deps;
|
|
67
|
+
const options = ctx.options;
|
|
68
|
+
const sender = options.from || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
|
|
52
69
|
|
|
53
70
|
if (!sender) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
code: 2,
|
|
74
|
+
error: "memo requires --from <sender> or LIBEVAL_AGENT_PROFILE env var",
|
|
75
|
+
};
|
|
58
76
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
cli.usageError("memo requires --to <target|all>");
|
|
62
|
-
process.exit(2);
|
|
77
|
+
if (!options.to) {
|
|
78
|
+
return { ok: false, code: 2, error: "memo requires --to <target|all>" };
|
|
63
79
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cli.usageError("memo requires --message <text>");
|
|
67
|
-
process.exit(2);
|
|
80
|
+
if (!options.message) {
|
|
81
|
+
return { ok: false, code: 2, error: "memo requires --message <text>" };
|
|
68
82
|
}
|
|
69
83
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
73
|
-
|
|
74
|
-
const wikiRoot = values["wiki-root"] || path.join(projectRoot, "wiki");
|
|
84
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
85
|
+
const wikiRoot = options["wiki-root"] || path.join(projectRoot, "wiki");
|
|
75
86
|
const agentsDir = path.join(projectRoot, ".claude", "agents");
|
|
76
|
-
const today =
|
|
87
|
+
const today = currentDayIso(runtime);
|
|
77
88
|
|
|
78
|
-
if (
|
|
79
|
-
writeBroadcast({
|
|
89
|
+
if (options.to === BROADCAST_TARGET) {
|
|
90
|
+
return writeBroadcast(runtime, {
|
|
80
91
|
agentsDir,
|
|
81
92
|
wikiRoot,
|
|
82
93
|
sender,
|
|
83
|
-
message:
|
|
84
|
-
today,
|
|
85
|
-
});
|
|
86
|
-
} else {
|
|
87
|
-
writeSingleTarget({
|
|
88
|
-
wikiRoot,
|
|
89
|
-
target: values.to,
|
|
90
|
-
sender,
|
|
91
|
-
message: values.message,
|
|
94
|
+
message: options.message,
|
|
92
95
|
today,
|
|
93
|
-
cli,
|
|
94
96
|
});
|
|
95
97
|
}
|
|
98
|
+
return writeSingleTarget(runtime, {
|
|
99
|
+
wikiRoot,
|
|
100
|
+
target: options.to,
|
|
101
|
+
sender,
|
|
102
|
+
message: options.message,
|
|
103
|
+
today,
|
|
104
|
+
});
|
|
96
105
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
1
|
import path from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import { Finder } from "@forwardimpact/libutil";
|
|
2
|
+
import { yearMonth } from "@forwardimpact/libutil";
|
|
6
3
|
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
7
4
|
import { scanMarkers } from "../marker-scanner.js";
|
|
8
5
|
import { renderBlock, BlockRenderError } from "../block-renderer.js";
|
|
9
6
|
import { renderIssueList, parseRepoSlug } from "../issue-list-renderer.js";
|
|
10
|
-
import {
|
|
7
|
+
import { currentDayIso } from "../util/clock.js";
|
|
8
|
+
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
11
9
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
15
|
-
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
10
|
+
function currentStoryboardRelPath(runtime) {
|
|
11
|
+
return `wiki/storyboard-${yearMonth(currentDayIso(runtime))}.md`;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
function deriveParentRepo(parentDir, env) {
|
|
14
|
+
async function deriveParentRepo(gitClient, parentDir, env) {
|
|
19
15
|
if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
try {
|
|
17
|
+
const url = await gitClient.remoteGetUrl("origin", { cwd: parentDir });
|
|
18
|
+
return parseRepoSlug(url);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
function renderForBlock(block, projectRoot, ghContext) {
|
|
24
|
+
async function renderForBlock(block, projectRoot, ghContext, runtime) {
|
|
29
25
|
if (block.kind === "xmr") {
|
|
30
26
|
return renderBlock({
|
|
31
27
|
metric: block.metric,
|
|
32
28
|
csvPath: block.csvPath,
|
|
33
29
|
projectRoot,
|
|
30
|
+
fs: runtime.fsSync,
|
|
34
31
|
});
|
|
35
32
|
}
|
|
36
33
|
if (block.kind === "issue-list") {
|
|
@@ -41,6 +38,8 @@ function renderForBlock(block, projectRoot, ghContext) {
|
|
|
41
38
|
cwd: ghContext.cwd,
|
|
42
39
|
repo: ghContext.repo,
|
|
43
40
|
token: ghContext.token,
|
|
41
|
+
today: currentDayIso(runtime),
|
|
42
|
+
runtime,
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
45
|
return null;
|
|
@@ -55,23 +54,20 @@ function spliceBlock(lines, block, rendered) {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
|
|
58
|
-
export async function runRefreshCommand(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
io = createDefaultIo(),
|
|
63
|
-
) {
|
|
64
|
-
const logger = { debug() {} };
|
|
65
|
-
const finder = new Finder(fsAsync, logger, { cwd: io.cwd });
|
|
66
|
-
const projectRoot = finder.findProjectRoot(io.cwd());
|
|
57
|
+
export async function runRefreshCommand(ctx) {
|
|
58
|
+
const { runtime, gitClient } = ctx.deps;
|
|
59
|
+
const options = ctx.options;
|
|
60
|
+
const projectRoot = resolveProjectRoot(runtime);
|
|
67
61
|
|
|
68
62
|
const storyboardPath = path.resolve(
|
|
69
63
|
projectRoot,
|
|
70
|
-
args[
|
|
64
|
+
ctx.args["storyboard-path"] || currentStoryboardRelPath(runtime),
|
|
71
65
|
);
|
|
72
|
-
const text = readFileSync(storyboardPath, "utf-8");
|
|
73
|
-
const blocks = scanMarkers(text
|
|
74
|
-
|
|
66
|
+
const text = runtime.fsSync.readFileSync(storyboardPath, "utf-8");
|
|
67
|
+
const blocks = scanMarkers(text, {
|
|
68
|
+
warn: (message) => runtime.proc.stderr.write(message),
|
|
69
|
+
});
|
|
70
|
+
if (blocks.length === 0) return { ok: true };
|
|
75
71
|
|
|
76
72
|
const config = await createScriptConfig("wiki");
|
|
77
73
|
let token = null;
|
|
@@ -89,7 +85,7 @@ export async function runRefreshCommand(
|
|
|
89
85
|
// overrides the parsed origin.
|
|
90
86
|
const ghContext = {
|
|
91
87
|
cwd: projectRoot,
|
|
92
|
-
repo: deriveParentRepo(projectRoot,
|
|
88
|
+
repo: await deriveParentRepo(gitClient, projectRoot, runtime.proc.env),
|
|
93
89
|
token,
|
|
94
90
|
};
|
|
95
91
|
|
|
@@ -99,20 +95,28 @@ export async function runRefreshCommand(
|
|
|
99
95
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
100
96
|
const block = blocks[i];
|
|
101
97
|
try {
|
|
102
|
-
const rendered = renderForBlock(
|
|
98
|
+
const rendered = await renderForBlock(
|
|
99
|
+
block,
|
|
100
|
+
projectRoot,
|
|
101
|
+
ghContext,
|
|
102
|
+
runtime,
|
|
103
|
+
);
|
|
103
104
|
if (!rendered) continue;
|
|
104
105
|
spliceBlock(lines, block, rendered);
|
|
105
106
|
spliced = true;
|
|
106
107
|
} catch (err) {
|
|
107
108
|
if (!(err instanceof BlockRenderError)) throw err;
|
|
108
|
-
|
|
109
|
+
runtime.proc.stderr.write(
|
|
109
110
|
`refresh-error ${storyboardPath}:${block.openLine + 1} ${err.message}\n`,
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
if (spliced) writeFileSync(storyboardPath, lines.join("\n"));
|
|
115
|
-
if (
|
|
116
|
-
|
|
115
|
+
if (spliced) runtime.fsSync.writeFileSync(storyboardPath, lines.join("\n"));
|
|
116
|
+
if (options.format === "json") {
|
|
117
|
+
runtime.proc.stdout.write(
|
|
118
|
+
JSON.stringify({ blocks: blocks.length, spliced }) + "\n",
|
|
119
|
+
);
|
|
117
120
|
}
|
|
121
|
+
return { ok: true };
|
|
118
122
|
}
|
package/src/commands/rotate.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
import fsAsync from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { Finder } from "@forwardimpact/libutil";
|
|
4
1
|
import { rotateIfOverBudget } from "../weekly-log.js";
|
|
2
|
+
import { currentDayIso } from "../util/clock.js";
|
|
3
|
+
import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
5
4
|
|
|
6
5
|
/** Force-rotate the current weekly log to a sealed part file. */
|
|
7
|
-
export function runRotateCommand(
|
|
8
|
-
const
|
|
6
|
+
export function runRotateCommand(ctx) {
|
|
7
|
+
const { runtime } = ctx.deps;
|
|
8
|
+
const options = ctx.options;
|
|
9
|
+
const agent = options.agent || runtime.proc.env.LIBEVAL_AGENT_PROFILE;
|
|
9
10
|
if (!agent) {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
code: 2,
|
|
14
|
+
error: "rotate requires --agent or LIBEVAL_AGENT_PROFILE",
|
|
15
|
+
};
|
|
12
16
|
}
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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);
|
|
17
|
+
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
18
|
+
const today = options.today || currentDayIso(runtime);
|
|
18
19
|
|
|
19
|
-
const result = rotateIfOverBudget(
|
|
20
|
+
const result = rotateIfOverBudget(
|
|
21
|
+
wikiRoot,
|
|
22
|
+
agent,
|
|
23
|
+
today,
|
|
24
|
+
0,
|
|
25
|
+
{ force: true },
|
|
26
|
+
runtime.fsSync,
|
|
27
|
+
);
|
|
20
28
|
if (result.rotated) {
|
|
21
|
-
|
|
29
|
+
runtime.proc.stdout.write(
|
|
30
|
+
`rotated ${result.fromPath} → ${result.toPath}\n`,
|
|
31
|
+
);
|
|
22
32
|
} else {
|
|
23
|
-
|
|
33
|
+
runtime.proc.stdout.write(`no rotation needed for ${agent}\n`);
|
|
24
34
|
}
|
|
35
|
+
return { ok: true };
|
|
25
36
|
}
|
package/src/commands/sync.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
import { WikiPullConflict } from "../wiki-
|
|
2
|
-
import { buildRepo } from "../build-repo.js";
|
|
1
|
+
import { WikiPullConflict } from "../wiki-sync.js";
|
|
3
2
|
|
|
4
3
|
/** Commit all wiki changes and push them to the remote wiki repository. */
|
|
5
|
-
export async function runPushCommand(
|
|
6
|
-
const
|
|
7
|
-
|
|
4
|
+
export async function runPushCommand(ctx) {
|
|
5
|
+
const { runtime, wikiSync } = ctx.deps;
|
|
6
|
+
await wikiSync.inheritIdentity();
|
|
8
7
|
|
|
9
|
-
const result =
|
|
8
|
+
const result = await wikiSync.commitAndPush("wiki: update from session");
|
|
10
9
|
if (result.pushed) {
|
|
11
|
-
|
|
10
|
+
runtime.proc.stdout.write("push: committed and pushed\n");
|
|
12
11
|
} else {
|
|
13
|
-
|
|
12
|
+
runtime.proc.stdout.write("push: nothing to push\n");
|
|
14
13
|
}
|
|
14
|
+
return { ok: true };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** Fetch and rebase the local wiki on origin/master; on rebase conflict,
|
|
18
|
-
export async function runPullCommand(
|
|
19
|
-
const
|
|
20
|
-
|
|
17
|
+
/** Fetch and rebase the local wiki on origin/master; on rebase conflict, return a non-zero envelope with a message to resolve manually or push first. */
|
|
18
|
+
export async function runPullCommand(ctx) {
|
|
19
|
+
const { runtime, wikiSync } = ctx.deps;
|
|
20
|
+
await wikiSync.inheritIdentity();
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
await wikiSync.pull();
|
|
24
|
+
runtime.proc.stdout.write("pull: up to date\n");
|
|
25
|
+
return { ok: true };
|
|
25
26
|
} catch (err) {
|
|
26
27
|
if (err instanceof WikiPullConflict) {
|
|
27
|
-
|
|
28
|
+
runtime.proc.stderr.write(
|
|
28
29
|
"fit-wiki pull: rebase conflict — local divergence detected; resolve manually or push first\n",
|
|
29
30
|
);
|
|
30
|
-
|
|
31
|
+
return { ok: false, code: 1 };
|
|
31
32
|
}
|
|
32
33
|
throw err;
|
|
33
34
|
}
|
package/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export {
|
|
|
20
20
|
export { scanMarkers } from "./marker-scanner.js";
|
|
21
21
|
export { renderBlock } from "./block-renderer.js";
|
|
22
22
|
export { renderIssueList } from "./issue-list-renderer.js";
|
|
23
|
-
export {
|
|
23
|
+
export { WikiSync, WikiPullConflict } from "./wiki-sync.js";
|
|
24
24
|
export { listSkills } from "./skill-roster.js";
|
|
25
25
|
export {
|
|
26
26
|
parseClaims,
|
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function defaultGh(args, options) {
|
|
4
|
-
const env = options?.token
|
|
5
|
-
? { ...process.env, GH_TOKEN: options.token }
|
|
6
|
-
: undefined;
|
|
7
|
-
return spawnSync("gh", args, {
|
|
8
|
-
encoding: "utf-8",
|
|
9
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
-
cwd: options?.cwd,
|
|
11
|
-
env,
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function daysAgo(today, n) {
|
|
16
|
-
const d = today instanceof Date ? new Date(today.getTime()) : new Date(today);
|
|
17
|
-
d.setUTCDate(d.getUTCDate() - n);
|
|
18
|
-
return d.toISOString().slice(0, 10);
|
|
19
|
-
}
|
|
1
|
+
import { addDays } from "@forwardimpact/libutil";
|
|
20
2
|
|
|
21
3
|
/** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
|
|
22
4
|
export function parseRepoSlug(originUrl) {
|
|
@@ -27,16 +9,34 @@ export function parseRepoSlug(originUrl) {
|
|
|
27
9
|
return `${match[1]}/${match[2]}`;
|
|
28
10
|
}
|
|
29
11
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Render an issue-list block for an obstacles/experiments marker. Returns
|
|
14
|
+
* markdown lines. `cwd` should be the parent monorepo's project root so `gh`
|
|
15
|
+
* resolves the correct origin; `repo` is an explicit `owner/name` slug used when
|
|
16
|
+
* the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token`
|
|
17
|
+
* is the resolved GH token (e.g. via `Config.ghToken()`). The `gh` command runs
|
|
18
|
+
* through `runtime.subprocess`, and stderr warnings through `runtime.proc`.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {string} options.topic
|
|
22
|
+
* @param {string} options.state
|
|
23
|
+
* @param {string|null} options.window
|
|
24
|
+
* @param {string} options.cwd
|
|
25
|
+
* @param {string} [options.repo]
|
|
26
|
+
* @param {string} [options.token]
|
|
27
|
+
* @param {string} options.today - ISO date string used for the closed-window cutoff.
|
|
28
|
+
* @param {import('@forwardimpact/libutil/runtime').Runtime} options.runtime
|
|
29
|
+
* @returns {Promise<string[]>}
|
|
30
|
+
*/
|
|
31
|
+
export async function renderIssueList({
|
|
32
32
|
topic,
|
|
33
33
|
state,
|
|
34
34
|
window,
|
|
35
35
|
cwd,
|
|
36
36
|
repo,
|
|
37
37
|
token,
|
|
38
|
-
today
|
|
39
|
-
|
|
38
|
+
today,
|
|
39
|
+
runtime,
|
|
40
40
|
}) {
|
|
41
41
|
const ghState = state === "closed" ? "closed" : "open";
|
|
42
42
|
const args = ["issue", "list"];
|
|
@@ -51,9 +51,10 @@ export function renderIssueList({
|
|
|
51
51
|
"--limit",
|
|
52
52
|
"100",
|
|
53
53
|
);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const env = token ? { ...runtime.proc.env, GH_TOKEN: token } : undefined;
|
|
55
|
+
const result = await runtime.subprocess.run("gh", args, { cwd, env });
|
|
56
|
+
if (result.exitCode !== 0) {
|
|
57
|
+
runtime.proc.stderr.write(
|
|
57
58
|
`refresh: gh issue list failed for ${topic}:${state}\n`,
|
|
58
59
|
);
|
|
59
60
|
return [];
|
|
@@ -62,7 +63,7 @@ export function renderIssueList({
|
|
|
62
63
|
try {
|
|
63
64
|
issues = JSON.parse(result.stdout || "[]");
|
|
64
65
|
} catch {
|
|
65
|
-
|
|
66
|
+
runtime.proc.stderr.write(
|
|
66
67
|
`refresh: gh issue list JSON parse failed for ${topic}:${state}\n`,
|
|
67
68
|
);
|
|
68
69
|
return [];
|
|
@@ -72,7 +73,7 @@ export function renderIssueList({
|
|
|
72
73
|
const windowDays = window
|
|
73
74
|
? Number.parseInt(window.replace("d", ""), 10)
|
|
74
75
|
: 7;
|
|
75
|
-
const cutoff =
|
|
76
|
+
const cutoff = addDays(today, -windowDays);
|
|
76
77
|
issues = issues.filter(
|
|
77
78
|
(i) => i.closedAt && i.closedAt.slice(0, 10) >= cutoff,
|
|
78
79
|
);
|