blun-king-cli 9.1.567 → 9.1.569

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 (262) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  4. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  5. package/agent-spine-plugin/README.md +30 -4
  6. package/agent-spine-plugin/blun.plugin.json +3 -3
  7. package/agent-spine-plugin/docs/acceptance.md +61 -0
  8. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  9. package/agent-spine-plugin/docs/host-integration.md +178 -0
  10. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  11. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  12. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  13. package/agent-spine-plugin/docs/releasing.md +85 -0
  14. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  15. package/agent-spine-plugin/docs/source-roots.md +113 -0
  16. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  17. package/agent-spine-plugin/docs/world-model.md +94 -0
  18. package/agent-spine-plugin/hooks/codex.json +2 -2
  19. package/agent-spine-plugin/hooks/hooks.json +1 -1
  20. package/agent-spine-plugin/hooks/version.json +1 -1
  21. package/agent-spine-plugin/package.json +13 -3
  22. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  23. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  24. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  25. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  26. package/agent-spine-plugin/scripts/check-install.js +478 -0
  27. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  28. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  29. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  30. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  31. package/agent-spine-plugin/scripts/release-check.js +145 -0
  32. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  33. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  34. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  35. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  36. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  37. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  38. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  39. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  40. package/agent-spine-plugin/src/cli-agent.js +296 -0
  41. package/agent-spine-plugin/src/cli-attention.js +95 -0
  42. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  43. package/agent-spine-plugin/src/cli-common.js +71 -0
  44. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  45. package/agent-spine-plugin/src/cli-core.js +128 -0
  46. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  47. package/agent-spine-plugin/src/cli-host.js +21 -0
  48. package/agent-spine-plugin/src/cli-learning.js +305 -0
  49. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  50. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  51. package/agent-spine-plugin/src/cli.js +40 -1350
  52. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  53. package/agent-spine-plugin/src/hook.js +185 -567
  54. package/agent-spine-plugin/src/index.js +16 -2
  55. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  56. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  57. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  58. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  59. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  60. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  61. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  62. package/agent-spine-plugin/src/lib/attention.js +9 -600
  63. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  64. package/agent-spine-plugin/src/lib/audit.js +56 -6
  65. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  66. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  67. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  68. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  69. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  70. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  71. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  72. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  73. package/agent-spine-plugin/src/lib/context.js +3 -1
  74. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  75. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  76. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  77. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  78. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  91. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  94. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  95. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  96. package/agent-spine-plugin/src/lib/documents.js +27 -5
  97. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  98. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  99. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  100. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  101. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  102. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  103. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  104. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  105. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  106. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  108. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  109. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  112. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  113. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  114. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  115. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  116. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  117. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  118. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  119. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  120. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  121. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  122. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  123. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  124. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  125. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  126. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  127. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  128. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  129. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  130. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  131. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  132. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  133. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  136. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  137. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  138. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  139. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  140. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  141. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  142. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  143. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  144. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  145. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  146. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  147. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  148. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  149. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  150. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  153. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  154. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  155. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  156. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  158. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  159. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  160. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  161. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  162. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  163. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  164. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  165. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  170. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  171. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  172. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  194. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  195. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  196. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  197. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  198. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  199. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  200. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  201. package/agent-spine-plugin/src/mcp.js +20 -161
  202. package/agent-spine-plugin/src/version.js +1 -1
  203. package/agent-spine-plugin/src/worker.js +22 -5
  204. package/bin/agent-resume-snapshot.cjs +2 -2
  205. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  206. package/bin/agentspine-king-goal-intake.mjs +106 -0
  207. package/bin/baseline-skill-performance-policy.cjs +1 -16
  208. package/bin/core-bootstrap.js +2 -0
  209. package/bin/curiosity-scout-policy.cjs +5 -1
  210. package/bin/input-draft-persistence.cjs +2 -2
  211. package/bin/king-tui-function-contract.json +33 -0
  212. package/bin/launcher-restart-policy.cjs +150 -0
  213. package/bin/launcher-runtime.js +56 -44
  214. package/bin/managed-context-startup-policy.cjs +27 -0
  215. package/bin/managed-plugin-selection.cjs +116 -0
  216. package/bin/mistake-relevance-policy.cjs +1 -1
  217. package/bin/observer-hooks.cjs +14 -0
  218. package/bin/oversized-context-offload-policy.cjs +86 -0
  219. package/bin/plugin-bootstrap.js +7 -40
  220. package/bin/proactive-compaction-policy.cjs +1 -1
  221. package/bin/provider-model-refresh-deadline.cjs +53 -0
  222. package/bin/provider-model-refresh-policy.cjs +107 -0
  223. package/bin/release-artifact-freeze-policy.cjs +30 -0
  224. package/bin/repeated-user-message-projection.cjs +3 -126
  225. package/bin/research-page-result.cjs +74 -0
  226. package/bin/runtime-exit-ledger.cjs +1 -0
  227. package/bin/session-compaction-policy.cjs +84 -0
  228. package/bin/skill-listing-performance-policy.cjs +2 -2
  229. package/bin/standard-tools-bootstrap.js +0 -37
  230. package/bin/subagent-skill-policy.cjs +3 -1
  231. package/bin/telegram-approval-relay.cjs +12 -7
  232. package/bin/telegram-private-conversation-policy.cjs +3 -2
  233. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  234. package/bin/thinking-activity-status-policy.cjs +1 -1
  235. package/bin/thinking-only-guard.cjs +16 -12
  236. package/bin/tool-call-loop-policy.cjs +0 -2
  237. package/bin/tool-result-offload-policy.cjs +11 -2
  238. package/bin/tui-functional-contract.cjs +55 -0
  239. package/bin/update-notice.js +18 -14
  240. package/bin/user-message-offload-policy.cjs +1 -1
  241. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  242. package/bin/windows-node-crash-dump.cjs +110 -0
  243. package/blun.mjs +2517 -1048
  244. package/codebase-index/codebase_index.py +4 -3
  245. package/package.json +3 -17
  246. package/standard-skills/research-evidence/SKILL.md +39 -0
  247. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  248. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  249. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  250. package/standard-skills/web-lesen/SKILL.md +37 -22
  251. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  252. package/telegram-plugin/DELIVERY.md +36 -0
  253. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  254. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  255. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  256. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  257. package/telegram-plugin/dist/bridge.mjs +7 -56
  258. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  259. package/agent-spine-plugin/skill/SKILL.md +0 -76
  260. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  261. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  262. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -1,12 +1,16 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { lstat, mkdir, open, opendir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { lstat, mkdir, open, readFile, stat, unlink, writeFile } from "node:fs/promises";
5
5
  import { ancestorsBetween, canonicalPath, isInside, stateRoot } from "./paths.js";
6
6
  import { indexExplicitDocuments } from "./documents.js";
7
7
  import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
8
8
  import { purgeIndexedMemoryCache, resolveIndexedMemory } from "./indexed-memory.js";
9
+ import { hookMemoryQuery, loadLessonRecallSelection, rememberLessonRecallSelection } from "./lesson-recall-session.js";
9
10
  import { catalogScanPolicy } from "./catalog.js";
11
+ import {
12
+ SOURCE_SCAN_INCOMPLETE, boundedMarkdownTree, existingDirectory, existingRegular, sourceScanError
13
+ } from "./source-tree-scan.js";
10
14
 
11
15
  export const SOURCE_REGISTRY_SCHEMA = "agentspine.source-roots/v1";
12
16
  const MAX_REGISTRY_BYTES = 1024 * 1024;
@@ -18,10 +22,8 @@ const MAX_DIRECTORY_ENTRIES = 4096;
18
22
  const MAX_PROJECT_DIRECTORY_ENTRIES = 8192;
19
23
  const SOURCE_RESOLUTION_MS = 2000;
20
24
  const SAFE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
21
- const SKIP_EXTRA_DIRS = new Set([".git", ".hg", ".svn", ".claude", ".codex", "node_modules", "vendor", "dist", "build", "coverage"]);
22
25
 
23
26
  function digest(value) { return createHash("sha256").update(value).digest("hex"); }
24
- function sourceScanError(error) { error.agentSpineScan = true; return error; }
25
27
  function registryPath(env = process.env) { return join(stateRoot(env), "source-roots.json"); }
26
28
  function emptyRegistry() { return { schema: SOURCE_REGISTRY_SCHEMA, revision: 0, bindings: [], history: [] }; }
27
29
 
@@ -83,28 +85,6 @@ async function mutateRegistry(task, env = process.env) {
83
85
  }
84
86
  }
