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

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 (115) hide show
  1. package/CHANGELOG.md +40 -12
  2. package/README.md +7 -8
  3. package/SECURITY.md +5 -3
  4. package/dist/akm +44 -33
  5. package/dist/akm-migrate-storage +44 -33
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/improve-strategies/catchup.json +3 -1
  8. package/dist/assets/improve-strategies/consolidate.json +3 -1
  9. package/dist/assets/improve-strategies/frequent.json +3 -1
  10. package/dist/assets/improve-strategies/graph-refresh.json +3 -1
  11. package/dist/assets/improve-strategies/memory-focus.json +4 -1
  12. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
  13. package/dist/assets/improve-strategies/quick.json +3 -1
  14. package/dist/assets/improve-strategies/recombine-only.json +2 -0
  15. package/dist/assets/improve-strategies/reflect-distill.json +1 -0
  16. package/dist/assets/improve-strategies/synthesize.json +2 -0
  17. package/dist/assets/tasks/core/backup.yml +2 -2
  18. package/dist/cli/config-migrate.js +554 -26
  19. package/dist/cli.js +28 -11
  20. package/dist/commands/backup-cli.js +6 -4
  21. package/dist/commands/config-cli.js +10 -3
  22. package/dist/commands/feedback-cli.js +24 -7
  23. package/dist/commands/health/checks.js +94 -15
  24. package/dist/commands/health/html-report.js +23 -2
  25. package/dist/commands/health/improve-metrics.js +45 -6
  26. package/dist/commands/health/md-report.js +10 -1
  27. package/dist/commands/health/surfaces.js +2 -1
  28. package/dist/commands/health/windows.js +1 -1
  29. package/dist/commands/health.js +1 -1
  30. package/dist/commands/improve/anti-collapse.js +3 -3
  31. package/dist/commands/improve/collapse-detector.js +5 -5
  32. package/dist/commands/improve/consolidate.js +123 -66
  33. package/dist/commands/improve/distill/promote-memory.js +8 -6
  34. package/dist/commands/improve/distill/quality-gate.js +1 -1
  35. package/dist/commands/improve/distill-guards.js +1 -1
  36. package/dist/commands/improve/distill-promotion-policy.js +32 -26
  37. package/dist/commands/improve/distill.js +52 -36
  38. package/dist/commands/improve/eligibility.js +9 -8
  39. package/dist/commands/improve/extract-prompt.js +2 -2
  40. package/dist/commands/improve/extract.js +43 -11
  41. package/dist/commands/improve/improve-auto-accept.js +14 -28
  42. package/dist/commands/improve/improve-cli.js +4 -2
  43. package/dist/commands/improve/improve-strategies.js +2 -1
  44. package/dist/commands/improve/improve.js +49 -8
  45. package/dist/commands/improve/loop-stages.js +11 -6
  46. package/dist/commands/improve/preparation.js +51 -17
  47. package/dist/commands/improve/procedural.js +10 -6
  48. package/dist/commands/improve/recombine.js +31 -7
  49. package/dist/commands/improve/reflect.js +24 -14
  50. package/dist/commands/improve/salience.js +5 -1
  51. package/dist/commands/improve/source-identity.js +43 -0
  52. package/dist/commands/migrate-cli.js +36 -0
  53. package/dist/commands/mv-cli.js +647 -258
  54. package/dist/commands/proposal/drain.js +16 -4
  55. package/dist/commands/proposal/proposal-cli.js +3 -2
  56. package/dist/commands/proposal/proposal.js +16 -55
  57. package/dist/commands/proposal/repository.js +664 -58
  58. package/dist/commands/read/curate.js +4 -2
  59. package/dist/commands/read/knowledge.js +4 -1
  60. package/dist/commands/read/search.js +6 -2
  61. package/dist/commands/read/show.js +8 -7
  62. package/dist/commands/sources/self-update.js +156 -112
  63. package/dist/commands/sources/sources-cli.js +7 -2
  64. package/dist/commands/tasks/default-tasks.js +5 -5
  65. package/dist/commands/tasks/tasks-cli.js +7 -3
  66. package/dist/commands/tasks/tasks.js +261 -68
  67. package/dist/core/common.js +36 -3
  68. package/dist/core/config/config-io.js +2 -2
  69. package/dist/core/config/config-schema.js +15 -3
  70. package/dist/core/config/config.js +14 -13
  71. package/dist/core/file-lock.js +2 -2
  72. package/dist/core/improve-result.js +87 -7
  73. package/dist/core/migration-backup.js +816 -171
  74. package/dist/core/migration-operation.js +44 -0
  75. package/dist/core/state/migrations.js +4 -7
  76. package/dist/core/state-db.js +23 -7
  77. package/dist/core/write-source.js +86 -18
  78. package/dist/indexer/db/db.js +109 -53
  79. package/dist/indexer/index-writer-lock.js +38 -37
  80. package/dist/indexer/index-written-assets.js +73 -56
  81. package/dist/indexer/indexer.js +5 -1
  82. package/dist/indexer/search/search-source.js +2 -2
  83. package/dist/indexer/usage/usage-events.js +8 -2
  84. package/dist/integrations/agent/engine-resolution.js +14 -7
  85. package/dist/scripts/migrate-storage.js +887 -992
  86. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +494 -574
  87. package/dist/setup/detected-engines.js +7 -13
  88. package/dist/setup/engine-config.js +14 -4
  89. package/dist/setup/setup.js +1 -1
  90. package/dist/setup/steps/connection.js +1 -1
  91. package/dist/setup/steps/tasks.js +1 -1
  92. package/dist/sources/providers/git-stash.js +58 -21
  93. package/dist/sources/providers/git.js +1 -1
  94. package/dist/storage/engines/sqlite-migrations.js +138 -3
  95. package/dist/storage/repositories/events-repository.js +24 -0
  96. package/dist/storage/repositories/proposals-repository.js +14 -0
  97. package/dist/storage/repositories/task-history-repository.js +30 -3
  98. package/dist/tasks/backends/cron.js +71 -38
  99. package/dist/tasks/backends/exec-utils.js +55 -3
  100. package/dist/tasks/backends/launchd.js +241 -50
  101. package/dist/tasks/backends/schtasks.js +434 -59
  102. package/dist/tasks/command-executable.js +93 -0
  103. package/dist/tasks/embedded.js +2 -0
  104. package/dist/tasks/parser.js +142 -6
  105. package/dist/tasks/resolve-akm-bin.js +19 -4
  106. package/dist/tasks/runner.js +258 -93
  107. package/dist/tasks/schedule.js +108 -19
  108. package/dist/tasks/scheduler-invocation.js +86 -0
  109. package/dist/tasks/task-id.js +37 -0
  110. package/dist/workflows/db.js +24 -13
  111. package/dist/workflows/validator.js +24 -2
  112. package/docs/README.md +103 -0
  113. package/docs/migration/release-notes/0.9.0.md +28 -9
  114. package/docs/migration/v0.8-to-v0.9.md +186 -26
  115. package/package.json +2 -3
