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.
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
- package/agent-spine-plugin/CHANGELOG.md +1581 -0
- package/agent-spine-plugin/README.md +30 -4
- package/agent-spine-plugin/blun.plugin.json +3 -3
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
- package/agent-spine-plugin/docs/host-integration.md +178 -0
- package/agent-spine-plugin/docs/preflight-recall.md +69 -0
- package/agent-spine-plugin/docs/preservation-contract.md +53 -0
- package/agent-spine-plugin/docs/quality-gates.md +50 -0
- package/agent-spine-plugin/docs/releasing.md +85 -0
- package/agent-spine-plugin/docs/session-timeline.md +251 -0
- package/agent-spine-plugin/docs/source-roots.md +113 -0
- package/agent-spine-plugin/docs/structured-completion.md +67 -0
- package/agent-spine-plugin/docs/world-model.md +94 -0
- package/agent-spine-plugin/hooks/codex.json +2 -2
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +13 -3
- package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
- package/agent-spine-plugin/scripts/check-hosts.js +14 -5
- package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
- package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
- package/agent-spine-plugin/scripts/check-install.js +478 -0
- package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
- package/agent-spine-plugin/scripts/check-syntax.js +29 -0
- package/agent-spine-plugin/scripts/github-actions.js +11 -0
- package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
- package/agent-spine-plugin/scripts/release-check.js +145 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +47 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
- package/agent-spine-plugin/spine-example/1-identity.md +12 -0
- package/agent-spine-plugin/spine-example/2-voice.md +6 -0
- package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
- package/agent-spine-plugin/spine-example/4-history.md +4 -0
- package/agent-spine-plugin/src/cli-agent.js +296 -0
- package/agent-spine-plugin/src/cli-attention.js +95 -0
- package/agent-spine-plugin/src/cli-autonomy.js +36 -0
- package/agent-spine-plugin/src/cli-common.js +71 -0
- package/agent-spine-plugin/src/cli-continuity.js +116 -0
- package/agent-spine-plugin/src/cli-core.js +128 -0
- package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
- package/agent-spine-plugin/src/cli-host.js +21 -0
- package/agent-spine-plugin/src/cli-learning.js +305 -0
- package/agent-spine-plugin/src/cli-premortem.js +16 -0
- package/agent-spine-plugin/src/cli-sharing.js +230 -0
- package/agent-spine-plugin/src/cli.js +40 -1350
- package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
- package/agent-spine-plugin/src/hook.js +185 -567
- package/agent-spine-plugin/src/index.js +16 -2
- package/agent-spine-plugin/src/lib/acceptance.js +113 -11
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
- package/agent-spine-plugin/src/lib/attention-context.js +167 -0
- package/agent-spine-plugin/src/lib/attention-events.js +113 -0
- package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
- package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
- package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
- package/agent-spine-plugin/src/lib/attention.js +9 -600
- package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
- package/agent-spine-plugin/src/lib/audit.js +56 -6
- package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
- package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
- package/agent-spine-plugin/src/lib/autonomy.js +8 -0
- package/agent-spine-plugin/src/lib/briefing.js +67 -4
- package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
- package/agent-spine-plugin/src/lib/catalog.js +1 -1
- package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
- package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
- package/agent-spine-plugin/src/lib/context.js +3 -1
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
- package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
- package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
- package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
- package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
- package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
- package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
- package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
- package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
- package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
- package/agent-spine-plugin/src/lib/documents.js +27 -5
- package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
- package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
- package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
- package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
- package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
- package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
- package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
- package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
- package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
- package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
- package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
- package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
- package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
- package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
- package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
- package/agent-spine-plugin/src/lib/hook-context.js +424 -0
- package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
- package/agent-spine-plugin/src/lib/hook-input.js +27 -0
- package/agent-spine-plugin/src/lib/hook-output.js +140 -0
- package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
- package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
- package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
- package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
- package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
- package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
- package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
- package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
- package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
- package/agent-spine-plugin/src/lib/learning-context.js +130 -0
- package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
- package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
- package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
- package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
- package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
- package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
- package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
- package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
- package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
- package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
- package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
- package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
- package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
- package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
- package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
- package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
- package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
- package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
- package/agent-spine-plugin/src/lib/learning.js +36 -6923
- package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
- package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
- package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
- package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
- package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
- package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
- package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
- package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
- package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
- package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
- package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
- package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
- package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
- package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
- package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
- package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
- package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
- package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
- package/agent-spine-plugin/src/lib/source-roots.js +69 -136
- package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
- package/agent-spine-plugin/src/lib/world-model.js +278 -0
- package/agent-spine-plugin/src/mcp.js +20 -161
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +22 -5
- package/bin/agent-resume-snapshot.cjs +2 -2
- package/bin/agentspine-king-goal-inbox.mjs +111 -0
- package/bin/agentspine-king-goal-intake.mjs +106 -0
- package/bin/baseline-skill-performance-policy.cjs +1 -16
- package/bin/core-bootstrap.js +2 -0
- package/bin/curiosity-scout-policy.cjs +5 -1
- package/bin/input-draft-persistence.cjs +2 -2
- package/bin/king-tui-function-contract.json +33 -0
- package/bin/launcher-restart-policy.cjs +150 -0
- package/bin/launcher-runtime.js +56 -44
- package/bin/managed-context-startup-policy.cjs +27 -0
- package/bin/managed-plugin-selection.cjs +116 -0
- package/bin/mistake-relevance-policy.cjs +1 -1
- package/bin/observer-hooks.cjs +14 -0
- package/bin/oversized-context-offload-policy.cjs +86 -0
- package/bin/plugin-bootstrap.js +7 -40
- package/bin/proactive-compaction-policy.cjs +1 -1
- package/bin/provider-model-refresh-deadline.cjs +53 -0
- package/bin/provider-model-refresh-policy.cjs +107 -0
- package/bin/release-artifact-freeze-policy.cjs +30 -0
- package/bin/repeated-user-message-projection.cjs +3 -126
- package/bin/research-page-result.cjs +74 -0
- package/bin/runtime-exit-ledger.cjs +1 -0
- package/bin/session-compaction-policy.cjs +84 -0
- package/bin/skill-listing-performance-policy.cjs +2 -2
- package/bin/standard-tools-bootstrap.js +0 -37
- package/bin/subagent-skill-policy.cjs +3 -1
- package/bin/telegram-approval-relay.cjs +12 -7
- package/bin/telegram-private-conversation-policy.cjs +3 -2
- package/bin/telegram-queue-handoff-policy.cjs +24 -0
- package/bin/thinking-activity-status-policy.cjs +1 -1
- package/bin/thinking-only-guard.cjs +16 -12
- package/bin/tool-call-loop-policy.cjs +0 -2
- package/bin/tool-result-offload-policy.cjs +11 -2
- package/bin/tui-functional-contract.cjs +55 -0
- package/bin/update-notice.js +18 -14
- package/bin/user-message-offload-policy.cjs +1 -1
- package/bin/user-prompt-hook-origin-policy.cjs +34 -0
- package/bin/windows-node-crash-dump.cjs +110 -0
- package/blun.mjs +2517 -1048
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +3 -17
- package/standard-skills/research-evidence/SKILL.md +39 -0
- package/standard-skills/research-evidence/references/evidence-format.md +82 -0
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
- package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
- package/standard-skills/web-lesen/SKILL.md +37 -22
- package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
- package/telegram-plugin/DELIVERY.md +36 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
- package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
- package/telegram-plugin/dist/bridge.mjs +7 -56
- package/telegram-plugin/dist/mcp-server.mjs +33 -4
- package/agent-spine-plugin/skill/SKILL.md +0 -76
- package/bin/mnemo-connect-heartbeat.cjs +0 -204
- package/bin/mnemo-tool-agent-policy.cjs +0 -22
- 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,
|
|
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,
|
|
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,
|
|
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
|
|
300
|
+
prompt: hookMemoryQuery(input),
|
|
301
|
+
pinnedPaths: recall.paths
|
|
407
302
|
}
|
|
408
303
|
});
|
|
409
|
-
|
|
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(`
|
|
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:
|
|
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
|
-
|
|
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
|
|
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"
|
|
509
|
-
|
|
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
|
+
}
|