akm-cli 0.10.261004-beta → 0.10.261006-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 +58 -0
- package/STABILITY.md +9 -0
- package/dist/assets/ui/app.js +91 -0
- package/dist/assets/ui/asset.js +67 -0
- package/dist/assets/ui/bundles.js +80 -0
- package/dist/assets/ui/changes.js +32 -0
- package/dist/assets/ui/charts.js +93 -0
- package/dist/assets/ui/config.js +69 -0
- package/dist/assets/ui/diff-view.js +202 -0
- package/dist/assets/ui/diff.js +261 -0
- package/dist/assets/ui/index.html +30 -0
- package/dist/assets/ui/lib.js +238 -0
- package/dist/assets/ui/markdown.js +103 -0
- package/dist/assets/ui/overview.js +145 -0
- package/dist/assets/ui/proposal-actions.js +273 -0
- package/dist/assets/ui/proposals.js +219 -0
- package/dist/assets/ui/search.js +62 -0
- package/dist/assets/ui/styles.css +1415 -0
- package/dist/cli.js +3 -0
- package/dist/commands/health/checks.js +5 -3
- package/dist/commands/health/egress.js +5 -1
- package/dist/commands/health/engine-usage.js +2 -2
- package/dist/commands/health/llm-usage.js +2 -2
- package/dist/commands/health/windows.js +5 -5
- package/dist/commands/health.js +40 -8
- package/dist/commands/improve/improve.js +17 -4
- package/dist/commands/improve/reflect-noise.js +4 -0
- package/dist/commands/improve/reflect.js +14 -5
- 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 +67 -31
- package/dist/commands/read/search.js +2 -1
- package/dist/commands/read/show.js +8 -6
- package/dist/commands/ui/api.js +556 -0
- package/dist/commands/ui/git-read.js +101 -0
- package/dist/commands/ui/open-browser.js +30 -0
- package/dist/commands/ui/proposal-actions.js +179 -0
- package/dist/commands/ui/redact.js +88 -0
- package/dist/commands/ui/server.js +352 -0
- package/dist/commands/ui/ui-cli.js +62 -0
- package/dist/core/write-source.js +9 -5
- package/dist/indexer/search/db-search.js +2 -1
- package/dist/output/format-exempt.js +3 -0
- package/dist/sources/providers/git-stash.js +46 -15
- package/dist/storage/repositories/improve-runs-repository.js +6 -0
- package/dist/storage/repositories/metrics-repository.js +17 -0
- package/docs/architecture/0.10-process-rules.md +4 -5
- package/docs/architecture/improvement.md +9 -5
- package/docs/architecture/internals/improve-workflow.md +2 -2
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +62 -3
- package/docs/reference/ui-api.md +576 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -85,6 +85,7 @@ import { renderMigrationHelp } from "./commands/sources/migration-help.js";
|
|
|
85
85
|
import { cloneCommand, syncCommand, upgradeCommand } from "./commands/sources/sources-cli.js";
|
|
86
86
|
import { importKnowledgeCommand, indexCommand, infoCommand } from "./commands/sources/stash-cli.js";
|
|
87
87
|
import { taskCommand } from "./commands/tasks/tasks-cli.js";
|
|
88
|
+
import { uiCommand } from "./commands/ui/ui-cli.js";
|
|
88
89
|
import { workflowCommand } from "./commands/workflow-cli.js";
|
|
89
90
|
import { DEFAULT_CONFIG, loadConfig } from "./core/config/config.js";
|
|
90
91
|
import { UsageError } from "./core/errors.js";
|
|
@@ -448,6 +449,7 @@ const commands = {
|
|
|
448
449
|
index: indexCommand,
|
|
449
450
|
health: healthCommand,
|
|
450
451
|
metrics: metricsCommand,
|
|
452
|
+
ui: uiCommand,
|
|
451
453
|
info: infoCommand,
|
|
452
454
|
bundle: bundleCommand,
|
|
453
455
|
upgrade: upgradeCommand,
|
|
@@ -778,6 +780,7 @@ const HELP_SECTIONS = [
|
|
|
778
780
|
"lint",
|
|
779
781
|
"health",
|
|
780
782
|
"metrics",
|
|
783
|
+
"ui",
|
|
781
784
|
"config",
|
|
782
785
|
"models",
|
|
783
786
|
"registry",
|
|
@@ -902,9 +902,11 @@ export const HEALTH_CHECKS = [
|
|
|
902
902
|
kind: "deterministic",
|
|
903
903
|
status: ctx.probe.ok ? "pass" : "fail",
|
|
904
904
|
confidence: "high",
|
|
905
|
-
message: ctx.probe.
|
|
906
|
-
? "state.db append/read round-trip
|
|
907
|
-
:
|
|
905
|
+
message: ctx.probe.skipped
|
|
906
|
+
? "state.db append/read round-trip skipped: this run is read-only."
|
|
907
|
+
: ctx.probe.ok
|
|
908
|
+
? "state.db append/read round-trip succeeded."
|
|
909
|
+
: `state.db round-trip failed: ${ctx.probe.error}`,
|
|
908
910
|
evidence: { path: ctx.stateDbPath, durationMs: ctx.probe.durationMs },
|
|
909
911
|
}),
|
|
910
912
|
},
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* configured, matching the `stash-git-exposure` / `binary-config-skew`
|
|
9
9
|
* advisories' pattern.
|
|
10
10
|
*/
|
|
11
|
+
import { redactCredentialPatterns } from "../../core/redaction.js";
|
|
11
12
|
import { formatRegistryUrl } from "../../core/registry-url.js";
|
|
12
13
|
/**
|
|
13
14
|
* `egress-endpoints` (08 surfaces 3/9): the full list of remote destinations
|
|
@@ -42,6 +43,9 @@ export function collectEgressAdvisory(config) {
|
|
|
42
43
|
endpoints.push(`embedding: ${config.embedding.endpoint}`);
|
|
43
44
|
if (endpoints.length === 0)
|
|
44
45
|
return undefined;
|
|
46
|
+
// A configured URL can carry a credential (`?token=…`, `user:pass@`); the list is for eyeballing destinations, so
|
|
47
|
+
// print the host and path and never the secret.
|
|
48
|
+
const safe = endpoints.map(redactCredentialPatterns);
|
|
45
49
|
return {
|
|
46
50
|
name: "egress-endpoints",
|
|
47
51
|
kind: "deterministic",
|
|
@@ -49,6 +53,6 @@ export function collectEgressAdvisory(config) {
|
|
|
49
53
|
confidence: "high",
|
|
50
54
|
message: `${endpoints.length} remote endpoint(s) in the effective config (registries/sources/LLM/embedding) — ` +
|
|
51
55
|
"review the evidence list for unexpected destinations.",
|
|
52
|
-
evidence: { endpoints },
|
|
56
|
+
evidence: { endpoints: safe },
|
|
53
57
|
};
|
|
54
58
|
}
|
|
@@ -28,9 +28,9 @@ export function engineLastUsedSince(now) {
|
|
|
28
28
|
* `engine` field (pre-#576 rows, or a call outside any resolved engine) is
|
|
29
29
|
* skipped rather than bucketed under a synthetic key.
|
|
30
30
|
*/
|
|
31
|
-
export function readLastEngineUsage(stateDbPath, now) {
|
|
31
|
+
export function readLastEngineUsage(stateDbPath, now, readOnly = false) {
|
|
32
32
|
const since = engineLastUsedSince(now);
|
|
33
|
-
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events;
|
|
33
|
+
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath, readOnly }).events;
|
|
34
34
|
const lastUsed = new Map();
|
|
35
35
|
for (const event of events) {
|
|
36
36
|
const record = decodeLlmUsageRecord(event.metadata);
|
|
@@ -59,8 +59,8 @@ export function summarizeLlmUsage(events) {
|
|
|
59
59
|
}
|
|
60
60
|
return aggregate;
|
|
61
61
|
}
|
|
62
|
-
export function readLlmUsageAggregate(stateDbPath, since, until) {
|
|
63
|
-
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
|
|
62
|
+
export function readLlmUsageAggregate(stateDbPath, since, until, readOnly = false) {
|
|
63
|
+
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath, readOnly }).events.filter((event) => {
|
|
64
64
|
if (until === undefined)
|
|
65
65
|
return true;
|
|
66
66
|
return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
|
|
@@ -151,9 +151,9 @@ export function computeTaskWindowRates(taskRows, now) {
|
|
|
151
151
|
* stats from the same per-run rows, and accepted-proposal coverage, all over
|
|
152
152
|
* one `[since, until)`.
|
|
153
153
|
*/
|
|
154
|
-
export function buildImproveWindowSummary(db, stateDbPath, since, until) {
|
|
154
|
+
export function buildImproveWindowSummary(db, stateDbPath, since, until, readOnly = false) {
|
|
155
155
|
const untilMs = new Date(until).getTime();
|
|
156
|
-
const eventsBeforeUntil = (type) => readEvents({ since, type }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < untilMs);
|
|
156
|
+
const eventsBeforeUntil = (type) => readEvents({ since, type }, { dbPath: stateDbPath, readOnly }).events.filter((event) => new Date(event.ts ?? since).getTime() < untilMs);
|
|
157
157
|
const { metrics: improve, runCount } = summarizeImproveRuns(db, since, until);
|
|
158
158
|
improve.invoked = eventsBeforeUntil("improve_invoked").length;
|
|
159
159
|
improve.completed = eventsBeforeUntil(IMPROVE_COMPLETED_EVENT).length;
|
|
@@ -167,20 +167,20 @@ export function buildImproveWindowSummary(db, stateDbPath, since, until) {
|
|
|
167
167
|
improve.coverage = computeWindowProposalCoverage(db, since, until);
|
|
168
168
|
return { improve, runCount, perRunSummaries };
|
|
169
169
|
}
|
|
170
|
-
export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now()) {
|
|
170
|
+
export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(), readOnly = false) {
|
|
171
171
|
const untilMs = new Date(until).getTime();
|
|
172
172
|
const taskRows = queryTaskHistory(db, { since }).filter((row) => {
|
|
173
173
|
const startMs = new Date(row.started_at).getTime();
|
|
174
174
|
return !Number.isFinite(untilMs) || startMs < untilMs;
|
|
175
175
|
});
|
|
176
176
|
const rates = computeTaskWindowRates(taskRows, now);
|
|
177
|
-
const { improve, runCount } = buildImproveWindowSummary(db, stateDbPath, since, until);
|
|
177
|
+
const { improve, runCount } = buildImproveWindowSummary(db, stateDbPath, since, until, readOnly);
|
|
178
178
|
const metrics = {
|
|
179
179
|
taskFailRate: roundRate(rates.taskFailRate),
|
|
180
180
|
agentFailureRate: roundRate(rates.agentFailureRate),
|
|
181
181
|
agentFailureReasonCounts: rates.agentFailureReasonCounts,
|
|
182
182
|
stuckActiveRuns: rates.stuckActiveRuns,
|
|
183
|
-
llmUsage: readLlmUsageAggregate(stateDbPath, since, until),
|
|
183
|
+
llmUsage: readLlmUsageAggregate(stateDbPath, since, until, readOnly),
|
|
184
184
|
};
|
|
185
185
|
return { improve, metrics, runs: runCount };
|
|
186
186
|
}
|
package/dist/commands/health.js
CHANGED
|
@@ -11,12 +11,14 @@ import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from ".
|
|
|
11
11
|
import { listExistingTableNames, listPendingStateMigrations, openStateDatabaseWithReport, withStateDb, } from "../core/state-db.js";
|
|
12
12
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
13
13
|
import { probeLlmEndpoint } from "../llm/client.js";
|
|
14
|
+
import { openDatabase } from "../storage/database.js";
|
|
14
15
|
import { insertEvent } from "../storage/repositories/events-repository.js";
|
|
15
16
|
import { getExtractOutcomeCountsSince } from "../storage/repositories/extract-sessions-repository.js";
|
|
16
17
|
import { countImproveRunsSince } from "../storage/repositories/improve-runs-repository.js";
|
|
17
18
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
18
19
|
import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
|
|
19
20
|
import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
21
|
+
import { applyReadonlyPragmas } from "../storage/sqlite-pragmas.js";
|
|
20
22
|
import { getStateDbFreelistInfo, runStateDbIntegrityCheck } from "../storage/state-db-integrity.js";
|
|
21
23
|
import { pkgVersion } from "../version.js";
|
|
22
24
|
import { collectArchiveUsageAdvisory } from "./health/archive-usage.js";
|
|
@@ -176,12 +178,12 @@ function computeWorstTaskFailRate(rows) {
|
|
|
176
178
|
return worst;
|
|
177
179
|
}
|
|
178
180
|
/** Table presence, the state.db round-trip probe, and task_history-derived rates. */
|
|
179
|
-
function gatherTaskHistoryPhase(db, since, stateDbPath, now) {
|
|
181
|
+
function gatherTaskHistoryPhase(db, since, stateDbPath, now, readOnly) {
|
|
180
182
|
const tables = listExistingTableNames(db, ["events", "task_history", "proposals", "schema_migrations"]);
|
|
181
183
|
const tableNames = tables.map((row) => row.name).sort();
|
|
182
184
|
const requiredTables = ["events", "proposals", "schema_migrations", "task_history"];
|
|
183
185
|
const missingTables = requiredTables.filter((name) => !tableNames.includes(name));
|
|
184
|
-
const probe = probeStateDbRoundTrip(stateDbPath);
|
|
186
|
+
const probe = readOnly ? { ok: true, durationMs: null, skipped: true } : probeStateDbRoundTrip(stateDbPath);
|
|
185
187
|
// R0: read-only, independent of the round-trip probe above — integrity_check
|
|
186
188
|
// catches corruption a successful append/read cannot (out-of-order rowids,
|
|
187
189
|
// bad index entry counts), and the freelist reading is purely informational.
|
|
@@ -440,7 +442,7 @@ function resolveWindowComparePhase(options, db, stateDbPath, now) {
|
|
|
440
442
|
windowResults = windowSpecs.map((spec) => {
|
|
441
443
|
const winSince = parseHealthSince(spec.since);
|
|
442
444
|
const winUntil = spec.until ? parseHealthSince(spec.until) : new Date(now()).toISOString();
|
|
443
|
-
const bundle = buildWindowMetrics(db, stateDbPath, winSince, winUntil, now);
|
|
445
|
+
const bundle = buildWindowMetrics(db, stateDbPath, winSince, winUntil, now, options.readOnly === true);
|
|
444
446
|
return {
|
|
445
447
|
name: spec.name,
|
|
446
448
|
since: winSince,
|
|
@@ -508,6 +510,25 @@ function unreadableStateDbCheck(detail) {
|
|
|
508
510
|
evidence: { detail },
|
|
509
511
|
};
|
|
510
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Why a read-only health run cannot use `stateDbPath`, as the failing check to
|
|
515
|
+
* report: the file is missing, or it is behind on migrations (a read-only open
|
|
516
|
+
* must not apply them). `undefined` when it can be read as it is.
|
|
517
|
+
*/
|
|
518
|
+
function readOnlyStateDbUnavailable(stateDbPath) {
|
|
519
|
+
if (!fs.existsSync(stateDbPath)) {
|
|
520
|
+
return {
|
|
521
|
+
name: "state-db-readable",
|
|
522
|
+
kind: "deterministic",
|
|
523
|
+
status: "fail",
|
|
524
|
+
confidence: "high",
|
|
525
|
+
message: `state.db does not exist at ${stateDbPath}; a read-only health run does not create it. Run any akm command that records state, such as \`akm health\`.`,
|
|
526
|
+
evidence: { path: stateDbPath },
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const pending = runPendingStateMigrationsCheck(stateDbPath, { listPendingStateMigrations });
|
|
530
|
+
return pending.status === "fail" ? pending : undefined;
|
|
531
|
+
}
|
|
511
532
|
export async function akmHealth(options = {}) {
|
|
512
533
|
validateAkmHealthOptions(options);
|
|
513
534
|
const now = options.now ?? (() => Date.now());
|
|
@@ -521,9 +542,20 @@ export async function akmHealth(options = {}) {
|
|
|
521
542
|
// to find out why everything else is behaving oddly. Report it as a finding
|
|
522
543
|
// instead of dying before any check runs. The open itself applies every
|
|
523
544
|
// pending migration; `state-db-migrations` reports what it applied.
|
|
545
|
+
const readOnly = options.readOnly === true;
|
|
524
546
|
let opened;
|
|
525
547
|
try {
|
|
526
|
-
|
|
548
|
+
if (readOnly) {
|
|
549
|
+
const unavailable = readOnlyStateDbUnavailable(stateDbPath);
|
|
550
|
+
if (unavailable)
|
|
551
|
+
return degradedStateDbReport(unavailable, options);
|
|
552
|
+
const db = openDatabase(stateDbPath, { readonly: true, create: false });
|
|
553
|
+
applyReadonlyPragmas(db);
|
|
554
|
+
opened = { db, applied: [] };
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
opened = openStateDatabaseWithReport(stateDbPath);
|
|
558
|
+
}
|
|
527
559
|
}
|
|
528
560
|
catch (error) {
|
|
529
561
|
const { access, code } = classifyPathAccess(stateDbPath);
|
|
@@ -561,14 +593,14 @@ export async function akmHealth(options = {}) {
|
|
|
561
593
|
cliVersion: pkgVersion,
|
|
562
594
|
});
|
|
563
595
|
schedulerBinaryDriftPromise.catch(() => undefined);
|
|
564
|
-
const taskHistory = gatherTaskHistoryPhase(db, since, stateDbPath, now);
|
|
596
|
+
const taskHistory = gatherTaskHistoryPhase(db, since, stateDbPath, now, readOnly);
|
|
565
597
|
const { tableNames, missingTables, probe } = taskHistory;
|
|
566
598
|
const { egressConfigView, thinkingOffEngines } = gatherEgressConfigPhase();
|
|
567
599
|
// Same window bundle `--window-compare` builds per-window (windows.ts) —
|
|
568
600
|
// reused here for the main `--since` window's improve summary and for
|
|
569
601
|
// `--group-by run`, so neither re-reads `improve_runs`.
|
|
570
602
|
const until = new Date(now()).toISOString();
|
|
571
|
-
const { improve: improveSummary, perRunSummaries } = buildImproveWindowSummary(db, stateDbPath, since, until);
|
|
603
|
+
const { improve: improveSummary, perRunSummaries } = buildImproveWindowSummary(db, stateDbPath, since, until, readOnly);
|
|
572
604
|
advisories.push(...gatherAncillaryAdvisories(db, options, egressConfigView));
|
|
573
605
|
const sessionExtractionLedger = gatherSessionExtractionLedgerPhase(db, now);
|
|
574
606
|
// #950: computed once (no IO beyond config/env, same as gatherEgressConfigPhase)
|
|
@@ -578,14 +610,14 @@ export async function akmHealth(options = {}) {
|
|
|
578
610
|
// #950: `engine-last-used` reads a fixed lookback window independent of
|
|
579
611
|
// `--since` (mirrors sessionExtractionLedger's independent window above).
|
|
580
612
|
const engineLastUsedSinceIso = engineLastUsedSince(now);
|
|
581
|
-
const engineLastUsed = readLastEngineUsage(stateDbPath, now);
|
|
613
|
+
const engineLastUsed = readLastEngineUsage(stateDbPath, now, readOnly);
|
|
582
614
|
const improveRunsInLookbackWindow = countImproveRunsSince(db, engineLastUsedSinceIso);
|
|
583
615
|
const engineProbes = await engineProbesPromise;
|
|
584
616
|
const versionDrift = await versionDriftPromise;
|
|
585
617
|
const schedulerBinaryDrift = await schedulerBinaryDriftPromise;
|
|
586
618
|
// Read once, shared by the `thinking-control` check (#949) and the
|
|
587
619
|
// `metrics.llmUsage` report field below — same window, same aggregate.
|
|
588
|
-
const llmUsage = readLlmUsageAggregate(stateDbPath, since);
|
|
620
|
+
const llmUsage = readLlmUsageAggregate(stateDbPath, since, undefined, readOnly);
|
|
589
621
|
// Run the ordered health-check registry. Each check projects the shared
|
|
590
622
|
// context computed above into one HealthCheckResult; `channel` routes it to
|
|
591
623
|
// hardChecks or advisories. Declaration order in HEALTH_CHECKS is the
|
|
@@ -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
|
}
|
|
@@ -158,6 +158,10 @@ export function splitFrontmatter(raw) {
|
|
|
158
158
|
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
159
159
|
return m ? { fmText: m[1] ?? "", body: m[2] ?? "" } : { fmText: null, body: raw };
|
|
160
160
|
}
|
|
161
|
+
/** A level-1 heading line (`# text`) anywhere in a body: what reflect's title adds only when none is there. */
|
|
162
|
+
export function bodyHasH1(body) {
|
|
163
|
+
return /^#[ \t]+\S/m.test(body);
|
|
164
|
+
}
|
|
161
165
|
/** Frontmatter text, fenced code blocks, and prose lines (an unclosed fence counts as prose). */
|
|
162
166
|
function parseSections(text) {
|
|
163
167
|
const { fmText, body } = splitFrontmatter(normalizeTrailingWhitespace(text));
|
|
@@ -44,7 +44,7 @@ import { contentHash } from "./content-hash.js";
|
|
|
44
44
|
import { findAssetFilePath } from "./eligibility.js";
|
|
45
45
|
import { resolveImproveExecution } from "./execution.js";
|
|
46
46
|
import { recordLedgerAttempt } from "./ledger.js";
|
|
47
|
-
import { classifyReflectChange, findReflectDefect, splitFrontmatter } from "./reflect-noise.js";
|
|
47
|
+
import { bodyHasH1, classifyReflectChange, findReflectDefect, splitFrontmatter, } from "./reflect-noise.js";
|
|
48
48
|
import { callStageOnce, errMessage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runReflectQualityJudge, } from "./stage.js";
|
|
49
49
|
const MAX_FEEDBACK_LINES = 10;
|
|
50
50
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
@@ -151,10 +151,17 @@ function firstProseSentence(body) {
|
|
|
151
151
|
}
|
|
152
152
|
return "";
|
|
153
153
|
}
|
|
154
|
+
/** The body with `# title` and one blank line inserted at its first non-blank line; the rest is untouched. */
|
|
155
|
+
function withH1(body, title) {
|
|
156
|
+
const eol = body.match(/\r?\n/)?.[0] ?? "\n";
|
|
157
|
+
const leading = body.match(/^(?:\r?\n)*/)?.[0] ?? "";
|
|
158
|
+
return `${leading}# ${title}${eol}${eol}${body.slice(leading.length)}`;
|
|
159
|
+
}
|
|
154
160
|
/**
|
|
155
161
|
* The asset with the patch applied, or `undefined` when the patch changes
|
|
156
162
|
* nothing (or there is no asset to patch). The body is the source's own, byte
|
|
157
|
-
* for byte; the one thing akm adds to it is a `# title` heading it lacks
|
|
163
|
+
* for byte; the one thing akm adds to it is a `# title` heading it lacks, with
|
|
164
|
+
* one blank line after it (#1183). A
|
|
158
165
|
* required description that neither the source nor the patch has is derived
|
|
159
166
|
* from the asset's own text (#636). Only the changed keys' frontmatter lines are
|
|
160
167
|
* rewritten: every other line is kept as it is, so a source whose YAML the
|
|
@@ -170,8 +177,10 @@ export function applyReflectPatch(patch, sourceContent, targetRef) {
|
|
|
170
177
|
return undefined;
|
|
171
178
|
const sourceFm = fmText !== null ? parseFrontmatter(sourceContent).data : {};
|
|
172
179
|
const { title, ...changes } = patch;
|
|
173
|
-
const addTitle = title !== undefined &&
|
|
174
|
-
|
|
180
|
+
const addTitle = title !== undefined && !bodyHasH1(sourceBody);
|
|
181
|
+
// The heading and one blank line go in at the body's first non-blank line, in
|
|
182
|
+
// the body's own line ending; every other byte of the body stays where it was.
|
|
183
|
+
const body = addTitle ? withH1(sourceBody, title) : sourceBody;
|
|
175
184
|
// Only a source that already has frontmatter but no description gets one:
|
|
176
185
|
// injecting a whole block, or overwriting an authored one, is out of scope.
|
|
177
186
|
const refType = lenientRefType(targetRef);
|
|
@@ -198,7 +207,7 @@ export function applyReflectPatch(patch, sourceContent, targetRef) {
|
|
|
198
207
|
const content = `---\n${serializeFrontmatter(changes)}\n---\n\n${body}`;
|
|
199
208
|
return { content, frontmatter: parseFrontmatter(content).data };
|
|
200
209
|
}
|
|
201
|
-
const content = `---\n${patchFrontmatterLines(fmText, changes)}\n---\n${
|
|
210
|
+
const content = `---\n${patchFrontmatterLines(fmText, changes)}\n---\n${body}`;
|
|
202
211
|
return { content, frontmatter: parseFrontmatter(content).data };
|
|
203
212
|
}
|
|
204
213
|
/** The frontmatter text with each changed key's lines (the key line and its indented continuation) replaced, or appended. */
|
|
@@ -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
|
}
|