@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/budget-gate.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Post-landing, pre-push budget re-validation on the size (word/line) axis.
|
|
2
2
|
//
|
|
3
3
|
// The wiki landing flow re-runs the audit's budget predicates over the
|
|
4
|
-
// outgoing tree between landing and push
|
|
4
|
+
// outgoing tree between landing and push. It refuses a push that introduces
|
|
5
5
|
// or deepens a per-file budget breach this writer's push would publish. The
|
|
6
|
-
// gate reuses the audit's budget rules by reference
|
|
7
|
-
// objects named by `BUDGET_RULE_IDS
|
|
8
|
-
// over-cap predicate) plus the same `countWords` / `countLines` the audit
|
|
9
|
-
// builds its subjects from. It never re-defines a budget
|
|
10
|
-
// the `runRules` engine
|
|
11
|
-
// cap
|
|
6
|
+
// gate reuses the audit's budget rules by reference. It resolves the rule
|
|
7
|
+
// objects named by `BUDGET_RULE_IDS`. It then calls each rule's own `check`
|
|
8
|
+
// (the over-cap predicate) plus the same `countWords` / `countLines` the audit
|
|
9
|
+
// builds its subjects from. It never re-defines a budget. It never routes
|
|
10
|
+
// through the `runRules` engine, which drops the numeric value and emits
|
|
11
|
+
// nothing under cap. It never edits. It refuses, which keeps commits local.
|
|
12
12
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { BUDGET_RULE_IDS, RULES } from "./audit/rules.js";
|
|
@@ -16,9 +16,10 @@ import { buildContext, resolveScope } from "./audit/scopes.js";
|
|
|
16
16
|
import { countLines, countWords } from "./budget.js";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES
|
|
20
|
-
* the count axis its id implies.
|
|
21
|
-
* so a rule rename surfaces here
|
|
19
|
+
* Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES`. Tag each one with
|
|
20
|
+
* the count axis its id implies. It throws if a named id is missing from
|
|
21
|
+
* `RULES`, so a rule rename surfaces here and does not silently drop a
|
|
22
|
+
* predicate.
|
|
22
23
|
* @returns {Array<{id: string, scope: string, axis: 'words'|'lines', check: Function}>}
|
|
23
24
|
*/
|
|
24
25
|
export function budgetRules() {
|
|
@@ -36,10 +37,10 @@ export function budgetRules() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Enumerate which wiki files are budgeted
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Enumerate which wiki files are budgeted. Reuse the audit's classification.
|
|
41
|
+
* Subjects carry an absolute `path`. Reduce each one to the `<file>` half of
|
|
42
|
+
* `git show <ref>:<file>`, relative to `wikiRoot`. This function reads no count
|
|
43
|
+
* off the working-dir subject. It reads only the file identity and its scope.
|
|
43
44
|
* @param {object} ctx - An audit context from `buildContext`.
|
|
44
45
|
* @param {string} wikiRoot - The wiki clone directory the paths are relative to.
|
|
45
46
|
* @returns {Array<{relPath: string, scope: string}>}
|
|
@@ -56,12 +57,12 @@ export function budgetedFiles(ctx, wikiRoot) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Measure the budget predicates for the tree at `ref`.
|
|
60
|
-
* file's blob
|
|
61
|
-
* counters,
|
|
60
|
+
* Measure the budget predicates for the tree at `ref`. Read each budgeted
|
|
61
|
+
* file's blob through the cwd-bound `showFile`. Count it once with the audit's
|
|
62
|
+
* counters. Then, for every budget rule on that file's scope, record the axis
|
|
62
63
|
* value and whether the rule's own `check` flags it over cap. An absent path
|
|
63
|
-
* at the ref counts as 0
|
|
64
|
-
* posture
|
|
64
|
+
* at the ref counts as 0. This matches the audit's "missing counts as empty"
|
|
65
|
+
* posture. An unreadable ref makes `showFile` throw, and the throw propagates.
|
|
65
66
|
* @param {(ref: string, file: string) => Promise<string|null>} showFile
|
|
66
67
|
* @param {string} ref - The tree-ish to measure (e.g. "HEAD", a SHA).
|
|
67
68
|
* @param {Array<{relPath: string, scope: string}>} budgeted
|
|
@@ -88,15 +89,16 @@ export async function measureRef(showFile, ref, budgeted) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Compare the outgoing tree against the two push-input baselines
|
|
92
|
+
* Compare the outgoing tree against the two push-input baselines. Return the
|
|
92
93
|
* per-file/per-predicate refusal delta. For each (file, rule) the baseline is
|
|
93
|
-
* the worse (higher) of the session-base and origin-tip values
|
|
94
|
-
*
|
|
95
|
-
* cap AND strictly exceeds that baseline
|
|
96
|
-
* a foreign breach the writer did not worsen passes.
|
|
97
|
-
* file listed in `exemptSummaryFiles`
|
|
98
|
-
* memo-delivery seam
|
|
99
|
-
* enforce a contradiction
|
|
94
|
+
* the worse (higher) of the session-base and origin-tip values. An absent
|
|
95
|
+
* measurement counts as 0. A predicate refuses iff the outgoing value is over
|
|
96
|
+
* cap AND strictly exceeds that baseline. So equal-or-better states pass, and
|
|
97
|
+
* a foreign breach the writer did not worsen passes. The gate surfaces a
|
|
98
|
+
* `summary.*` breach on a file listed in `exemptSummaryFiles` instead of
|
|
99
|
+
* refusing it. Those files are the memo-delivery seam. A block on a delivery
|
|
100
|
+
* into deficient headroom would enforce a contradiction. The memo-headroom
|
|
101
|
+
* measures exist to resolve that contradiction.
|
|
100
102
|
*
|
|
101
103
|
* @param {object} args
|
|
102
104
|
* @param {Map<string, Map<string, {value: number, overCap: boolean}>>} args.outgoing
|
|
@@ -137,14 +139,14 @@ export function revalidateBudgets({
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
|
-
* Run the gate end to end over the outgoing tree.
|
|
141
|
-
*
|
|
142
|
-
* and the two push-input baselines through the one `measureRef` path
|
|
143
|
-
*
|
|
144
|
-
* `showFile` throw, which aborts the gate WITHOUT refusing
|
|
145
|
-
* refuses a regression it can prove
|
|
146
|
-
* proceeds
|
|
147
|
-
* a foreign pre-existing breach.
|
|
142
|
+
* Run the gate end to end over the outgoing tree. Build the audit context.
|
|
143
|
+
* Enumerate the budgeted files. Measure the committed `HEAD` (what publishes)
|
|
144
|
+
* and the two push-input baselines through the one `measureRef` path. Then
|
|
145
|
+
* compute the per-file/per-predicate delta. An unreadable baseline ref makes
|
|
146
|
+
* `showFile` throw, which aborts the gate WITHOUT refusing. The gate only
|
|
147
|
+
* refuses a regression it can prove. So a read failure surfaces and the push
|
|
148
|
+
* proceeds. The gate does not fabricate a value-0 baseline that would wrongly
|
|
149
|
+
* block a foreign pre-existing breach.
|
|
148
150
|
*
|
|
149
151
|
* @param {object} args
|
|
150
152
|
* @param {(ref: string, file: string) => Promise<string|null>} args.showFile
|
|
@@ -181,7 +183,8 @@ export async function runBudgetGate({
|
|
|
181
183
|
}
|
|
182
184
|
originTip = await measureRef(showFile, originRef, budgeted);
|
|
183
185
|
} catch {
|
|
184
|
-
// Cannot prove a regression (unreadable ref) ⇒ do not refuse
|
|
186
|
+
// Cannot prove a regression (unreadable ref) ⇒ do not refuse. This is the
|
|
187
|
+
// fail-visible posture.
|
|
185
188
|
return { refusals: [], surfaced: [] };
|
|
186
189
|
}
|
|
187
190
|
return revalidateBudgets({
|
package/src/budget.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Canonical line- and word-counters for the budgeted wiki surfaces. The audit
|
|
2
2
|
// (`audit/scopes.js`) and the rotation primitive's bisecting seal
|
|
3
|
-
// (`weekly-log.js`) both import this one pair
|
|
4
|
-
//
|
|
3
|
+
// (`weekly-log.js`) both import this one pair. So no audit that counts
|
|
4
|
+
// differently can later flag a part the seal accepts as conforming.
|
|
5
5
|
|
|
6
|
-
/** Count lines
|
|
6
|
+
/** Count lines. Do not count a trailing newline as an empty final line. */
|
|
7
7
|
export function countLines(text) {
|
|
8
8
|
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
9
9
|
}
|
package/src/cli-definition.js
CHANGED
|
@@ -14,13 +14,13 @@ import { runFixCommand } from "./commands/fix.js";
|
|
|
14
14
|
import { runLedgerCommand } from "./commands/ledger.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Build the `
|
|
18
|
-
* the environment
|
|
19
|
-
* (`--from` for `memo`)
|
|
20
|
-
* ambient agent identity.
|
|
21
|
-
* the bin's `packageJsonUrl`. Each subcommand carries a `handler
|
|
22
|
-
*
|
|
23
|
-
* the per-command handler with a frozen `ctx`.
|
|
17
|
+
* Build the `gemba-wiki` libcli definition. This module never resolves agent
|
|
18
|
+
* identity from the environment. Agent-scoped subcommands require an explicit
|
|
19
|
+
* `--agent` (`--from` for `memo`). They fail closed without it, so this module
|
|
20
|
+
* carries no ambient agent identity. libcli's `createCli` resolves the version
|
|
21
|
+
* from the bin's `packageJsonUrl`. Each subcommand carries a `handler`. A
|
|
22
|
+
* command that takes subcommands also carries `args`/`argsUsage`, so
|
|
23
|
+
* `cli.dispatch` can route to the per-command handler with a frozen `ctx`.
|
|
24
24
|
*
|
|
25
25
|
* @returns {object} The libcli definition.
|
|
26
26
|
*/
|
|
@@ -47,13 +47,13 @@ export function createDefinition() {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
|
-
name: "
|
|
51
|
-
description: "
|
|
50
|
+
name: "gemba-wiki",
|
|
51
|
+
description: "Manage the wiki lifecycle for the Kata agent system",
|
|
52
52
|
commands: [
|
|
53
53
|
{
|
|
54
54
|
name: "boot",
|
|
55
55
|
description:
|
|
56
|
-
"Print on-boot digest (priorities, claims, storyboard items) as JSON",
|
|
56
|
+
"Print the on-boot digest (priorities, claims, storyboard items) as JSON",
|
|
57
57
|
handler: runBootCommand,
|
|
58
58
|
options: {
|
|
59
59
|
...agentOpt,
|
|
@@ -101,9 +101,12 @@ export function createDefinition() {
|
|
|
101
101
|
...todayOpt,
|
|
102
102
|
target: {
|
|
103
103
|
type: "string",
|
|
104
|
-
description: "
|
|
104
|
+
description: "Target to claim (spec id, PR id, etc.)",
|
|
105
|
+
},
|
|
106
|
+
branch: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Branch that carries the work",
|
|
105
109
|
},
|
|
106
|
-
branch: { type: "string", description: "Branch carrying the work" },
|
|
107
110
|
pr: { type: "string", description: "Optional PR id" },
|
|
108
111
|
"expires-at": {
|
|
109
112
|
type: "string",
|
|
@@ -142,7 +145,7 @@ export function createDefinition() {
|
|
|
142
145
|
},
|
|
143
146
|
owner: {
|
|
144
147
|
type: "string",
|
|
145
|
-
description: "Owner field
|
|
148
|
+
description: "Owner field for promote (default: --agent)",
|
|
146
149
|
},
|
|
147
150
|
},
|
|
148
151
|
},
|
|
@@ -190,7 +193,7 @@ export function createDefinition() {
|
|
|
190
193
|
"dry-run": {
|
|
191
194
|
type: "boolean",
|
|
192
195
|
description:
|
|
193
|
-
"Print the issue body and intended action
|
|
196
|
+
"Print the issue body and intended action. Do not call gh",
|
|
194
197
|
},
|
|
195
198
|
},
|
|
196
199
|
},
|
|
@@ -217,7 +220,7 @@ export function createDefinition() {
|
|
|
217
220
|
to: {
|
|
218
221
|
type: "string",
|
|
219
222
|
description:
|
|
220
|
-
'Target agent name, or "all" to broadcast (
|
|
223
|
+
'Target agent name, or "all" to broadcast (skips the sender)',
|
|
221
224
|
},
|
|
222
225
|
message: {
|
|
223
226
|
type: "string",
|
|
@@ -287,7 +290,7 @@ export function createDefinition() {
|
|
|
287
290
|
type: "string",
|
|
288
291
|
multiple: true,
|
|
289
292
|
description:
|
|
290
|
-
"Pathspec(s)
|
|
293
|
+
"Pathspec(s) that limit the write-set. Omit to land the session's dirty set",
|
|
291
294
|
},
|
|
292
295
|
},
|
|
293
296
|
},
|
|
@@ -330,7 +333,7 @@ export function createDefinition() {
|
|
|
330
333
|
gapped: {
|
|
331
334
|
type: "boolean",
|
|
332
335
|
description:
|
|
333
|
-
"Render double-allocation losers as a gap
|
|
336
|
+
"Render double-allocation losers as a gap instead of a renumber",
|
|
334
337
|
},
|
|
335
338
|
issue: {
|
|
336
339
|
type: "string",
|
|
@@ -348,21 +351,21 @@ export function createDefinition() {
|
|
|
348
351
|
},
|
|
349
352
|
},
|
|
350
353
|
examples: [
|
|
351
|
-
"
|
|
352
|
-
'
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
'
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
354
|
+
"gemba-wiki boot --agent staff-engineer",
|
|
355
|
+
'gemba-wiki log decision --agent staff-engineer --surveyed "..." --chosen "..." --rationale "..."',
|
|
356
|
+
"gemba-wiki claim --agent staff-engineer --target spec-NNNN --branch claude/...",
|
|
357
|
+
"gemba-wiki release --agent staff-engineer --target spec-NNNN",
|
|
358
|
+
"gemba-wiki inbox list --agent staff-engineer",
|
|
359
|
+
"gemba-wiki rotate --agent staff-engineer",
|
|
360
|
+
"gemba-wiki audit",
|
|
361
|
+
"gemba-wiki curate",
|
|
362
|
+
"gemba-wiki fix",
|
|
363
|
+
'gemba-wiki memo --from staff-engineer --to security-engineer --message "audit d642ff0c"',
|
|
364
|
+
"gemba-wiki refresh",
|
|
365
|
+
"gemba-wiki product-mix",
|
|
366
|
+
"gemba-wiki init",
|
|
367
|
+
"gemba-wiki push",
|
|
368
|
+
"gemba-wiki pull",
|
|
366
369
|
],
|
|
367
370
|
documentation: [
|
|
368
371
|
{
|
package/src/commands/audit.js
CHANGED
|
@@ -11,8 +11,8 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Run the wiki audit and return its findings plus the resolved project root.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* `runAuditCommand` emits the findings. `runCurateCommand` routes them to an
|
|
15
|
+
* issue. Both share this function, so the two cannot drift.
|
|
16
16
|
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
17
17
|
* @returns {{ findings: object[], projectRoot: string }}
|
|
18
18
|
*/
|
|
@@ -32,7 +32,7 @@ export function auditWiki(ctx) {
|
|
|
32
32
|
return { findings: runRules(RULES, auditCtx, { resolveScope }), projectRoot };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Run the wiki audit and emit findings.
|
|
35
|
+
/** Run the wiki audit and emit findings. Use --format json for JSON. */
|
|
36
36
|
export function runAuditCommand(ctx) {
|
|
37
37
|
const { runtime } = ctx.deps;
|
|
38
38
|
const options = ctx.options;
|
package/src/commands/boot.js
CHANGED
|
@@ -40,13 +40,13 @@ function renderMarkdown(digest) {
|
|
|
40
40
|
return lines.join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Print the on-boot digest for the
|
|
43
|
+
/** Print the on-boot digest for the agent that runs it. JSON by default. --format markdown renders prose. */
|
|
44
44
|
export function runBootCommand(ctx) {
|
|
45
45
|
const { runtime } = ctx.deps;
|
|
46
46
|
const options = ctx.options;
|
|
47
47
|
const resolved = requireAgentFlag(options, {
|
|
48
48
|
command: "boot",
|
|
49
|
-
example: "
|
|
49
|
+
example: "gemba-wiki boot --agent staff-engineer",
|
|
50
50
|
});
|
|
51
51
|
if (!resolved.ok) return resolved;
|
|
52
52
|
const agent = resolved.agent;
|
package/src/commands/claim.js
CHANGED
|
@@ -24,9 +24,9 @@ const NOT_PUBLISHED = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// Failure reasons that, on the claim/release surfaces, are an unsafe-state
|
|
27
|
-
// refusal (D7/D9 family) rather than a saved-locally success (D1)
|
|
28
|
-
// fires before the local write is publishable
|
|
29
|
-
// later whole-tree sweep
|
|
27
|
+
// refusal (D7/D9 family) rather than a saved-locally success (D1). The refusal
|
|
28
|
+
// fires before the local write is publishable. Or it leaves the tree unsafe
|
|
29
|
+
// for a later whole-tree sweep. So the surface must exit non-zero.
|
|
30
30
|
const UNSAFE_STATE_REASONS = new Set([
|
|
31
31
|
PUSH_REASONS.PRECONDITION,
|
|
32
32
|
PUSH_REASONS.RESIDUE_CONFLICT,
|
|
@@ -37,7 +37,7 @@ const UNSAFE_STATE_REASONS = new Set([
|
|
|
37
37
|
function notPublishedMessage(err) {
|
|
38
38
|
return (
|
|
39
39
|
`${err.message}\n` +
|
|
40
|
-
"The row
|
|
40
|
+
"The row is in MEMORY.md but is NOT published. It remains an " +
|
|
41
41
|
"uncommitted working-tree change.\n"
|
|
42
42
|
);
|
|
43
43
|
}
|
|
@@ -52,23 +52,25 @@ function memoryPath(runtime, options) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Push the claim/release MEMORY.md change
|
|
56
|
-
*
|
|
57
|
-
* discipline (the singleton merge discipline) and the secret/ancestry
|
|
58
|
-
*
|
|
55
|
+
* Push the claim/release MEMORY.md change. Translate the honest outcome (the
|
|
56
|
+
* honest-CLI contract) into a command envelope. Compose it with the singleton
|
|
57
|
+
* merge discipline (the singleton merge discipline) and the secret/ancestry
|
|
58
|
+
* guards:
|
|
59
|
+
* - landed (grounded or re-applied) ⇒ `{ ok: true }` with a printed success
|
|
60
|
+
* message;
|
|
59
61
|
* - `rejected`/`transport` ⇒ `{ ok: true }` with a saved-locally warning (the
|
|
60
|
-
* landed-locally row is complete
|
|
62
|
+
* landed-locally row is complete, and the session-end push is its retry);
|
|
61
63
|
* - `precondition`/`residue-conflict`/`conservation` ⇒ `{ ok: false, code: 1 }`
|
|
62
|
-
* (D7/D9 unsafe-state family
|
|
63
|
-
* left unsafe for a later whole-tree sweep);
|
|
64
|
+
* (the D7/D9 unsafe-state family, where the row is not published and the
|
|
65
|
+
* tree may be left unsafe for a later whole-tree sweep);
|
|
64
66
|
* - a secret-gate refusal ⇒ `{ ok: false, code: 1 }` ({@link refusalEnvelope});
|
|
65
|
-
* - an {@link AncestryRefusal}
|
|
66
|
-
* not-published non-zero envelope;
|
|
67
|
+
* - this function rethrows an {@link AncestryRefusal} so `pushRowOrRefuse`
|
|
68
|
+
* maps it to the not-published non-zero envelope;
|
|
67
69
|
* - any other thrown error is a network/credential failure that degrades to
|
|
68
70
|
* "saved locally" (`{ ok: true }`).
|
|
69
71
|
*
|
|
70
72
|
* The `reapply` closure re-derives this row against the fresh tip if the
|
|
71
|
-
* landing contends, so a parallel writer's row
|
|
73
|
+
* landing contends, so this command never erases a parallel writer's row.
|
|
72
74
|
*
|
|
73
75
|
* @param {object} wikiSync - The WikiSync collaborator (may be absent in tests).
|
|
74
76
|
* @param {object} runtime - The runtime bag (for stdout/stderr).
|
|
@@ -82,37 +84,38 @@ async function pushWiki(wikiSync, runtime, message, reapply) {
|
|
|
82
84
|
let result;
|
|
83
85
|
try {
|
|
84
86
|
await wikiSync.inheritIdentity();
|
|
85
|
-
// claim/release contract is a 1-line MEMORY.md change
|
|
86
|
-
// foreign uncommitted files from parallel writers out of the commit.
|
|
87
|
-
// `reapply` closure re-derives this row against the fresh tip if the
|
|
88
|
-
// contends (the singleton merge discipline), so
|
|
87
|
+
// The claim/release contract is a 1-line MEMORY.md change. The pathspec
|
|
88
|
+
// keeps foreign uncommitted files from parallel writers out of the commit.
|
|
89
|
+
// The `reapply` closure re-derives this row against the fresh tip if the
|
|
90
|
+
// landing contends (the singleton merge discipline), so this command never
|
|
91
|
+
// erases a parallel writer's row.
|
|
89
92
|
result = await wikiSync.commitAndPush(message, ["MEMORY.md"], { reapply });
|
|
90
93
|
} catch (err) {
|
|
91
|
-
// An ancestry-guard refusal pierces the saved-locally degradation
|
|
92
|
-
// so pushRowOrRefuse maps it to the not-published non-zero envelope.
|
|
94
|
+
// An ancestry-guard refusal pierces the saved-locally degradation. Rethrow
|
|
95
|
+
// it so pushRowOrRefuse maps it to the not-published non-zero envelope.
|
|
93
96
|
if (err instanceof AncestryRefusal) throw err;
|
|
94
97
|
if (err instanceof WikiPushFailure) {
|
|
95
|
-
// D7/D9 unsafe-state family
|
|
96
|
-
// left unsafe for a later sweep
|
|
98
|
+
// D7/D9 unsafe-state family. The row is not published, and the tree may
|
|
99
|
+
// be left unsafe for a later sweep. Fail the command closed (non-zero).
|
|
97
100
|
if (UNSAFE_STATE_REASONS.has(err.reason)) {
|
|
98
101
|
runtime.proc.stderr.write(`${err.message}\n`);
|
|
99
102
|
return { ok: false, code: 1 };
|
|
100
103
|
}
|
|
101
|
-
// rejected / transport: the local row landed
|
|
104
|
+
// rejected / transport: the local row landed. Warn and keep zero exit.
|
|
102
105
|
runtime.proc.stderr.write(
|
|
103
|
-
`saved locally
|
|
106
|
+
`saved locally, not yet visible to parallel sessions (${err.reason}): ${err.message}\n`,
|
|
104
107
|
);
|
|
105
108
|
return { ok: true };
|
|
106
109
|
}
|
|
107
|
-
// Any other failure: preserve fire-and-forget "saved locally"
|
|
108
|
-
// is on disk and the command still succeeds.
|
|
110
|
+
// Any other failure: preserve fire-and-forget "saved locally". The change
|
|
111
|
+
// is on disk, and the command still succeeds.
|
|
109
112
|
createLogger("wiki", runtime).warn(
|
|
110
113
|
"claim",
|
|
111
114
|
`push failed (saved locally): ${err.message}`,
|
|
112
115
|
);
|
|
113
116
|
return { ok: true };
|
|
114
117
|
}
|
|
115
|
-
// A secret-gate refusal fails the command closed
|
|
118
|
+
// A secret-gate refusal fails the command closed. A grounded-landed or a
|
|
116
119
|
// re-applied push reports success.
|
|
117
120
|
const refusal = refusalEnvelope(runtime, result);
|
|
118
121
|
if (refusal) return refusal;
|
|
@@ -123,16 +126,17 @@ async function pushWiki(wikiSync, runtime, message, reapply) {
|
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/**
|
|
126
|
-
* Push a written claim/release row
|
|
127
|
-
* not-published non-zero envelope
|
|
128
|
-
* envelope. The row is already
|
|
129
|
+
* Push a written claim/release row. Map an ancestry-guard refusal to the
|
|
130
|
+
* not-published non-zero envelope. Map any other outcome to `pushWiki`'s
|
|
131
|
+
* envelope. The row is already in MEMORY.md. On a refusal it stays as an
|
|
129
132
|
* uncommitted working-tree change. The `reapply` closure re-derives the same
|
|
130
133
|
* row against the fresh tip when the landing contends.
|
|
131
134
|
*/
|
|
132
135
|
async function pushRowOrRefuse(wikiSync, runtime, message, reapply) {
|
|
133
136
|
try {
|
|
134
137
|
// Propagate pushWiki's envelope so a secret-gate or unsafe-state refusal
|
|
135
|
-
// ({ ok: false }) fails the command closed
|
|
138
|
+
// ({ ok: false }) fails the command closed. A clean push returns
|
|
139
|
+
// { ok: true }.
|
|
136
140
|
return await pushWiki(wikiSync, runtime, message, reapply);
|
|
137
141
|
} catch (err) {
|
|
138
142
|
if (err instanceof AncestryRefusal) {
|
|
@@ -143,14 +147,14 @@ async function pushRowOrRefuse(wikiSync, runtime, message, reapply) {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
/** Insert a row into MEMORY.md `## Active Claims`.
|
|
150
|
+
/** Insert a row into MEMORY.md `## Active Claims`. It refuses if (agent, target) is already present. */
|
|
147
151
|
export async function runClaimCommand(ctx) {
|
|
148
152
|
const { runtime, wikiSync } = ctx.deps;
|
|
149
153
|
const options = ctx.options;
|
|
150
154
|
const resolved = requireAgentFlag(options, {
|
|
151
155
|
command: "claim",
|
|
152
156
|
example:
|
|
153
|
-
"
|
|
157
|
+
"gemba-wiki claim --agent staff-engineer --target spec-NNNN --branch claude/...",
|
|
154
158
|
});
|
|
155
159
|
if (!resolved.ok) return resolved;
|
|
156
160
|
const agent = resolved.agent;
|
|
@@ -162,8 +166,9 @@ export async function runClaimCommand(ctx) {
|
|
|
162
166
|
};
|
|
163
167
|
}
|
|
164
168
|
const today = options.today || currentDayIso(runtime);
|
|
165
|
-
// Default expiry is claim+1 day
|
|
166
|
-
// this now" assertion
|
|
169
|
+
// Default expiry is claim+1 day. A claim is a short-lived "actively shipping
|
|
170
|
+
// this now" assertion. It is not a long lease. A run that outlives one day
|
|
171
|
+
// re-claims.
|
|
167
172
|
const expires = options["expires-at"] || addDays(today, 1);
|
|
168
173
|
const memPath = memoryPath(runtime, options);
|
|
169
174
|
const text = readMemory(runtime, memPath);
|
|
@@ -220,8 +225,8 @@ export async function runReleaseCommand(ctx) {
|
|
|
220
225
|
}
|
|
221
226
|
runtime.fsSync.writeFileSync(memPath, current);
|
|
222
227
|
runtime.proc.stdout.write(`released ${count} expired claim(s)\n`);
|
|
223
|
-
// Re-derive expiry against the fresh tip so a renewal landed since the
|
|
224
|
-
// read survives
|
|
228
|
+
// Re-derive expiry against the fresh tip so a renewal landed since the
|
|
229
|
+
// stale read survives. Remove only the rows that are still expired.
|
|
225
230
|
const reapply = (fresh) => {
|
|
226
231
|
const freshExpired = filterExpired(parseClaims(fresh), today).expired;
|
|
227
232
|
let next = fresh;
|
|
@@ -245,7 +250,7 @@ export async function runReleaseCommand(ctx) {
|
|
|
245
250
|
|
|
246
251
|
const resolved = requireAgentFlag(options, {
|
|
247
252
|
command: "release",
|
|
248
|
-
example: "
|
|
253
|
+
example: "gemba-wiki release --agent staff-engineer --target spec-NNNN",
|
|
249
254
|
});
|
|
250
255
|
if (!resolved.ok) return resolved;
|
|
251
256
|
const agent = resolved.agent;
|
|
@@ -265,8 +270,9 @@ export async function runReleaseCommand(ctx) {
|
|
|
265
270
|
return { ok: true };
|
|
266
271
|
}
|
|
267
272
|
runtime.proc.stdout.write(`released ${options.target}\n`);
|
|
268
|
-
// Re-apply the same removal against the fresh tip if the landing contends
|
|
269
|
-
//
|
|
273
|
+
// Re-apply the same removal against the fresh tip if the landing contends.
|
|
274
|
+
// A second removal of an absent row is a no-op, so a re-release never
|
|
275
|
+
// resurrects it.
|
|
270
276
|
const reapply = (fresh) => {
|
|
271
277
|
const r = removeClaim(fresh, { agent, target: options.target });
|
|
272
278
|
return r.removed ? r.text : null;
|