@forwardimpact/libwiki 0.3.0 → 0.3.2
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/README.md +53 -33
- package/package.json +1 -1
- package/src/active-claims.js +5 -5
- package/src/agent-roster.js +2 -2
- package/src/audit/admission.js +14 -11
- package/src/audit/conflict-markers-rule.js +9 -9
- package/src/audit/grammar.js +21 -18
- package/src/audit/rule-builders.js +20 -19
- package/src/audit/rules.js +30 -27
- package/src/audit/scopes.js +34 -33
- package/src/audit/status-row.js +14 -15
- package/src/block-renderer.js +5 -4
- package/src/boot.js +10 -8
- package/src/budget-gate.js +39 -36
- package/src/budget.js +3 -3
- package/src/cli-definition.js +23 -20
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +1 -1
- package/src/commands/claim.js +44 -38
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +68 -64
- package/src/commands/inbox.js +1 -1
- package/src/commands/init.js +12 -7
- package/src/commands/ledger.js +11 -11
- package/src/commands/log.js +19 -17
- package/src/commands/memo.js +4 -1
- package/src/commands/product-mix.js +16 -15
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +9 -8
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +37 -33
- package/src/gitattributes.js +10 -9
- package/src/integrity.js +29 -27
- package/src/issue-list-renderer.js +24 -16
- package/src/lane-files.js +11 -10
- package/src/ledger/anchor.js +6 -6
- package/src/ledger/projection.js +35 -32
- package/src/ledger/reader.js +4 -4
- package/src/marker-scanner.js +3 -2
- package/src/sanitize.js +12 -11
- package/src/secret-gate.js +41 -40
- package/src/status.js +12 -11
- package/src/storyboard-skeleton.js +20 -18
- package/src/util/agent-flag.js +8 -8
- package/src/util/clock.js +1 -1
- package/src/util/wiki-dir.js +7 -7
- package/src/weekly-log.js +115 -101
- package/src/wiki-sync.js +393 -361
package/src/commands/fix.js
CHANGED
|
@@ -18,15 +18,15 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
|
18
18
|
import { FAST_MODEL } from "@forwardimpact/libutil/models";
|
|
19
19
|
import { createLogger } from "@forwardimpact/libtelemetry";
|
|
20
20
|
|
|
21
|
-
// Pipeline: audit → deterministic rotation (the one fix
|
|
22
|
-
// agent
|
|
23
|
-
// flag what neither should touch. MAX_ROUNDS still caps the agent loop
|
|
24
|
-
// unresolvable agent-class finding fails loudly
|
|
21
|
+
// Pipeline: audit → deterministic rotation (the one fix that needs a file seal
|
|
22
|
+
// the agent cannot do) → re-audit → Haiku agent on the prose-judgment residual
|
|
23
|
+
// → flag what neither should touch. MAX_ROUNDS still caps the agent loop. An
|
|
24
|
+
// unresolvable agent-class finding fails loudly and does not loop forever.
|
|
25
25
|
const MAX_ROUNDS = 3;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* A finding's remediation class, from the declarative rule. Rules without a
|
|
29
|
-
* `remediation` field default to `"agent"
|
|
29
|
+
* `remediation` field default to `"agent"`. The Haiku agent handles all
|
|
30
30
|
* prose-judgment fixes (summary trims, section order, MEMORY.md prose).
|
|
31
31
|
*/
|
|
32
32
|
function classOf(finding) {
|
|
@@ -34,14 +34,16 @@ function classOf(finding) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Every rule
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Every rule that governs a scope with an open finding, as `id — hint` lines.
|
|
38
|
+
* The agent gets the full contract for the files it edits. The contract covers
|
|
39
|
+
* more than the rules that fail. It stops the agent from breaking one
|
|
40
|
+
* invariant while it fixes another finding (a dropped `**Last run**:` line, a
|
|
41
|
+
* section appended after `## Open Blockers`, …).
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
* resolved command (a `rotate` remediation the agent never performs)
|
|
44
|
-
* file invariant, so
|
|
43
|
+
* This lists only static-string hints. A function hint is a per-finding
|
|
44
|
+
* resolved command (a `rotate` remediation the agent never performs). A
|
|
45
|
+
* function hint is not a file invariant, so an entry for it would leak its
|
|
46
|
+
* source text into the prompt.
|
|
45
47
|
*/
|
|
46
48
|
function invariantContract(findings) {
|
|
47
49
|
const scopes = new Set(
|
|
@@ -53,52 +55,53 @@ function invariantContract(findings) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
|
-
* The opening task
|
|
57
|
-
* rule hints
|
|
58
|
-
* weekly-log files
|
|
59
|
-
* Write.
|
|
58
|
+
* The opening task. It carries the findings, the invariant contract, and the
|
|
59
|
+
* things the rule hints do not cover. It says where trimmed history goes (only
|
|
60
|
+
* existing weekly-log files, because rotation mints the new ones). It also
|
|
61
|
+
* says to prefer a single Write.
|
|
60
62
|
*/
|
|
61
63
|
function composeTask(findings, wikiRoot, projectRoot) {
|
|
62
64
|
return [
|
|
63
|
-
`Fix these wiki audit findings
|
|
65
|
+
`Fix these wiki audit findings. Edit files under ${wikiRoot}.`,
|
|
64
66
|
``,
|
|
65
67
|
emitFindingsText(findings, { cwd: projectRoot }),
|
|
66
68
|
``,
|
|
67
|
-
`All of these invariants must hold when you finish
|
|
68
|
-
`
|
|
69
|
+
`All of these invariants must hold when you finish. Never fix one finding`,
|
|
70
|
+
`and break another:`,
|
|
69
71
|
...invariantContract(findings),
|
|
70
72
|
``,
|
|
71
73
|
`Move history out of an over-budget summary into the agent's existing`,
|
|
72
|
-
`weekly-log file or its current part (wiki/<agent>-YYYY-Www[-partN].md)
|
|
73
|
-
`
|
|
74
|
-
`when part files
|
|
75
|
-
`trimmed narrative already exists in the weekly log, replace it in
|
|
76
|
-
`summary with a pointer to that file instead of copying it anywhere.`,
|
|
74
|
+
`weekly-log file or its current part (wiki/<agent>-YYYY-Www[-partN].md).`,
|
|
75
|
+
`Never write a new summary section, and never a new file. The rotation`,
|
|
76
|
+
`tool owns when it creates part files, so do not mint filenames yourself.`,
|
|
77
|
+
`If the trimmed narrative already exists in the weekly log, replace it in`,
|
|
78
|
+
`the summary with a pointer to that file instead of copying it anywhere.`,
|
|
77
79
|
`Prefer a single Write over many Edits.`,
|
|
78
80
|
].join("\n");
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
/** The resume task
|
|
83
|
+
/** The resume task. It carries the findings that survived the last edit. */
|
|
82
84
|
function composeFollowup(findings, projectRoot) {
|
|
83
85
|
return [
|
|
84
|
-
`The wiki still fails the audit.
|
|
86
|
+
`The wiki still fails the audit. The findings that remain:`,
|
|
85
87
|
``,
|
|
86
88
|
emitFindingsText(findings, { cwd: projectRoot }),
|
|
87
89
|
``,
|
|
88
|
-
`Fix every one
|
|
90
|
+
`Fix every one. Do not break any invariant listed earlier.`,
|
|
89
91
|
].join("\n");
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
|
-
* Deterministic pre-pass
|
|
94
|
-
*
|
|
95
|
-
* subjects (keyed by path)
|
|
96
|
-
* word-over/line-under file.
|
|
95
|
+
* Deterministic pre-pass. It seals every over-budget current-week main log
|
|
96
|
+
* with `rotateIfOverBudget`. The agent name comes from the audit's own
|
|
97
|
+
* subjects (keyed by path), so nothing parses the filename. `force: true`
|
|
98
|
+
* rotates even a word-over/line-under file.
|
|
97
99
|
*
|
|
98
|
-
* `rotateIfOverBudget` always seals the agent's *current-week* log, so we
|
|
99
|
-
*
|
|
100
|
-
* untouched
|
|
101
|
-
*
|
|
100
|
+
* `rotateIfOverBudget` always seals the agent's *current-week* log, so we call
|
|
101
|
+
* it only when the finding IS that file. A prior-week over-budget main log
|
|
102
|
+
* stays untouched. A rotation would force-seal a healthy current-week log
|
|
103
|
+
* instead. The over-budget main survives the re-audit, and the run flags it
|
|
104
|
+
* for a human.
|
|
102
105
|
*/
|
|
103
106
|
/**
|
|
104
107
|
* Seal one over-budget current-week main log. A failed seal leaves the source
|
|
@@ -133,8 +136,8 @@ function rotateOverBudgetMainLogs(findings, deps) {
|
|
|
133
136
|
];
|
|
134
137
|
const agentByPath = new Map(subjects.map((s) => [s.path, s.agentPrefix]));
|
|
135
138
|
// A log over BOTH budgets yields two `rotate` findings with the same path
|
|
136
|
-
// (different rule ids)
|
|
137
|
-
// bisect the
|
|
139
|
+
// (different rule ids). Seal each path once. A second force call would
|
|
140
|
+
// bisect the newly written main file into a spurious near-empty part.
|
|
138
141
|
const sealed = new Set();
|
|
139
142
|
for (const f of findings) {
|
|
140
143
|
if (classOf(f) !== "rotate") continue;
|
|
@@ -147,9 +150,9 @@ function rotateOverBudgetMainLogs(findings, deps) {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
/**
|
|
150
|
-
* Re-bisect one over-budget sealed part
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
+
* Re-bisect one over-budget sealed part. Log each new sibling slot it produces
|
|
154
|
+
* (the reused source slot is not a new file). A failed reseal leaves the
|
|
155
|
+
* source intact (the writer rolled back), so the re-audit re-flags it.
|
|
153
156
|
*/
|
|
154
157
|
function resealPart(partPath, { fs, projectRoot, out, err }) {
|
|
155
158
|
try {
|
|
@@ -171,15 +174,15 @@ function resealPart(partPath, { fs, projectRoot, out, err }) {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/**
|
|
174
|
-
* Deterministic pass for over-budget sealed parts
|
|
175
|
-
* day-section seams. A part with no splittable seam
|
|
176
|
-
* re-audit re-flags it for a human). Agent and week come from the part
|
|
177
|
-
* so no subject lookup
|
|
178
|
-
* findings `rotateOverBudgetMainLogs` handles.
|
|
177
|
+
* Deterministic pass for over-budget sealed parts. It re-bisects each part at
|
|
178
|
+
* its day-section seams. A part with no splittable seam stays byte-identical
|
|
179
|
+
* (the re-audit re-flags it for a human). Agent and week come from the part
|
|
180
|
+
* filename, so the pass needs no subject lookup. Part findings are disjoint
|
|
181
|
+
* from the main-log findings `rotateOverBudgetMainLogs` handles.
|
|
179
182
|
*/
|
|
180
183
|
function rebisectOverBudgetParts(findings, deps) {
|
|
181
|
-
// A part over both budgets yields two findings with the same path
|
|
182
|
-
// each path once.
|
|
184
|
+
// A part over both budgets yields two findings with the same path.
|
|
185
|
+
// Re-bisect each path once.
|
|
183
186
|
const done = new Set();
|
|
184
187
|
for (const f of findings) {
|
|
185
188
|
if (classOf(f) !== "rotate") continue;
|
|
@@ -190,7 +193,7 @@ function rebisectOverBudgetParts(findings, deps) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
/** Report findings that need human judgment
|
|
196
|
+
/** Report findings that need human judgment. Nothing auto-fixes them. */
|
|
194
197
|
function reportFlags(err, flagFindings, projectRoot) {
|
|
195
198
|
err(
|
|
196
199
|
`gemba-wiki fix: ${flagFindings.length} finding(s) need human judgment ` +
|
|
@@ -200,11 +203,11 @@ function reportFlags(err, flagFindings, projectRoot) {
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
/**
|
|
203
|
-
* Surface a round's agent error, if any. Returns true when
|
|
204
|
-
* missing sessionId means the process never started (e.g. the SDK refused
|
|
206
|
+
* Surface a round's agent error, if any. Returns true when the error is fatal.
|
|
207
|
+
* A missing sessionId means the process never started (e.g. the SDK refused
|
|
205
208
|
* bypass-permissions as root), so there is nothing to resume. A turn-limit or
|
|
206
|
-
* transient error keeps its session and
|
|
207
|
-
*
|
|
209
|
+
* transient error keeps its session and can make partial progress. The
|
|
210
|
+
* function notes it and does not treat it as fatal. The re-audit decides.
|
|
208
211
|
*/
|
|
209
212
|
function isFatalError(result, round, err) {
|
|
210
213
|
if (!result.error) return false;
|
|
@@ -237,10 +240,10 @@ async function buildFixRunner(ctx, projectRoot, runtime) {
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
/**
|
|
240
|
-
* Run the agent on the prose-judgment findings
|
|
241
|
-
* clean,
|
|
242
|
-
* the agent's self-report
|
|
243
|
-
* large for one round.
|
|
243
|
+
* Run the agent on the prose-judgment findings. Re-audit each round until the
|
|
244
|
+
* wiki is clean, until only flags remain, or until MAX_ROUNDS is exhausted.
|
|
245
|
+
* The audit gives the verdict. The agent's self-report does not. A resume
|
|
246
|
+
* extends the turn budget for a trim too large for one round.
|
|
244
247
|
*/
|
|
245
248
|
async function runAgentRounds(runner, agentFindings, deps) {
|
|
246
249
|
const { wikiRoot, projectRoot, audit, partition, out, err } = deps;
|
|
@@ -291,10 +294,10 @@ export async function runFixCommand(ctx) {
|
|
|
291
294
|
buildContext({ wikiRoot, today, fs, subprocess: runtime.subprocess }),
|
|
292
295
|
{ resolveScope },
|
|
293
296
|
);
|
|
294
|
-
// The agent only ever gets prose-judgment (`agent`-class) findings.
|
|
295
|
-
// `rotate` finding that survived the pre-pass (e.g. a
|
|
296
|
-
//
|
|
297
|
-
//
|
|
297
|
+
// The agent only ever gets prose-judgment (`agent`-class) findings. The
|
|
298
|
+
// agent cannot fix a `rotate` finding that survived the pre-pass (e.g. a
|
|
299
|
+
// prior-week log). A trim of append-only history to satisfy a budget would
|
|
300
|
+
// corrupt the history. So the finding joins the flag set for a human.
|
|
298
301
|
const partition = (found) => ({
|
|
299
302
|
agentFindings: found.filter((f) => classOf(f) === "agent"),
|
|
300
303
|
flagFindings: found.filter((f) => classOf(f) !== "agent"),
|
|
@@ -307,7 +310,7 @@ export async function runFixCommand(ctx) {
|
|
|
307
310
|
}
|
|
308
311
|
|
|
309
312
|
// Deterministic layer: seal over-budget main logs, then re-bisect over-budget
|
|
310
|
-
// sealed parts. Both
|
|
313
|
+
// sealed parts. Both preserve content, with no agent and no history rewrite.
|
|
311
314
|
if (findings.some((f) => classOf(f) === "rotate")) {
|
|
312
315
|
const rotateDeps = { wikiRoot, today, projectRoot, fs, out, err };
|
|
313
316
|
rotateOverBudgetMainLogs(findings, rotateDeps);
|
|
@@ -319,15 +322,16 @@ export async function runFixCommand(ctx) {
|
|
|
319
322
|
}
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
// Residual: agent-class goes to the writer
|
|
323
|
-
// rotate finding the deterministic pass could not handle)
|
|
325
|
+
// Residual: agent-class goes to the writer. Everything else needs a human
|
|
326
|
+
// (flag, plus any rotate finding the deterministic pass could not handle).
|
|
324
327
|
const { agentFindings, flagFindings } = partition(findings);
|
|
325
328
|
if (agentFindings.length === 0) {
|
|
326
329
|
reportFlags(err, flagFindings, projectRoot);
|
|
327
330
|
return { ok: false, code: 2 };
|
|
328
331
|
}
|
|
329
332
|
|
|
330
|
-
//
|
|
333
|
+
// Build the runner only now, so a rotation-only or flag-only run never
|
|
334
|
+
// spawns it.
|
|
331
335
|
const runner = await buildFixRunner(ctx, projectRoot, runtime);
|
|
332
336
|
return runAgentRounds(runner, agentFindings, {
|
|
333
337
|
wikiRoot,
|
package/src/commands/inbox.js
CHANGED
|
@@ -104,7 +104,7 @@ function appendPriorityRow(memoryText, { item, agents, owner, status, added }) {
|
|
|
104
104
|
];
|
|
105
105
|
return memoryText.replace(/\n*$/, "") + "\n" + block.join("\n");
|
|
106
106
|
}
|
|
107
|
-
// Find last data row.
|
|
107
|
+
// Find the last data row.
|
|
108
108
|
let sepIdx = -1;
|
|
109
109
|
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
110
110
|
if (/^\|\s*---/.test(lines[i])) {
|
package/src/commands/init.js
CHANGED
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
10
10
|
} from "../constants.js";
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the wiki clone URL. The FIT_WIKI_URL env var is an explicit
|
|
14
|
+
* override. Use it in a sandboxed environment where a local proxy replaces
|
|
15
|
+
* `origin` and does not serve wiki repos. Without that override, the function
|
|
16
|
+
* appends `.wiki.git` to the parent repo's `origin` remote.
|
|
17
|
+
*/
|
|
13
18
|
export async function deriveWikiUrl(gitClient, parentDir, env) {
|
|
14
19
|
if (env.FIT_WIKI_URL) return env.FIT_WIKI_URL;
|
|
15
20
|
try {
|
|
@@ -31,7 +36,7 @@ function scaffoldActiveClaims(runtime, memoryPath) {
|
|
|
31
36
|
"",
|
|
32
37
|
ACTIVE_CLAIMS_HEADING,
|
|
33
38
|
"",
|
|
34
|
-
"In-flight work claimed by an agent. Row present = active
|
|
39
|
+
"In-flight work claimed by an agent. Row present = active. Row absent = settled.",
|
|
35
40
|
"Writers: `gemba-wiki claim`, `gemba-wiki release`. Reader: `gemba-wiki boot`.",
|
|
36
41
|
"",
|
|
37
42
|
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
@@ -65,14 +70,14 @@ async function maybeCloneWiki(wikiSync, gitClient, projectRoot, runtime) {
|
|
|
65
70
|
if (cloneResult.cloned) {
|
|
66
71
|
await wikiSync.inheritIdentity();
|
|
67
72
|
} else {
|
|
68
|
-
logger.warn(
|
|
69
|
-
"init",
|
|
70
|
-
"could not clone wiki, continuing with local-only steps",
|
|
71
|
-
);
|
|
73
|
+
logger.warn("init", "could not clone wiki, so only the local steps run");
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Clone the wiki if it is absent. Scaffold Active Claims in MEMORY.md. Create
|
|
79
|
+
* a metric directory for each skill.
|
|
80
|
+
*/
|
|
76
81
|
export async function runInitCommand(ctx) {
|
|
77
82
|
const { runtime, wikiSync, gitClient } = ctx.deps;
|
|
78
83
|
const options = ctx.options;
|
package/src/commands/ledger.js
CHANGED
|
@@ -38,7 +38,7 @@ function nextFreeIds(fold, kind, count) {
|
|
|
38
38
|
return ids;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Read the ordered anchor sequence
|
|
41
|
+
/** Read the ordered anchor sequence, fold it, and resolve the repo slug. */
|
|
42
42
|
async function loadFold({ gitClient, ghClient, wikiDir, issue }) {
|
|
43
43
|
const url = await gitClient.remoteGetUrl("origin", { cwd: wikiDir });
|
|
44
44
|
const { owner, repo } = parseOwnerRepo(url);
|
|
@@ -70,10 +70,10 @@ async function allocate(env, options) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
const { fold, owner, repo } = await loadFold(env);
|
|
73
|
-
// Backfill registers an anchor for ids that predate the anchor surface
|
|
74
|
-
//
|
|
75
|
-
// allocate mints the next free ids of the kind. The conflict detector
|
|
76
|
-
// rebuild guards against
|
|
73
|
+
// Backfill registers an anchor for ids that predate the anchor surface. The
|
|
74
|
+
// --ids flag names them, and their event keys already exist in history. A
|
|
75
|
+
// plain allocate mints the next free ids of the kind. The conflict detector
|
|
76
|
+
// at rebuild guards against an id that already has an anchor.
|
|
77
77
|
let ids;
|
|
78
78
|
if (options.ids) {
|
|
79
79
|
ids = options.ids
|
|
@@ -93,8 +93,8 @@ async function allocate(env, options) {
|
|
|
93
93
|
ids = nextFreeIds(fold, kind, count);
|
|
94
94
|
}
|
|
95
95
|
const body = renderAnchorBody({ kind, ids, event, note: options.note ?? "" });
|
|
96
|
-
// The anchor publication is the allocation
|
|
97
|
-
// The printed ids are provisional
|
|
96
|
+
// The anchor publication is the allocation. This code writes no projection.
|
|
97
|
+
// The printed ids are provisional. A rebuild over the published sequence is
|
|
98
98
|
// authoritative and resolves any concurrent interleave first-published-wins.
|
|
99
99
|
await ghClient.apiPost(
|
|
100
100
|
`repos/${owner}/${repo}/issues/${env.issue}/comments`,
|
|
@@ -118,7 +118,7 @@ function readLedgerPage(runtime, wikiDir) {
|
|
|
118
118
|
: "";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** Project the anchor record onto the ledger-page body
|
|
121
|
+
/** Project the anchor record onto the ledger-page body. Keep cited prose. */
|
|
122
122
|
async function project(env, options) {
|
|
123
123
|
const { runtime, wikiDir } = env;
|
|
124
124
|
const labelMode = options.gapped ? "gapped" : "renumber";
|
|
@@ -183,9 +183,9 @@ const SUBS = { allocate, rebuild, verify };
|
|
|
183
183
|
/**
|
|
184
184
|
* `gemba-wiki ledger <allocate|rebuild|verify>` — the allocation procedure that
|
|
185
185
|
* keeps identity off the merge-contested page. Allocation publishes an anchor
|
|
186
|
-
* comment to the obstacle issue
|
|
187
|
-
*
|
|
188
|
-
* row
|
|
186
|
+
* comment to the obstacle issue and writes no projection at allocation time.
|
|
187
|
+
* Rebuild and verify project the anchor record onto the ledger page and MEMORY
|
|
188
|
+
* row. Both keep anchor-cited prose.
|
|
189
189
|
*/
|
|
190
190
|
export async function runLedgerCommand(ctx) {
|
|
191
191
|
const { runtime, gitClient, ghClient } = ctx.deps;
|
package/src/commands/log.js
CHANGED
|
@@ -28,8 +28,8 @@ function commonContext(runtime, options) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function lastDateHeading(text) {
|
|
31
|
-
// Match `## YYYY-MM-DD` at the start of a line
|
|
32
|
-
//
|
|
31
|
+
// Match `## YYYY-MM-DD` at the start of a line. Suffix text can follow
|
|
32
|
+
// (e.g. `## 2026-05-19 (third activation)`).
|
|
33
33
|
const re = new RegExp(WEEKLY_LOG_SEAM_RE.source, "gm");
|
|
34
34
|
let last = null;
|
|
35
35
|
let match;
|
|
@@ -38,12 +38,13 @@ function lastDateHeading(text) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Rotate before an append
|
|
42
|
-
* multiple parts
|
|
43
|
-
* `incomplete` residue
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Rotate before an append. The rotation never blocks the append. A seal that
|
|
42
|
+
* bisects can now produce multiple parts, and the append still proceeds
|
|
43
|
+
* against the fresh current file. An `incomplete` residue is a lone over-cap
|
|
44
|
+
* day-section sealed as its own part, never the live file. The function
|
|
45
|
+
* reports that residue to stderr, and it does not block the append. The
|
|
46
|
+
* function reports a thrown fs error and swallows it. The writer rolled back,
|
|
47
|
+
* so the (intact) current file still receives the new entry.
|
|
47
48
|
*/
|
|
48
49
|
function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
49
50
|
try {
|
|
@@ -60,7 +61,7 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
|
60
61
|
createLogger("wiki", runtime).warn(
|
|
61
62
|
"log",
|
|
62
63
|
`day-section ${res.residue.section} alone exceeds the budget ` +
|
|
63
|
-
`(${res.residue.lines} lines, ${res.residue.words} words)
|
|
64
|
+
`(${res.residue.lines} lines, ${res.residue.words} words), ` +
|
|
64
65
|
`sealed as ${res.residue.path} for manual recovery`,
|
|
65
66
|
);
|
|
66
67
|
}
|
|
@@ -70,10 +71,11 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Report the
|
|
74
|
-
* headroom for both the line
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* Report the budget state of the weekly log just written. The report gives the
|
|
75
|
+
* value, the cap, and the headroom left for both the line budget and the word
|
|
76
|
+
* budget. A writer then sees the ceiling before the next entry, and does not
|
|
77
|
+
* find it at a red gate. This runs after every successful append, on the
|
|
78
|
+
* rotated path and the non-rotated path alike.
|
|
77
79
|
*/
|
|
78
80
|
function reportBudget(target, runtime) {
|
|
79
81
|
const text = runtime.fsSync.existsSync(target)
|
|
@@ -131,13 +133,13 @@ function runNote(runtime, options) {
|
|
|
131
133
|
return { ok: false, code: 2 };
|
|
132
134
|
}
|
|
133
135
|
const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
|
|
134
|
-
// Conservative budget: assume
|
|
135
|
-
//
|
|
136
|
+
// Conservative budget: assume a prepended date heading. Rotate on the larger
|
|
137
|
+
// `withHeading` body so the word/line projection never under-counts.
|
|
136
138
|
const withHeading = `## ${today}\n\n${fieldBlock}`;
|
|
137
139
|
rotateBeforeAppend(wikiRoot, agent, today, withHeading, runtime);
|
|
138
140
|
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
139
|
-
// Append under the open entry if the file's last `## YYYY-MM-DD` is today
|
|
140
|
-
//
|
|
141
|
+
// Append under the open entry if the file's last `## YYYY-MM-DD` is today.
|
|
142
|
+
// If it is not, prepend a date heading to open a new entry.
|
|
141
143
|
const existing = runtime.fsSync.existsSync(target)
|
|
142
144
|
? runtime.fsSync.readFileSync(target, "utf-8")
|
|
143
145
|
: "";
|
package/src/commands/memo.js
CHANGED
|
@@ -64,7 +64,10 @@ function writeBroadcast(
|
|
|
64
64
|
return { ok: true };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Write a memo to a target agent's summary file. A broadcast reaches every
|
|
69
|
+
* agent except the sender. The required --from flag names the sender.
|
|
70
|
+
*/
|
|
68
71
|
export function runMemoCommand(ctx) {
|
|
69
72
|
const { runtime } = ctx.deps;
|
|
70
73
|
const options = ctx.options;
|
|
@@ -5,10 +5,10 @@ import { parseRepoSlug } from "../issue-list-renderer.js";
|
|
|
5
5
|
import { currentDayIso } from "../util/clock.js";
|
|
6
6
|
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
7
7
|
|
|
8
|
-
// Resolve the monorepo's `owner/repo` slug the way `refresh.js` does
|
|
9
|
-
// explicit `FIT_GH_REPO` env override (sandbox proxy URLs)
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Resolve the monorepo's `owner/repo` slug the way `refresh.js` does. An
|
|
9
|
+
// explicit `FIT_GH_REPO` env override wins (sandbox proxy URLs). Otherwise the
|
|
10
|
+
// injected git client parses the origin remote. Returns null when nothing
|
|
11
|
+
// parses. `gh` then falls back to its own cwd resolution.
|
|
12
12
|
async function deriveRepo(gitClient, cwd, env) {
|
|
13
13
|
if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
|
|
14
14
|
if (!gitClient) return null;
|
|
@@ -20,8 +20,8 @@ async function deriveRepo(gitClient, cwd, env) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// A missing token is non-fatal
|
|
24
|
-
//
|
|
23
|
+
// A missing token is non-fatal. `gh` can still resolve ambient auth. A hard
|
|
24
|
+
// fetch failure downstream collapses to a logged warning and no row.
|
|
25
25
|
async function resolveToken() {
|
|
26
26
|
try {
|
|
27
27
|
return (await createScriptConfig("wiki")).ghToken();
|
|
@@ -30,8 +30,8 @@ async function resolveToken() {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// `gh pr list` returns at most this many PRs
|
|
34
|
-
// truncated, so the caller warns
|
|
33
|
+
// `gh pr list` returns at most this many PRs. A window that hits the cap is
|
|
34
|
+
// truncated, so the caller warns and does not undercount in silence.
|
|
35
35
|
const FETCH_LIMIT = 200;
|
|
36
36
|
|
|
37
37
|
// Fetch merged PRs in `[since, until]` and return their parsed JSON, or null on
|
|
@@ -60,7 +60,7 @@ async function fetchMergedPrs({ runtime, cwd, repo, since, until, token }) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Tally merged PRs by their classification label. A PR with neither label is
|
|
63
|
-
// unlabeled
|
|
63
|
+
// unlabeled. `product` wins if both labels are somehow present.
|
|
64
64
|
function countByLabel(prs) {
|
|
65
65
|
const counts = { product: 0, internal: 0, unlabeled: 0 };
|
|
66
66
|
for (const pr of prs) {
|
|
@@ -75,10 +75,11 @@ function countByLabel(prs) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Emit the product-vs-internal mix of merged PRs as a `product_share` metric
|
|
77
77
|
* row. Counts PRs merged in `[since, until]` by their `product` / `internal`
|
|
78
|
-
* label
|
|
79
|
-
*
|
|
80
|
-
* path.
|
|
81
|
-
* value. A window with no labeled merged PRs emits no row
|
|
78
|
+
* label. Appends `product_share = round(product / (product + internal) * 100)`
|
|
79
|
+
* to `wiki/metrics/product-mix/<YYYY>.csv` through the `gemba-xmr record`
|
|
80
|
+
* write path. The result is deterministic. A second run over the same merged
|
|
81
|
+
* PRs yields the same value. A window with no labeled merged PRs emits no row
|
|
82
|
+
* (it avoids a 0/0 ratio).
|
|
82
83
|
*/
|
|
83
84
|
export async function runProductMixCommand(ctx) {
|
|
84
85
|
const { runtime, gitClient } = ctx.deps;
|
|
@@ -101,7 +102,7 @@ export async function runProductMixCommand(ctx) {
|
|
|
101
102
|
if (prs.length >= FETCH_LIMIT) {
|
|
102
103
|
logger.warn(
|
|
103
104
|
"product-mix",
|
|
104
|
-
`window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap
|
|
105
|
+
`window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap, so product_share may undercount`,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -110,7 +111,7 @@ export async function runProductMixCommand(ctx) {
|
|
|
110
111
|
if (total === 0) {
|
|
111
112
|
logger.info(
|
|
112
113
|
"product-mix",
|
|
113
|
-
`no labeled merged PRs in ${since}..${until}
|
|
114
|
+
`no labeled merged PRs in ${since}..${until}, so this run emits no row`,
|
|
114
115
|
);
|
|
115
116
|
return { ok: true };
|
|
116
117
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -30,11 +30,12 @@ async function deriveParentRepo(gitClient, parentDir, env) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Compose the agent-experiments block body. On a successful tracker query the
|
|
33
|
-
// body is a fresh last-successful-sync stamp
|
|
34
|
-
// label-re-checked, sanitized item lines. On a tracker failure the
|
|
35
|
-
//
|
|
36
|
-
// the last good routing surface
|
|
37
|
-
//
|
|
33
|
+
// body is a fresh last-successful-sync stamp, then freshly rendered,
|
|
34
|
+
// label-re-checked, sanitized item lines. On a tracker failure the function
|
|
35
|
+
// keeps the body it materialized before (stamp + items) verbatim. Boot then
|
|
36
|
+
// still serves the last good routing surface. It does not serve an empty one.
|
|
37
|
+
// The function does not advance the timestamp, so the stamp keeps
|
|
38
|
+
// staleness auditable.
|
|
38
39
|
async function renderAgentExperimentsBlock(block, lines, ghContext, runtime) {
|
|
39
40
|
const priorBody = lines.slice(block.openLine + 1, block.closeLine);
|
|
40
41
|
try {
|
|
@@ -91,8 +92,8 @@ function spliceBlock(lines, block, rendered) {
|
|
|
91
92
|
);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// A missing current-month storyboard is non-fatal
|
|
95
|
-
// can create it (see createStoryboardSkeleton)
|
|
95
|
+
// A missing current-month storyboard is non-fatal. Return null so the caller
|
|
96
|
+
// can create it (see createStoryboardSkeleton) and does not fail the job.
|
|
96
97
|
function readStoryboardOrNull(runtime, storyboardPath) {
|
|
97
98
|
try {
|
|
98
99
|
return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
|
|
@@ -104,10 +105,11 @@ function readStoryboardOrNull(runtime, storyboardPath) {
|
|
|
104
105
|
|
|
105
106
|
// Create the current-month storyboard from the minimal skeleton when it does
|
|
106
107
|
// not exist. Refresh is the deterministic "freshen the wiki" step and runs
|
|
107
|
-
// before the session (kata-agent pre-run)
|
|
108
|
-
// is on disk before participants look for it
|
|
109
|
-
//
|
|
110
|
-
// markers
|
|
108
|
+
// before the session (kata-agent pre-run). Creation here guarantees the file
|
|
109
|
+
// is on disk before participants look for it, and no lead needs a write tool.
|
|
110
|
+
// The skeleton carries the section structure and the generic issue-list
|
|
111
|
+
// markers. The render pass below fills them, and participants seed metric
|
|
112
|
+
// blocks.
|
|
111
113
|
function createStoryboardSkeleton(runtime, storyboardPath, logger) {
|
|
112
114
|
const skeleton = renderStoryboardSkeleton(currentDayIso(runtime));
|
|
113
115
|
runtime.fsSync.mkdirSync(path.dirname(storyboardPath), { recursive: true });
|
|
@@ -116,12 +118,12 @@ function createStoryboardSkeleton(runtime, storyboardPath, logger) {
|
|
|
116
118
|
return skeleton;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
// Drop every MEMORY.md `## Active Claims` row past its `expires_at
|
|
121
|
+
// Drop every MEMORY.md `## Active Claims` row past its `expires_at`. Write the
|
|
120
122
|
// trimmed table back in place. Refresh is the deterministic "freshen the wiki"
|
|
121
|
-
// step, so
|
|
122
|
-
//
|
|
123
|
-
// write is local
|
|
124
|
-
// it. A missing wiki or claims table is a clean no-op.
|
|
123
|
+
// step, so a sweep of lapsed claims belongs here beside the storyboard render.
|
|
124
|
+
// It runs whether or not the storyboard has marker blocks to regenerate. The
|
|
125
|
+
// write is local and mirrors the storyboard splice. The caller's push
|
|
126
|
+
// publishes it. A missing wiki or claims table is a clean no-op.
|
|
125
127
|
function clearExpiredClaims(runtime, options, today, logger) {
|
|
126
128
|
const memPath = path.join(resolveWikiRoot(runtime, options), "MEMORY.md");
|
|
127
129
|
if (!runtime.fsSync.existsSync(memPath)) return;
|
|
@@ -148,8 +150,9 @@ export async function runRefreshCommand(ctx) {
|
|
|
148
150
|
const logger = createLogger("wiki", runtime);
|
|
149
151
|
const projectRoot = resolveProjectRoot(runtime);
|
|
150
152
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
+
// This is independent of the storyboard render below (and its early
|
|
154
|
+
// returns). The sweep still runs on a wiki with no storyboard and on a wiki
|
|
155
|
+
// with no marker blocks.
|
|
153
156
|
clearExpiredClaims(runtime, options, currentDayIso(runtime), logger);
|
|
154
157
|
|
|
155
158
|
const storyboardPath = path.resolve(
|
|
@@ -171,14 +174,14 @@ export async function runRefreshCommand(ctx) {
|
|
|
171
174
|
try {
|
|
172
175
|
token = config.ghToken();
|
|
173
176
|
} catch {
|
|
174
|
-
//
|
|
175
|
-
// warning and the block
|
|
177
|
+
// A missing token is non-fatal. An issue-list render then fails with a
|
|
178
|
+
// stderr warning, and the block collapses to the notice line.
|
|
176
179
|
}
|
|
177
180
|
// Spawn `gh` from the project root so it resolves the monorepo's origin
|
|
178
181
|
// instead of whatever git context the caller's cwd happens to be in (the
|
|
179
182
|
// wiki sibling repo, a subagent worktree, a service dir, etc.). Also
|
|
180
|
-
// resolve an explicit owner/repo slug so `gh` works when
|
|
181
|
-
//
|
|
183
|
+
// resolve an explicit owner/repo slug so `gh` works when a proxy URL
|
|
184
|
+
// replaced origin (sandbox environments). The `FIT_GH_REPO` env var
|
|
182
185
|
// overrides the parsed origin.
|
|
183
186
|
const ghContext = {
|
|
184
187
|
cwd: projectRoot,
|