akm-cli 0.9.25-alpha.4 → 0.9.26-alpha.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/CHANGELOG.md +43 -0
- package/dist/assets/prompts/consolidate-pair-check.md +5 -0
- package/dist/assets/prompts/consolidate-pair.md +13 -14
- package/dist/commands/improve/consolidate/pair-pass.js +94 -20
- package/dist/commands/proposal/drain.js +15 -7
- package/dist/commands/proposal/proposal-types.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.26-alpha.1] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Consolidate's pair judge lists what each note alone holds before it
|
|
14
|
+
classifies the pair, and akm never retires a note the judge listed anything
|
|
15
|
+
for.** The judge also reads 12,000 characters of each note instead of 2,500.
|
|
16
|
+
On 650 reviewed pairs from the owner's library, 64% of the old judge's
|
|
17
|
+
retirements lost nothing; with the lists, 92% do, and it picks the wrong
|
|
18
|
+
note to keep far less often.
|
|
19
|
+
- **A confirmed duplicate retires unattended.** When the judge finds nothing
|
|
20
|
+
unique on either side, one more call asks only what the retired note holds
|
|
21
|
+
that the kept one lacks; an empty answer stages the proposal, and `triage`
|
|
22
|
+
`applyMode: "promote"` accepts it like a judged revision (it counts against
|
|
23
|
+
`maxAcceptsPerRun`, and a continuity risk keeps it for review). 109 of the
|
|
24
|
+
111 duplicates that passed in the reviewed pairs were safe to retire, and an
|
|
25
|
+
accepted retirement can be undone with `akm proposal revert`. Every other
|
|
26
|
+
retirement still waits for `akm proposal accept`.
|
|
27
|
+
|
|
28
|
+
## [0.9.25] - 2026-10-04
|
|
29
|
+
|
|
30
|
+
The stable release of the 0.9.25 line: 0.9.25-alpha.1 to alpha.4, unchanged. Their
|
|
31
|
+
sections below list every change. When upgrading from 0.9.24:
|
|
32
|
+
|
|
33
|
+
- **Reflect edits only an asset's `description`, `when_to_use` and title, and
|
|
34
|
+
keeps the body byte for byte;** it may answer "nothing to change" (alpha.3).
|
|
35
|
+
Its quality judge has a rubric for these revisions and tells one answering
|
|
36
|
+
negative feedback from maintenance (alpha.4). A proposal deferred as
|
|
37
|
+
`body-edit` by 0.9.24 stays deferred.
|
|
38
|
+
- **Reflect refuses defective revisions before the judge runs** (placeholder
|
|
39
|
+
text, talk about its own edit, frontmatter copied into the body), with the
|
|
40
|
+
wording lists under `processes.reflect.defectFilter` (alpha.2).
|
|
41
|
+
- **`akm improve judge`** runs reflect's quality judge on one revision, for
|
|
42
|
+
testing a judge engine (alpha.2).
|
|
43
|
+
- **akm learns from Codex sessions:** `akm proposal extract --type codex`,
|
|
44
|
+
`--auto` and `akm improve` read `$CODEX_HOME/sessions` (alpha.3).
|
|
45
|
+
- **Engines are interchangeable for model work:** one tool policy, one config
|
|
46
|
+
rule for which engines model work may use, a 600-second bound on every engine
|
|
47
|
+
kind, and `reasoningEffort` as the one effort word (alpha.1 and alpha.2).
|
|
48
|
+
- **Removed:** the `--track-usage` / `--no-track-usage` flag on `akm search`,
|
|
49
|
+
`akm curate` and `akm show` (either spelling now fails as an unknown flag),
|
|
50
|
+
and opencode model work's step limit (alpha.2).
|
|
51
|
+
|
|
9
52
|
## [0.9.25-alpha.4] - 2026-10-04
|
|
10
53
|
|
|
11
54
|
### Changed
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
You check whether deleting note X from one person's agent-memory library would lose anything. Note Y stays. List every durable claim of X that Y does not state.
|
|
2
|
+
|
|
3
|
+
A durable claim is a fact, decision, value, command, flag, path, file name, URL, number, version, name, error message, condition or rule someone would act on. Ignore wording, headings, dates of writing, tags and boilerplate. A claim is stated when Y says it in any wording or format, or replaces it with a newer value. It is not stated when Y is vaguer (a config file instead of the specific path) or drops a number, command, step or condition. Read X's description as part of X. Write each item as a short phrase that quotes its identifier. Use [] when Y states everything.
|
|
4
|
+
|
|
5
|
+
Answer ONLY with JSON: {"missing": ["..."]}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
You compare two assets from one person's agent-memory library (memories and knowledge notes an AI coding agent reads). Asset A is the OLDER one and asset B the NEWER one, by the dates shown.
|
|
1
|
+
You compare two assets from one person's agent-memory library (memories and knowledge notes an AI coding agent reads). Asset A is the OLDER one and asset B the NEWER one, by the dates shown. akm deletes one of them only when the other keeps every claim a reader would need, so your two lists decide what is safe to delete.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
First list what each asset has that the other lacks:
|
|
4
|
+
- "onlyInA": every durable claim of A that B neither states nor updates.
|
|
5
|
+
- "onlyInB": every durable claim of B that A neither states nor updates.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
A durable claim is a fact, decision, value, command, flag, path, file name, URL, number, version, name, error message, condition or rule someone would act on. Ignore wording, headings, dates of writing, tags and boilerplate. A claim is stated when the other asset says it in any wording or format. It is not stated when the other asset is vaguer (a config file instead of the specific path) or drops a number, command, step or condition. Write each item as a short phrase that quotes its identifier. Use [] when there is nothing.
|
|
8
|
+
|
|
9
|
+
Then classify the relation as exactly one of:
|
|
10
|
+
- "duplicate": both lists are empty.
|
|
11
|
+
- "subsumed": exactly one list is empty. The asset with the empty list is redundant.
|
|
12
|
+
- "supersedes": B updates, corrects, reverses or replaces a claim in A (a newer version, a changed decision, a fixed bug, a new value), and onlyInA is empty.
|
|
8
13
|
- "contradicts": they make logically exclusive claims about the same thing and nothing shows which one is current.
|
|
9
|
-
- "overlap": same subject,
|
|
14
|
+
- "overlap": same subject, and both lists have items.
|
|
10
15
|
- "unrelated": different subjects or different facts that happen to share words.
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
- A durable claim is a fact, decision, value, command, path, number or rule someone would act on. Ignore dates, headings, tags and phrasing.
|
|
14
|
-
- Prefer "overlap" over "duplicate" when either asset has a specific detail (a number, command, file, version or condition) that the other lacks.
|
|
15
|
-
- "supersedes" needs a specific claim in A that B changes. Being newer or longer is not enough.
|
|
16
|
-
- "contradicts" needs two claims that cannot both be true. Different scope, project or time is not a contradiction.
|
|
17
|
-
|
|
18
|
-
Set "redundant" to "A" or "B" when that asset could be removed with no loss (only for "duplicate" or "subsumed"; for "duplicate" name the less complete or older one), else null. Set "stale" to "A" when the relation is "supersedes", else null.
|
|
17
|
+
Set "redundant" to the asset that could be deleted with no loss: "A" for "duplicate", the asset with the empty list for "subsumed", "A" for "supersedes"; else null. Set "stale" to "A" when the relation is "supersedes", else null.
|
|
19
18
|
|
|
20
|
-
Answer ONLY with JSON: {"relation": "...", "redundant": "A"|"B"|null, "stale": "A"|null, "confidence": 0.0-1.0, "reason": "<at most 25 words>"}
|
|
19
|
+
Answer ONLY with JSON: {"onlyInA": ["..."], "onlyInB": ["..."], "relation": "...", "redundant": "A"|"B"|null, "stale": "A"|null, "confidence": 0.0-1.0, "reason": "<at most 25 words>"}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import fs from "node:fs";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import consolidatePairPrompt from "../../../assets/prompts/consolidate-pair.md" with { type: "text" };
|
|
32
|
+
import consolidatePairCheckPrompt from "../../../assets/prompts/consolidate-pair-check.md" with { type: "text" };
|
|
32
33
|
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
33
34
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
34
35
|
import { asNonEmptyString } from "../../../core/common.js";
|
|
@@ -42,8 +43,8 @@ import { runGit } from "../../../sources/providers/git-install.js";
|
|
|
42
43
|
import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../../storage/repositories/index-connection.js";
|
|
43
44
|
import { getAllEntries, getEntryById } from "../../../storage/repositories/index-entries-repository.js";
|
|
44
45
|
import { getNeighborsByEntryId } from "../../../storage/repositories/index-vec-repository.js";
|
|
45
|
-
import { isRetireProposal } from "../../proposal/proposal-types.js";
|
|
46
|
-
import { createRetireProposal, listProposalsReadOnly } from "../../proposal/repository.js";
|
|
46
|
+
import { isRetireProposal, PAIR_PASS_GATE, } from "../../proposal/proposal-types.js";
|
|
47
|
+
import { createRetireProposal, listProposalsReadOnly, proposalContentHash, recordGateDecision, } from "../../proposal/repository.js";
|
|
47
48
|
import { isHotCapturedMemory } from "../consolidate.js";
|
|
48
49
|
import { contentHash, stripFrontmatterBody } from "../content-hash.js";
|
|
49
50
|
import { loadLedgerSnapshot, PAIR_PASS_LEDGER_SOURCE, recordLedgerAttempt, stripBundle } from "../ledger.js";
|
|
@@ -67,8 +68,8 @@ export const BACKFILL_FLOOR = 0.95;
|
|
|
67
68
|
export const NEW_MATERIAL_DAYS = 7;
|
|
68
69
|
/** Pairs judged per run, highest cosine first (plan §7's nightly cost budget). */
|
|
69
70
|
export const MAX_PAIRS_PER_RUN = 300;
|
|
70
|
-
/** Body characters sent to the judge per side
|
|
71
|
-
const PAIR_BODY_TRUNCATE_CHARS =
|
|
71
|
+
/** Body characters sent to the judge per side: enough for nearly every note, so a retirement is judged on the whole text. */
|
|
72
|
+
const PAIR_BODY_TRUNCATE_CHARS = 12_000;
|
|
72
73
|
const MS_PER_DAY = 86_400_000;
|
|
73
74
|
const RELATION_LABELS = ["duplicate", "subsumed", "supersedes", "contradicts", "overlap", "unrelated"];
|
|
74
75
|
const RETIRE_LABELS = new Set(["duplicate", "subsumed", "supersedes"]);
|
|
@@ -76,11 +77,14 @@ const RETIRE_LABELS = new Set(["duplicate", "subsumed", "supersedes"]);
|
|
|
76
77
|
function isRetireLabel(label) {
|
|
77
78
|
return RETIRE_LABELS.has(label);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
/** Exported, with {@link buildPairUserPrompt}, so a replay can drive the exact judge call. */
|
|
81
|
+
export const PAIR_JUDGE_JSON_SCHEMA = {
|
|
80
82
|
type: "object",
|
|
81
|
-
required: ["relation", "redundant", "stale", "confidence", "reason"],
|
|
83
|
+
required: ["onlyInA", "onlyInB", "relation", "redundant", "stale", "confidence", "reason"],
|
|
82
84
|
additionalProperties: false,
|
|
83
85
|
properties: {
|
|
86
|
+
onlyInA: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 },
|
|
87
|
+
onlyInB: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 },
|
|
84
88
|
relation: { type: "string", enum: [...RELATION_LABELS] },
|
|
85
89
|
redundant: { type: ["string", "null"], enum: ["A", "B", null] },
|
|
86
90
|
stale: { type: ["string", "null"], enum: ["A", null] },
|
|
@@ -88,6 +92,12 @@ const PAIR_JUDGE_JSON_SCHEMA = {
|
|
|
88
92
|
reason: { type: "string", maxLength: 400 },
|
|
89
93
|
},
|
|
90
94
|
};
|
|
95
|
+
const PAIR_CHECK_JSON_SCHEMA = {
|
|
96
|
+
type: "object",
|
|
97
|
+
required: ["missing"],
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
properties: { missing: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 20 } },
|
|
100
|
+
};
|
|
91
101
|
/** Hand-validates the judge's JSON, independent of whatever the provider's own schema enforcement did. */
|
|
92
102
|
export function parsePairJudgeResponse(raw) {
|
|
93
103
|
const parsed = parseEmbeddedJsonResponse(raw);
|
|
@@ -103,7 +113,16 @@ export function parsePairJudgeResponse(raw) {
|
|
|
103
113
|
return undefined;
|
|
104
114
|
const confidence = Math.max(0, Math.min(1, parsed.confidence));
|
|
105
115
|
const reason = typeof parsed.reason === "string" ? parsed.reason : "";
|
|
106
|
-
|
|
116
|
+
const onlyInA = claimList(parsed.onlyInA);
|
|
117
|
+
const onlyInB = claimList(parsed.onlyInB);
|
|
118
|
+
if (!onlyInA || !onlyInB)
|
|
119
|
+
return undefined;
|
|
120
|
+
return { onlyInA, onlyInB, relation: parsed.relation, redundant, confidence, reason };
|
|
121
|
+
}
|
|
122
|
+
function claimList(value) {
|
|
123
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string"))
|
|
124
|
+
return undefined;
|
|
125
|
+
return value.map((v) => v.trim()).filter(Boolean);
|
|
107
126
|
}
|
|
108
127
|
function isFlatName(name) {
|
|
109
128
|
return !name.includes("/");
|
|
@@ -445,7 +464,7 @@ function orderByAge(x, y) {
|
|
|
445
464
|
return xIsOlder ? { older: x, newer: y } : { older: y, newer: x };
|
|
446
465
|
}
|
|
447
466
|
/** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
|
|
448
|
-
function buildPairUserPrompt(older, newer) {
|
|
467
|
+
export function buildPairUserPrompt(older, newer) {
|
|
449
468
|
return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
|
|
450
469
|
}
|
|
451
470
|
/** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
|
|
@@ -456,19 +475,59 @@ export function tombstoneReason(label) {
|
|
|
456
475
|
* The calibrated outcome table (owner grades, replacing plan §5.2's
|
|
457
476
|
* "shorter body" rule): `duplicate`/`supersedes` keep the newer copy;
|
|
458
477
|
* `subsumed` keeps the side the judge did NOT name `redundant` (no proposal
|
|
459
|
-
* if that pointer is missing or invalid).
|
|
478
|
+
* if that pointer is missing or invalid). A side is retired only when the
|
|
479
|
+
* judge listed nothing that it alone holds.
|
|
460
480
|
*/
|
|
461
|
-
export function decideRetirement(label, redundant, older, newer) {
|
|
481
|
+
export function decideRetirement(label, redundant, older, newer, only) {
|
|
482
|
+
let decision;
|
|
462
483
|
if (label === "duplicate" || label === "supersedes")
|
|
463
|
-
|
|
464
|
-
if (label === "subsumed")
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return undefined;
|
|
470
|
-
|
|
471
|
-
|
|
484
|
+
decision = { retired: older, successor: newer };
|
|
485
|
+
else if (label === "subsumed" && redundant === "A")
|
|
486
|
+
decision = { retired: older, successor: newer };
|
|
487
|
+
else if (label === "subsumed" && redundant === "B")
|
|
488
|
+
decision = { retired: newer, successor: older };
|
|
489
|
+
if (!decision)
|
|
490
|
+
return undefined;
|
|
491
|
+
return (decision.retired === older ? only.onlyInA : only.onlyInB).length === 0 ? decision : undefined;
|
|
492
|
+
}
|
|
493
|
+
function checkSection(label, side) {
|
|
494
|
+
return [
|
|
495
|
+
`Note ${label}:`,
|
|
496
|
+
`Ref: ${side.asset.ref}`,
|
|
497
|
+
`Description: ${asNonEmptyString(side.frontmatter.description) ?? "(none)"}`,
|
|
498
|
+
"Content:",
|
|
499
|
+
"```",
|
|
500
|
+
stripFrontmatterBody(side.raw).slice(0, PAIR_BODY_TRUNCATE_CHARS),
|
|
501
|
+
"```",
|
|
502
|
+
"",
|
|
503
|
+
].join("\n");
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The second look a duplicate gets before it may retire unattended: one call
|
|
507
|
+
* that asks only what the retired note holds that the kept one lacks. True
|
|
508
|
+
* only on a clean, empty answer (it caught 2 of 4 duplicates the judge got
|
|
509
|
+
* wrong, and held back none of 109 right ones).
|
|
510
|
+
*/
|
|
511
|
+
async function confirmNothingLost(ctx, retired, successor) {
|
|
512
|
+
const outcome = await callStage({
|
|
513
|
+
feature: "memory_consolidation",
|
|
514
|
+
runner: ctx.llmRunner,
|
|
515
|
+
system: consolidatePairCheckPrompt,
|
|
516
|
+
prompt: `${checkSection("X (to delete)", retired)}\n${checkSection("Y (kept)", successor)}`,
|
|
517
|
+
gate: { config: ctx.config, enabled: true },
|
|
518
|
+
request: {
|
|
519
|
+
responseSchema: PAIR_CHECK_JSON_SCHEMA,
|
|
520
|
+
enableThinking: false,
|
|
521
|
+
...(Object.hasOwn(ctx.llmRunner, "timeoutMs") ? { timeoutMs: ctx.llmRunner.timeoutMs } : {}),
|
|
522
|
+
signal: ctx.opts.signal,
|
|
523
|
+
...(ctx.chat ? { chat: ctx.chat } : {}),
|
|
524
|
+
},
|
|
525
|
+
parse: (raw) => claimList(parseEmbeddedJsonResponse(raw)?.missing),
|
|
526
|
+
...(ctx.opts.onNotices ? { onNotices: ctx.opts.onNotices } : {}),
|
|
527
|
+
});
|
|
528
|
+
if (!outcome.ok)
|
|
529
|
+
return false;
|
|
530
|
+
return claimList(parseEmbeddedJsonResponse(outcome.raw)?.missing)?.length === 0;
|
|
472
531
|
}
|
|
473
532
|
/**
|
|
474
533
|
* One pair: judge it, then (for a retire class) apply the guards and mint
|
|
@@ -519,7 +578,7 @@ async function judgeOne(ctx, candidate) {
|
|
|
519
578
|
return { failed: false }; // counted; stays human — no proposal, no belief write
|
|
520
579
|
if (!isRetireLabel(verdict.relation))
|
|
521
580
|
return { failed: false }; // overlap / unrelated: judged_no_action
|
|
522
|
-
const decision = decideRetirement(verdict.relation, verdict.redundant, older, newer);
|
|
581
|
+
const decision = decideRetirement(verdict.relation, verdict.redundant, older, newer, verdict);
|
|
523
582
|
if (!decision)
|
|
524
583
|
return { failed: false };
|
|
525
584
|
const { retired, successor } = decision;
|
|
@@ -599,6 +658,21 @@ async function judgeOne(ctx, candidate) {
|
|
|
599
658
|
}, ctx.opts.proposalsCtx);
|
|
600
659
|
ctx.retired.push(proposal.id);
|
|
601
660
|
ctx.perInitiatorProposed.add(candidate.initiator.ref);
|
|
661
|
+
// A duplicate with nothing unique on either side, confirmed by a second
|
|
662
|
+
// look, is the one class that retires unattended (109 of 111 safe on the
|
|
663
|
+
// owner's reviewed pairs, 2026-10-04): the triage drain accepts it under
|
|
664
|
+
// its usual applyMode. Every other retirement waits for a person.
|
|
665
|
+
if (verdict.relation === "duplicate" &&
|
|
666
|
+
verdict.onlyInA.length + verdict.onlyInB.length === 0 &&
|
|
667
|
+
!continuityRisk &&
|
|
668
|
+
(await confirmNothingLost(ctx, retired, successor))) {
|
|
669
|
+
recordGateDecision(ctx.stashDir, proposal.id, {
|
|
670
|
+
outcome: "staged",
|
|
671
|
+
reason: "duplicate",
|
|
672
|
+
gate: PAIR_PASS_GATE,
|
|
673
|
+
contentHash: proposalContentHash(proposal),
|
|
674
|
+
}, ctx.opts.proposalsCtx);
|
|
675
|
+
}
|
|
602
676
|
return { failed: false };
|
|
603
677
|
}
|
|
604
678
|
catch (error) {
|
|
@@ -30,7 +30,7 @@ import { buildExecution, resolveExecution } from "../../integrations/agent/execu
|
|
|
30
30
|
import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
|
|
31
31
|
import { errMessage, noticeSet } from "../improve/stage.js";
|
|
32
32
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
33
|
-
import { isRetireProposal, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
33
|
+
import { isRetireProposal, PAIR_PASS_GATE, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
34
34
|
import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, proposalContentHash, readFreshProposalTarget, recordGateDecision, } from "./repository.js";
|
|
35
35
|
/** The gate label on every decision the drain records. */
|
|
36
36
|
const DRAIN_GATE = "triage";
|
|
@@ -313,13 +313,21 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
313
313
|
const accepts = [];
|
|
314
314
|
const empties = [];
|
|
315
315
|
for (const proposal of pending) {
|
|
316
|
-
// A consolidate pair-pass `retire` proposal is
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// Checked before isEmptyDiff, which
|
|
320
|
-
// nothing meaningful to read on a delete-primary change
|
|
321
|
-
if (isRetireProposal(proposal))
|
|
316
|
+
// A consolidate pair-pass `retire` proposal is auto-accepted only when the
|
|
317
|
+
// pair judge staged it as a duplicate with nothing unique on either side
|
|
318
|
+
// (spec §25.9, equivalent content); every other one waits for a direct
|
|
319
|
+
// `akm proposal accept` (spec §25.6). Checked before isEmptyDiff, which
|
|
320
|
+
// has nothing meaningful to read on a delete-primary change.
|
|
321
|
+
if (isRetireProposal(proposal)) {
|
|
322
|
+
const staged = proposal.gateDecision;
|
|
323
|
+
if (staged?.outcome === "staged" &&
|
|
324
|
+
staged.gate === PAIR_PASS_GATE &&
|
|
325
|
+
staged.contentHash === proposalContentHash(proposal) &&
|
|
326
|
+
!proposal.retirement?.continuityRisk) {
|
|
327
|
+
accepts.push({ id: proposal.id, reason: "duplicate" });
|
|
328
|
+
}
|
|
322
329
|
continue;
|
|
330
|
+
}
|
|
323
331
|
const decision = proposal.gateDecision;
|
|
324
332
|
// Another gate's rejection stands, and another gate's deferral is a
|
|
325
333
|
// generating stage's hand-off to a person: it is left for that person.
|
|
@@ -53,6 +53,8 @@ export function isRetireProposal(proposal) {
|
|
|
53
53
|
}
|
|
54
54
|
/** A promote refused because the target changed after mint (STALE, R20) — not a merit judgement. */
|
|
55
55
|
export const STALE_TARGET_GATE_REASON = "stale-target";
|
|
56
|
+
/** The gate on a retire proposal the triage drain may accept unattended: a pair-judged duplicate. */
|
|
57
|
+
export const PAIR_PASS_GATE = "consolidate-pair";
|
|
56
58
|
export const EXPIRED_GATE_REASON = "expired";
|
|
57
59
|
export const ASSET_MISSING_GATE_REASON = "asset-missing";
|
|
58
60
|
const PROCEDURAL_GATE_REASONS = new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.26-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|