akm-cli 0.10.261003-beta → 0.10.261005-beta
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 +29 -0
- package/dist/commands/feedback-cli.js +19 -11
- package/dist/commands/improve/improve.js +17 -4
- package/dist/commands/proposal/drain.js +13 -1
- package/dist/commands/proposal/proposal-cli.js +23 -6
- package/dist/commands/proposal/proposal.js +58 -11
- package/dist/commands/proposal/repository.js +33 -2
- package/dist/core/errors.js +1 -0
- package/dist/core/write-source.js +9 -5
- package/dist/indexer/usage/usage-events.js +6 -2
- package/dist/scripts/akm-migrate-node.js +1 -0
- package/dist/scripts/akm-migrate.js +1 -0
- package/dist/sources/providers/git-stash.js +46 -15
- package/docs/architecture/0.10-process-rules.md +1 -1
- package/docs/architecture/improvement.md +7 -4
- package/docs/reference/cli.md +13 -3
- package/docs/reference/data-and-telemetry.md +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.261005] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **A process that accepts several proposals commits once, not once per proposal.** `akm proposal drain`,
|
|
14
|
+
`akm proposal accept --generator` and the triage drain inside `akm improve` used to make one commit (and, on a git-kind
|
|
15
|
+
bundle, one push) per accepted proposal. They now commit once when they finish, with a subject such as
|
|
16
|
+
`akm proposal drain: 37 accepted (consolidate-pair 30, consolidate 7)` and one `<id8> <source> <ref>` body line per
|
|
17
|
+
proposal. In `akm improve`, triage accepts join the run's end-of-run batch commit. A single `akm proposal accept <id>`
|
|
18
|
+
is still one commit, and `akm proposal accept <id> <id> ...` now accepts several ids in one process and commits them
|
|
19
|
+
once; an id that fails is reported in the batch result (exit 1) and the others still land.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`akm sync` pushes commits that were made earlier.** With a clean working tree it fetched and merged the upstream
|
|
24
|
+
but never pushed, so a commit made outside `akm sync` (an `akm proposal accept` on a filesystem bundle, an improve
|
|
25
|
+
run killed before its end-of-run sync, a manual commit) stayed ahead of the remote and `akm sync` reported
|
|
26
|
+
`pushed: false`. It now pushes those commits when the bundle is writable, has an upstream and push is allowed, and
|
|
27
|
+
reports `pushed: true`. `--no-push`, read-only bundles and branches without an upstream are unchanged.
|
|
28
|
+
|
|
29
|
+
## [0.10.261004] - 2026-10-13
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`akm feedback` fails when it cannot record the feedback** (also in 0.9.32). When `state.db` was read-only to the
|
|
34
|
+
process, as it is inside an agent sandbox that only allows writes in the workspace, the feedback row was silently
|
|
35
|
+
dropped and the command still printed `"ok": true`. It now fails with a `DATA_DIR_NOT_WRITABLE` config error (exit
|
|
36
|
+
78) and records nothing. Add akm's data directory to the sandbox's writable paths.
|
|
37
|
+
|
|
9
38
|
## [0.10.261003] - 2026-10-12
|
|
10
39
|
|
|
11
40
|
### Fixed
|
|
@@ -13,17 +13,17 @@ import { parseFrontmatter, parseFrontmatterBlock, spliceFrontmatterLine } from "
|
|
|
13
13
|
import { conceptIdFromTypeName, parseRefInput, typeNameFromConceptId } from "../core/asset/resolve-ref.js";
|
|
14
14
|
import { isWithin, resolveStashDir, safeRealpath, writeFileAtomic } from "../core/common.js";
|
|
15
15
|
import { loadConfig } from "../core/config/config.js";
|
|
16
|
-
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
16
|
+
import { ConfigError, NotFoundError, UsageError } from "../core/errors.js";
|
|
17
17
|
import { appendEvent } from "../core/events.js";
|
|
18
18
|
import { resolveMutationTarget } from "../core/mutation-target.js";
|
|
19
19
|
import { isPathAbsent } from "../core/path-access.js";
|
|
20
20
|
import { getDbPath } from "../core/paths.js";
|
|
21
|
-
import { withStateDb } from "../core/state-db.js";
|
|
21
|
+
import { getStateDbPath, withStateDb } from "../core/state-db.js";
|
|
22
22
|
import { warn } from "../core/warn.js";
|
|
23
23
|
import { withWriteTargetMutation } from "../core/write-source.js";
|
|
24
24
|
import { recomputeUtilityScores } from "../indexer/indexer.js";
|
|
25
25
|
import { resolveSourceEntries } from "../indexer/search/search-source.js";
|
|
26
|
-
import {
|
|
26
|
+
import { insertUsageEventStrict, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
|
|
27
27
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
28
28
|
import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
29
29
|
import { findEntryIdByRef, getEntryFilePathById, getItemRefById, } from "../storage/repositories/index-entries-repository.js";
|
|
@@ -161,20 +161,28 @@ const UTILITY_REVIEW_THRESHOLD = 0.5;
|
|
|
161
161
|
* boosting its own picks. This is by design and must not be removed. The
|
|
162
162
|
* utility score is read by improve and metrics only; its update is best-effort
|
|
163
163
|
* and its failure never fails the feedback record or changes the ranking effect.
|
|
164
|
+
* A failed usage-event write fails the command (DATA_DIR_NOT_WRITABLE).
|
|
164
165
|
*/
|
|
165
166
|
function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadataStr) {
|
|
166
167
|
let utilityResult;
|
|
167
168
|
let rankingSkippedReason;
|
|
168
169
|
const eventSource = resolveUsageEventSource();
|
|
169
170
|
withStateDb((stateDb) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
// The usage event IS the feedback record, so a failed write fails the command
|
|
172
|
+
// instead of reporting ok while nothing was stored.
|
|
173
|
+
try {
|
|
174
|
+
insertUsageEventStrict(stateDb, {
|
|
175
|
+
event_type: "feedback",
|
|
176
|
+
entry_ref: durableEntryRef,
|
|
177
|
+
entry_id: entryId,
|
|
178
|
+
signal,
|
|
179
|
+
metadata: metadataStr,
|
|
180
|
+
source: eventSource,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
throw new ConfigError(`Feedback was not recorded: could not write ${getStateDbPath()} (${err instanceof Error ? err.message : String(err)}).`, "DATA_DIR_NOT_WRITABLE");
|
|
185
|
+
}
|
|
178
186
|
if (eventSource !== "user") {
|
|
179
187
|
rankingSkippedReason =
|
|
180
188
|
`feedback source is "${eventSource}", not "user" — it does not count toward search ranking, which counts ` +
|
|
@@ -34,6 +34,7 @@ import { getEntryCount } from "../../storage/repositories/index-entries-reposito
|
|
|
34
34
|
import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
|
|
35
35
|
import { summarizeLlmUsageRecordsCrossTab } from "../health/llm-usage.js";
|
|
36
36
|
import { drainProposals } from "../proposal/drain.js";
|
|
37
|
+
import { commitCollectedAccepts } from "../proposal/repository.js";
|
|
37
38
|
import { describeGatedLanes, isAutonomyLaneAllowed } from "./autonomy-gate.js";
|
|
38
39
|
import { akmDistill } from "./distill.js";
|
|
39
40
|
import { collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
|
|
@@ -695,9 +696,13 @@ async function runTriagePrePass(run) {
|
|
|
695
696
|
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
|
|
696
697
|
return undefined;
|
|
697
698
|
}
|
|
699
|
+
// Triage accepts are journaled, so the end-of-run batch commit covers them; the drain does not commit
|
|
700
|
+
// them one by one (#1185). Without that batch (--no-sync, no git repository) they are committed here.
|
|
701
|
+
const commitCollector = [];
|
|
698
702
|
try {
|
|
699
703
|
const triageConfig = improveProfile.processes?.triage;
|
|
700
704
|
return await withLlmStage("triage", () => run.drainProposalsFn({
|
|
705
|
+
commitCollector,
|
|
701
706
|
stashDir: primaryStashDir,
|
|
702
707
|
...(options.target ? { target: options.target } : {}),
|
|
703
708
|
config: options.config,
|
|
@@ -712,6 +717,10 @@ async function runTriagePrePass(run) {
|
|
|
712
717
|
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
|
|
713
718
|
return undefined;
|
|
714
719
|
}
|
|
720
|
+
finally {
|
|
721
|
+
if (!syncsStash(run))
|
|
722
|
+
commitCollectedAccepts(commitCollector, "akm improve triage");
|
|
723
|
+
}
|
|
715
724
|
}
|
|
716
725
|
/**
|
|
717
726
|
* The exact repo-relative paths the auto-sync commit stages (#652): the paths
|
|
@@ -740,6 +749,11 @@ export function resolveSyncPathSet(input) {
|
|
|
740
749
|
unattributed: changedPaths.filter((p) => !attributed.has(p) && !initialPaths.has(p) && inScope(p)).sort(),
|
|
741
750
|
};
|
|
742
751
|
}
|
|
752
|
+
/** True when the run's end-of-run sync will commit what it wrote: sync is on and the write target is a git repository. */
|
|
753
|
+
function syncsStash(run) {
|
|
754
|
+
const repoDir = run.writeTarget?.source.repoPath ?? run.primaryStashDir;
|
|
755
|
+
return Boolean(run.primaryStashDir && repoDir && run.effectiveSync.enabled !== false && isGitBackedStash(repoDir));
|
|
756
|
+
}
|
|
743
757
|
/**
|
|
744
758
|
* One `saveGitStash` call for the run's write-target bundle, shared by the
|
|
745
759
|
* pre-run sync and the auto-sync commit: never throws, records a `stash_synced`
|
|
@@ -749,11 +763,10 @@ export function resolveSyncPathSet(input) {
|
|
|
749
763
|
* `akm sync` stages (the managed paths).
|
|
750
764
|
*/
|
|
751
765
|
function runStashSync(run, eventsCtx, phase, input) {
|
|
752
|
-
const { writeTarget, primaryStashDir,
|
|
766
|
+
const { writeTarget, primaryStashDir, options, config, improveProfile } = run;
|
|
753
767
|
const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
|
|
754
|
-
if (!primaryStashDir || !repoDir ||
|
|
768
|
+
if (!primaryStashDir || !repoDir || !syncsStash(run))
|
|
755
769
|
return undefined;
|
|
756
|
-
}
|
|
757
770
|
const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
|
|
758
771
|
const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
|
|
759
772
|
const push = input.push ?? options.sync?.push ?? improveProfile.sync?.push ?? true;
|
|
@@ -786,7 +799,7 @@ function runStashSync(run, eventsCtx, phase, input) {
|
|
|
786
799
|
const reason = errMessage(syncErr);
|
|
787
800
|
warn(`improve: stash sync failed (non-fatal): ${reason}`);
|
|
788
801
|
// A push that failed after the commit landed still left the commit in the repo.
|
|
789
|
-
const committed = syncErr instanceof GitStashPushError;
|
|
802
|
+
const committed = syncErr instanceof GitStashPushError && syncErr.committedNow;
|
|
790
803
|
record({ committed, pushed: false, skipped: true, reason });
|
|
791
804
|
return { committed, pushed: false, skipped: true, reason };
|
|
792
805
|
}
|
|
@@ -41,7 +41,7 @@ import { NEIGHBOUR_EXCERPT_CHARS, NEIGHBOUR_NOTE_COUNT, nearestKnowledgeNotes, s
|
|
|
41
41
|
import { errMessage, noticeSet } from "../improve/stage.js";
|
|
42
42
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
43
43
|
import { isRetireProposal, PAIR_PASS_GATE, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
44
|
-
import { listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, proposalContentHash, readFreshProposalTarget, recordGateDecision, } from "./repository.js";
|
|
44
|
+
import { commitCollectedAccepts, listProposals, listProposalsReadOnly, preflightProposalPromotion, proposalContent, proposalContentHash, readFreshProposalTarget, recordGateDecision, } from "./repository.js";
|
|
45
45
|
/** The gate label on every decision the drain records. */
|
|
46
46
|
const DRAIN_GATE = "triage";
|
|
47
47
|
/** A consolidate promotion: a memory proposed as a new knowledge note. The only kind the judgment tier may accept. */
|
|
@@ -89,6 +89,7 @@ async function acceptProposal(opts, proposal, id, reason, promoteFn, rejectFn, j
|
|
|
89
89
|
id,
|
|
90
90
|
...(opts.target ? { target: opts.target } : {}),
|
|
91
91
|
...(opts.config ? { config: opts.config } : {}),
|
|
92
|
+
...(opts.commitCollector ? { commitCollector: opts.commitCollector } : {}),
|
|
92
93
|
gateDecision,
|
|
93
94
|
});
|
|
94
95
|
}
|
|
@@ -416,6 +417,17 @@ function readLiveAssetContent(stashDir, ref) {
|
|
|
416
417
|
* test seams.
|
|
417
418
|
*/
|
|
418
419
|
export async function drainProposals(opts, promoteFn = akmProposalAccept, rejectFn = akmProposalReject, judgmentSeams = {}) {
|
|
420
|
+
if (opts.commitCollector)
|
|
421
|
+
return drainPending(opts, promoteFn, rejectFn, judgmentSeams);
|
|
422
|
+
const commitCollector = [];
|
|
423
|
+
try {
|
|
424
|
+
return await drainPending({ ...opts, commitCollector }, promoteFn, rejectFn, judgmentSeams);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
commitCollectedAccepts(commitCollector, "akm proposal drain");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function drainPending(opts, promoteFn, rejectFn, judgmentSeams) {
|
|
419
431
|
const exclude = opts.excludeIds ?? new Set();
|
|
420
432
|
// A judgment runner's credentials are validated before any live state
|
|
421
433
|
// connection, so its classification reads an isolated snapshot.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* registration remains for either spelling.
|
|
15
15
|
*/
|
|
16
16
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
17
|
-
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
17
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../../cli/shared.js";
|
|
18
18
|
import { resolveStashDir } from "../../core/common.js";
|
|
19
19
|
import { loadConfig } from "../../core/config/config.js";
|
|
20
20
|
import { UsageError } from "../../core/errors.js";
|
|
@@ -24,7 +24,7 @@ import { resolveImproveExecution } from "../improve/execution.js";
|
|
|
24
24
|
import { extractCommand } from "../improve/extract-cli.js";
|
|
25
25
|
import { resolveImproveStrategy } from "../improve/improve-strategies.js";
|
|
26
26
|
import { drainProposals } from "./drain.js";
|
|
27
|
-
import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalReopen, akmProposalRevert, akmProposalShow, bulkAdjudicateProposals, } from "./proposal.js";
|
|
27
|
+
import { akmProposalAccept, akmProposalAcceptMany, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalReopen, akmProposalRevert, akmProposalShow, bulkAdjudicateProposals, } from "./proposal.js";
|
|
28
28
|
import { proposalWaitingSince } from "./proposal-types.js";
|
|
29
29
|
import { proposeCommand } from "./propose-cli.js";
|
|
30
30
|
export function mergeProposalDrainNotices(resolutionNotices, dispatchNotices) {
|
|
@@ -94,11 +94,14 @@ const proposalListCommand = defineJsonCommand({
|
|
|
94
94
|
},
|
|
95
95
|
});
|
|
96
96
|
const proposalAcceptCommand = defineJsonCommand({
|
|
97
|
-
meta: {
|
|
97
|
+
meta: {
|
|
98
|
+
name: "accept",
|
|
99
|
+
description: "Accept proposals and promote them into the bundle. Several ids are accepted in one process and committed once.",
|
|
100
|
+
},
|
|
98
101
|
args: {
|
|
99
102
|
id: {
|
|
100
103
|
type: "positional",
|
|
101
|
-
description: "Proposal id (uuid / prefix) or asset ref (e.g. skills/akm-dream). Optional when --generator is provided.",
|
|
104
|
+
description: "Proposal id (uuid / prefix) or asset ref (e.g. skills/akm-dream); repeat it to accept several at once. Optional when --generator is provided.",
|
|
102
105
|
required: false,
|
|
103
106
|
},
|
|
104
107
|
queue: { type: "string", description: "Select the proposal queue by source name" },
|
|
@@ -161,10 +164,24 @@ const proposalAcceptCommand = defineJsonCommand({
|
|
|
161
164
|
return;
|
|
162
165
|
}
|
|
163
166
|
if (!args.id) {
|
|
164
|
-
throw new UsageError("Usage: akm proposal accept <id> OR akm proposal accept --generator <generator>", "MISSING_REQUIRED_ARGUMENT");
|
|
167
|
+
throw new UsageError("Usage: akm proposal accept <id> [<id> ...] OR akm proposal accept --generator <generator>", "MISSING_REQUIRED_ARGUMENT");
|
|
168
|
+
}
|
|
169
|
+
// citty keeps every positional token in `_`, though it declares only `id`.
|
|
170
|
+
const ids = (Array.isArray(args._) && args._.length > 0 ? args._ : [args.id]).map(String);
|
|
171
|
+
if (ids.length > 1) {
|
|
172
|
+
const results = await akmProposalAcceptMany({
|
|
173
|
+
ids,
|
|
174
|
+
queue: args.queue,
|
|
175
|
+
target: args.bundle,
|
|
176
|
+
});
|
|
177
|
+
const failed = results.filter((result) => !result.ok).length;
|
|
178
|
+
output("proposal-accept-batch", { accepted: results.length - failed, failed, results });
|
|
179
|
+
if (failed > 0)
|
|
180
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
181
|
+
return;
|
|
165
182
|
}
|
|
166
183
|
const result = await akmProposalAccept({
|
|
167
|
-
id:
|
|
184
|
+
id: ids[0],
|
|
168
185
|
queue: args.queue,
|
|
169
186
|
target: args.bundle,
|
|
170
187
|
});
|
|
@@ -17,7 +17,7 @@ import { NotFoundError } from "../../core/errors.js";
|
|
|
17
17
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
18
18
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
19
19
|
import { isRetireProposal, proposalWaitingSince } from "./proposal-types.js";
|
|
20
|
-
import { diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, rejectProposalDurably, reopenProposals, resolveProposalId, revertProposal, } from "./repository.js";
|
|
20
|
+
import { commitCollectedAccepts, diffProposal, listProposals, preflightProposalPromotion, promoteProposal, proposalContent, rejectProposalDurably, reopenProposals, resolveProposalId, revertProposal, } from "./repository.js";
|
|
21
21
|
import { validateProposal } from "./validators/proposals.js";
|
|
22
22
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
23
23
|
function resolveStash(stashDir) {
|
|
@@ -95,7 +95,12 @@ export async function akmProposalAccept(options) {
|
|
|
95
95
|
const queue = resolveProposalQueue(options.stashDir, options.queue, config);
|
|
96
96
|
const stash = queue.stashDir;
|
|
97
97
|
const resolvedId = resolveProposalId(stash, options.id).id;
|
|
98
|
-
const result = await promoteProposal(stash, config, resolvedId, {
|
|
98
|
+
const result = await promoteProposal(stash, config, resolvedId, {
|
|
99
|
+
target: options.target,
|
|
100
|
+
queueTarget: queue.target,
|
|
101
|
+
gateDecision: options.gateDecision,
|
|
102
|
+
...(options.commitCollector ? { commitCollector: options.commitCollector } : {}),
|
|
103
|
+
}, options.ctx);
|
|
99
104
|
return {
|
|
100
105
|
schemaVersion: 1,
|
|
101
106
|
ok: true,
|
|
@@ -105,6 +110,35 @@ export async function akmProposalAccept(options) {
|
|
|
105
110
|
proposal: result.proposal,
|
|
106
111
|
};
|
|
107
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Accept several proposals in one process and commit them once. An id that
|
|
115
|
+
* fails is reported and the rest carry on; what was accepted is committed
|
|
116
|
+
* either way.
|
|
117
|
+
*/
|
|
118
|
+
export async function akmProposalAcceptMany(options) {
|
|
119
|
+
const { ids, ...rest } = options;
|
|
120
|
+
const commitCollector = [];
|
|
121
|
+
const results = [];
|
|
122
|
+
try {
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
try {
|
|
125
|
+
results.push(await akmProposalAccept({ ...rest, id, commitCollector }));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
results.push({
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
ok: false,
|
|
131
|
+
id,
|
|
132
|
+
error: error instanceof Error ? error.message : String(error),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
commitCollectedAccepts(commitCollector, "akm proposal accept");
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
108
142
|
export async function akmProposalReject(options) {
|
|
109
143
|
return withAssetMutationLease("proposal-reject", async () => {
|
|
110
144
|
const config = options.config ?? loadConfig();
|
|
@@ -273,16 +307,29 @@ export async function bulkAdjudicateProposals(options) {
|
|
|
273
307
|
const skippedForContinuityRisk = matched.filter(isContinuityExcluded).length;
|
|
274
308
|
const pending = matched.filter((p) => !isContinuityExcluded(p));
|
|
275
309
|
const results = [];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
310
|
+
const commitCollector = [];
|
|
311
|
+
try {
|
|
312
|
+
for (const proposal of pending) {
|
|
313
|
+
if (options.dryRun) {
|
|
314
|
+
results.push({ id: proposal.id, ref: proposal.ref, source: proposal.source, dryRun: true });
|
|
315
|
+
}
|
|
316
|
+
else if (options.action === "accept") {
|
|
317
|
+
results.push(await akmProposalAccept({
|
|
318
|
+
stashDir,
|
|
319
|
+
id: proposal.id,
|
|
320
|
+
queue: options.queue,
|
|
321
|
+
target: options.target,
|
|
322
|
+
config,
|
|
323
|
+
commitCollector,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
results.push(await akmProposalReject({ stashDir, id: proposal.id, queue: options.queue, reason: options.reason, config }));
|
|
328
|
+
}
|
|
285
329
|
}
|
|
286
330
|
}
|
|
331
|
+
finally {
|
|
332
|
+
commitCollectedAccepts(commitCollector, "akm proposal accept");
|
|
333
|
+
}
|
|
287
334
|
return { count: results.length, results, skippedForContinuityRisk };
|
|
288
335
|
}
|
|
@@ -1060,6 +1060,37 @@ function requireAcceptedTarget(proposal) {
|
|
|
1060
1060
|
function acceptCommitMessage(proposal) {
|
|
1061
1061
|
return `akm accept: ${proposal.source} ${proposal.id.slice(0, 8)} ${proposal.ref}`;
|
|
1062
1062
|
}
|
|
1063
|
+
function commitAccept(collector, target, proposal, paths) {
|
|
1064
|
+
if (collector)
|
|
1065
|
+
collector.push({ target, proposal, paths });
|
|
1066
|
+
else
|
|
1067
|
+
commitAcceptedPaths(target, acceptCommitMessage(proposal), paths);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Commit everything `collector` holds, one commit per target repository: a lone
|
|
1071
|
+
* accept keeps its own subject; several get `<label>: N accepted (source count,
|
|
1072
|
+
* ...)` with one `<id8> <source> <ref>` body line per proposal. Empties the
|
|
1073
|
+
* collector. A failed commit only warns, as for a single accept.
|
|
1074
|
+
*/
|
|
1075
|
+
export function commitCollectedAccepts(collector, label) {
|
|
1076
|
+
const byRepo = new Map();
|
|
1077
|
+
for (const entry of collector.splice(0)) {
|
|
1078
|
+
const key = path.resolve(entry.target.source.repoPath ?? entry.target.source.path);
|
|
1079
|
+
byRepo.set(key, [...(byRepo.get(key) ?? []), entry]);
|
|
1080
|
+
}
|
|
1081
|
+
for (const entries of byRepo.values()) {
|
|
1082
|
+
const first = entries[0];
|
|
1083
|
+
if (entries.length === 1) {
|
|
1084
|
+
commitAcceptedPaths(first.target, acceptCommitMessage(first.proposal), first.paths);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const counts = new Map();
|
|
1088
|
+
for (const { proposal } of entries)
|
|
1089
|
+
counts.set(proposal.source, (counts.get(proposal.source) ?? 0) + 1);
|
|
1090
|
+
const bySource = [...counts].sort((a, b) => b[1] - a[1]).map(([source, count]) => `${source} ${count}`);
|
|
1091
|
+
commitAcceptedPaths(first.target, `${label}: ${entries.length} accepted (${bySource.join(", ")})`, entries.flatMap((entry) => entry.paths), entries.map(({ proposal }) => `${proposal.id.slice(0, 8)} ${proposal.source} ${proposal.ref}`));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1063
1094
|
/**
|
|
1064
1095
|
* O1 (alpha.9): an accepted consolidate PROMOTION retires its source memory
|
|
1065
1096
|
* (and its `.derived` twin), so promotion no longer leaves a memory/
|
|
@@ -1163,7 +1194,7 @@ async function promoteProposalWithLease(stashDir, config, id, options, ctx) {
|
|
|
1163
1194
|
const paths = [assetPath];
|
|
1164
1195
|
if (accepted.status === "accepted" && accepted.source === "consolidate")
|
|
1165
1196
|
retirePromotionSource(mutationTarget, accepted, paths);
|
|
1166
|
-
|
|
1197
|
+
commitAccept(options.commitCollector, mutationTarget, accepted, paths);
|
|
1167
1198
|
return { proposal: accepted, assetPath, ref: accepted.ref };
|
|
1168
1199
|
}
|
|
1169
1200
|
/**
|
|
@@ -1422,7 +1453,7 @@ async function retireProposalWithLease(stashDir, config, proposal, options, ctx)
|
|
|
1422
1453
|
paths.push(twinPath, path.join(mutationTarget.source.path, twinRecord.archivedPath), path.join(mutationTarget.source.path, twinRecord.auditPath));
|
|
1423
1454
|
}
|
|
1424
1455
|
if (paths.length > 0)
|
|
1425
|
-
|
|
1456
|
+
commitAccept(options.commitCollector, mutationTarget, proposal, paths);
|
|
1426
1457
|
if (archiveDirs.length === 0) {
|
|
1427
1458
|
// Recorded intent, but neither file is at its original location NOR
|
|
1428
1459
|
// archived under this proposal's id: something else removed the target
|
package/dist/core/errors.js
CHANGED
|
@@ -10,6 +10,7 @@ const CONFIG_HINTS = {
|
|
|
10
10
|
STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
|
|
11
11
|
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
|
|
12
12
|
DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
|
|
13
|
+
DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
|
|
13
14
|
INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
|
|
14
15
|
LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
|
|
15
16
|
TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* {@link commitWriteTargetBoundary} once — or wrap a custom mutation in
|
|
23
23
|
* {@link withWriteTargetMutation}, which does both under the asset lease. An
|
|
24
24
|
* accepted proposal commits its own paths ({@link commitAcceptedPaths}), on a
|
|
25
|
-
* filesystem stash that is a git repository too.
|
|
25
|
+
* filesystem stash that is a git repository too. A process that accepts many
|
|
26
|
+
* proposals collects them and commits once at its boundary (#1185).
|
|
26
27
|
*/
|
|
27
28
|
import fs from "node:fs";
|
|
28
29
|
import path from "node:path";
|
|
@@ -423,6 +424,7 @@ export function commitWriteTargetBoundary(target, message, options) {
|
|
|
423
424
|
const saved = saveGitStash(undefined, message, resolveWritable(target.config), {
|
|
424
425
|
repoDir,
|
|
425
426
|
paths: committable,
|
|
427
|
+
...(options?.body ? { body: options.body } : {}),
|
|
426
428
|
...(options?.push === undefined ? {} : { push: options.push }),
|
|
427
429
|
});
|
|
428
430
|
if (saved.reason)
|
|
@@ -430,9 +432,10 @@ export function commitWriteTargetBoundary(target, message, options) {
|
|
|
430
432
|
}
|
|
431
433
|
catch (error) {
|
|
432
434
|
if (error instanceof GitStashPushError) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
435
|
+
const what = error.committedNow
|
|
436
|
+
? `Changes were committed as ${error.commit}`
|
|
437
|
+
: `Nothing new to commit; earlier commits up to ${error.commit} are unpushed`;
|
|
438
|
+
throw new Error(`${what}, but publication failed: ${error.message}`, { cause: error });
|
|
436
439
|
}
|
|
437
440
|
throw error;
|
|
438
441
|
}
|
|
@@ -443,10 +446,11 @@ export function commitWriteTargetBoundary(target, message, options) {
|
|
|
443
446
|
* boundary commit skips, locally (the end-of-run sync pushes). A failed commit
|
|
444
447
|
* only warns, since the accept already landed.
|
|
445
448
|
*/
|
|
446
|
-
export function commitAcceptedPaths(target, message, paths) {
|
|
449
|
+
export function commitAcceptedPaths(target, message, paths, body) {
|
|
447
450
|
try {
|
|
448
451
|
commitWriteTargetBoundary(target, message, {
|
|
449
452
|
paths,
|
|
453
|
+
...(body ? { body } : {}),
|
|
450
454
|
filesystem: true,
|
|
451
455
|
...(target.source.kind === "git" ? {} : { push: false }),
|
|
452
456
|
});
|
|
@@ -55,14 +55,18 @@ export function ensureUsageEventsSchema(db) {
|
|
|
55
55
|
*/
|
|
56
56
|
export function insertUsageEvent(db, event) {
|
|
57
57
|
try {
|
|
58
|
-
db
|
|
59
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown");
|
|
58
|
+
insertUsageEventStrict(db, event);
|
|
60
59
|
}
|
|
61
60
|
catch (error) {
|
|
62
61
|
rethrowIfTestIsolationError(error);
|
|
63
62
|
/* fire-and-forget: silently ignore errors */
|
|
64
63
|
}
|
|
65
64
|
}
|
|
65
|
+
/** Insert a usage event and let a failed write throw, for callers whose record is the event itself. */
|
|
66
|
+
export function insertUsageEventStrict(db, event) {
|
|
67
|
+
db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source)
|
|
68
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown");
|
|
69
|
+
}
|
|
66
70
|
// ── Query ────────────────────────────────────────────────────────────────────
|
|
67
71
|
/**
|
|
68
72
|
* User feedback counts for many assets at once, keyed by durable `entry_ref`.
|
|
@@ -25658,6 +25658,7 @@ var CONFIG_HINTS = {
|
|
|
25658
25658
|
STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
|
|
25659
25659
|
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
|
|
25660
25660
|
DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
|
|
25661
|
+
DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
|
|
25661
25662
|
INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
|
|
25662
25663
|
LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
|
|
25663
25664
|
TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
|
|
@@ -24986,6 +24986,7 @@ var CONFIG_HINTS = {
|
|
|
24986
24986
|
STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
|
|
24987
24987
|
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
|
|
24988
24988
|
DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
|
|
24989
|
+
DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.",
|
|
24989
24990
|
INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
|
|
24990
24991
|
LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
|
|
24991
24992
|
TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
|
|
@@ -127,10 +127,13 @@ export function checkGitPathSafety(repoDir, pathspec) {
|
|
|
127
127
|
}
|
|
128
128
|
export class GitStashPushError extends Error {
|
|
129
129
|
commit;
|
|
130
|
-
|
|
130
|
+
/** False when the push was of commits made earlier (nothing was committed by this call). */
|
|
131
|
+
committedNow;
|
|
132
|
+
constructor(message, commit, committedNow = true) {
|
|
131
133
|
super(message);
|
|
132
134
|
this.name = "GitStashPushError";
|
|
133
135
|
this.commit = commit;
|
|
136
|
+
this.committedNow = committedNow;
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
const GIT_PUSH_TIMEOUT_MS = 120_000;
|
|
@@ -191,7 +194,11 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
191
194
|
// Co-Authored-By: someone-else"` and forge trailers in the commit log.
|
|
192
195
|
// Empty result falls back to the timestamped default.
|
|
193
196
|
const sanitized = message ? sanitizeCommitMessage(message) : "";
|
|
194
|
-
const
|
|
197
|
+
const bodyLines = (options?.body ?? []).map(sanitizeCommitMessage).filter(Boolean);
|
|
198
|
+
const commitMessage = [
|
|
199
|
+
sanitized || `akm save ${timestamp}`,
|
|
200
|
+
...(bodyLines.length > 0 ? ["", ...bodyLines] : []),
|
|
201
|
+
].join("\n");
|
|
195
202
|
const transactionId = options?.transactionId;
|
|
196
203
|
if (transactionId !== undefined && !/^[A-Za-z0-9._-]{1,128}$/.test(transactionId)) {
|
|
197
204
|
throw new Error("Invalid Git transaction identifier.");
|
|
@@ -317,22 +324,12 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
317
324
|
commit: exactCommit,
|
|
318
325
|
};
|
|
319
326
|
}
|
|
320
|
-
const
|
|
321
|
-
"-C",
|
|
322
|
-
repoDir,
|
|
323
|
-
"push",
|
|
324
|
-
`--force-with-lease=${pushTarget.mergeRef}:${pushTarget.upstreamHead}`,
|
|
325
|
-
pushTarget.remote,
|
|
326
|
-
`${readOptionalHead(repoDir) ?? exactCommit}:${pushTarget.mergeRef}`,
|
|
327
|
-
], { timeout: GIT_PUSH_TIMEOUT_MS });
|
|
328
|
-
if (pushResult.status !== 0) {
|
|
329
|
-
throw new GitStashPushError(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`, exactCommit);
|
|
330
|
-
}
|
|
327
|
+
const pushOutput = pushToUpstream(repoDir, pushTarget, readOptionalHead(repoDir) ?? exactCommit, exactCommit);
|
|
331
328
|
return {
|
|
332
329
|
committed: true,
|
|
333
330
|
pushed: true,
|
|
334
331
|
skipped: false,
|
|
335
|
-
output:
|
|
332
|
+
output: pushOutput || "changes committed and pushed",
|
|
336
333
|
commit: exactCommit,
|
|
337
334
|
};
|
|
338
335
|
}
|
|
@@ -343,12 +340,46 @@ function readHasRemote(repoDir) {
|
|
|
343
340
|
}
|
|
344
341
|
return remoteResult.stdout.trim().length > 0;
|
|
345
342
|
}
|
|
346
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* Push `head` to the upstream, leased on the upstream head just read. Throws {@link GitStashPushError}
|
|
345
|
+
* (carrying `commit`) when git refuses. Returns git's stdout.
|
|
346
|
+
*/
|
|
347
|
+
function pushToUpstream(repoDir, target, head, commit, committedNow = true) {
|
|
348
|
+
const pushResult = runGit([
|
|
349
|
+
"-C",
|
|
350
|
+
repoDir,
|
|
351
|
+
"push",
|
|
352
|
+
`--force-with-lease=${target.mergeRef}:${target.upstreamHead}`,
|
|
353
|
+
target.remote,
|
|
354
|
+
`${head}:${target.mergeRef}`,
|
|
355
|
+
], { timeout: GIT_PUSH_TIMEOUT_MS });
|
|
356
|
+
if (pushResult.status !== 0) {
|
|
357
|
+
throw new GitStashPushError(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`, commit, committedNow);
|
|
358
|
+
}
|
|
359
|
+
return pushResult.stdout.trim();
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Nothing to commit still brings in the upstream, so a host with no changes of its own keeps up. Commits
|
|
363
|
+
* made earlier and never pushed (a proposal accept, a killed improve run) are pushed here, once the
|
|
364
|
+
* branch is up to date with its upstream and ahead of it.
|
|
365
|
+
*/
|
|
347
366
|
function nothingToCommit(repoDir, output, syncWithRemote) {
|
|
348
367
|
const integration = syncWithRemote ? integrateUpstream(repoDir) : undefined;
|
|
349
368
|
if (integration && "blocked" in integration) {
|
|
350
369
|
return { committed: false, pushed: false, skipped: false, reason: `not pushed: ${integration.blocked}`, output };
|
|
351
370
|
}
|
|
371
|
+
// readUpstreamConfig is undefined on a detached HEAD or a branch with no upstream: nothing to push.
|
|
372
|
+
const head = syncWithRemote && readUpstreamConfig(repoDir) ? readOptionalHead(repoDir) : undefined;
|
|
373
|
+
const upstream = head ? readActualUpstream(repoDir, head) : undefined;
|
|
374
|
+
if (head && typeof upstream === "object" && upstream.upstreamHead !== head) {
|
|
375
|
+
const pushOutput = pushToUpstream(repoDir, upstream, head, head, false);
|
|
376
|
+
return {
|
|
377
|
+
committed: false,
|
|
378
|
+
pushed: true,
|
|
379
|
+
skipped: false,
|
|
380
|
+
output: pushOutput || `pushed ${upstream.upstreamHead.slice(0, 7)}..${head.slice(0, 7)}`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
352
383
|
return { committed: false, pushed: false, skipped: false, output: integration?.integrated ?? output };
|
|
353
384
|
}
|
|
354
385
|
function readUpstreamConfig(repoDir) {
|
|
@@ -658,7 +658,7 @@ Paths are relative to the rules-010 checkout. `drain.ts` is `src/commands/propos
|
|
|
658
658
|
- **Judgment verdict handling** — The judge returns accept, reject or defer, and parse failures and runner errors leave the item deferred (`judgment-parse-failure`, `judgment-error`). A judge reject becomes an auto-reject (`judgment-reject`). The judge sees the live asset (for a promotion, which is a create, the prompt prints "(none — this proposal would create a new asset)"), same-ref sibling proposals, and, only for such a create, the 5 nearest knowledge notes (300-character excerpts, plus promotions accepted earlier in the same drain). It is told to reject a promotion those notes already cover, or one that reports changing state ("as of <date>", a rollout, a plan not yet carried out). A lesson drawn from an incident is durable. *Why:* the model never sees `knowledge/` otherwise, and 18 of 51 promotions minted on 2026-10-07 and later rejected held 30% or more in a neighbouring doc. (`drain.ts:253-306,371-497,499-534`; `src/commands/improve/consolidate/coverage.ts:154-186`)
|
|
659
659
|
- **Judgment queue-mode staging** — In `queue` mode a judged accept is stamped `staged` (`judgment-accept`) with its content hash, for a later promote run. (`drain.ts:444-464`)
|
|
660
660
|
- **Excluded proposals** — `excludeIds` is always empty in improve (`improve.ts:869`); "this run's fresh proposals are never touched" is an ordering effect, because the pre-pass runs before consolidation mints. `--older-than <days>` on `proposal drain` excludes proposals younger than that, counted from creation or the last reopen, and fails safe by excluding any proposal whose age cannot be computed. (`drain.ts:559-564`; `src/commands/proposal/proposal-cli.ts:468-505`; `src/commands/proposal/proposal-types.ts:316-326`; `improve.ts:869`)
|
|
661
|
-
- **What auto-accept means for git** — Every auto-accept is the ordinary promote path, so it writes the asset and commits it to the target with the subject `akm accept: <source> <8-char id> <ref
|
|
661
|
+
- **What auto-accept means for git** — Every auto-accept is the ordinary promote path, so it writes the asset and commits it to the target with the subject `akm accept: <source> <8-char id> <ref>`, but a loop of accepts (`drain`, bulk `accept --generator`, `accept <id> <id>`) commits once per process: `<label>: N accepted (<source> n, ...)` with one `<id8> <source> <ref>` body line per proposal (#1185). Inside `akm improve` the triage accepts join the run's end-of-run batch commit (or, with sync off or no git repository, are committed once when the pre-pass ends). For a consolidate promotion the commit also carries the archived source memory and its tombstone. Filesystem targets never push; a git-kind target uses the default push behaviour. Commit failure is only a warning, not a failure of the accept. The standalone `drain --promote` asks for confirmation before promoting ("commit to git and cannot be batch-reverted"). A revert is per proposal (`akm proposal revert`). (`src/commands/proposal/repository.ts` (`commitAccept`, `commitCollectedAccepts`); `src/core/write-source.ts` (`commitAcceptedPaths`); `proposal-cli.ts:471-482`)
|
|
662
662
|
- **Drain failures stay pending** — An attempt that fails (validation, promote error) is listed under `failed` with a categorized reason and the proposal stays pending. A rejected proposal is recorded with a gate decision under gate `triage`. (`drain.ts:142-150,641-656`)
|
|
663
663
|
|
|
664
664
|
## Proposals and feedback
|
|
@@ -117,9 +117,12 @@ everything else routes through `akm proposal accept`.
|
|
|
117
117
|
7. `akm proposal accept` promotes the proposal into the bundle; `akm proposal
|
|
118
118
|
revert` restores the prior content from the backup captured at promotion
|
|
119
119
|
time, if the proposal overwrote an existing asset. When the bundle is a git
|
|
120
|
-
repository (a `.git` directory, whatever its source kind),
|
|
121
|
-
committed
|
|
122
|
-
|
|
120
|
+
repository (a `.git` directory, whatever its source kind), an accept is
|
|
121
|
+
committed locally with exactly the paths it wrote or removed:
|
|
122
|
+
`akm accept: <generator> <proposal-id-8> <ref>`. A process that accepts
|
|
123
|
+
several (`akm proposal drain`, a bulk accept, `accept <id> <id>`) commits them
|
|
124
|
+
once at its end, with a summary subject and one body line per proposal; in
|
|
125
|
+
`akm improve` the triage accepts join the end-of-run batch commit. A retirement's
|
|
123
126
|
archived copy and tombstone, and the source memory a consolidate promotion
|
|
124
127
|
retires, go in the same commit. A commit that fails warns and the accept
|
|
125
128
|
stands.
|
|
@@ -438,7 +441,7 @@ tombstone `retiredAt` is more than 30 days old (`RETIRE_GRACE_DAYS`) AND
|
|
|
438
441
|
whose archive directory is entirely git-tracked and clean (`git ls-files`
|
|
439
442
|
plus `git status --porcelain -uall`, both checked once per sweep, not once
|
|
440
443
|
per directory). `.git` presence alone does not guarantee a retirement was
|
|
441
|
-
ever committed: an accept commits its own paths (`commitAcceptedPaths` in
|
|
444
|
+
ever committed: an accept commits its own paths, once per process (`commitAcceptedPaths` in
|
|
442
445
|
`src/core/write-source.ts`), on a `kind: "filesystem"` source with a `.git`
|
|
443
446
|
directory too, but a commit that failed only warned, and a retirement accepted
|
|
444
447
|
by an older akm left archived bytes sitting on disk with no commit behind them
|
package/docs/reference/cli.md
CHANGED
|
@@ -3058,6 +3058,7 @@ UUID, an 8-character UUID prefix, or an asset ref.
|
|
|
3058
3058
|
|
|
3059
3059
|
```sh
|
|
3060
3060
|
akm proposal accept <id>
|
|
3061
|
+
akm proposal accept <id> <id> <id> # several at once: one commit
|
|
3061
3062
|
akm proposal accept 7c115132 # 8-char UUID prefix
|
|
3062
3063
|
akm proposal accept skills/akm-dream # Asset ref
|
|
3063
3064
|
akm proposal accept <id> --queue team-bundle
|
|
@@ -3081,10 +3082,19 @@ Bulk-accept all pending proposals from one generator with `--generator <name>`
|
|
|
3081
3082
|
(e.g. `reflect`, `distill`) and no positional id. Bulk accept requires
|
|
3082
3083
|
`-y`/`--yes` in non-interactive shells.
|
|
3083
3084
|
|
|
3085
|
+
Several ids accept in one process and return one `proposal-accept-batch`
|
|
3086
|
+
envelope (`accepted`, `failed`, and a `results` list in argument order). An id
|
|
3087
|
+
that fails appears as `{ ok: false, id, error }`, the command exits 1, and the
|
|
3088
|
+
others are still accepted and committed.
|
|
3089
|
+
|
|
3084
3090
|
When the destination bundle is a git repository (a `.git` directory, whatever
|
|
3085
|
-
its source kind),
|
|
3086
|
-
|
|
3087
|
-
|
|
3091
|
+
its source kind), one accept is committed with exactly the paths it wrote or
|
|
3092
|
+
removed and the subject `akm accept: <generator> <proposal-id-8> <ref>`. A
|
|
3093
|
+
process that accepts several proposals (several ids, `--generator`, or
|
|
3094
|
+
`akm proposal drain`) commits them once at its end: the subject is
|
|
3095
|
+
`akm proposal accept: N accepted (<generator> n, ...)` (`akm proposal drain: ...`
|
|
3096
|
+
for a drain) and the body has one `<proposal-id-8> <generator> <ref>` line per
|
|
3097
|
+
proposal. A retirement's archived copy and tombstone, and the source memory a
|
|
3088
3098
|
consolidate promotion retires, are part of the same commit. The commit is local:
|
|
3089
3099
|
`akm sync`, or the end-of-run sync of `akm improve`, pushes it. A commit that
|
|
3090
3100
|
fails warns and the accept stands.
|
|
@@ -79,6 +79,12 @@ mode and owner, and the uid you are running as. They do **not** report an empty
|
|
|
79
79
|
index. `akm health` stays runnable in that state and reports it as a failing
|
|
80
80
|
`state-db-readable` check, so it remains the command to reach for.
|
|
81
81
|
|
|
82
|
+
If akm can read this directory but **cannot write** it — an agent sandbox that
|
|
83
|
+
only allows writes inside the workspace, for instance — `akm feedback` fails with
|
|
84
|
+
a `DATA_DIR_NOT_WRITABLE` config error (exit 78) and records nothing, instead of
|
|
85
|
+
reporting success for feedback it could not store. Add the data directory to the
|
|
86
|
+
sandbox's writable paths.
|
|
87
|
+
|
|
82
88
|
> **0.9.1 note.** A pre-release build briefly chmodded this directory to `0700`
|
|
83
89
|
> and the databases to `0600` on every open. That was reverted: it silently
|
|
84
90
|
> changed the permissions of directories akm did not create, which broke installs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.261005-beta",
|
|
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": [
|