@@ -44,7 +44,13 @@ function deriveDescription(body, description) {
44
44
  }
45
45
  export function deriveKnowledgeRef(inputRef) {
46
46
  const parsed = parseAssetRef(inputRef);
47
- return `knowledge:${parsed.name}`;
47
+ const leaf = parsed.name.split("/").filter(Boolean).at(-1) ?? parsed.name;
48
+ const safe = leaf
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9-]+/g, "-")
51
+ .replace(/-+/g, "-")
52
+ .replace(/^-|-$/g, "");
53
+ return `knowledge:${safe}`;
48
54
  }
49
55
  function collectPromotionFeatures(input) {
50
56
  const parsed = parseAssetRef(input.inputRef);
@@ -112,14 +118,14 @@ function buildKnowledgeContent(input) {
112
118
  const body = parsedContent.content.trim();
113
119
  const description = typeof fm.description === "string" ? fm.description.trim() : undefined;
114
120
  const normalizedDescription = deriveDescription(body, description);
115
- const sources = [input.inputRef];
121
+ const xrefs = [input.inputRef];
116
122
  if (typeof fm.source === "string" && fm.source.trim())
117
- sources.push(fm.source.trim());
123
+ xrefs.push(fm.source.trim());
118
124
  const knowledgeFrontmatter = {
119
125
  ...(normalizedDescription ? { description: normalizedDescription } : {}),
120
126
  ...(Array.isArray(fm.tags) ? { tags: fm.tags } : {}),
121
127
  ...(typeof fm.observed_at === "string" && fm.observed_at.trim() ? { observed_at: fm.observed_at.trim() } : {}),
122
- sources,
128
+ xrefs,
123
129
  };
124
130
  return assembleAsset(knowledgeFrontmatter, body);
125
131
  }
@@ -517,7 +523,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
517
523
  score: 4.250000000000001,
518
524
  threshold: 3.8,
519
525
  knowledgeRef: "knowledge:deploy-vpn-required",
520
- content: "---\ndescription: VPN required before deploy\ntags:\n - deploy\n - ops\nobserved_at: 2026-04-20\nsources:\n - memory:deploy-vpn-required\n - skill:deploy\n---\n\nAlways connect the VPN before starting production deploys.\n",
526
+ content: "---\ndescription: VPN required before deploy\ntags:\n - deploy\n - ops\nobserved_at: 2026-04-20\nxrefs:\n - memory:deploy-vpn-required\n - skill:deploy\n---\n\nAlways connect the VPN before starting production deploys.\n",
521
527
  blockedBy: [],
522
528
  positiveSignals: [
523
529
  "2 positive feedback events",
@@ -543,7 +549,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
543
549
  score: 5.240000000000002,
544
550
  threshold: 3.8,
545
551
  knowledgeRef: "knowledge:release-smoke-test",
546
- content: "---\ndescription: Smoke test gates release\nobserved_at: 2026-04-18\nsources:\n - memory:release-smoke-test\n - skill:release\n---\n\nRun the smoke test before tagging a release candidate.\n",
552
+ content: "---\ndescription: Smoke test gates release\nobserved_at: 2026-04-18\nxrefs:\n - memory:release-smoke-test\n - skill:release\n---\n\nRun the smoke test before tagging a release candidate.\n",
547
553
  blockedBy: [],
548
554
  positiveSignals: [
549
555
  "3 positive feedback events",
@@ -569,7 +575,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
569
575
  score: 4.250000000000001,
570
576
  threshold: 3.8,
571
577
  knowledgeRef: "knowledge:kubernetes-rollout-check",
572
- content: "---\ndescription: Verify rollout status after apply\ntags:\n - k8s\nobserved_at: 2026-04-15\nsources:\n - memory:kubernetes-rollout-check\n - skill:k8s\n---\n\nCheck rollout status after kubectl apply before declaring success.\n",
578
+ content: "---\ndescription: Verify rollout status after apply\ntags:\n - k8s\nobserved_at: 2026-04-15\nxrefs:\n - memory:kubernetes-rollout-check\n - skill:k8s\n---\n\nCheck rollout status after kubectl apply before declaring success.\n",
573
579
  blockedBy: [],
574
580
  positiveSignals: [
575
581
  "2 positive feedback events",
@@ -595,7 +601,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
595
601
  score: 5.450000000000002,
596
602
  threshold: 3.8,
597
603
  knowledgeRef: "knowledge:incident-channel-rule",
598
- content: "---\ndescription: Incident bridge stays single-threaded\nobserved_at: 2026-04-12\nsources:\n - memory:incident-channel-rule\n - skill:incident\n---\n\nKeep one operator narrating decisions in the incident bridge to avoid conflicting instructions.\n",
604
+ content: "---\ndescription: Incident bridge stays single-threaded\nobserved_at: 2026-04-12\nxrefs:\n - memory:incident-channel-rule\n - skill:incident\n---\n\nKeep one operator narrating decisions in the incident bridge to avoid conflicting instructions.\n",
599
605
  blockedBy: [],
600
606
  positiveSignals: [
601
607
  "3 positive feedback events",
@@ -710,7 +716,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
710
716
  score: 4.1000000000000005,
711
717
  threshold: 3.8,
712
718
  knowledgeRef: "knowledge:feedback-conflict",
713
- content: "---\ndescription: VPN required before deploy\nobserved_at: 2026-04-20\nsources:\n - memory:feedback-conflict\n - skill:deploy\n---\n\nAlways connect the VPN before starting production deploys.\n",
719
+ content: "---\ndescription: VPN required before deploy\nobserved_at: 2026-04-20\nxrefs:\n - memory:feedback-conflict\n - skill:deploy\n---\n\nAlways connect the VPN before starting production deploys.\n",
714
720
  blockedBy: [],
715
721
  positiveSignals: [
716
722
  "2 positive feedback events",
@@ -758,7 +764,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
758
764
  score: 4.840000000000001,
759
765
  threshold: 3.8,
760
766
  knowledgeRef: "knowledge:staging-cutover-order",
761
- content: "---\ndescription: Cut over staging after migrations\ntags:\n - db\n - deploy\nobserved_at: 2026-04-10\nsources:\n - memory:staging-cutover-order\n - skill:database\n---\n\nRun database migrations before shifting staging traffic onto the new release.\n",
767
+ content: "---\ndescription: Cut over staging after migrations\ntags:\n - db\n - deploy\nobserved_at: 2026-04-10\nxrefs:\n - memory:staging-cutover-order\n - skill:database\n---\n\nRun database migrations before shifting staging traffic onto the new release.\n",
762
768
  blockedBy: [],
763
769
  positiveSignals: [
764
770
  "3 positive feedback events",
@@ -855,7 +861,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
855
861
  score: 5.600000000000002,
856
862
  threshold: 3.8,
857
863
  knowledgeRef: "knowledge:kafka-rebalance-note",
858
- content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nsources:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
864
+ content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nxrefs:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
859
865
  blockedBy: [],
860
866
  positiveSignals: [
861
867
  "3 positive feedback events",
@@ -882,7 +888,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
882
888
  score: 4.04,
883
889
  threshold: 3.8,
884
890
  knowledgeRef: "knowledge:gha-token-scope",
885
- content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nsources:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
891
+ content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nxrefs:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
886
892
  blockedBy: [],
887
893
  positiveSignals: [
888
894
  "2 positive feedback events",
@@ -932,7 +938,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
932
938
  score: 5.600000000000002,
933
939
  threshold: 3.8,
934
940
  knowledgeRef: "knowledge:terraform-state-location",
935
- content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nsources:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
941
+ content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nxrefs:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
936
942
  blockedBy: [],
937
943
  positiveSignals: [
938
944
  "3 positive feedback events",
@@ -984,7 +990,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
984
990
  score: 4.250000000000001,
985
991
  threshold: 3.8,
986
992
  knowledgeRef: "knowledge:cache-ttl-fact",
987
- content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nsources:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
993
+ content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nxrefs:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
988
994
  blockedBy: [],
989
995
  positiveSignals: [
990
996
  "2 positive feedback events",
@@ -1046,7 +1052,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1046
1052
  score: 3,
1047
1053
  threshold: 2,
1048
1054
  knowledgeRef: "knowledge:kafka-rebalance-note",
1049
- content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nsources:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
1055
+ content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nxrefs:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
1050
1056
  blockedBy: [],
1051
1057
  positiveSignals: ["baseline positive feedback rule"],
1052
1058
  negativeSignals: [],
@@ -1063,7 +1069,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1063
1069
  score: 2,
1064
1070
  threshold: 2,
1065
1071
  knowledgeRef: "knowledge:gha-token-scope",
1066
- content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nsources:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
1072
+ content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nxrefs:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
1067
1073
  blockedBy: [],
1068
1074
  positiveSignals: ["baseline positive feedback rule"],
1069
1075
  negativeSignals: [],
@@ -1080,7 +1086,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1080
1086
  score: 2,
1081
1087
  threshold: 2,
1082
1088
  knowledgeRef: "knowledge:helm-debug-guess",
1083
- content: "---\ndescription: Helm upgrade might need --debug\nobserved_at: 2026-04-05\nsources:\n - memory:helm-debug-guess\n - skill:helm\n---\n\nIt might help to add --debug to helm upgrade output during failures.\n",
1089
+ content: "---\ndescription: Helm upgrade might need --debug\nobserved_at: 2026-04-05\nxrefs:\n - memory:helm-debug-guess\n - skill:helm\n---\n\nIt might help to add --debug to helm upgrade output during failures.\n",
1084
1090
  blockedBy: [],
1085
1091
  positiveSignals: ["baseline positive feedback rule"],
1086
1092
  negativeSignals: [],
@@ -1097,7 +1103,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1097
1103
  score: 3,
1098
1104
  threshold: 2,
1099
1105
  knowledgeRef: "knowledge:terraform-state-location",
1100
- content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nsources:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
1106
+ content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nxrefs:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
1101
1107
  blockedBy: [],
1102
1108
  positiveSignals: ["baseline positive feedback rule"],
1103
1109
  negativeSignals: [],
@@ -1114,7 +1120,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1114
1120
  score: 2,
1115
1121
  threshold: 2,
1116
1122
  knowledgeRef: "knowledge:mixed-signal-rollback",
1117
- content: "---\ndescription: Rollback the cluster immediately\nobserved_at: 2026-04-03\nsources:\n - memory:mixed-signal-rollback\n - skill:incident\n---\n\nRollback the cluster immediately after any 5xx spike.\n",
1123
+ content: "---\ndescription: Rollback the cluster immediately\nobserved_at: 2026-04-03\nxrefs:\n - memory:mixed-signal-rollback\n - skill:incident\n---\n\nRollback the cluster immediately after any 5xx spike.\n",
1118
1124
  blockedBy: [],
1119
1125
  positiveSignals: ["baseline positive feedback rule"],
1120
1126
  negativeSignals: [],
@@ -1131,7 +1137,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1131
1137
  score: 2,
1132
1138
  threshold: 2,
1133
1139
  knowledgeRef: "knowledge:cache-ttl-fact",
1134
- content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nsources:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
1140
+ content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nxrefs:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
1135
1141
  blockedBy: [],
1136
1142
  positiveSignals: ["baseline positive feedback rule"],
1137
1143
  negativeSignals: [],
@@ -1187,7 +1193,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1187
1193
  score: 2,
1188
1194
  threshold: 3,
1189
1195
  knowledgeRef: "knowledge:kafka-rebalance-note",
1190
- content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nsources:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
1196
+ content: "---\ndescription: Pause consumers during rebalance\ntags:\n - kafka\n - ops\nobserved_at: 2026-04-08\nxrefs:\n - memory:kafka-rebalance-note\n - skill:kafka\n---\n\nPause consumers during partition rebalances to avoid duplicate processing while assignments settle.\n",
1191
1197
  blockedBy: [],
1192
1198
  positiveSignals: ["baseline metadata rule"],
1193
1199
  negativeSignals: [],
@@ -1204,7 +1210,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1204
1210
  score: 2,
1205
1211
  threshold: 3,
1206
1212
  knowledgeRef: "knowledge:gha-token-scope",
1207
- content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nsources:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
1213
+ content: "---\ndescription: Minimize GitHub token scopes\ntags:\n - gha\n - security\nobserved_at: 2026-04-07\nxrefs:\n - memory:gha-token-scope\n - skill:github-actions\n---\n\nUse the narrowest GitHub token scope that still allows the workflow step to succeed.\n",
1208
1214
  blockedBy: [],
1209
1215
  positiveSignals: ["baseline metadata rule"],
1210
1216
  negativeSignals: [],
@@ -1221,7 +1227,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1221
1227
  score: 2,
1222
1228
  threshold: 3,
1223
1229
  knowledgeRef: "knowledge:helm-debug-guess",
1224
- content: "---\ndescription: Helm upgrade might need --debug\nobserved_at: 2026-04-05\nsources:\n - memory:helm-debug-guess\n - skill:helm\n---\n\nIt might help to add --debug to helm upgrade output during failures.\n",
1230
+ content: "---\ndescription: Helm upgrade might need --debug\nobserved_at: 2026-04-05\nxrefs:\n - memory:helm-debug-guess\n - skill:helm\n---\n\nIt might help to add --debug to helm upgrade output during failures.\n",
1225
1231
  blockedBy: [],
1226
1232
  positiveSignals: ["baseline metadata rule"],
1227
1233
  negativeSignals: [],
@@ -1238,7 +1244,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1238
1244
  score: 2,
1239
1245
  threshold: 3,
1240
1246
  knowledgeRef: "knowledge:terraform-state-location",
1241
- content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nsources:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
1247
+ content: "---\ndescription: Use remote state locks\ntags:\n - terraform\nobserved_at: 2026-04-04\nxrefs:\n - memory:terraform-state-location\n - skill:terraform\n---\n\nUse remote state with locking enabled before applying shared Terraform stacks.\n",
1242
1248
  blockedBy: [],
1243
1249
  positiveSignals: ["baseline metadata rule"],
1244
1250
  negativeSignals: [],
@@ -1255,7 +1261,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1255
1261
  score: 2,
1256
1262
  threshold: 3,
1257
1263
  knowledgeRef: "knowledge:mixed-signal-rollback",
1258
- content: "---\ndescription: Rollback the cluster immediately\nobserved_at: 2026-04-03\nsources:\n - memory:mixed-signal-rollback\n - skill:incident\n---\n\nRollback the cluster immediately after any 5xx spike.\n",
1264
+ content: "---\ndescription: Rollback the cluster immediately\nobserved_at: 2026-04-03\nxrefs:\n - memory:mixed-signal-rollback\n - skill:incident\n---\n\nRollback the cluster immediately after any 5xx spike.\n",
1259
1265
  blockedBy: [],
1260
1266
  positiveSignals: ["baseline metadata rule"],
1261
1267
  negativeSignals: [],
@@ -1272,7 +1278,7 @@ export const DEFAULT_PROMOTION_POLICY_SELECTION = {
1272
1278
  score: 2,
1273
1279
  threshold: 3,
1274
1280
  knowledgeRef: "knowledge:cache-ttl-fact",
1275
- content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nsources:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
1281
+ content: "---\ndescription: Cache TTL defaults to five minutes\ntags:\n - cache\n - platform\nobserved_at: 2026-04-02\nxrefs:\n - memory:cache-ttl-fact\n - skill:platform\n---\n\nThe shared platform cache TTL defaults to five minutes unless the service opts out.\n",
1276
1282
  blockedBy: [],
1277
1283
  positiveSignals: ["baseline metadata rule"],
1278
1284
  negativeSignals: [],
@@ -34,14 +34,10 @@
34
34
  *
35
35
  * # Lesson-name derivation rule
36
36
  *
37
- * The proposed lesson ref is `lesson:<original-ref-slug>-lesson`, where
38
- * `<original-ref-slug>` is `<type>-<name>` from the parsed input ref (so
39
- * `skill:deploy` `lesson:skill-deploy-lesson`, and `team//memory:auth-tips`
40
- * `lesson:memory-auth-tips-lesson`). Origin prefixes are dropped from the
41
- * derived name so two sources with the same asset-type/name collapse onto
42
- * the same lesson queue entry rather than each generating its own — the
43
- * proposal queue tolerates duplicate refs (id is a UUID), so the human
44
- * reviewer can decide which one to accept.
37
+ * A nested input preserves its first legitimate scope segment
38
+ * (`memory:project-a/deploy` `lesson:project-a/memory-deploy-lesson`). An
39
+ * unscoped input stays flat; asset types are not project scopes. Origin prefixes
40
+ * remain durable provenance but are not embedded in the output path.
45
41
  *
46
42
  * # Why we do not call `runAgent`
47
43
  *
@@ -54,7 +50,7 @@ import fs from "node:fs";
54
50
  import distillKnowledgeSystemPrompt from "../../assets/prompts/distill-knowledge-system.md" with { type: "text" };
55
51
  import distillLessonSystemPrompt from "../../assets/prompts/distill-lesson-system.md" with { type: "text" };
56
52
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
57
- import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
53
+ import { assembleAsset, assembleAssetFromString } from "../../core/asset/asset-serialize.js";
58
54
  import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
59
55
  import { stripMarkdownFences } from "../../core/asset/markdown.js";
60
56
  import { authoringRulesForType } from "../../core/authoring-rules.js";
@@ -64,7 +60,7 @@ import { ConfigError, UsageError } from "../../core/errors.js";
64
60
  import { appendEvent, readEvents } from "../../core/events.js";
65
61
  import { lintLessonContent } from "../../core/lesson-lint.js";
66
62
  import { getDbPath } from "../../core/paths.js";
67
- import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
63
+ import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
68
64
  import { withStateDb } from "../../core/state-db.js";
69
65
  import { warnVerbose } from "../../core/warn.js";
70
66
  import { closeDatabase, getAllEntries, openIndexDatabase } from "../../indexer/db/db.js";
@@ -82,6 +78,7 @@ import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
82
78
  import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
83
79
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
84
80
  import { computeSalience, upsertAssetSalience } from "./salience.js";
81
+ import { bareImproveRef, durableImproveRef } from "./source-identity.js";
85
82
  // Re-exported for `reflect.ts`, which applies the same LLM-as-judge gate to
86
83
  // reflect proposals (R-5 / #374).
87
84
  export { runLessonQualityJudge };
@@ -125,14 +122,21 @@ export function deriveLessonRef(inputRef) {
125
122
  // distils into the same lesson namespace as `skill:deploy`. The proposal
126
123
  // id (a UUID) keeps the queue entries distinct, so collisions are not a
127
124
  // problem — and reviewers want to see them next to each other anyway.
128
- const slug = `${parsed.type}-${parsed.name}`.toLowerCase();
125
+ const parts = parsed.name.split("/");
126
+ const scope = parts.length > 1 ? parts.shift() : undefined;
127
+ const slug = `${parsed.type}-${parts.join("-")}`.toLowerCase();
129
128
  // Replace anything outside the canonical asset-name charset with `-`. Keep
130
129
  // it deterministic so re-runs produce the same ref.
131
130
  const safe = slug
132
131
  .replace(/[^a-z0-9-]+/g, "-")
133
132
  .replace(/-+/g, "-")
134
133
  .replace(/^-|-$/g, "");
135
- return `lesson:${safe}-lesson`;
134
+ const safeScope = scope
135
+ ?.toLowerCase()
136
+ .replace(/[^a-z0-9-]+/g, "-")
137
+ .replace(/-+/g, "-")
138
+ .replace(/^-|-$/g, "");
139
+ return `lesson:${safeScope ? `${safeScope}/` : ""}${safe}-lesson`;
136
140
  }
137
141
  // ── Content quality validators ──────────────────────────────────────────────
138
142
  //
@@ -254,7 +258,7 @@ export function assembleStructuredDistillMarkdown(payload, kind) {
254
258
  if (kind === "knowledge" && Array.isArray(payload.sources)) {
255
259
  const sources = payload.sources.filter((s) => typeof s === "string" && s.trim().length > 0);
256
260
  if (sources.length > 0)
257
- fm.sources = sources;
261
+ fm.xrefs = sources;
258
262
  }
259
263
  const fmLines = Object.entries(fm)
260
264
  .map(([k, v]) => {
@@ -429,6 +433,7 @@ export async function akmDistill(options) {
429
433
  }
430
434
  // Validate the ref shape up front so a typo never reaches the LLM.
431
435
  const parsedInputRef = parseAssetRef(inputRef);
436
+ const durableInputRef = durableImproveRef(inputRef, options.sourceName);
432
437
  const targetKind = options.proposalKind ?? "lesson";
433
438
  // Attribution tagging: spread into every distill_invoked event's metadata so
434
439
  // the lane that selected this asset is recorded uniformly across all outcome
@@ -457,7 +462,7 @@ export async function akmDistill(options) {
457
462
  : "Distill refuses lesson inputs — lessons are the distilled form, not a source.";
458
463
  appendEvent({
459
464
  eventType: "distill_invoked",
460
- ref: inputRef,
465
+ ref: durableInputRef,
461
466
  metadata: {
462
467
  outcome: "skipped",
463
468
  lessonRef: skippedRef,
@@ -485,7 +490,7 @@ export async function akmDistill(options) {
485
490
  const runner = resolveImproveProcessRunner(options.improveProfile, "distill", config);
486
491
  return runner ? materializeLlmRunnerConnection(runner) : getDefaultLlmConfig(config);
487
492
  })();
488
- const lookup = options.lookupFn ?? defaultLookup;
493
+ const lookup = options.lookupFn ?? ((ref) => defaultLookup(ref, stash));
489
494
  const readEventsImpl = options.readEventsFn ?? readEvents;
490
495
  // R1 opt-out must flow into every computeSalience call this command makes so
491
496
  // distill-written rank_score rows use the same weights as preparation's.
@@ -497,7 +502,7 @@ export async function akmDistill(options) {
497
502
  let assetContent = null;
498
503
  let assetFilePath = null;
499
504
  try {
500
- const filePath = await lookup(inputRef);
505
+ const filePath = await lookup(durableInputRef);
501
506
  if (filePath && fs.existsSync(filePath)) {
502
507
  assetFilePath = filePath;
503
508
  assetContent = fs.readFileSync(filePath, "utf8");
@@ -565,7 +570,7 @@ export async function akmDistill(options) {
565
570
  encodingSalience: salienceResult.score,
566
571
  outcomeWeightEnabled,
567
572
  });
568
- upsertAssetSalience(stateDb, inputRef, vector);
573
+ upsertAssetSalience(stateDb, durableInputRef, vector);
569
574
  });
570
575
  }
571
576
  catch {
@@ -576,12 +581,15 @@ export async function akmDistill(options) {
576
581
  // Scoring errors never block distillation.
577
582
  }
578
583
  }
579
- const { events } = readEventsImpl({
580
- ref: inputRef,
584
+ const { events: unfilteredEvents } = readEventsImpl({
585
+ ...(!options.legacyBareState ? { ref: durableInputRef } : {}),
581
586
  type: "feedback",
582
587
  excludeTags: options.excludeTags,
583
588
  includeTags: options.includeTags,
584
589
  });
590
+ const events = options.legacyBareState
591
+ ? unfilteredEvents.filter((event) => event.ref === durableInputRef || event.ref === bareImproveRef(inputRef))
592
+ : unfilteredEvents;
585
593
  // #267 — feedback exclusion. Filter events whose `ref` matches the
586
594
  // exclusion list BEFORE the prompt is built. The original event stream
587
595
  // is never mutated; only the `feedback` slice that reaches the LLM is
@@ -608,6 +616,8 @@ export async function akmDistill(options) {
608
616
  const promotionResult = await promoteMemoryToKnowledge({
609
617
  targetKind,
610
618
  inputRef,
619
+ durableInputRef,
620
+ sourceName: options.sourceName,
611
621
  assetContent,
612
622
  filteredEvents,
613
623
  config,
@@ -661,7 +671,7 @@ export async function akmDistill(options) {
661
671
  }
662
672
  // Distill output is a lesson/knowledge (non-wiki) → stash authoring
663
673
  // standards. Resolved once for this single call.
664
- const standardsContext = resolveStashStandards(stash);
674
+ const standardsContext = resolveStandardsContext(effectiveLessonRef, stash);
665
675
  const baseUserPrompt = buildDistillPrompt({
666
676
  inputRef,
667
677
  assetContent,
@@ -743,7 +753,7 @@ export async function akmDistill(options) {
743
753
  // is observable.
744
754
  appendEvent({
745
755
  eventType: "distill_invoked",
746
- ref: inputRef,
756
+ ref: durableInputRef,
747
757
  metadata: {
748
758
  outcome: "llm_failed",
749
759
  lessonRef: effectiveLessonRef,
@@ -812,7 +822,7 @@ export async function akmDistill(options) {
812
822
  if (findings.length > 0) {
813
823
  appendEvent({
814
824
  eventType: "distill_invoked",
815
- ref: inputRef,
825
+ ref: durableInputRef,
816
826
  metadata: {
817
827
  outcome: "validation_failed",
818
828
  lessonRef: effectiveLessonRef,
@@ -882,22 +892,22 @@ export async function akmDistill(options) {
882
892
  // structured payload alongside the raw content (matches the shape used by
883
893
  // other proposal sources).
884
894
  //
885
- // D-7 / #398: Inject `sources: [inputRef]` into the LLM-path proposal
886
- // frontmatter when the field is absent, providing reviewers with provenance
887
- // without requiring them to open event history. A-MEM arXiv:2502.12110 —
888
- // all notes carry explicit provenance links.
895
+ // Serialize canonical provenance into the content that promotion writes.
889
896
  const parsed = parseFrontmatter(content);
890
- const frontmatterWithSources = { ...parsed.data };
891
- if (!Array.isArray(frontmatterWithSources.sources) || frontmatterWithSources.sources.length === 0) {
892
- frontmatterWithSources.sources = [inputRef];
893
- }
897
+ const existingXrefs = Array.isArray(parsed.data.xrefs) ? parsed.data.xrefs.map(String) : [];
898
+ const frontmatterWithXrefs = {
899
+ ...parsed.data,
900
+ xrefs: [...new Set([...existingXrefs, durableInputRef])],
901
+ };
902
+ delete frontmatterWithXrefs.sources;
903
+ content = assembleAsset(frontmatterWithXrefs, parsed.content);
894
904
  const proposalResult2 = createProposal(stash, {
895
905
  ref: effectiveLessonRef,
896
906
  source: "distill",
897
907
  ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
898
908
  payload: {
899
909
  content,
900
- frontmatter: frontmatterWithSources,
910
+ frontmatter: frontmatterWithXrefs,
901
911
  },
902
912
  ...(lessonJudgeConfidence !== undefined ? { confidence: lessonJudgeConfidence } : {}),
903
913
  // Attribution tagging: persist the eligibility lane on the proposal.
@@ -906,7 +916,7 @@ export async function akmDistill(options) {
906
916
  if (isProposalSkipped(proposalResult2)) {
907
917
  appendEvent({
908
918
  eventType: "distill_invoked",
909
- ref: inputRef,
919
+ ref: durableInputRef,
910
920
  metadata: {
911
921
  outcome: "skipped",
912
922
  lessonRef: effectiveLessonRef,
@@ -928,10 +938,10 @@ export async function akmDistill(options) {
928
938
  // G4: content-score the distilled OUTPUT so it carries a real encoding
929
939
  // salience (encoding_source='content') from creation — lessons never get
930
940
  // another chance (they are refused as distill inputs).
931
- persistOutputEncodingSalience(effectiveLessonRef, content, existingRefVocabulary, outcomeWeightEnabled);
941
+ persistOutputEncodingSalience(durableImproveRef(effectiveLessonRef, options.sourceName), content, existingRefVocabulary, outcomeWeightEnabled);
932
942
  appendEvent({
933
943
  eventType: "distill_invoked",
934
- ref: inputRef,
944
+ ref: durableInputRef,
935
945
  metadata: {
936
946
  outcome: "queued",
937
947
  lessonRef: effectiveLessonRef,
@@ -962,6 +972,12 @@ export async function akmDistill(options) {
962
972
  };
963
973
  }
964
974
  // ── Helpers ─────────────────────────────────────────────────────────────────
965
- async function defaultLookup(ref) {
966
- return resolveAssetPath(ref, { mode: "index-only" });
975
+ async function defaultLookup(ref, stashDir) {
976
+ return resolveAssetPath(ref, {
977
+ stashDir,
978
+ mode: "disk-only",
979
+ directoryIndexNames: ["SKILL.md"],
980
+ preserveDirectNameFallback: true,
981
+ honorOrigin: false,
982
+ });
967
983
  }
@@ -13,6 +13,7 @@ import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search
13
13
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
14
14
  import { isDistillRefusedInputType } from "./distill.js";
15
15
  import { isStrategyFilteredForAllPasses } from "./improve-strategies.js";
16
+ import { improveStateReadRefs } from "./source-identity.js";
16
17
  // Eligibility / candidate-selection predicates for improve. Free functions
17
18
  // (no akmImprove closure state) extracted from improve.ts to shrink the
18
19
  // orchestrator and make candidate selection independently testable.
@@ -292,15 +293,15 @@ export function shouldDistillMemoryRef(ref, stashDir) {
292
293
  * metadata events are ignored so a stray `akm feedback <ref>` invocation
293
294
  * without a flag doesn't trigger downstream re-processing.
294
295
  */
295
- export function buildLatestFeedbackTsMap(refs, sinceIso) {
296
+ export function buildLatestFeedbackTsMap(refs, sinceIso, sourceName, includeLegacyBare = false) {
296
297
  const out = new Map();
297
298
  if (refs.length === 0)
298
299
  return out;
299
- const refSet = new Set(refs);
300
+ const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, sourceName, includeLegacyBare).map((key) => [key, ref])));
300
301
  const { events } = readEvents({ type: "feedback", since: sinceIso });
301
302
  for (const e of events) {
302
- const ref = e.ref;
303
- if (!ref || !refSet.has(ref))
303
+ const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
304
+ if (!ref)
304
305
  continue;
305
306
  const meta = e.metadata;
306
307
  const hasSignal = meta !== undefined && (typeof meta.signal === "string" || typeof meta.note === "string");
@@ -321,16 +322,16 @@ export function buildLatestFeedbackTsMap(refs, sinceIso) {
321
322
  * lesson/knowledge ref, not the source memory — joining back through the
322
323
  * payload would be fragile.
323
324
  */
324
- export function buildLatestProposalTsMap(refs, source) {
325
+ export function buildLatestProposalTsMap(refs, source, sourceName, includeLegacyBare = false) {
325
326
  const out = new Map();
326
327
  if (refs.length === 0)
327
328
  return out;
328
- const refSet = new Set(refs);
329
+ const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, sourceName, includeLegacyBare).map((key) => [key, ref])));
329
330
  const eventType = source === "reflect" ? "reflect_invoked" : "distill_invoked";
330
331
  const { events } = readEvents({ type: eventType });
331
332
  for (const e of events) {
332
- const ref = e.ref;
333
- if (!ref || !refSet.has(ref))
333
+ const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
334
+ if (!ref)
334
335
  continue;
335
336
  // For distill_invoked we only count attempts that produced (or attempted
336
337
  // to produce) a real proposal — config_disabled / parse-error outcomes
@@ -54,8 +54,8 @@ export const EXTRACT_JSON_SCHEMA = {
54
54
  },
55
55
  name: {
56
56
  type: "string",
57
- description: "Kebab-case slug for the new asset.",
58
- pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
57
+ description: "Kebab-case slug, optionally under one stable scope/domain segment.",
58
+ pattern: "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)?$",
59
59
  },
60
60
  description: {
61
61
  type: "string",