@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/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
|
);
|
package/src/marker-migrator.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { MEMO_INBOX_MARKER, INBOX_HEADING } from "./constants.js";
|
|
3
2
|
import { listAgents } from "./agent-roster.js";
|
|
4
3
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Insert memo:inbox markers into agent summary files that have an inbox heading
|
|
6
|
+
* but no marker yet.
|
|
7
|
+
* @param {{agentsDir: string, wikiRoot: string}} dirs
|
|
8
|
+
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
9
|
+
*/
|
|
10
|
+
export function insertMarkers({ agentsDir, wikiRoot }, fs) {
|
|
11
|
+
const agents = listAgents({ agentsDir, wikiRoot }, fs);
|
|
11
12
|
const inserted = [];
|
|
12
13
|
const skipped = [];
|
|
13
14
|
const errors = [];
|
package/src/marker-scanner.js
CHANGED
|
@@ -12,10 +12,8 @@ function openLabel(open) {
|
|
|
12
12
|
return open.kind === "xmr" ? open.metric : open.topic;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function warnDangling(open) {
|
|
16
|
-
|
|
17
|
-
`dangling-marker ${openLabel(open)} at line ${open.openLine + 1}\n`,
|
|
18
|
-
);
|
|
15
|
+
function warnDangling(open, warn) {
|
|
16
|
+
warn(`dangling-marker ${openLabel(open)} at line ${open.openLine + 1}\n`);
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
function tryOpen(line, i) {
|
|
@@ -68,8 +66,15 @@ function matchClose(line, open) {
|
|
|
68
66
|
return Boolean(m && open.kind === "issue-list" && open.topic === m[1]);
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Scan text for paired marker blocks (xmr or issue-list). Returns positions and
|
|
71
|
+
* metadata. Dangling open markers are reported through the injected `warn`
|
|
72
|
+
* callback (default: discard) instead of writing to the process directly.
|
|
73
|
+
* @param {string} text - The storyboard text to scan.
|
|
74
|
+
* @param {{warn?: (message: string) => void}} [options]
|
|
75
|
+
* @returns {Array<object>} The paired marker blocks.
|
|
76
|
+
*/
|
|
77
|
+
export function scanMarkers(text, { warn = () => {} } = {}) {
|
|
73
78
|
const lines = text.split("\n");
|
|
74
79
|
const pairs = [];
|
|
75
80
|
let open = null;
|
|
@@ -78,7 +83,7 @@ export function scanMarkers(text) {
|
|
|
78
83
|
const line = lines[i];
|
|
79
84
|
const newOpen = tryOpen(line, i);
|
|
80
85
|
if (newOpen) {
|
|
81
|
-
if (open) warnDangling(open);
|
|
86
|
+
if (open) warnDangling(open, warn);
|
|
82
87
|
open = newOpen;
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
@@ -88,7 +93,7 @@ export function scanMarkers(text) {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
if (open) warnDangling(open);
|
|
96
|
+
if (open) warnDangling(open, warn);
|
|
92
97
|
|
|
93
98
|
return pairs;
|
|
94
99
|
}
|
package/src/memo-writer.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { MEMO_INBOX_MARKER } from "./constants.js";
|
|
3
2
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Append a timestamped memo bullet below the inbox marker in an agent's summary
|
|
5
|
+
* file.
|
|
6
|
+
* @param {{summaryPath: string, sender: string, message: string, today: string}} memo
|
|
7
|
+
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
8
|
+
*/
|
|
9
|
+
export function writeMemo({ summaryPath, sender, message, today }, fs) {
|
|
9
10
|
const content = fs.readFileSync(summaryPath, "utf-8");
|
|
10
11
|
const lines = content.split("\n");
|
|
11
12
|
|
package/src/skill-roster.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { readdirSync, statSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* List all kata-prefixed skill directory names under the skills directory,
|
|
5
|
+
* sorted alphabetically.
|
|
6
|
+
* @param {{skillsDir: string}} dirs
|
|
7
|
+
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
8
|
+
*/
|
|
9
|
+
export function listSkills({ skillsDir }, fs) {
|
|
6
10
|
const entries = fs.readdirSync(skillsDir);
|
|
7
11
|
const skills = [];
|
|
8
12
|
|
package/src/status.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// STATUS.md row ids may carry a `/<unit>` suffix denoting a
|
|
2
|
+
// per-migration-unit sub-row of a master spec (`1370/libutil`, …). The master
|
|
3
|
+
// `NNNN` row advances only when every sub-row reads `plan implemented`.
|
|
4
|
+
|
|
5
|
+
/** Matches a status-row id: four digits, optionally a `/<unit>` suffix. */
|
|
6
|
+
export const STATUS_ID_REGEX = /^\d{4}(\/[a-z0-9-]+)?$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a status-row id into its master spec id and optional unit suffix.
|
|
10
|
+
* @param {string} id - The id field of a STATUS.md row.
|
|
11
|
+
* @returns {{ specId: string, unit: string|null }|null} Parsed parts, or null
|
|
12
|
+
* when the id does not match {@link STATUS_ID_REGEX}.
|
|
13
|
+
*/
|
|
14
|
+
export function parseStatusRowId(id) {
|
|
15
|
+
if (typeof id !== "string" || !STATUS_ID_REGEX.test(id)) return null;
|
|
16
|
+
const slash = id.indexOf("/");
|
|
17
|
+
if (slash === -1) return { specId: id, unit: null };
|
|
18
|
+
return { specId: id.slice(0, slash), unit: id.slice(slash + 1) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isoDate } from "@forwardimpact/libutil";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Today's ISO calendar date (`YYYY-MM-DD`) read from the injected clock.
|
|
5
|
+
* Commands that previously inlined `new Date().toISOString().slice(0, 10)` (or
|
|
6
|
+
* libwiki's `io.today()`) call this instead so the wall-clock read flows
|
|
7
|
+
* through `runtime.clock`.
|
|
8
|
+
* @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function currentDayIso(runtime) {
|
|
12
|
+
return isoDate(runtime.clock.now());
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find the project root by upward `package.json` discovery from the current
|
|
5
|
+
* working directory, using the injected `runtime.finder` (the one canonical
|
|
6
|
+
* Finder — SC9 keeps `new Finder(...)` inside libutil).
|
|
7
|
+
* @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function resolveProjectRoot(runtime) {
|
|
11
|
+
return runtime.finder.findProjectRoot(runtime.proc.cwd());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the wiki root, preserving the pre-1370 order: the `--wiki-root`
|
|
16
|
+
* option when given, else `<projectRoot>/wiki`. The finder is consulted only
|
|
17
|
+
* when no explicit `--wiki-root` is supplied.
|
|
18
|
+
* @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
|
|
19
|
+
* @param {Record<string, unknown>} [options] - Parsed CLI options (`ctx.options`).
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function resolveWikiRoot(runtime, options = {}) {
|
|
23
|
+
return options["wiki-root"] || path.join(resolveProjectRoot(runtime), "wiki");
|
|
24
|
+
}
|