85
87
 
86
- async function existingRegular(path) {
87
- try {
88
- const metadata = await lstat(path);
89
- if (metadata.isSymbolicLink() || !metadata.isFile()) return null;
90
- return realpath(path);
91
- } catch (error) {
92
- if (error.code === "ENOENT") return null;
93
- throw sourceScanError(error);
94
- }
95
- }
96
-
97
- async function existingDirectory(path) {
98
- try {
99
- const metadata = await lstat(path);
100
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) return null;
101
- return realpath(path);
102
- } catch (error) {
103
- if (error.code === "ENOENT") return null;
104
- throw sourceScanError(error);
105
- }
106
- }
107
-
108
88
  function expandHome(value, home) {
109
89
  if (value === "~") return home;
110
90
  if (value.startsWith("~/") || value.startsWith("~\\")) return join(home, value.slice(2));
@@ -177,100 +157,6 @@ async function findRoot(cwd, markers) {
177
157
  }
178
158
  }
179
159
 
180
- function skippedExtraDirectory(name) {
181
- const lower = name.toLowerCase();
182
- return SKIP_EXTRA_DIRS.has(name) || SKIP_EXTRA_DIRS.has(lower)
183
- || lower.includes("dropbox") || lower === "onedrive" || lower.startsWith("onedrive - ");
184
- }
185
-
186
- function skippableTraversalError(error) {
187
- return ["EPERM", "EACCES", "ENOENT"].includes(error?.code);
188
- }
189
-
190
- function recordSkippedPath(skipped, path, error, operation) {
191
- skipped.push({ path, code: error.code, operation });
192
- }
193
-
194
- async function containsProjectMarker(directory) {
195
- try {
196
- await lstat(join(directory, ".git"));
197
- return true;
198
- } catch (error) {
199
- if (error.code === "ENOENT") return false;
200
- throw error;
201
- }
202
- }
203
-
204
- async function containsEmbeddedHostProfile(directory) {
205
- return Boolean(
206
- await existingDirectory(join(directory, ".runtime-private"))
207
- && await existingRegular(join(directory, "config.toml"))
208
- );
209
- }
210
-
211
- async function boundedMarkdownTree(directory, prefix, host, scope, precedenceStart, deadline, {
212
- projectBoundary = false,
213
- maxFiles = MAX_RULE_FILES,
214
- maxDirectoryEntries = MAX_DIRECTORY_ENTRIES,
215
- skipped = []
216
- } = {}) {
217
- let root;
218
- try {
219
- root = await existingDirectory(directory);
220
- } catch (error) {
221
- if (!skippableTraversalError(error)) throw sourceScanError(error);
222
- recordSkippedPath(skipped, directory, error, "inspect-directory");
223
- return [];
224
- }
225
- if (!root) return [];
226
- const output = [];
227
- let visitedEntries = 0;
228
- async function walk(current) {
229
- if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
230
- try {
231
- if (projectBoundary && current !== root
232
- && (await containsProjectMarker(current) || await containsEmbeddedHostProfile(current))) return;
233
- } catch (error) {
234
- if (!skippableTraversalError(error)) throw sourceScanError(error);
235
- recordSkippedPath(skipped, current, error, "inspect-directory-boundary");
236
- return;
237
- }
238
- const entries = [];
239
- let stream;
240
- try {
241
- stream = await opendir(current);
242
- } catch (error) {
243
- if (!skippableTraversalError(error)) throw sourceScanError(error);
244
- recordSkippedPath(skipped, error.path || current, error, "opendir");
245
- return;
246
- }
247
- try {
248
- for await (const entry of stream) {
249
- visitedEntries += 1;
250
- if (visitedEntries > maxDirectoryEntries) throw new Error(`host-native source tree exceeds ${maxDirectoryEntries} entries`);
251
- entries.push(entry);
252
- }
253
- } catch (error) {
254
- if (!skippableTraversalError(error)) throw sourceScanError(error);
255
- recordSkippedPath(skipped, current, error, "readdir");
256
- return;
257
- }
258
- entries.sort((a, b) => a.name.localeCompare(b.name));
259
- for (const entry of entries) {
260
- if (output.length >= maxFiles) throw new Error(`host-native rule tree exceeds ${maxFiles} files`);
261
- if (entry.isSymbolicLink()) continue;
262
- const path = join(current, entry.name);
263
- if (entry.isDirectory() && !entry.name.startsWith(".") && !skippedExtraDirectory(entry.name)) await walk(path);
264
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
265
- output.push({ path, id: `${prefix}/${relative(root, path).replaceAll("\\", "/")}`, host, scope,
266
- binding: "host-native-rule-tree", precedence: precedenceStart + output.length });
267
- }
268
- }
269
- }
270
- await walk(root);
271
- return output;
272
- }
273
-
274
160
  async function addFile(output, path, metadata) {
275
161
  const file = await existingRegular(path);
276
162
  if (file) output.push({ path: file, ...metadata });
@@ -349,7 +235,8 @@ async function claudeSources({ cwd, projectRoot, configDir, input, env, registry
349
235
  const sources = [];
350
236
  const skipped = [];
351
237
  await addFile(sources, join(configDir, "CLAUDE.md"), { id: "claude:user/CLAUDE.md", host: "claude", scope: "user", binding: "CLAUDE_CONFIG_DIR", precedence: 100 });
352
- sources.push(...await boundedMarkdownTree(join(configDir, "rules"), "claude:user/rules", "claude", "user", 110, deadline, { skipped }));
238
+ sources.push(...await boundedMarkdownTree(join(configDir, "rules"), "claude:user/rules", "claude", "user", 110, deadline,
239
+ { maxFiles: MAX_RULE_FILES, maxDirectoryEntries: MAX_DIRECTORY_ENTRIES, skipped, timeoutMs: SOURCE_RESOLUTION_MS }));
353
240
  let precedence = 1000;
354
241
  for (const directory of ancestorsBetween(projectRoot, cwd)) {
355
242
  for (const name of ["CLAUDE.md", "CLAUDE.local.md"]) {
@@ -359,7 +246,8 @@ async function claudeSources({ cwd, projectRoot, configDir, input, env, registry
359
246
  await addFile(sources, join(directory, ".claude", "CLAUDE.md"), { id: `claude:project/${relative(projectRoot, join(directory, ".claude", "CLAUDE.md")).replaceAll("\\", "/")}`,
360
247
  host: "claude", scope: "project", binding: "native-project-chain", precedence: precedence++ });
361
248
  }
362
- sources.push(...await boundedMarkdownTree(join(projectRoot, ".claude", "rules"), "claude:project/.claude/rules", "claude", "project", precedence, deadline, { skipped }));
249
+ sources.push(...await boundedMarkdownTree(join(projectRoot, ".claude", "rules"), "claude:project/.claude/rules", "claude", "project", precedence, deadline,
250
+ { maxFiles: MAX_RULE_FILES, maxDirectoryEntries: MAX_DIRECTORY_ENTRIES, skipped, timeoutMs: SOURCE_RESOLUTION_MS }));
363
251
 
364
252
  let memoryRoot = null;
365
253
  let memoryProvenance = null;
@@ -396,6 +284,12 @@ async function claudeSources({ cwd, projectRoot, configDir, input, env, registry
396
284
  if (memoryRoot) {
397
285
  const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
398
286
  ? input.agent_spine_scope : input;
287
+ let recall = { status: "empty", paths: [], authority: "context-only" };
288
+ try {
289
+ recall = await loadLessonRecallSelection({ root: projectRoot, input, now: input.timestamp || new Date() });
290
+ } catch (error) {
291
+ recall = { status: "degraded", paths: [], reason: error.message, authority: "context-only" };
292
+ }
399
293
  const resolved = await resolveIndexedMemory({
400
294
  root: memoryRoot, env, hooks: memoryHooks, deadline,
401
295
  scope: {
@@ -403,10 +297,21 @@ async function claudeSources({ cwd, projectRoot, configDir, input, env, registry
403
297
  groupId: supplied.group_id ?? supplied.groupId ?? null,
404
298
  projectId: supplied.project_id ?? supplied.projectId ?? null,
405
299
  currentTaskId: supplied.task_id ?? supplied.currentTaskId ?? null,
406
- prompt: input.prompt ?? input.user_prompt ?? input.message ?? input.input ?? null
300
+ prompt: hookMemoryQuery(input),
301
+ pinnedPaths: recall.paths
407
302
  }
408
303
  });
409
- memoryDiagnostics = resolved.diagnostics;
304
+ let recorded = { status: "not-recorded", paths: [], authority: "context-only" };
305
+ try {
306
+ recorded = await rememberLessonRecallSelection({ root: projectRoot, input,
307
+ sources: resolved.sources, now: input.timestamp || new Date() });
308
+ } catch (error) {
309
+ recorded = { status: "degraded", paths: [], reason: error.message, authority: "context-only" };
310
+ }
311
+ memoryDiagnostics = { ...resolved.diagnostics, recall: {
312
+ restored: recall.paths.length, loadStatus: recall.status, recordStatus: recorded.status,
313
+ recorded: recorded.paths.length, authority: "context-only"
314
+ } };
410
315
  for (const item of resolved.sources) {
411
316
  sources.push({
412
317
  path: item.path, id: `claude:memory/${item.relativePath}`, host: "claude", scope: "project-memory",
@@ -442,7 +347,7 @@ async function codexSources({ cwd, projectRoot, codexHome, config }) {
442
347
  }
443
348
 
444
349
  export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), input = {}, env = process.env, memoryHooks = {} } = {}) {
445
- if (!["claude", "codex"].includes(host)) throw new Error(`host-native source resolution requires claude or codex, received ${host}`);
350
+ if (!["claude", "codex", "generic"].includes(host)) throw new Error(`unsupported source host: ${host}`);
446
351
  const canonicalCwd = await canonicalPath(cwd);
447
352
  const deadline = Date.now() + SOURCE_RESOLUTION_MS;
448
353
  const { registry } = await readRegistry(env);
@@ -460,7 +365,7 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
460
365
  else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, config.rootMarkers));
461
366
  sources = await codexSources({ cwd: canonicalCwd, projectRoot, codexHome: hostHome, config });
462
367
  hostDetails = { rootMarkers: config.rootMarkers, fallbackNames: config.fallbackNames };
463
- } else {
368
+ } else if (host === "claude") {
464
369
  hostHome = await existingDirectory(resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")))
465
370
  || resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
466
371
  if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
@@ -470,23 +375,43 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
470
375
  skipped = result.skipped;
471
376
  hostDetails = { memoryRoot: result.memoryRoot, memoryProvenance: result.memoryProvenance,
472
377
  memoryDiagnostics: result.memoryDiagnostics };
378
+ } else {
379
+ // Generic hosts get project sources, never another provider's profile.
380
+ hostHome = await canonicalPath(homedir());
381
+ if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
382
+ else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, [".git"]));
383
+ sources = [];
384
+ for (const directory of ancestorsBetween(projectRoot, canonicalCwd)) {
385
+ for (const name of ["AGENTS.md", "SOUL.md", "MEMORY.md"]) {
386
+ await addFile(sources, join(directory, name), {
387
+ id: `generic:project/${relative(projectRoot, join(directory, name)).replaceAll("\\", "/")}`,
388
+ host, scope: "project", binding: "native-project-chain", precedence: 1000 + sources.length
389
+ });
390
+ }
391
+ }
473
392
  }
393
+ sources = [...new Map(sources.map((item) => [item.path, item])).values()];
394
+ if (sources.length > MAX_SOURCES) throw new Error(`host-native required source set exceeds ${MAX_SOURCES} files`);
395
+ const nativeNames = new Set(host === "codex"
396
+ ? ["AGENTS.override.md", "AGENTS.md", ...(hostDetails.fallbackNames || [])]
397
+ : host === "claude" ? ["CLAUDE.md", "CLAUDE.local.md"] : ["AGENTS.md", "SOUL.md", "MEMORY.md"]);
474
398
  const knownHomeRoots = await homeRoots(env);
475
399
  const skippedHomeTree = knownHomeRoots.some((root) => samePath(root, projectRoot));
476
400
  const skippedProfileTree = samePath(hostHome, projectRoot);
477
401
  const skippedFallbackHomeTree = skippedHomeTree && rootResolution === "cwd-fallback";
478
402
  if (!skippedHomeTree && !skippedProfileTree) {
403
+ const optionalBudget = Math.min(MAX_PROJECT_FILES, MAX_SOURCES - sources.length);
479
404
  sources.push(...await boundedMarkdownTree(projectRoot, "agentspine:project", host, "project", 3000, deadline,
480
- { projectBoundary: true, maxFiles: MAX_PROJECT_FILES, maxDirectoryEntries: MAX_PROJECT_DIRECTORY_ENTRIES, skipped }));
405
+ { projectBoundary: true, maxFiles: optionalBudget, maxDirectoryEntries: MAX_PROJECT_DIRECTORY_ENTRIES,
406
+ skipped, truncateOnLimit: true, excludedNames: nativeNames, label: "project Markdown",
407
+ timeoutMs: SOURCE_RESOLUTION_MS }));
481
408
  }
482
- const nativeNames = new Set(host === "codex"
483
- ? ["AGENTS.override.md", "AGENTS.md", ...(hostDetails.fallbackNames || [])]
484
- : ["CLAUDE.md", "CLAUDE.local.md"]);
485
- sources = sources.filter((item) => item.binding !== "host-native-rule-tree"
486
- || !item.id.startsWith("agentspine:project/") || !nativeNames.has(basename(item.path)));
487
409
  sources = [...new Map(sources.map((item) => [item.path, item])).values()];
488
410
  if (sources.length > MAX_SOURCES) throw new Error(`host-native source set exceeds ${MAX_SOURCES} files`);
489
- if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
411
+ const projectScanIncomplete = skipped.some((item) => item.code === SOURCE_SCAN_INCOMPLETE);
412
+ if (Date.now() > deadline && !projectScanIncomplete) {
413
+ throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
414
+ }
490
415
  let documents;
491
416
  try {
492
417
  documents = await indexExplicitDocuments(sources);
@@ -494,10 +419,14 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
494
419
  if (typeof error?.code === "string" && (error.path || error.syscall)) throw sourceScanError(error);
495
420
  throw error;
496
421
  }
497
- if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
422
+ if (Date.now() > deadline && !projectScanIncomplete) {
423
+ throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
424
+ }
498
425
  const totalBytes = documents.reduce((sum, document) => sum + document.bytes, 0);
499
426
  if (totalBytes > MAX_TOTAL_SOURCE_BYTES) throw new Error("host-native source set exceeds 8 MiB");
500
427
  const activeUserState = activeBinding(registry, host, hostHome, projectRoot, "state-user");
428
+ const orderedSkipped = skipped.sort((a, b) => a.path.localeCompare(b.path) || a.operation.localeCompare(b.operation));
429
+ const warnings = orderedSkipped.filter((item) => item.code === SOURCE_SCAN_INCOMPLETE);
501
430
  const diagnostics = {
502
431
  schema: SOURCE_REGISTRY_SCHEMA, host, status: documents.length ? "loaded" : "empty", projectRoot,
503
432
  hostHomeDigest: digest(hostHome).slice(0, 16), checked: ["host-profile", "project-chain", ...(host === "claude" ? ["project-memory"] : [])],
@@ -505,15 +434,19 @@ export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), inpu
505
434
  reason: documents.length ? null : "No regular, non-symlink host-native Markdown source exists in the checked scope.",
506
435
  personalContinuityLoaded: documents.some((item) => item.sourceScope === "user") || Boolean(activeUserState),
507
436
  broadHomeScan: false, projectTreeScan: skippedFallbackHomeTree ? "skipped-unmarked-home"
508
- : skippedHomeTree ? "skipped-home-root" : skippedProfileTree ? "skipped-profile-root" : "bounded",
509
- skipped: skipped.sort((a, b) => a.path.localeCompare(b.path) || a.operation.localeCompare(b.operation)),
437
+ : skippedHomeTree ? "skipped-home-root" : skippedProfileTree ? "skipped-profile-root"
438
+ : warnings.length ? "bounded-truncated" : "bounded",
439
+ incomplete: warnings.length > 0,
440
+ warning: warnings[0]?.message || null,
441
+ warnings,
442
+ skipped: orderedSkipped,
510
443
  rootResolution, registryRevision: registry.revision,
511
444
  ...(host === "claude" ? {
512
445
  memoryBound: Boolean(hostDetails.memoryRoot),
513
446
  memoryRootDigest: hostDetails.memoryRoot ? digest(hostDetails.memoryRoot).slice(0, 16) : null,
514
447
  memoryProvenance: hostDetails.memoryProvenance,
515
448
  memory: hostDetails.memoryDiagnostics || {
516
- indexed: 0, relevant: 0, loaded: 0, cacheHits: 0, cacheMisses: 0, missing: 0,
449
+ indexed: 0, relevant: 0, selected: 0, omittedRelevant: 0, loaded: 0, cacheHits: 0, cacheMisses: 0, missing: 0,
517
450
  rejected: { scope: 0, path: 0, symlink: 0, race: 0, size: 0 }, directoryEnumeration: 0
518
451
  }
519
452
  } : hostDetails)
@@ -0,0 +1,178 @@
1
+ import { lstat, opendir, realpath } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+
4
+ export const SOURCE_SCAN_INCOMPLETE = "AGENTSPINE_SCAN_INCOMPLETE";
5
+
6
+ const SKIP_EXTRA_DIRS = new Set([
7
+ ".git", ".hg", ".svn", ".claude", ".codex", "node_modules", "vendor", "dist", "build", "coverage"
8
+ ]);
9
+
10
+ export function sourceScanError(error) {
11
+ error.agentSpineScan = true;
12
+ return error;
13
+ }
14
+
15
+ export async function existingRegular(path) {
16
+ try {
17
+ const metadata = await lstat(path);
18
+ if (metadata.isSymbolicLink() || !metadata.isFile()) return null;
19
+ return realpath(path);
20
+ } catch (error) {
21
+ if (error.code === "ENOENT") return null;
22
+ throw sourceScanError(error);
23
+ }
24
+ }
25
+
26
+ export async function existingDirectory(path) {
27
+ try {
28
+ const metadata = await lstat(path);
29
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) return null;
30
+ return realpath(path);
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return null;
33
+ throw sourceScanError(error);
34
+ }
35
+ }
36
+
37
+ function skippedExtraDirectory(name) {
38
+ const lower = name.toLowerCase();
39
+ return SKIP_EXTRA_DIRS.has(name) || SKIP_EXTRA_DIRS.has(lower)
40
+ || lower.includes("dropbox") || lower === "onedrive" || lower.startsWith("onedrive - ");
41
+ }
42
+
43
+ export function skippableTraversalError(error) {
44
+ return ["EPERM", "EACCES", "ENOENT", "ENOTDIR"].includes(error?.code);
45
+ }
46
+
47
+ function recordSkippedPath(skipped, path, error, operation) {
48
+ skipped.push({ path, code: error.code, operation });
49
+ }
50
+
51
+ function recordIncomplete(skipped, { path, operation, message, limit, retained }) {
52
+ skipped.push({
53
+ path, code: SOURCE_SCAN_INCOMPLETE, operation, message, limit, retained,
54
+ authority: "diagnostic-only"
55
+ });
56
+ }
57
+
58
+ async function containsProjectMarker(directory) {
59
+ try {
60
+ await lstat(join(directory, ".git"));
61
+ return true;
62
+ } catch (error) {
63
+ if (error.code === "ENOENT") return false;
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ async function containsEmbeddedHostProfile(directory) {
69
+ return Boolean(
70
+ await existingDirectory(join(directory, ".runtime-private"))
71
+ && await existingRegular(join(directory, "config.toml"))
72
+ );
73
+ }
74
+
75
+ function limitMessage(label, detail) {
76
+ return `Host-native ${label} context is incomplete: ${detail}; remaining files were skipped.`;
77
+ }
78
+
79
+ export async function boundedMarkdownTree(directory, prefix, host, scope, precedenceStart, deadline, {
80
+ projectBoundary = false,
81
+ maxFiles,
82
+ maxDirectoryEntries,
83
+ skipped = [],
84
+ truncateOnLimit = false,
85
+ excludedNames = new Set(),
86
+ label = "rule-tree",
87
+ timeoutMs
88
+ } = {}) {
89
+ let root;
90
+ try {
91
+ root = await existingDirectory(directory);
92
+ } catch (error) {
93
+ if (!skippableTraversalError(error)) throw sourceScanError(error);
94
+ recordSkippedPath(skipped, directory, error, "inspect-directory");
95
+ return [];
96
+ }
97
+ if (!root) return [];
98
+ const output = [];
99
+ let visitedEntries = 0;
100
+
101
+ function stopForLimit(path, operation, message, limit) {
102
+ if (!truncateOnLimit) throw new Error(message);
103
+ recordIncomplete(skipped, { path, operation,
104
+ message: limitMessage(label, message), limit, retained: output.length });
105
+ return false;
106
+ }
107
+
108
+ function checkDeadline(path) {
109
+ if (Date.now() <= deadline) return true;
110
+ const message = `source resolution exceeded ${timeoutMs} ms`;
111
+ if (!truncateOnLimit) throw new Error(`host-native ${message}`);
112
+ recordIncomplete(skipped, { path, operation: "deadline",
113
+ message: limitMessage(label, message), limit: timeoutMs, retained: output.length });
114
+ return false;
115
+ }
116
+
117
+ async function walk(current) {
118
+ if (!checkDeadline(current)) return false;
119
+ try {
120
+ if (projectBoundary && current !== root
121
+ && (await containsProjectMarker(current) || await containsEmbeddedHostProfile(current))) return true;
122
+ } catch (error) {
123
+ if (!skippableTraversalError(error)) throw sourceScanError(error);
124
+ recordSkippedPath(skipped, current, error, "inspect-directory-boundary");
125
+ return true;
126
+ }
127
+ const entries = [];
128
+ let stream;
129
+ try {
130
+ stream = await opendir(current);
131
+ } catch (error) {
132
+ if (!skippableTraversalError(error)) throw sourceScanError(error);
133
+ recordSkippedPath(skipped, error.path || current, error, "opendir");
134
+ return true;
135
+ }
136
+ let stopAfterDirectory = false;
137
+ try {
138
+ for await (const entry of stream) {
139
+ if (truncateOnLimit && !checkDeadline(current)) return false;
140
+ visitedEntries += 1;
141
+ if (visitedEntries > maxDirectoryEntries) {
142
+ const message = `source tree exceeds ${maxDirectoryEntries} directory entries`;
143
+ if (!truncateOnLimit) throw new Error(`host-native ${message}`);
144
+ recordIncomplete(skipped, { path: current, operation: "directory-entry-limit",
145
+ message: limitMessage(label, message), limit: maxDirectoryEntries, retained: output.length });
146
+ stopAfterDirectory = true;
147
+ break;
148
+ }
149
+ entries.push(entry);
150
+ }
151
+ } catch (error) {
152
+ if (!skippableTraversalError(error)) throw sourceScanError(error);
153
+ recordSkippedPath(skipped, current, error, "readdir");
154
+ return true;
155
+ }
156
+ entries.sort((a, b) => a.name.localeCompare(b.name));
157
+ for (const entry of entries) {
158
+ if (truncateOnLimit && !checkDeadline(current)) return false;
159
+ if (entry.isSymbolicLink()) continue;
160
+ const path = join(current, entry.name);
161
+ if (entry.isDirectory() && !stopAfterDirectory && !entry.name.startsWith(".")
162
+ && !skippedExtraDirectory(entry.name)) {
163
+ if (!await walk(path)) return false;
164
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")
165
+ && !excludedNames.has(entry.name)) {
166
+ if (output.length >= maxFiles) {
167
+ return stopForLimit(root, "file-limit", `rule tree exceeds ${maxFiles} files`, maxFiles);
168
+ }
169
+ output.push({ path, id: `${prefix}/${relative(root, path).replaceAll("\\", "/")}`, host, scope,
170
+ binding: "host-native-rule-tree", precedence: precedenceStart + output.length });
171
+ }
172
+ }
173
+ return !stopAfterDirectory;
174
+ }
175
+
176
+ await walk(root);
177
+ return output;
178
+ }
@@ -0,0 +1,78 @@
1
+ const MAX_ITEMS = 6;
2
+ const STOP_WORDS = new Set([
3
+ "and", "are", "das", "der", "die", "eine", "einer", "for", "from", "für", "ist",
4
+ "mit", "soll", "that", "the", "this", "und", "von", "was", "with"
5
+ ]);
6
+ const KIND_WEIGHT = {
7
+ "error-lesson": 40,
8
+ decision: 30,
9
+ "user-preference": 20,
10
+ fact: 10
11
+ };
12
+
13
+ function terms(value) {
14
+ return [...new Set(String(value).normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase()
15
+ .match(/[\p{L}\p{N}]+/gu)?.filter((item) => item.length >= 3 && !STOP_WORDS.has(item)) || [])]
16
+ .slice(0, 64);
17
+ }
18
+
19
+ function taskText(task) {
20
+ return [task.objective, task.nextStep?.summary,
21
+ ...task.openQuestions.map((item) => item.question)].filter(Boolean).join(" ");
22
+ }
23
+
24
+ function entryText(entry) {
25
+ return [entry.subjectId, entry.predicate, JSON.stringify(entry.value), entry.rationale]
26
+ .filter(Boolean).join(" ").slice(0, 12_288);
27
+ }
28
+
29
+ function empty(taskId, status) {
30
+ return {
31
+ schema: "agentspine.task-knowledge-context/v1",
32
+ taskId,
33
+ status,
34
+ items: [],
35
+ omitted: 0,
36
+ authority: "context-only"
37
+ };
38
+ }
39
+
40
+ export function taskKnowledgeContext(entries, { taskId = null, continuation, maxItems = MAX_ITEMS } = {}) {
41
+ if (!taskId) return empty(null, "not-requested");
42
+ const task = continuation.tasks.find((item) => item.taskId === taskId);
43
+ if (!task) {
44
+ const terminal = continuation.terminal.some((item) => item.taskId === taskId);
45
+ return empty(taskId, terminal ? "terminal" : "unavailable");
46
+ }
47
+ const queryTerms = terms(taskText(task));
48
+ if (!queryTerms.length) return empty(taskId, "no-query");
49
+ const querySet = new Set(queryTerms);
50
+ const minimumMatches = Math.min(2, queryTerms.length);
51
+ const seen = new Set();
52
+ const ranked = [];
53
+ for (const entry of entries) {
54
+ if (entry.status !== "confirmed" || !(entry.kind in KIND_WEIGHT)) continue;
55
+ const matchedTerms = terms(entryText(entry)).filter((item) => querySet.has(item));
56
+ if (matchedTerms.length < minimumMatches) continue;
57
+ const key = `${entry.kind}\0${entry.subjectId}\0${entry.predicate}\0${JSON.stringify(entry.value)}`;
58
+ if (seen.has(key)) continue;
59
+ seen.add(key);
60
+ ranked.push({ entry, matchedTerms,
61
+ score: matchedTerms.length * 100 + KIND_WEIGHT[entry.kind] });
62
+ }
63
+ ranked.sort((left, right) => right.score - left.score
64
+ || right.entry.observedAt.localeCompare(left.entry.observedAt)
65
+ || left.entry.id.localeCompare(right.entry.id));
66
+ const limit = Math.min(MAX_ITEMS, Math.max(1, maxItems));
67
+ return {
68
+ schema: "agentspine.task-knowledge-context/v1",
69
+ taskId,
70
+ status: ranked.length ? "available" : "no-match",
71
+ items: ranked.slice(0, limit).map(({ entry, matchedTerms }) => ({
72
+ ...structuredClone(entry),
73
+ relevance: { matchedTerms, authority: "context-only" }
74
+ })),
75
+ omitted: Math.max(0, ranked.length - limit),
76
+ authority: "context-only"
77
+ };
78
+ }