akm-cli 0.9.0-rc.2 → 0.9.0-rc.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +34 -8
  2. package/dist/assets/improve-strategies/catchup.json +3 -1
  3. package/dist/assets/improve-strategies/consolidate.json +3 -1
  4. package/dist/assets/improve-strategies/frequent.json +3 -1
  5. package/dist/assets/improve-strategies/graph-refresh.json +3 -1
  6. package/dist/assets/improve-strategies/memory-focus.json +4 -1
  7. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
  8. package/dist/assets/improve-strategies/quick.json +3 -1
  9. package/dist/assets/improve-strategies/recombine-only.json +2 -0
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -0
  11. package/dist/assets/improve-strategies/synthesize.json +2 -0
  12. package/dist/assets/tasks/core/backup.yml +2 -2
  13. package/dist/cli/config-migrate.js +554 -26
  14. package/dist/cli.js +3 -1
  15. package/dist/commands/backup-cli.js +6 -4
  16. package/dist/commands/config-cli.js +10 -3
  17. package/dist/commands/feedback-cli.js +24 -7
  18. package/dist/commands/health/checks.js +94 -15
  19. package/dist/commands/health/surfaces.js +2 -1
  20. package/dist/commands/improve/anti-collapse.js +3 -3
  21. package/dist/commands/improve/collapse-detector.js +5 -5
  22. package/dist/commands/improve/consolidate.js +123 -66
  23. package/dist/commands/improve/distill/promote-memory.js +8 -6
  24. package/dist/commands/improve/distill/quality-gate.js +1 -1
  25. package/dist/commands/improve/distill-guards.js +1 -1
  26. package/dist/commands/improve/distill-promotion-policy.js +32 -26
  27. package/dist/commands/improve/distill.js +52 -36
  28. package/dist/commands/improve/eligibility.js +9 -8
  29. package/dist/commands/improve/extract-prompt.js +2 -2
  30. package/dist/commands/improve/extract.js +43 -11
  31. package/dist/commands/improve/improve-auto-accept.js +14 -28
  32. package/dist/commands/improve/improve-cli.js +4 -2
  33. package/dist/commands/improve/improve-strategies.js +2 -1
  34. package/dist/commands/improve/improve.js +49 -8
  35. package/dist/commands/improve/loop-stages.js +11 -6
  36. package/dist/commands/improve/preparation.js +51 -17
  37. package/dist/commands/improve/procedural.js +10 -6
  38. package/dist/commands/improve/recombine.js +31 -7
  39. package/dist/commands/improve/reflect.js +24 -14
  40. package/dist/commands/improve/salience.js +5 -1
  41. package/dist/commands/improve/source-identity.js +43 -0
  42. package/dist/commands/migrate-cli.js +36 -0
  43. package/dist/commands/mv-cli.js +647 -258
  44. package/dist/commands/proposal/drain.js +16 -4
  45. package/dist/commands/proposal/proposal-cli.js +3 -2
  46. package/dist/commands/proposal/proposal.js +16 -55
  47. package/dist/commands/proposal/repository.js +664 -58
  48. package/dist/commands/read/curate.js +4 -2
  49. package/dist/commands/read/knowledge.js +4 -1
  50. package/dist/commands/read/search.js +6 -2
  51. package/dist/commands/read/show.js +8 -7
  52. package/dist/commands/sources/self-update.js +156 -112
  53. package/dist/commands/sources/sources-cli.js +7 -2
  54. package/dist/core/common.js +36 -3
  55. package/dist/core/config/config-io.js +2 -2
  56. package/dist/core/config/config-schema.js +15 -3
  57. package/dist/core/config/config.js +14 -13
  58. package/dist/core/file-lock.js +2 -2
  59. package/dist/core/migration-backup.js +816 -171
  60. package/dist/core/migration-operation.js +44 -0
  61. package/dist/core/state/migrations.js +4 -7
  62. package/dist/core/state-db.js +22 -7
  63. package/dist/core/write-source.js +86 -18
  64. package/dist/indexer/db/db.js +109 -53
  65. package/dist/indexer/index-writer-lock.js +38 -37
  66. package/dist/indexer/index-written-assets.js +73 -56
  67. package/dist/indexer/indexer.js +5 -1
  68. package/dist/indexer/search/search-source.js +2 -2
  69. package/dist/indexer/usage/usage-events.js +8 -2
  70. package/dist/integrations/agent/engine-resolution.js +14 -7
  71. package/dist/scripts/migrate-storage.js +867 -990
  72. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +405 -566
  73. package/dist/setup/detected-engines.js +7 -13
  74. package/dist/setup/engine-config.js +14 -4
  75. package/dist/setup/setup.js +1 -1
  76. package/dist/setup/steps/connection.js +1 -1
  77. package/dist/setup/steps/tasks.js +1 -1
  78. package/dist/sources/providers/git-stash.js +58 -21
  79. package/dist/sources/providers/git.js +1 -1
  80. package/dist/storage/engines/sqlite-migrations.js +138 -3
  81. package/dist/storage/repositories/events-repository.js +24 -0
  82. package/dist/storage/repositories/proposals-repository.js +14 -0
  83. package/dist/tasks/embedded.js +2 -0
  84. package/dist/workflows/db.js +24 -13
  85. package/dist/workflows/validator.js +24 -2
  86. package/docs/migration/release-notes/0.9.0.md +24 -7
  87. package/docs/migration/v0.8-to-v0.9.md +124 -19
  88. package/package.json +1 -1
