@forwardimpact/libwiki 0.2.35 → 0.3.1
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 -52
- package/package.json +6 -8
- 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 +35 -32
- package/src/audit/scopes.js +35 -34
- 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 +35 -32
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +2 -2
- package/src/commands/claim.js +46 -40
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +74 -70
- package/src/commands/inbox.js +2 -2
- package/src/commands/init.js +13 -8
- package/src/commands/ledger.js +12 -12
- package/src/commands/log.js +20 -18
- package/src/commands/memo.js +5 -2
- package/src/commands/product-mix.js +18 -17
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +10 -9
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +38 -34
- 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 +411 -379
- package/bin/fit-wiki.js +0 -94
package/src/commands/curate.js
CHANGED
|
@@ -7,8 +7,8 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
|
7
7
|
import { auditWiki } from "./audit.js";
|
|
8
8
|
|
|
9
9
|
// The routing contract: a single open issue, addressed to the technical-writer,
|
|
10
|
-
//
|
|
11
|
-
// dirty wiki appends to one issue
|
|
10
|
+
// that holds the audit findings. Every run matches the title verbatim, so a
|
|
11
|
+
// dirty wiki appends to one issue and does not open a new one each day.
|
|
12
12
|
const LABEL = {
|
|
13
13
|
name: "wiki-curation",
|
|
14
14
|
color: "BFD4F2",
|
|
@@ -19,13 +19,14 @@ const TITLE = "Wiki curation: shared-state audit findings";
|
|
|
19
19
|
// GitHub rejects an issue or comment body over 65536 characters. Keep the whole
|
|
20
20
|
// body under a margin below that so the preamble, JSON fence, and truncation
|
|
21
21
|
// notice always fit. When the findings overflow, the body carries the first N
|
|
22
|
-
// that fit plus a count
|
|
22
|
+
// that fit plus a count. `gemba-wiki audit` still reproduces the full list.
|
|
23
23
|
const MAX_BODY = 65000;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Compose the issue body from the audit's JSON findings. The findings ride a
|
|
27
|
-
* fenced ```json block
|
|
28
|
-
* file)
|
|
27
|
+
* fenced ```json block. The caller passes the body to `gh` through
|
|
28
|
+
* `--body-file` (a temp file) and never through argv. So `gh` cannot misread
|
|
29
|
+
* untrusted finding text as a flag.
|
|
29
30
|
* @param {string} findingsJson
|
|
30
31
|
* @param {{shown?: number, total?: number}} [trunc]
|
|
31
32
|
* @returns {string}
|
|
@@ -34,12 +35,12 @@ function buildBody(findingsJson, { shown, total } = {}) {
|
|
|
34
35
|
const lines = [
|
|
35
36
|
"Scheduled `curate-wiki` audit found shared-wiki violations.",
|
|
36
37
|
"",
|
|
37
|
-
"Owner: **technical-writer
|
|
38
|
+
"Owner: **technical-writer**. Service these through the curation shift. The per-PR `wiki` gate no longer reads shared wiki state.",
|
|
38
39
|
"",
|
|
39
40
|
];
|
|
40
41
|
if (total != null && shown != null && shown < total) {
|
|
41
42
|
lines.push(
|
|
42
|
-
`Showing ${shown} of ${total} findings
|
|
43
|
+
`Showing ${shown} of ${total} findings. The body is truncated to fit GitHub's comment limit. Run \`gemba-wiki audit\` for the full list.`,
|
|
43
44
|
"",
|
|
44
45
|
);
|
|
45
46
|
}
|
|
@@ -49,9 +50,10 @@ function buildBody(findingsJson, { shown, total } = {}) {
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Build the largest postable body for the audit findings. The full findings
|
|
52
|
-
* usually fit
|
|
53
|
-
* under GitHub's body limit
|
|
54
|
-
* down proportionally to the overflow, so it converges in a couple of
|
|
53
|
+
* usually fit. When they do not fit, keep the first N `fail` findings that
|
|
54
|
+
* stay under GitHub's body limit. Label the body as truncated. The shrink
|
|
55
|
+
* steps down proportionally to the overflow, so it converges in a couple of
|
|
56
|
+
* passes.
|
|
55
57
|
* @param {{level: string}[]} findings
|
|
56
58
|
* @returns {string}
|
|
57
59
|
*/
|
|
@@ -76,8 +78,8 @@ function fitBody(findings) {
|
|
|
76
78
|
|
|
77
79
|
// Resolve the monorepo's `owner/repo` slug the way refresh.js/product-mix.js
|
|
78
80
|
// do: an explicit FIT_GH_REPO override (sandbox proxy URLs), else the origin
|
|
79
|
-
// remote parsed
|
|
80
|
-
// own cwd resolution.
|
|
81
|
+
// remote parsed through the injected git client. Null lets `gh` fall back to
|
|
82
|
+
// its own cwd resolution.
|
|
81
83
|
async function deriveRepo(gitClient, cwd, env) {
|
|
82
84
|
if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
|
|
83
85
|
if (!gitClient) return null;
|
|
@@ -88,7 +90,7 @@ async function deriveRepo(gitClient, cwd, env) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
// A missing token is non-fatal
|
|
93
|
+
// A missing token is non-fatal. `gh` may still resolve ambient auth.
|
|
92
94
|
async function resolveToken() {
|
|
93
95
|
try {
|
|
94
96
|
return (await createScriptConfig("wiki")).ghToken();
|
|
@@ -98,8 +100,9 @@ async function resolveToken() {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
|
-
* Find the open `wiki-curation` issue by its verbatim title, or null.
|
|
102
|
-
* parse failure or empty result
|
|
103
|
+
* Find the open `wiki-curation` issue by its verbatim title, or null. This
|
|
104
|
+
* function treats any parse failure or empty result as "no issue" (create
|
|
105
|
+
* path).
|
|
103
106
|
* @param {import("@forwardimpact/libcli").InvocationContext["deps"]["runtime"]} runtime
|
|
104
107
|
* @param {string[]} repoArgs
|
|
105
108
|
* @param {{cwd: string, env: object}} opts
|
|
@@ -129,11 +132,11 @@ async function findOpenIssue(runtime, repoArgs, opts) {
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
|
-
* Route the composed body to the single `wiki-curation` issue:
|
|
133
|
-
* label, find the open issue by title, then comment on it or create it.
|
|
134
|
-
* body goes through a temp file
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* Route the composed body to the single `wiki-curation` issue: make sure the
|
|
136
|
+
* label exists, find the open issue by title, then comment on it or create it.
|
|
137
|
+
* The body goes through a temp file and never through argv. So `gh` cannot
|
|
138
|
+
* read untrusted finding text as a flag. On a `gh` failure this function logs
|
|
139
|
+
* the reason and returns `ok:false`, so the caller exits non-zero.
|
|
137
140
|
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
138
141
|
* @param {string} body
|
|
139
142
|
* @param {ReturnType<typeof createLogger>} logger
|
|
@@ -150,8 +153,8 @@ async function routeFindings(ctx, body, logger) {
|
|
|
150
153
|
: runtime.proc.env;
|
|
151
154
|
const repoArgs = repo ? ["--repo", repo] : [];
|
|
152
155
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
156
|
+
// Make sure the label exists. A re-create on an existing label exits
|
|
157
|
+
// non-zero. That is expected, and the code ignores it.
|
|
155
158
|
await runtime.subprocess.run(
|
|
156
159
|
"gh",
|
|
157
160
|
[
|
|
@@ -169,8 +172,8 @@ async function routeFindings(ctx, body, logger) {
|
|
|
169
172
|
|
|
170
173
|
const number = await findOpenIssue(runtime, repoArgs, { cwd, env });
|
|
171
174
|
|
|
172
|
-
// Pass the body through a temp file
|
|
173
|
-
// to
|
|
175
|
+
// Pass the body through a temp file instead of argv. A temp file is robust
|
|
176
|
+
// to length, and `gh` cannot read finding text as a flag.
|
|
174
177
|
const tmp = runtime.proc.env.RUNNER_TEMP || runtime.proc.env.TMPDIR || "/tmp";
|
|
175
178
|
const bodyFile = path.join(tmp, "wiki-curation-body.md");
|
|
176
179
|
runtime.fsSync.writeFileSync(bodyFile, body);
|
|
@@ -208,12 +211,12 @@ async function routeFindings(ctx, body, logger) {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
/**
|
|
211
|
-
* Audit the shared wiki
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* Audit the shared wiki. When it is dirty, route the findings to the single
|
|
215
|
+
* `wiki-curation` issue (create or comment) addressed to the technical-writer.
|
|
216
|
+
* This is the SOLE home of the shared-wiki audit verdict. The per-PR `wiki`
|
|
217
|
+
* gate no longer reads live wiki state. A clean wiki routes nothing. The
|
|
218
|
+
* label/search/create-or-comment logic lives here and not in the workflow, so
|
|
219
|
+
* the curation step is one CLI call.
|
|
217
220
|
*
|
|
218
221
|
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
219
222
|
* @returns {Promise<{ok: boolean}>}
|
|
@@ -224,7 +227,7 @@ export async function runCurateCommand(ctx) {
|
|
|
224
227
|
const { findings } = auditWiki(ctx);
|
|
225
228
|
|
|
226
229
|
if (!findings.some((f) => f.level === "fail")) {
|
|
227
|
-
runtime.proc.stdout.write("wiki audit clean
|
|
230
|
+
runtime.proc.stdout.write("wiki audit clean, no curation issue routed\n");
|
|
228
231
|
return { ok: true };
|
|
229
232
|
}
|
|
230
233
|
|
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
|
|
@@ -122,7 +125,7 @@ function sealMainLog(agent, { wikiRoot, today, projectRoot, fs, out, err }) {
|
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
} catch (e) {
|
|
125
|
-
err(`
|
|
128
|
+
err(`gemba-wiki fix: rotate failed for ${agent}: ${e.message}\n`);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -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 {
|
|
@@ -164,22 +167,22 @@ function resealPart(partPath, { fs, projectRoot, out, err }) {
|
|
|
164
167
|
}
|
|
165
168
|
} catch (e) {
|
|
166
169
|
err(
|
|
167
|
-
`
|
|
170
|
+
`gemba-wiki fix: rebisect failed for ` +
|
|
168
171
|
`${path.relative(projectRoot, partPath)}: ${e.message}\n`,
|
|
169
172
|
);
|
|
170
173
|
}
|
|
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,29 +193,29 @@ 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 ` +
|
|
197
200
|
`(not auto-fixable):\n` +
|
|
198
201
|
emitFindingsText(flagFindings, { cwd: projectRoot }),
|
|
199
202
|
);
|
|
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;
|
|
211
214
|
if (!result.sessionId) {
|
|
212
|
-
err(`
|
|
215
|
+
err(`gemba-wiki fix: agent run failed: ${result.error.message}\n`);
|
|
213
216
|
return true;
|
|
214
217
|
}
|
|
215
|
-
err(`
|
|
218
|
+
err(`gemba-wiki fix: round ${round} agent error: ${result.error.message}\n`);
|
|
216
219
|
return false;
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -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;
|
|
@@ -265,7 +268,7 @@ async function runAgentRounds(runner, agentFindings, deps) {
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
err(
|
|
268
|
-
`
|
|
271
|
+
`gemba-wiki fix: ${agentFindings.length} finding(s) remain after ` +
|
|
269
272
|
`${MAX_ROUNDS} round(s):\n` +
|
|
270
273
|
emitFindingsText(agentFindings, { cwd: projectRoot }),
|
|
271
274
|
);
|
|
@@ -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
|
@@ -13,7 +13,7 @@ function paths(runtime, options) {
|
|
|
13
13
|
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
14
14
|
const resolved = requireAgentFlag(options, {
|
|
15
15
|
command: "inbox",
|
|
16
|
-
example: "
|
|
16
|
+
example: "gemba-wiki inbox list --agent staff-engineer",
|
|
17
17
|
});
|
|
18
18
|
if (!resolved.ok) return { error: resolved };
|
|
19
19
|
const agent = resolved.agent;
|
|
@@ -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,8 +36,8 @@ function scaffoldActiveClaims(runtime, memoryPath) {
|
|
|
31
36
|
"",
|
|
32
37
|
ACTIVE_CLAIMS_HEADING,
|
|
33
38
|
"",
|
|
34
|
-
"In-flight work claimed by an agent. Row present = active
|
|
35
|
-
"Writers: `
|
|
39
|
+
"In-flight work claimed by an agent. Row present = active. Row absent = settled.",
|
|
40
|
+
"Writers: `gemba-wiki claim`, `gemba-wiki release`. Reader: `gemba-wiki boot`.",
|
|
36
41
|
"",
|
|
37
42
|
ACTIVE_CLAIMS_TABLE_HEADER,
|
|
38
43
|
ACTIVE_CLAIMS_TABLE_SEPARATOR,
|
|
@@ -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";
|
|
@@ -181,11 +181,11 @@ async function verify(env, options) {
|
|
|
181
181
|
const SUBS = { allocate, rebuild, verify };
|
|
182
182
|
|
|
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;
|