akm-cli 0.10.261005-beta → 0.10.261007-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 +65 -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/reflect-noise.js +4 -0
- package/dist/commands/improve/reflect.js +14 -5
- package/dist/commands/metrics/metrics-cli.js +3 -3
- package/dist/commands/proposal/propose-cli.js +5 -0
- package/dist/commands/proposal/propose.js +6 -2
- package/dist/commands/proposal/repository.js +34 -29
- package/dist/commands/read/search.js +2 -1
- package/dist/commands/read/show.js +18 -12
- package/dist/commands/sources/source-clone.js +123 -11
- 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/asset/resolve-ref.js +7 -3
- package/dist/indexer/indexer.js +14 -2
- package/dist/indexer/search/db-search.js +7 -3
- package/dist/output/format-exempt.js +3 -0
- package/dist/storage/repositories/improve-runs-repository.js +6 -0
- package/dist/storage/repositories/index-db.js +10 -2
- package/dist/storage/repositories/metrics-repository.js +17 -0
- package/docs/architecture/0.10-process-rules.md +3 -4
- package/docs/architecture/improvement.md +2 -1
- package/docs/architecture/internals/improve-workflow.md +2 -2
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +68 -0
- 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
|
|
@@ -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. */
|
|
@@ -53,7 +53,7 @@ export function metricsIncludeRows(mode) {
|
|
|
53
53
|
* `bundle//conceptId`; resolving here keeps one asset from showing as two rows.
|
|
54
54
|
* A ref that does not resolve (registry results, removed assets) is kept as is.
|
|
55
55
|
*/
|
|
56
|
-
async function durableSelects(selects) {
|
|
56
|
+
async function durableSelects(selects, indexSnapshot = true) {
|
|
57
57
|
const unqualified = [];
|
|
58
58
|
for (const ref of new Set(selects.map((select) => select.ref))) {
|
|
59
59
|
try {
|
|
@@ -65,7 +65,7 @@ async function durableSelects(selects) {
|
|
|
65
65
|
// Not a local bundle ref: keep the ref as recorded.
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
const entries = await lookupBundleRefsReadonly(unqualified.map(({ parsed }) => parsed));
|
|
68
|
+
const entries = await lookupBundleRefsReadonly(unqualified.map(({ parsed }) => parsed), { isolatedSnapshot: indexSnapshot });
|
|
69
69
|
const resolved = new Map(unqualified.map(({ ref }, i) => [ref, entries[i]?.itemRef ?? ref]));
|
|
70
70
|
return selects.map((select) => ({ ts: select.ts, ref: resolved.get(select.ref) ?? select.ref }));
|
|
71
71
|
}
|
|
@@ -190,7 +190,7 @@ export async function akmMetrics(options = {}) {
|
|
|
190
190
|
finally {
|
|
191
191
|
stateDb.close();
|
|
192
192
|
}
|
|
193
|
-
input.selects = await durableSelects(selects);
|
|
193
|
+
input.selects = await durableSelects(selects, options.indexSnapshot ?? true);
|
|
194
194
|
return buildMetricsResult(input);
|
|
195
195
|
}
|
|
196
196
|
export const metricsCommand = defineJsonCommand({
|
|
@@ -47,6 +47,10 @@ export const proposeCommand = defineCommand({
|
|
|
47
47
|
},
|
|
48
48
|
task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
|
|
49
49
|
file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
|
|
50
|
+
bundle: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Configured writable bundle to queue the proposal for and draw convention facts from (default: the working bundle)",
|
|
53
|
+
},
|
|
50
54
|
engine: { type: "string", description: "Engine to use (defaults to defaults.engine)" },
|
|
51
55
|
"timeout-ms": { type: "string", description: "Override the engine timeout in milliseconds" },
|
|
52
56
|
},
|
|
@@ -73,6 +77,7 @@ export const proposeCommand = defineCommand({
|
|
|
73
77
|
name: proposedName,
|
|
74
78
|
task: taskText,
|
|
75
79
|
engine: getStringArg(args, "engine"),
|
|
80
|
+
bundle: getStringArg(args, "bundle"),
|
|
76
81
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
77
82
|
});
|
|
78
83
|
output("proposal-new", result);
|
|
@@ -23,6 +23,7 @@ import { redactSensitiveText } from "../../core/redaction.js";
|
|
|
23
23
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
24
24
|
import { runStructured } from "../../core/structured.js";
|
|
25
25
|
import { warn } from "../../core/warn.js";
|
|
26
|
+
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
26
27
|
import { deriveEntryProvenance } from "../../indexer/installations.js";
|
|
27
28
|
import { fallbackAnnouncement } from "../../integrations/agent/engine-fallback.js";
|
|
28
29
|
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
@@ -144,10 +145,13 @@ export async function akmPropose(options) {
|
|
|
144
145
|
if (!stashDirFor(options.type)) {
|
|
145
146
|
throw new UsageError(`propose: unknown asset type "${options.type}". Known types: ${[...placementTypes()].sort().join(", ")}.`, "INVALID_FLAG_VALUE");
|
|
146
147
|
}
|
|
147
|
-
const stash = options.stashDir ?? resolveStashDir();
|
|
148
148
|
// 1. Resolve the write target. Engine/model/inference resolution happens
|
|
149
|
-
// exactly once below through the shared execution cascade.
|
|
149
|
+
// exactly once below through the shared execution cascade. `--bundle` names a
|
|
150
|
+
// configured, writable bundle (same resolver and errors as `akm remember
|
|
151
|
+
// --bundle`); its root becomes the stash the proposal is queued for and the
|
|
152
|
+
// prompt's standards are read from.
|
|
150
153
|
const config = options.agentConfig ?? (await import("../../core/config/config.js")).loadConfig();
|
|
154
|
+
const stash = options.stashDir ?? (options.bundle ? resolveWriteTarget(config, options.bundle).source.path : resolveStashDir());
|
|
151
155
|
const target = resolveProposalQueueTarget(stash, config);
|
|
152
156
|
// 2. Build terminal user content.
|
|
153
157
|
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
@@ -436,34 +436,36 @@ function requireProposal(db, stashDir, id) {
|
|
|
436
436
|
* proposal (then most recent archived); else by a unique pending id prefix.
|
|
437
437
|
*/
|
|
438
438
|
export function resolveProposalId(stashDir, idOrRef, ctx) {
|
|
439
|
-
return withProposalsDb(ctx, (db) =>
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
439
|
+
return withProposalsDb(ctx, (db) => resolveProposalIdIn(db, stashDir, idOrRef));
|
|
440
|
+
}
|
|
441
|
+
/** {@link resolveProposalId} over a connection the caller holds (`akm ui` reads state.db read-only). */
|
|
442
|
+
export function resolveProposalIdIn(db, stashDir, idOrRef) {
|
|
443
|
+
const exact = getStateProposal(db, idOrRef, stashDir);
|
|
444
|
+
if (exact)
|
|
445
|
+
return exact;
|
|
446
|
+
if (idOrRef.includes(":") || idOrRef.includes("/")) {
|
|
447
|
+
const wantRef = filterRefIdentity(idOrRef);
|
|
448
|
+
// Should-fix 7: by-ref resolution never picks a retire proposal — the
|
|
449
|
+
// newest pending proposal for a ref could be a `consolidate-pair`
|
|
450
|
+
// retirement rather than the reflect/distill edit a person typed the
|
|
451
|
+
// ref to accept, and accepting it archives the asset instead. A retire
|
|
452
|
+
// is reached by its own proposal id, or by the explicit generator
|
|
453
|
+
// `consolidate-pair` (bulk accept/reject).
|
|
454
|
+
const newest = (status) => listStateProposals(db, { stashDir, ...(status !== undefined ? { status } : {}) })
|
|
455
|
+
.filter((p) => proposalMatchesRef(p.ref, wantRef) && !isRetireProposal(p))
|
|
456
|
+
.sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime())[0];
|
|
457
|
+
const found = newest("pending") ?? newest();
|
|
458
|
+
if (found)
|
|
459
|
+
return found;
|
|
460
|
+
throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "PROPOSAL_NOT_FOUND");
|
|
461
|
+
}
|
|
462
|
+
const prefixMatches = listStateProposalIdsByPrefix(db, stashDir, idOrRef);
|
|
463
|
+
if (prefixMatches.length === 1)
|
|
464
|
+
return requireProposal(db, stashDir, prefixMatches[0]);
|
|
465
|
+
if (prefixMatches.length > 1) {
|
|
466
|
+
throw new UsageError(`Ambiguous prefix "${idOrRef}" — matches: ${prefixMatches.join(", ")}`, "INVALID_FLAG_VALUE");
|
|
467
|
+
}
|
|
468
|
+
throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "PROPOSAL_NOT_FOUND");
|
|
467
469
|
}
|
|
468
470
|
/**
|
|
469
471
|
* The ledger outcome of a decision. A procedural refusal (retention expiry, a
|
|
@@ -1869,7 +1871,10 @@ export function reopenProposals(stashDir, config, ids, options = {}, ctx) {
|
|
|
1869
1871
|
}
|
|
1870
1872
|
/** The proposal against the asset its accept would overwrite (same target resolution as accept). */
|
|
1871
1873
|
export function diffProposal(stashDir, config, id, options = {}, ctx) {
|
|
1872
|
-
|
|
1874
|
+
return diffLoadedProposal(config, getProposal(stashDir, id, ctx), options);
|
|
1875
|
+
}
|
|
1876
|
+
/** {@link diffProposal} for a proposal the caller already read (`akm ui` reads state.db read-only). */
|
|
1877
|
+
export function diffLoadedProposal(config, proposal, options = {}) {
|
|
1873
1878
|
const target = resolveProposalWriteTarget(config, proposal, options.target, options.queueTarget);
|
|
1874
1879
|
const targetPath = resolveAssetFilePathSafe(target.source, parseRefInput(proposal.ref));
|
|
1875
1880
|
const existing = targetPath && fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
@@ -123,6 +123,7 @@ export async function akmSearch(input) {
|
|
|
123
123
|
// would leak hits from sources the caller did not request.
|
|
124
124
|
restrictToSources: namedSourceName !== undefined,
|
|
125
125
|
includeExcludedTypes: input.includeSessions === true,
|
|
126
|
+
readOnly: input.readOnly,
|
|
126
127
|
});
|
|
127
128
|
const registryResult = source === "local"
|
|
128
129
|
? undefined
|
|
@@ -201,7 +202,7 @@ function usageSearchMode(mode) {
|
|
|
201
202
|
return mode === "semantic" ? "semantic" : "keyword";
|
|
202
203
|
}
|
|
203
204
|
function maybeLogSearchEvent(input, query, response, mode) {
|
|
204
|
-
if (input.skipLogging)
|
|
205
|
+
if (input.skipLogging || input.readOnly)
|
|
205
206
|
return;
|
|
206
207
|
logSearchEvent(query, response, mode, input.eventSource, input.attributionProjection);
|
|
207
208
|
}
|
|
@@ -33,7 +33,7 @@ import { appendEvent } from "../../core/events.js";
|
|
|
33
33
|
import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
|
|
34
34
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
35
35
|
import { warn, warnOnce } from "../../core/warn.js";
|
|
36
|
-
import { lookupBundleRef, lookupBundleRefWithResolution } from "../../indexer/indexer.js";
|
|
36
|
+
import { lookupBundleRef, lookupBundleRefWithResolution, lookupBundleRefWithResolutionReadonly, } from "../../indexer/indexer.js";
|
|
37
37
|
import { projectMarkdownFragmentContent } from "../../indexer/passes/metadata.js";
|
|
38
38
|
import { ensurePrimaryIndexForRead, resolveReadSources } from "../../indexer/read-preflight.js";
|
|
39
39
|
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries, } from "../../indexer/search/search-source.js";
|
|
@@ -65,7 +65,7 @@ export async function akmShowUnified(input) {
|
|
|
65
65
|
{
|
|
66
66
|
const metaRef = parseMetaRef(ref);
|
|
67
67
|
if (metaRef)
|
|
68
|
-
return showStashMeta(metaRef);
|
|
68
|
+
return showStashMeta(metaRef, input.readOnly === true);
|
|
69
69
|
}
|
|
70
70
|
// Env/secret bodies have no safe fragment surface, and a fragment cannot
|
|
71
71
|
// widen what the env/secret renderers expose: both always omit the body
|
|
@@ -77,7 +77,7 @@ export async function akmShowUnified(input) {
|
|
|
77
77
|
// safe substrate below is its source of truth. Friendly heading selectors
|
|
78
78
|
// intentionally retain the normal source-live read behavior.
|
|
79
79
|
const parsedRef = parseBundleRef(ref);
|
|
80
|
-
if (!parsedRef.fragment?.startsWith("akm-fragment-")) {
|
|
80
|
+
if (!input.readOnly && !parsedRef.fragment?.startsWith("akm-fragment-")) {
|
|
81
81
|
const { primarySource } = resolveReadSources();
|
|
82
82
|
await ensurePrimaryIndexForRead(primarySource);
|
|
83
83
|
}
|
|
@@ -91,7 +91,7 @@ export async function akmShowUnified(input) {
|
|
|
91
91
|
enforceScopeOrThrow(result.path, ref, input.scope);
|
|
92
92
|
}
|
|
93
93
|
// Count prior shows of this ref before logging the current one.
|
|
94
|
-
if (!input.skipLogging) {
|
|
94
|
+
if (!input.skipLogging && !input.readOnly) {
|
|
95
95
|
const consumedRef = result.ref ?? makeBundleRef(undefined, parseBundleRef(ref).conceptId);
|
|
96
96
|
const priorShowCount = recentShowCount(consumedRef);
|
|
97
97
|
recordShowUsage(consumedRef, result.type, result.name, input.eventSource, result.path);
|
|
@@ -110,7 +110,7 @@ export async function akmShowUnified(input) {
|
|
|
110
110
|
* to that stash; an uninstalled origin yields an actionable "not installed"
|
|
111
111
|
* error. The file is read directly from disk — `.meta/` is never indexed.
|
|
112
112
|
*/
|
|
113
|
-
async function showStashMeta(metaRef) {
|
|
113
|
+
async function showStashMeta(metaRef, readOnly = false) {
|
|
114
114
|
const allSources = resolveSourceEntries();
|
|
115
115
|
const sources = resolveSourcesForOrigin(metaRef.origin, allSources);
|
|
116
116
|
if (metaRef.origin && sources.length === 0) {
|
|
@@ -123,7 +123,9 @@ async function showStashMeta(metaRef) {
|
|
|
123
123
|
if (!metaFile)
|
|
124
124
|
continue;
|
|
125
125
|
const editable = isEditable(metaFile.path, config, allSources);
|
|
126
|
-
|
|
126
|
+
if (!readOnly) {
|
|
127
|
+
appendEvent({ eventType: "show", ref: `meta:${metaRef.name}`, metadata: { type: "meta", name: metaRef.name } });
|
|
128
|
+
}
|
|
127
129
|
return {
|
|
128
130
|
type: "meta",
|
|
129
131
|
name: metaRef.name,
|
|
@@ -211,7 +213,9 @@ export async function showLocal(input) {
|
|
|
211
213
|
const allSources = resolveSourceEntries(input.stashDir);
|
|
212
214
|
const searchSources = resolveSourcesForOrigin(parsed.bundle, allSources);
|
|
213
215
|
const allSourceDirs = searchSources.map((s) => s.path);
|
|
214
|
-
const resolution =
|
|
216
|
+
const resolution = input.readOnly
|
|
217
|
+
? await lookupBundleRefWithResolutionReadonly(parsed)
|
|
218
|
+
: await lookupBundleRefWithResolution(parsed);
|
|
215
219
|
if (resolution.indexError !== undefined)
|
|
216
220
|
throw resolution.indexError;
|
|
217
221
|
const indexedEntry = resolution.entry;
|
|
@@ -260,7 +264,9 @@ export async function showLocal(input) {
|
|
|
260
264
|
// Only resolve the indexed-safe revision when the selector itself is opaque,
|
|
261
265
|
// or when the caller explicitly opts into indexed-safe contextual output.
|
|
262
266
|
const indexedFragment = parsed.fragment && (opaqueFragmentSelector || input.contextMode === "lead")
|
|
263
|
-
? withIndexDb((db) => getIndexedMarkdownFragment(db, indexedEntry.itemRef, parsed.fragment)
|
|
267
|
+
? withIndexDb((db) => getIndexedMarkdownFragment(db, indexedEntry.itemRef, parsed.fragment), {
|
|
268
|
+
readOnly: input.readOnly === true,
|
|
269
|
+
})
|
|
264
270
|
: undefined;
|
|
265
271
|
const indexedFragmentContent = opaqueFragmentSelector ? indexedFragment?.content : undefined;
|
|
266
272
|
const indexedRenderer = rendererForIndexedEntry(indexedEntry, fileCtx);
|
|
@@ -364,9 +370,9 @@ export async function showLocal(input) {
|
|
|
364
370
|
origin: source?.registryId ?? null,
|
|
365
371
|
editable,
|
|
366
372
|
...(!editable ? { editHint: buildEditHint(canonicalRef) } : {}),
|
|
367
|
-
...showLinks(indexedEntry.itemRef, displayDefaultBundle),
|
|
373
|
+
...showLinks(indexedEntry.itemRef, displayDefaultBundle, input.readOnly === true),
|
|
368
374
|
};
|
|
369
|
-
const activeRun = await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
375
|
+
const activeRun = input.readOnly ? null : await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
370
376
|
if (activeRun) {
|
|
371
377
|
fullResponse.activeRun = activeRun;
|
|
372
378
|
}
|
|
@@ -385,10 +391,10 @@ const LINKS_PER_KIND = 10;
|
|
|
385
391
|
* authored order, incoming by ref, and unresolved tokens as authored. Empty
|
|
386
392
|
* parts are omitted, and the field when nothing links either way.
|
|
387
393
|
*/
|
|
388
|
-
function showLinks(itemRef, defaultBundle) {
|
|
394
|
+
function showLinks(itemRef, defaultBundle, readOnly = false) {
|
|
389
395
|
let rows;
|
|
390
396
|
try {
|
|
391
|
-
rows = withIndexDb((db) => readEntryLinks(db, itemRef));
|
|
397
|
+
rows = withIndexDb((db) => readEntryLinks(db, itemRef), { readOnly });
|
|
392
398
|
}
|
|
393
399
|
catch (err) {
|
|
394
400
|
rethrowIfTestIsolationError(err);
|