@@ -33,6 +33,7 @@ import { appendEvent } from "../../core/events.js";
33
33
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
34
34
  import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
35
35
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
36
+ import { resolveTypeConventions, typeConventionRef } from "../../core/standards/resolve-type-conventions.js";
36
37
  import { getStateDbPath, openStateDatabase, withStateDb } from "../../core/state-db.js";
37
38
  import { repairTruncatedDescription } from "../../core/text-truncation.js";
38
39
  import { warn } from "../../core/warn.js";
@@ -244,15 +245,15 @@ function resolveHarness(type, harnesses) {
244
245
  * so the accept-time descriptionQualityValidator passes — same pattern as
245
246
  * the consolidate-writer fix at consolidate.ts.
246
247
  */
247
- function buildCandidateProposal(candidate, sourceRef) {
248
- const ref = `${candidate.type}:${candidate.name}`;
248
+ function buildCandidateProposal(candidate, sourceRef, sessionAssetRef) {
249
+ const ref = deriveExtractCandidateRef(candidate, sourceRef);
249
250
  // Post-generation repair pass (#556): deterministically complete a
250
251
  // description the LLM sliced mid-sentence before it reaches the
251
252
  // auto-accept validators. No-op (byte-identical) for valid descriptions.
252
253
  const description = repairTruncatedDescription(candidate.description, candidate.body);
253
254
  const fm = {
254
255
  description,
255
- sources: [`session:${sourceRef.harness}:${sourceRef.sessionId}`],
256
+ ...(sessionAssetRef ? { xrefs: [sessionAssetRef] } : {}),
256
257
  };
257
258
  if (candidate.type === "lesson" && candidate.when_to_use) {
258
259
  fm.when_to_use = candidate.when_to_use;
@@ -269,6 +270,36 @@ function buildCandidateProposal(candidate, sourceRef) {
269
270
  const content = assembleAsset(fm, candidate.body);
270
271
  return { ref, content, description };
271
272
  }
273
+ function canonicalSegment(value) {
274
+ return value
275
+ .trim()
276
+ .toLowerCase()
277
+ .replace(/[^a-z0-9-]+/g, "-")
278
+ .replace(/-+/g, "-")
279
+ .replace(/^-|-$/g, "");
280
+ }
281
+ export function deriveExtractCandidateRef(candidate, sourceRef) {
282
+ const candidateParts = candidate.name.split("/").map(canonicalSegment).filter(Boolean);
283
+ const leaf = candidateParts.at(-1) ?? "extracted-insight";
284
+ if (candidate.type === "memory" || candidate.type === "lesson") {
285
+ const projectName = sourceRef.projectHint?.split(/[\\/]/).filter(Boolean).at(-1);
286
+ const scope = projectName ? canonicalSegment(projectName) : "";
287
+ return `${candidate.type}:${scope ? `${scope}/` : ""}${leaf}`;
288
+ }
289
+ return `knowledge:${leaf}`;
290
+ }
291
+ function resolveExtractStandards(stashDir) {
292
+ const sections = [];
293
+ const general = resolveStashStandards(stashDir);
294
+ if (general)
295
+ sections.push(general);
296
+ for (const type of ["memory", "lesson", "knowledge"]) {
297
+ const body = resolveTypeConventions(stashDir, type);
298
+ if (body)
299
+ sections.push(`# ${typeConventionRef(type)} (soft per-type conventions — guidance, not enforced)\n${body}`);
300
+ }
301
+ return sections.join("\n\n");
302
+ }
272
303
  /**
273
304
  * Canonicalize a session's content into a single deterministic string for
274
305
  * hashing (#602). Each event is rendered `<role>\n<text>` and events are joined
@@ -473,10 +504,12 @@ standardsContext) {
473
504
  }
474
505
  const payload = parseExtractPayload(llmRaw);
475
506
  const proposalIds = [];
507
+ // Provenance refs are added only after the cited session asset exists.
508
+ const sessionAsset = await maybeWriteSessionAsset();
476
509
  if (payload.candidates.length === 0) {
477
510
  appendEvent({
478
511
  eventType: "extract_invoked",
479
- ref: `session:${harness.name}:${sessionRef.sessionId}`,
512
+ ...(sessionAsset.sessionAssetRef ? { ref: sessionAsset.sessionAssetRef } : {}),
480
513
  metadata: {
481
514
  outcome: "no_candidates",
482
515
  sessionId: sessionRef.sessionId,
@@ -487,7 +520,6 @@ standardsContext) {
487
520
  preFilterOutput: filtered.stats.outputCount,
488
521
  },
489
522
  }, ctx);
490
- const sessionAsset = await maybeWriteSessionAsset();
491
523
  return {
492
524
  sessionId: sessionRef.sessionId,
493
525
  harness: harness.name,
@@ -510,8 +542,9 @@ standardsContext) {
510
542
  // dedup+quality pass promotes them. Default OFF.
511
543
  // The invocation boundary resolves this from the frozen extract process.
512
544
  for (const candidate of payload.candidates) {
545
+ const built = buildCandidateProposal(candidate, data.ref, sessionAsset.sessionAssetRef);
513
546
  if (dryRun) {
514
- proposalIds.push(`dry-run:${candidate.type}:${candidate.name}`);
547
+ proposalIds.push(`dry-run:${built.ref}`);
515
548
  continue;
516
549
  }
517
550
  try {
@@ -527,7 +560,7 @@ standardsContext) {
527
560
  const effectiveConfidence = gateResult.effectiveConfidence;
528
561
  if (gateResult.warning)
529
562
  warn(gateResult.warning);
530
- const { ref, content, description } = buildCandidateProposal(candidate, sessionRef);
563
+ const { ref, content, description } = built;
531
564
  const result = createProposal(stashDir, {
532
565
  ref,
533
566
  source: "extract",
@@ -538,7 +571,7 @@ standardsContext) {
538
571
  description,
539
572
  ...(candidate.when_to_use ? { when_to_use: candidate.when_to_use } : {}),
540
573
  ...(effectiveConfidence !== undefined ? { confidence: effectiveConfidence } : {}),
541
- sources: [`session:${sessionRef.harness}:${sessionRef.sessionId}`],
574
+ ...(sessionAsset.sessionAssetRef ? { xrefs: [sessionAsset.sessionAssetRef] } : {}),
542
575
  evidence: candidate.evidence,
543
576
  // #615 WS-0: mirror ordered-action + outcome data in the proposal
544
577
  // frontmatter record so downstream tooling can read it without
@@ -569,7 +602,7 @@ standardsContext) {
569
602
  }
570
603
  appendEvent({
571
604
  eventType: "extract_invoked",
572
- ref: `session:${harness.name}:${sessionRef.sessionId}`,
605
+ ...(sessionAsset.sessionAssetRef ? { ref: sessionAsset.sessionAssetRef } : {}),
573
606
  metadata: {
574
607
  outcome: "candidates_queued",
575
608
  sessionId: sessionRef.sessionId,
@@ -581,7 +614,6 @@ standardsContext) {
581
614
  preFilterOutput: filtered.stats.outputCount,
582
615
  },
583
616
  }, ctx);
584
- const sessionAsset = await maybeWriteSessionAsset();
585
617
  return {
586
618
  sessionId: sessionRef.sessionId,
587
619
  harness: harness.name,
@@ -827,7 +859,7 @@ export async function akmExtract(options) {
827
859
  // Stash authoring standards (convention/meta fact bodies) for non-wiki
828
860
  // extract output. Resolved ONCE per run and threaded into each session's
829
861
  // prompt so facts are not re-read per session.
830
- const extractStandardsContext = resolveStashStandards(stashDir);
862
+ const extractStandardsContext = resolveExtractStandards(stashDir);
831
863
  for (const summary of candidates) {
832
864
  // #602 — the already-extracted skip moved INTO processSession (the content
833
865
  // hash needs the session body, only available after readSession). The prior
@@ -3,7 +3,6 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { loadConfig } from "../../core/config/config.js";
5
5
  import { UsageError } from "../../core/errors.js";
6
- import { appendEvent } from "../../core/events.js";
7
6
  import { withStateDb } from "../../core/state-db.js";
8
7
  import { info, warn } from "../../core/warn.js";
9
8
  import { getPhaseThreshold } from "../../storage/repositories/improve-runs-repository.js";
@@ -119,42 +118,29 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
119
118
  continue;
120
119
  }
121
120
  try {
122
- const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
123
- stamp(promotion.proposal.id, {
124
- outcome: "auto-accepted",
125
- reason: promoteReason,
126
- confidence,
127
- thresholds: { autoAccept: effectiveThreshold },
128
- ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
129
- gate: gateLabel,
130
- });
131
- // Resolve the eligibilitySource: exploration-promoted proposals get
132
- // eligibilitySource="exploration" (WS-4); normal auto-accepts carry
133
- // whatever the proposal was tagged with at selection time.
121
+ const target = resolvedConfig.defaultWriteTarget;
134
122
  const resolvedEligibilitySource = isExploration
135
123
  ? "exploration"
136
- : promotion.proposal.eligibilitySource;
137
- appendEvent({
138
- eventType: "promoted",
139
- ref: promotion.ref,
140
- metadata: {
141
- proposalId: promotion.proposal.id,
142
- source: promotion.proposal.source,
143
- ...(promotion.proposal.sourceRun !== undefined ? { sourceRun: promotion.proposal.sourceRun } : {}),
144
- assetPath: promotion.assetPath,
124
+ : currentProposal?.eligibilitySource;
125
+ const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {
126
+ ...(target ? { target } : {}),
127
+ eventMetadata: {
145
128
  autoAccept: true,
146
129
  confidence,
147
130
  threshold: effectiveThreshold,
148
131
  phase: cfg.phase,
149
- // Attribution tagging: carry the eligibility lane from the proposal
150
- // record onto the auto-accept promoted event so the lane survives to
151
- // accept time even when promotion happens in a later run.
152
132
  ...(resolvedEligibilitySource !== undefined ? { eligibilitySource: resolvedEligibilitySource } : {}),
153
- // WS-4: mark exploration promotions so health/telemetry can
154
- // distinguish them from calibration-signal promotions.
155
133
  ...(isExploration ? { explorationBudget: true } : {}),
156
134
  },
157
- }, cfg.eventsCtx ?? {});
135
+ }, undefined);
136
+ stamp(promotion.proposal.id, {
137
+ outcome: "auto-accepted",
138
+ reason: promoteReason,
139
+ confidence,
140
+ thresholds: { autoAccept: effectiveThreshold },
141
+ ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
142
+ gate: gateLabel,
143
+ });
158
144
  if (isExploration) {
159
145
  info(`[improve] exploration-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)}; budgetRemaining=${explorationRemaining})`);
160
146
  }
@@ -11,8 +11,8 @@ import { getCacheDir } from "../../core/paths.js";
11
11
  import { redactSensitiveText } from "../../core/redaction.js";
12
12
  import { withStateDb } from "../../core/state-db.js";
13
13
  import { clearLogFile, setLogFile } from "../../core/warn.js";
14
+ import { resolveWriteTarget } from "../../core/write-source.js";
14
15
  import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
15
- import { resolveSourceEntries } from "../../indexer/search/search-source.js";
16
16
  import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
17
17
  import { parseFlagValue } from "../../output/context.js";
18
18
  import { getActiveCanaries, queryRecentCycleMetrics } from "../../storage/repositories/canaries-repository.js";
@@ -176,6 +176,7 @@ export const improveCommand = defineCommand({
176
176
  const skipIfLocked = args["skip-if-locked"];
177
177
  const strategyArg = getStringArg(args, "strategy");
178
178
  const effectiveConfig = loadConfig();
179
+ const writeTarget = resolveWriteTarget(effectiveConfig, targetArg, { requireWritable: !dryRun });
179
180
  // Resolve every enabled model-backed process before logging, signal
180
181
  // lifecycle setup, or any filesystem/database side effect.
181
182
  const resolvedPlan = resolveImprovePlan(strategyArg, effectiveConfig);
@@ -202,7 +203,7 @@ export const improveCommand = defineCommand({
202
203
  // was minted at end-of-run, so SIGTERM'd runs (cron timeout) left no
203
204
  // row in improve_runs and effectively disappeared from `akm health`.
204
205
  const runId = buildImproveRunId();
205
- const primaryStashDir = resolveSourceEntries(undefined, effectiveConfig)[0]?.path;
206
+ const primaryStashDir = writeTarget.source.path;
206
207
  const scopeArg = getStringArg(args, "scope");
207
208
  const inferredScopeMode = (scopeArg ?? "").includes(":") ? "ref" : scopeArg ? "type" : "all";
208
209
  // Signal handler + exception path both flow through this helper so
@@ -248,6 +249,7 @@ export const improveCommand = defineCommand({
248
249
  dryRun,
249
250
  resolvedPlan,
250
251
  target: targetArg,
252
+ writeTarget,
251
253
  autoAccept,
252
254
  ...(runId !== undefined ? { runId } : {}),
253
255
  ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
@@ -67,7 +67,8 @@ export function resolveImproveStrategy(name, config) {
67
67
  const valid = [...new Set([...Object.keys(BUILTIN_STRATEGIES), ...Object.keys(userStrategies)])].sort();
68
68
  throw new ConfigError(`Improve strategy "${selectedName}" not found. Valid strategies: ${valid.join(", ")}.`, "UNKNOWN_IMPROVE_STRATEGY");
69
69
  }
70
- const baseStrategy = BUILTIN_STRATEGIES[selectedName] ?? BUILTIN_STRATEGIES.default;
70
+ const selectedStrategy = BUILTIN_STRATEGIES[selectedName] ?? {};
71
+ const baseStrategy = deepMergeConfig(BUILTIN_STRATEGIES.default, selectedStrategy);
71
72
  const resolved = deepMergeConfig(baseStrategy, (userStrategies[selectedName] ?? {}));
72
73
  return { name: selectedName, config: resolved };
73
74
  }
@@ -13,6 +13,7 @@ import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
13
13
  import { redactSensitiveText } from "../../core/redaction.js";
14
14
  import { openStateDatabase } from "../../core/state-db.js";
15
15
  import { info, warn } from "../../core/warn.js";
16
+ import { resolveWritable, resolveWriteTarget } from "../../core/write-source.js";
16
17
  import { closeDatabase, getEntryCount, openExistingDatabase } from "../../indexer/db/db.js";
17
18
  import { ensureIndex } from "../../indexer/ensure-index.js";
18
19
  import { akmIndex } from "../../indexer/indexer.js";
@@ -21,7 +22,7 @@ import { collectEngineCredentialValues } from "../../integrations/agent/engine-r
21
22
  import { materializeLlmRunnerConnection } from "../../integrations/agent/runner.js";
22
23
  import { installLlmUsagePersistence } from "../../llm/usage-persist.js";
23
24
  import { withLlmStage } from "../../llm/usage-telemetry.js";
24
- import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
25
+ import { isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
25
26
  import { drainProposals } from "../proposal/drain.js";
26
27
  import { resolveDrainPolicy } from "../proposal/drain-policies.js";
27
28
  import { akmDistill } from "./distill.js";
@@ -41,6 +42,7 @@ import { runImprovePreparationStage } from "./preparation.js";
41
42
  import { DEFAULT_DUE_DAYS, filterProactiveDue } from "./proactive-maintenance.js";
42
43
  import { akmReflect } from "./reflect.js";
43
44
  import { errMessage } from "./shared.js";
45
+ import { shouldReadLegacyBareImproveState } from "./source-identity.js";
44
46
  export { resetHeldProcessLocks } from "./locks.js";
45
47
  // Re-exported from ./loop-stages for test importers (improve-db-locking).
46
48
  export { runImproveMaintenancePasses } from "./loop-stages.js";
@@ -99,7 +101,14 @@ export async function akmImprove(options = {}) {
99
101
  const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
100
102
  // Resolve the improve profile for this run. Profile drives type filtering,
101
103
  // process gating, and default autoAccept/limit values.
102
- const _earlyConfig = options.config ?? loadConfig();
104
+ let _earlyConfig = options.config ?? loadConfig();
105
+ const writeTarget = options.writeTarget ??
106
+ (options.target
107
+ ? resolveWriteTarget(_earlyConfig, options.target, { requireWritable: !options.dryRun })
108
+ : undefined);
109
+ if (writeTarget && options.target) {
110
+ _earlyConfig = { ..._earlyConfig, defaultWriteTarget: writeTarget.source.name };
111
+ }
103
112
  const resolvedPlan = options.resolvedPlan ??
104
113
  resolveImprovePlan(options.strategy, _earlyConfig, {
105
114
  repairValidationFailures: options.repairValidationFailures,
@@ -115,6 +124,20 @@ export async function akmImprove(options = {}) {
115
124
  // Pin nested calls and quality gates to the same config snapshot as the
116
125
  // invocation plan. They must never reload a changed config mid-run.
117
126
  config: _earlyConfig,
127
+ ...(writeTarget
128
+ ? {
129
+ target: writeTarget.source.name,
130
+ writeTarget,
131
+ stashDir: writeTarget.source.path,
132
+ consolidateOptions: {
133
+ ...options.consolidateOptions,
134
+ target: writeTarget.source.name,
135
+ writeTarget,
136
+ },
137
+ }
138
+ : {}),
139
+ legacyBareState: options.legacyBareState ??
140
+ shouldReadLegacyBareImproveState(writeTarget?.source.name, writeTarget?.source.path, _earlyConfig),
118
141
  autoAccept: options.autoAccept ?? improveProfile.autoAccept,
119
142
  // Profile-level limit, then process-level reflect.limit as fallback.
120
143
  // CLI --limit takes precedence over both.
@@ -130,6 +153,8 @@ export async function akmImprove(options = {}) {
130
153
  catch {
131
154
  primaryStashDir = undefined;
132
155
  }
156
+ const syncRepoDir = writeTarget?.source.repoPath ?? primaryStashDir;
157
+ const initialGitPaths = syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
133
158
  // C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
134
159
  // command boundary — before the first `await` below. Every state.db open in
135
160
  // this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
@@ -310,6 +335,8 @@ export async function akmImprove(options = {}) {
310
335
  const maxAccepts = triageConfig?.maxAcceptsPerRun ?? 25;
311
336
  triageDrain = await drainProposalsFn({
312
337
  stashDir: primaryStashDir,
338
+ ...(options.target ? { target: options.target } : {}),
339
+ config: options.config,
313
340
  policy,
314
341
  applyMode,
315
342
  maxAccepts,
@@ -412,15 +439,29 @@ export async function akmImprove(options = {}) {
412
439
  // after the db-backed context is installed inside the try use the live handle.
413
440
  const effectiveSync = { ...improveProfile.sync, ...options.sync };
414
441
  const commitStashBatch = (messageContext) => {
415
- if (!primaryStashDir || effectiveSync.enabled === false || !isGitBackedStash(primaryStashDir)) {
442
+ const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
443
+ if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
416
444
  return undefined;
417
445
  }
418
446
  const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
419
- const writableOverride = resolveWritableOverride(_earlyConfig);
420
- const push = effectiveSync.push !== false;
447
+ const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(_earlyConfig);
448
+ const push = options.sync?.push ?? writeTarget?.config.options?.pushOnCommit ?? improveProfile.sync?.push ?? true;
421
449
  const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
422
450
  try {
423
- const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir: primaryStashDir });
451
+ const assetRoot = writeTarget?.source.path ?? primaryStashDir;
452
+ const assetPrefix = assetRoot ? path.relative(repoDir, assetRoot).replaceAll(path.sep, "/") : "";
453
+ const paths = listGitChangedPaths(repoDir).filter((changedPath) => {
454
+ if (initialGitPaths.has(changedPath))
455
+ return false;
456
+ if (path.basename(changedPath).includes(".lock"))
457
+ return false;
458
+ return !assetPrefix || changedPath === assetPrefix || changedPath.startsWith(`${assetPrefix}/`);
459
+ });
460
+ const syncResult = saveGitStashFn(undefined, message, writableOverride, {
461
+ push,
462
+ repoDir,
463
+ paths,
464
+ });
424
465
  appendEvent({
425
466
  eventType: "stash_synced",
426
467
  metadata: {
@@ -621,8 +662,8 @@ export async function akmImprove(options = {}) {
621
662
  let postLockLoopRefs = preparation.loopRefs;
622
663
  if (proactiveLoopRefs.length > 0) {
623
664
  const proactiveRefStrs = proactiveLoopRefs.map((r) => r.ref);
624
- const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect");
625
- const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill");
665
+ const freshReflectTs = buildLatestProposalTsMap(proactiveRefStrs, "reflect", options.target, options.legacyBareState);
666
+ const freshDistillTs = buildLatestProposalTsMap(proactiveRefStrs, "distill", options.target, options.legacyBareState);
626
667
  const pmDueDays = improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS;
627
668
  const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, freshReflectTs, freshDistillTs, pmDueDays, Date.now()).map((r) => r.ref));
628
669
  const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
@@ -38,6 +38,7 @@ import { akmProcedural } from "./procedural.js";
38
38
  import { akmRecombine } from "./recombine.js";
39
39
  import { recordNoOp, resetConsecutiveNoOps } from "./salience.js";
40
40
  import { errMessage, refSlug } from "./shared.js";
41
+ import { durableImproveRef } from "./source-identity.js";
41
42
  // ── improve loop / post-loop / maintenance stages ───────────────────
42
43
  // The cycle stages run by akmImprove, extracted from improve.ts.
43
44
  export async function runImproveLoopStage(args) {
@@ -210,11 +211,13 @@ export async function runImproveLoopStage(args) {
210
211
  const reflectProfileRunner = resolvedPlan.processes.reflect.runner;
211
212
  const reflectCallArgs = {
212
213
  ref: planned.ref,
214
+ ...(options.target ? { sourceName: options.target } : {}),
215
+ ...(options.legacyBareState ? { legacyBareState: true } : {}),
213
216
  task: options.task,
214
217
  // Active strategy supplies non-engine process tuning.
215
218
  ...(improveProfile ? { improveProfile } : {}),
216
219
  config: options.config,
217
- ...(options.stashDir ? { stashDir: options.stashDir } : {}),
220
+ ...(primaryStashDir ? { stashDir: primaryStashDir } : {}),
218
221
  ...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
219
222
  eventSource: "improve",
220
223
  // #639 — resolve the low-value filter from the ACTIVE improve profile
@@ -347,7 +350,7 @@ export async function runImproveLoopStage(args) {
347
350
  // the asset changed; reset the counter so the dampener lifts.
348
351
  if (isNoChange && eventsCtx?.db) {
349
352
  try {
350
- recordNoOp(eventsCtx.db, planned.ref);
353
+ recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.target));
351
354
  }
352
355
  catch {
353
356
  // best-effort: plasticity counter failure never blocks the run
@@ -355,7 +358,7 @@ export async function runImproveLoopStage(args) {
355
358
  }
356
359
  else if (reflectResult.ok && eventsCtx?.db) {
357
360
  try {
358
- resetConsecutiveNoOps(eventsCtx.db, planned.ref);
361
+ resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.target));
359
362
  }
360
363
  catch {
361
364
  // best-effort
@@ -482,8 +485,10 @@ export async function runImproveLoopStage(args) {
482
485
  }
483
486
  const distillResult = await withLlmStage("distill", () => distillFn({
484
487
  ref: planned.ref,
488
+ ...(options.target ? { sourceName: options.target } : {}),
489
+ ...(options.legacyBareState ? { legacyBareState: true } : {}),
485
490
  ...(parsedPlannedRef.type === "memory" ? { proposalKind: "auto" } : {}),
486
- ...(options.stashDir ? { stashDir: options.stashDir } : {}),
491
+ ...(primaryStashDir ? { stashDir: primaryStashDir } : {}),
487
492
  // Active profile so distill's per-process reads honor `--profile`.
488
493
  ...(improveProfile ? { improveProfile } : {}),
489
494
  config: options.config,
@@ -512,10 +517,10 @@ export async function runImproveLoopStage(args) {
512
517
  if (eventsCtx?.db) {
513
518
  try {
514
519
  if (distillResult.outcome === "quality_rejected" || distillResult.outcome === "skipped") {
515
- recordNoOp(eventsCtx.db, planned.ref);
520
+ recordNoOp(eventsCtx.db, durableImproveRef(planned.ref, options.target));
516
521
  }
517
522
  else if (distillResult.outcome === "queued") {
518
- resetConsecutiveNoOps(eventsCtx.db, planned.ref);
523
+ resetConsecutiveNoOps(eventsCtx.db, durableImproveRef(planned.ref, options.target));
519
524
  }
520
525
  }
521
526
  catch {
@@ -31,7 +31,19 @@ import { makeGateConfig, resolveExtractConfidence, runAutoAcceptGate } from "./i
31
31
  import { applyMemoryCleanup } from "./memory/memory-improve.js";
32
32
  import { computeProxyAdequacy, getAllAssetOutcomes, getOutcomeScoresByRef, OUTCOME_SCORE_MAX, outcomeScoreToSalience, updateAssetOutcome, } from "./outcome-loop.js";
33
33
  import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, selectProactiveMaintenanceRefs } from "./proactive-maintenance.js";
34
- import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getConsecutiveNoOps, getLastUseMsByRef, isContentEncodingRow, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
34
+ import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getLastUseMsByRef, isContentEncodingRow, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
35
+ import { bareImproveRef, durableImproveRef, improveStateReadRefs } from "./source-identity.js";
36
+ function readAssetSalienceForImproveRef(db, ref, sourceName, includeLegacyBare = false) {
37
+ for (const key of improveStateReadRefs(ref, sourceName, includeLegacyBare)) {
38
+ const row = getAssetSalience(db, key);
39
+ if (row)
40
+ return row;
41
+ }
42
+ return undefined;
43
+ }
44
+ function readConsecutiveNoOpsForImproveRef(db, ref, sourceName, includeLegacyBare = false) {
45
+ return readAssetSalienceForImproveRef(db, ref, sourceName, includeLegacyBare)?.consecutive_no_ops ?? 0;
46
+ }
35
47
  // ── improve preparation stage ───────────────────────
36
48
  // The pre-loop preparation pipeline (consolidation, session-extract, validation/
37
49
  // repair, eligibility partitioning, selectors) extracted from improve.ts.
@@ -814,9 +826,9 @@ export async function runImprovePreparationStage(args) {
814
826
  // Per-ref queries would be N+1 and the planner is already the hottest path
815
827
  // in `akm improve`.
816
828
  const candidateRefs = postCleanupRefs.filter((r) => !validationFailureRefs.has(r.ref)).map((r) => r.ref);
817
- const latestFeedbackTs = buildLatestFeedbackTsMap(candidateRefs, feedbackSinceCutoff);
818
- const lastReflectProposalTs = buildLatestProposalTsMap(candidateRefs, "reflect");
819
- const lastDistillProposalTs = buildLatestProposalTsMap(candidateRefs, "distill");
829
+ const latestFeedbackTs = buildLatestFeedbackTsMap(candidateRefs, feedbackSinceCutoff, options.target, options.legacyBareState);
830
+ const lastReflectProposalTs = buildLatestProposalTsMap(candidateRefs, "reflect", options.target, options.legacyBareState);
831
+ const lastDistillProposalTs = buildLatestProposalTsMap(candidateRefs, "distill", options.target, options.legacyBareState);
820
832
  // Refs the distill signal-delta gate rejected at planning time. The main
821
833
  // loop reads this to skip distill for these refs without re-checking
822
834
  // eligibility per iteration.
@@ -951,15 +963,17 @@ export async function runImprovePreparationStage(args) {
951
963
  // to feedbackSinceCutoff (same as the old inline `(e.ts ?? "") >= cutoff` guard).
952
964
  const feedbackSummary = new Map();
953
965
  {
954
- const feedbackCandidateSet = new Set([...processableRefs, ...noFeedbackPool].map((r) => r.ref));
966
+ const feedbackCandidateRefs = [...processableRefs, ...noFeedbackPool].map((r) => r.ref);
967
+ const feedbackCandidateSet = new Set(feedbackCandidateRefs);
968
+ const feedbackRefByDurableKey = new Map(feedbackCandidateRefs.flatMap((ref) => improveStateReadRefs(ref, options.target, options.legacyBareState).map((key) => [key, ref])));
955
969
  if (feedbackCandidateSet.size > 0) {
956
970
  // Fetch ALL feedback events in one query (no ref filter, no since filter =
957
971
  // single full table scan). Filtering per-ref in memory avoids N sequential
958
972
  // state.db opens — the dominant FD-leak path on large stashes.
959
973
  const { events: allFeedbackEvents } = readEvents({ type: "feedback" }, eventsCtx);
960
974
  for (const e of allFeedbackEvents) {
961
- const ref = e.ref;
962
- if (!ref || !feedbackCandidateSet.has(ref))
975
+ const ref = e.ref ? feedbackRefByDurableKey.get(e.ref) : undefined;
976
+ if (!ref)
963
977
  continue;
964
978
  const entry = feedbackSummary.get(ref) ?? { hasSignal: false, positive: 0, negative: 0 };
965
979
  const meta = e.metadata;
@@ -1021,8 +1035,12 @@ export async function runImprovePreparationStage(args) {
1021
1035
  // heavily-retrieved, one never-touched — would receive identical rankScores.
1022
1036
  // Fix (WS-1 blocker 3): union the feedback pool into the lookup.
1023
1037
  const allCandidateRefs = [...new Set([...signalFiltered, ...noFeedbackCandidates].map((r) => r.ref))];
1024
- retrievalCounts = getRetrievalCounts(dbForRetrieval, allCandidateRefs);
1025
- lastUseMsForProactive = getLastUseMsByRef(dbForRetrieval, noFeedbackCandidates.map((r) => r.ref));
1038
+ retrievalCounts = getRetrievalCounts(dbForRetrieval, allCandidateRefs, {
1039
+ sourceName: options.target,
1040
+ stashDir: primaryStashDir,
1041
+ includeLegacyBare: options.legacyBareState || !options.target,
1042
+ });
1043
+ lastUseMsForProactive = getLastUseMsByRef(dbForRetrieval, noFeedbackCandidates.map((r) => r.ref), primaryStashDir);
1026
1044
  // High-retrieval signal-delta (simplified rule, 0.8.0): a no-feedback
1027
1045
  // ref qualifies exactly once — when it has actually been retrieved
1028
1046
  // (retrievalCount ≥ 1) AND retrievalCount ≥ threshold AND no prior reflect
@@ -1160,7 +1178,7 @@ export async function runImprovePreparationStage(args) {
1160
1178
  // found later in the scan lost its slot to an earlier lower-scoring one.
1161
1179
  const qualifying = [];
1162
1180
  for (const r of candidates) {
1163
- const row = getAssetSalience(dbForHighSalience, r.ref);
1181
+ const row = readAssetSalienceForImproveRef(dbForHighSalience, r.ref, options.target, options.legacyBareState);
1164
1182
  if (row &&
1165
1183
  isContentEncodingRow(row, parseAssetRef(r.ref).type) &&
1166
1184
  row.encoding_salience >= salienceThreshold &&
@@ -1283,7 +1301,7 @@ export async function runImprovePreparationStage(args) {
1283
1301
  let dbForSalience;
1284
1302
  try {
1285
1303
  dbForSalience = openExistingDatabase();
1286
- lastUseMsByRef = getLastUseMsByRef(dbForSalience, mergedRefs.map((r) => r.ref));
1304
+ lastUseMsByRef = getLastUseMsByRef(dbForSalience, mergedRefs.map((r) => r.ref), primaryStashDir);
1287
1305
  }
1288
1306
  catch (err) {
1289
1307
  rethrowIfTestIsolationError(err);
@@ -1439,7 +1457,7 @@ export async function runImprovePreparationStage(args) {
1439
1457
  withStateDb((dbForStoredEncoding) => {
1440
1458
  for (const r of mergedRefs) {
1441
1459
  const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
1442
- const row = getAssetSalience(dbForStoredEncoding, r.ref);
1460
+ const row = readAssetSalienceForImproveRef(dbForStoredEncoding, r.ref, options.target, options.legacyBareState);
1443
1461
  if (row && isContentEncodingRow(row, type)) {
1444
1462
  storedEncodingByRef.set(r.ref, row.encoding_salience);
1445
1463
  }
@@ -1532,7 +1550,15 @@ export async function runImprovePreparationStage(args) {
1532
1550
  // Step 7: stash-wide rank-change report BEFORE overwriting the table.
1533
1551
  //
1534
1552
  // Load ALL existing rows so rank positions are stash-relative, not pool-relative.
1535
- const existingAllScores = getAllRankScores(stateDb);
1553
+ const allStoredScores = getAllRankScores(stateDb);
1554
+ const existingAllScores = options.target
1555
+ ? new Map([...allStoredScores].flatMap(([ref, score]) => {
1556
+ const parsed = parseAssetRef(ref);
1557
+ return parsed.origin === options.target || (options.legacyBareState && !parsed.origin)
1558
+ ? [[bareImproveRef(ref), score]]
1559
+ : [];
1560
+ }))
1561
+ : allStoredScores;
1536
1562
  if (existingAllScores.size === 0) {
1537
1563
  // Scenario A: first WS-1 run — table empty.
1538
1564
  //
@@ -1629,7 +1655,7 @@ export async function runImprovePreparationStage(args) {
1629
1655
  }, eventsCtx);
1630
1656
  }
1631
1657
  for (const [ref, vector] of salienceMap) {
1632
- upsertAssetSalience(stateDb, ref, vector, nowForSalience);
1658
+ upsertAssetSalience(stateDb, durableImproveRef(ref, options.target), vector, nowForSalience);
1633
1659
  }
1634
1660
  }, { path: eventsCtx?.dbPath });
1635
1661
  }
@@ -1715,7 +1741,15 @@ export async function runImprovePreparationStage(args) {
1715
1741
  try {
1716
1742
  withStateDb((replayDb) => {
1717
1743
  const alreadyInPool = new Set(mergedRefs.map((r) => r.ref));
1718
- const allRankScores = getAllRankScores(replayDb);
1744
+ const storedRankScores = getAllRankScores(replayDb);
1745
+ const allRankScores = options.target
1746
+ ? new Map([...storedRankScores].flatMap(([ref, score]) => {
1747
+ const parsed = parseAssetRef(ref);
1748
+ return parsed.origin === options.target || (options.legacyBareState && !parsed.origin)
1749
+ ? [[bareImproveRef(ref), score]]
1750
+ : [];
1751
+ }))
1752
+ : storedRankScores;
1719
1753
  // Candidate universe = every salience row NOT already in the pool, ordered by
1720
1754
  // rank_score desc with a deterministic ref-string tie-break (mirrors the main
1721
1755
  // sort). Converged refs (consecutive_no_ops >= dampener threshold) are fully
@@ -1725,7 +1759,7 @@ export async function runImprovePreparationStage(args) {
1725
1759
  for (const [ref, rankScore] of allRankScores) {
1726
1760
  if (alreadyInPool.has(ref))
1727
1761
  continue;
1728
- const noOps = getConsecutiveNoOps(replayDb, ref);
1762
+ const noOps = readConsecutiveNoOpsForImproveRef(replayDb, ref, options.target, options.legacyBareState);
1729
1763
  if (noOps >= SALIENCE_NO_OP_DAMPEN_THRESHOLD) {
1730
1764
  convergedSkipped++;
1731
1765
  continue;
@@ -1802,7 +1836,7 @@ export async function runImprovePreparationStage(args) {
1802
1836
  const ownsNoOpDb = !eventsCtx?.db;
1803
1837
  try {
1804
1838
  for (const r of mergedRefs) {
1805
- noOpMap.set(r.ref, getConsecutiveNoOps(noOpDb, r.ref));
1839
+ noOpMap.set(r.ref, readConsecutiveNoOpsForImproveRef(noOpDb, r.ref, options.target, options.legacyBareState));
1806
1840
  }
1807
1841
  }
1808
1842
  finally {