@dotdotgod/hermes 0.6.3 → 0.6.5
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/README.md +19 -15
- package/__init__.py +1 -1
- package/mcp/cli.mjs +51 -26
- package/mcp/server.mjs +31 -52
- package/package.json +2 -2
- package/plugin.yaml +1 -5
- package/policy.py +4 -12
- package/runtime.py +49 -56
- package/skills/project-initializer/templates/case-and-evidence.json +1 -6
- package/skills/project-initializer/templates/dotdotgod.config.json +1 -6
- package/skills/project-initializer/templates/policy.json +1 -6
- package/skills/project-initializer/templates/portfolio.json +1 -6
- package/skills/project-initializer/templates/publication.json +1 -6
- package/skills/project-initializer/templates/research.json +1 -6
- package/skills/project-initializer/templates/software.json +1 -6
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ Do not enable a separate profile-wide dotdotgod MCP server for these tools.
|
|
|
29
29
|
Optional semantic embeddings use the existing explicit installation workflow;
|
|
30
30
|
map/README routing works without them. Do not automatically install dependencies.
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Project Routing
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Optional project labels can be configured in config.yaml:
|
|
35
35
|
|
|
36
36
|
```yaml
|
|
37
37
|
plugins:
|
|
@@ -42,27 +42,31 @@ plugins:
|
|
|
42
42
|
roots:
|
|
43
43
|
app: /absolute/path/to/app
|
|
44
44
|
docs: /absolute/path/to/docs-project
|
|
45
|
-
gateway_access:
|
|
46
|
-
"telegram:123456": [app]
|
|
47
|
-
"discord:987654": [docs]
|
|
48
45
|
```
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
Host session IDs are optional bookkeeping, not permissions. Tools work before
|
|
48
|
+
pre-LLM registration. Without an ID, dotdotgod uses a disposable connection with
|
|
49
|
+
an explicit load root or cli_root/launch-directory default, never another chat's
|
|
50
|
+
selection. Ordinary host tools without known routing state pass through unchanged.
|
|
51
|
+
Missing parents and changed senders do not deny tools. Impact interception needs
|
|
52
|
+
known project state; nested host calls omitting it cannot be attributed reliably.
|
|
53
|
+
|
|
54
|
+
Any host session may select a configured label or an existing absolute directory
|
|
55
|
+
using dotdotgod_select_root. Legacy gateway_access settings are ignored.
|
|
56
|
+
Dotdotgod imposes no filesystem-area restrictions. Gateway operators must rely
|
|
57
|
+
on host permissions and OS isolation, not plugin labels, for access control.
|
|
54
58
|
CLI starts at the launch directory; optional cli_root sets its repository.
|
|
55
59
|
|
|
56
|
-
One bridge and MCP server are created lazily per
|
|
60
|
+
One bridge and MCP server are created lazily per selected project/host
|
|
57
61
|
session. Same-repository connections share the project SQLite DB, not mutable
|
|
58
62
|
connection-session identity. Explicit context session_resume affects only the
|
|
59
63
|
caller's connection; its last resumed ID survives transport reconnects.
|
|
60
|
-
Selections persist in profile-owned plugin state and are
|
|
64
|
+
Selections persist in profile-owned plugin state and are resolved on restore.
|
|
61
65
|
Startup/restored sessions conservatively reassess project memory; abandoned
|
|
62
66
|
sibling completion is not inherited.
|
|
63
67
|
|
|
64
68
|
This is routing isolation, not multi-tenant OS isolation. Hermes and enabled
|
|
65
|
-
plugins retain host permissions.
|
|
69
|
+
plugins retain host permissions. Sessions using the same repository share
|
|
66
70
|
project data; context scope/session filters are not access-control lists.
|
|
67
71
|
|
|
68
72
|
## Features
|
|
@@ -82,7 +86,7 @@ project data; context scope/session filters are not access-control lists.
|
|
|
82
86
|
checked fingerprints. Successful checks clear only checked, unchanged paths.
|
|
83
87
|
Source edits reopen pending state; reads and impact checks remain available.
|
|
84
88
|
- Native Hermes delegate_task is the delegation mechanism. Child hooks inherit
|
|
85
|
-
|
|
89
|
+
an active matching-principal parent's project by default, with a separate
|
|
86
90
|
context connection; impact covers their changes in the shared worktree.
|
|
87
91
|
|
|
88
92
|
No dedicated planning state, wizard, source-edit approval UI, compaction feature,
|
|
@@ -95,8 +99,8 @@ Python/Node test commands are blocked while impact is pending. Execute batches
|
|
|
95
99
|
are checked inside the adapter too. Opaque execute_file calls are blocked while
|
|
96
100
|
pending because their effects cannot be inferred safely.
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
Relative native file paths and terminal workdir default to the selected project;
|
|
103
|
+
explicit paths may refer anywhere accessible to the host. This adapter targets local host worktrees, not remote/container path
|
|
100
104
|
translation. Shell paths, patch bodies, arbitrary programs and other plugins are
|
|
101
105
|
not a filesystem sandbox; keep Hermes's own approvals and OS isolation.
|
|
102
106
|
Shell mutations hidden inside one combined mutation/release command are not fully
|
package/__init__.py
CHANGED
|
@@ -31,7 +31,7 @@ def register(ctx):
|
|
|
31
31
|
}, handler=handle, description=tool["description"])
|
|
32
32
|
|
|
33
33
|
for name, schema, description in [
|
|
34
|
-
("dotdotgod_select_root", {"type": "object", "properties": {"label": {"type": "string"}}, "required": ["label"], "additionalProperties": False}, "Select
|
|
34
|
+
("dotdotgod_select_root", {"type": "object", "properties": {"label": {"type": "string"}}, "required": ["label"], "additionalProperties": False}, "Select a project for this host session using a configured label or absolute directory path."),
|
|
35
35
|
("dotdotgod_impact_status", {"type": "object", "properties": {}, "additionalProperties": False}, "Read pending impact paths for the selected repository; does not clear them."),
|
|
36
36
|
]:
|
|
37
37
|
def handle_local(args, session_id="", _name=name, **kwargs):
|
package/mcp/cli.mjs
CHANGED
|
@@ -855,6 +855,15 @@ import { homedir } from "node:os";
|
|
|
855
855
|
import { join } from "node:path";
|
|
856
856
|
var DEFAULT_EMBEDDING_PROFILE = Object.freeze({ provider: "local", model: "Xenova/multilingual-e5-small" });
|
|
857
857
|
var EMBEDDING_PROVIDERS = Object.freeze(["local", "openai-compatible", "ollama"]);
|
|
858
|
+
var DEFAULT_INPUT_FORMAT = Object.freeze({ queryPrefix: "query: ", documentPrefix: "passage: " });
|
|
859
|
+
function embeddingInputFormat(profile, label = "embedding") {
|
|
860
|
+
if (profile.inputFormat === void 0) return { ...DEFAULT_INPUT_FORMAT };
|
|
861
|
+
const format = profile.inputFormat;
|
|
862
|
+
if (!format || typeof format !== "object" || Array.isArray(format) || Object.keys(format).some((key) => !Object.hasOwn(DEFAULT_INPUT_FORMAT, key)) || Object.keys(DEFAULT_INPUT_FORMAT).some((key) => typeof format[key] !== "string")) {
|
|
863
|
+
throw new Error(`${label}.inputFormat must contain exactly queryPrefix and documentPrefix strings.`);
|
|
864
|
+
}
|
|
865
|
+
return { queryPrefix: format.queryPrefix, documentPrefix: format.documentPrefix };
|
|
866
|
+
}
|
|
858
867
|
function readJson(path, label) {
|
|
859
868
|
if (!existsSync(path)) return null;
|
|
860
869
|
try {
|
|
@@ -865,7 +874,7 @@ function readJson(path, label) {
|
|
|
865
874
|
}
|
|
866
875
|
function validateEmbeddingProfile(profile, label = "embedding") {
|
|
867
876
|
if (!profile || typeof profile !== "object" || Array.isArray(profile)) throw new Error(`${label} must be an object.`);
|
|
868
|
-
const allowed = /* @__PURE__ */ new Set(["provider", "model", "baseUrl", "apiKey", "apiKeyEnv"]);
|
|
877
|
+
const allowed = /* @__PURE__ */ new Set(["provider", "model", "baseUrl", "apiKey", "apiKeyEnv", "inputFormat"]);
|
|
869
878
|
const unknown = Object.keys(profile).find((key) => !allowed.has(key));
|
|
870
879
|
if (unknown) throw new Error(`${label}.${unknown} is not supported.`);
|
|
871
880
|
if (!EMBEDDING_PROVIDERS.includes(profile.provider)) throw new Error(`${label}.provider must be local, openai-compatible, or ollama.`);
|
|
@@ -883,6 +892,7 @@ function validateEmbeddingProfile(profile, label = "embedding") {
|
|
|
883
892
|
if (!["http:", "https:"].includes(url.protocol)) throw new Error(`${label}.baseUrl must use HTTP or HTTPS.`);
|
|
884
893
|
if (url.username || url.password) throw new Error(`${label}.baseUrl must not contain credentials.`);
|
|
885
894
|
}
|
|
895
|
+
embeddingInputFormat(profile, label);
|
|
886
896
|
return { ...profile, model: profile.model.trim(), ...profile.baseUrl ? { baseUrl: profile.baseUrl.replace(/\/$/, "") } : {} };
|
|
887
897
|
}
|
|
888
898
|
function sanitizeEmbeddingProfile(profile) {
|
|
@@ -890,8 +900,13 @@ function sanitizeEmbeddingProfile(profile) {
|
|
|
890
900
|
return { ...safe, ...apiKey ? { apiKey: "[redacted]" } : {} };
|
|
891
901
|
}
|
|
892
902
|
function embeddingProfileIdentity(profile) {
|
|
893
|
-
|
|
894
|
-
|
|
903
|
+
return {
|
|
904
|
+
provider: profile.provider,
|
|
905
|
+
model: profile.model,
|
|
906
|
+
...profile.baseUrl !== void 0 ? { baseUrl: profile.baseUrl } : {},
|
|
907
|
+
inputFormatVersion: 1,
|
|
908
|
+
inputFormat: embeddingInputFormat(profile)
|
|
909
|
+
};
|
|
895
910
|
}
|
|
896
911
|
function resolveEmbeddingProfile(root = ".", options = {}) {
|
|
897
912
|
const home = options.home ?? homedir();
|
|
@@ -938,9 +953,6 @@ var DEFAULT_LOAD_POLICY = {
|
|
|
938
953
|
exclude: ["docs/plan", "docs/archive"]
|
|
939
954
|
}
|
|
940
955
|
};
|
|
941
|
-
var DEFAULT_PLAN_MODE_POLICY = {
|
|
942
|
-
writablePaths: ["docs/plan/**", "docs/archive/**"]
|
|
943
|
-
};
|
|
944
956
|
var DEFAULT_IMPACT_RANKING_POLICY = {
|
|
945
957
|
connectionCap: 80,
|
|
946
958
|
memoryCap: 20,
|
|
@@ -1115,8 +1127,8 @@ function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
|
1115
1127
|
}
|
|
1116
1128
|
};
|
|
1117
1129
|
}
|
|
1118
|
-
function clonePlanModePolicy(
|
|
1119
|
-
return {
|
|
1130
|
+
function clonePlanModePolicy() {
|
|
1131
|
+
return {};
|
|
1120
1132
|
}
|
|
1121
1133
|
function normalizeDocumentationRoot(value = DEFAULT_DOCUMENTATION_ROOT) {
|
|
1122
1134
|
return String(value).trim().replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "");
|
|
@@ -1130,7 +1142,7 @@ function isValidDocumentationRoot(value) {
|
|
|
1130
1142
|
}
|
|
1131
1143
|
function defaultMemoryConfig({ documentationRoot = DEFAULT_DOCUMENTATION_ROOT } = {}) {
|
|
1132
1144
|
const root = normalizeDocumentationRoot(documentationRoot);
|
|
1133
|
-
return { source: "default", documentation: { root }, areas: memoryAreasForRoot(root).map(cloneArea), traceability: cloneTraceabilityPolicy({ ...DEFAULT_TRACEABILITY_POLICY, required: [`${root}/spec/**`] }), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy({ ...DEFAULT_LOAD_POLICY, documentationSummary: { exclude: [`${root}/plan`, `${root}/archive`] } }), planMode: clonePlanModePolicy(
|
|
1145
|
+
return { source: "default", documentation: { root }, areas: memoryAreasForRoot(root).map(cloneArea), traceability: cloneTraceabilityPolicy({ ...DEFAULT_TRACEABILITY_POLICY, required: [`${root}/spec/**`] }), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy({ ...DEFAULT_LOAD_POLICY, documentationSummary: { exclude: [`${root}/plan`, `${root}/archive`] } }), planMode: clonePlanModePolicy() };
|
|
1134
1146
|
}
|
|
1135
1147
|
function normalizePathPattern(value = "") {
|
|
1136
1148
|
return value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$|^\/+/, "");
|
|
@@ -1191,10 +1203,8 @@ function isSecretLikePathPattern(value = "") {
|
|
|
1191
1203
|
const normalized = normalizePathPattern(value);
|
|
1192
1204
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
1193
1205
|
}
|
|
1194
|
-
function normalizePlanModePolicy(
|
|
1195
|
-
|
|
1196
|
-
const writablePaths = raw.writablePaths === void 0 ? DEFAULT_PLAN_MODE_POLICY.writablePaths : [...new Set((Array.isArray(raw.writablePaths) ? raw.writablePaths : []).map(normalizePathPattern))];
|
|
1197
|
-
return clonePlanModePolicy({ writablePaths });
|
|
1206
|
+
function normalizePlanModePolicy(_raw) {
|
|
1207
|
+
return clonePlanModePolicy();
|
|
1198
1208
|
}
|
|
1199
1209
|
function normalizeLoadPolicy(raw) {
|
|
1200
1210
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return cloneLoadPolicy();
|
|
@@ -1339,10 +1349,6 @@ Fix: ${fix ?? `update ${field2 ?? "this config"} in ${file} to match the expecte
|
|
|
1339
1349
|
if (planMode !== void 0) {
|
|
1340
1350
|
if (!planMode || typeof planMode !== "object" || Array.isArray(planMode)) {
|
|
1341
1351
|
add("PLAN_MODE_CONFIG_INVALID", "planMode", "Expected an object.");
|
|
1342
|
-
} else if (planMode.writablePaths !== void 0) {
|
|
1343
|
-
const paths = planMode.writablePaths;
|
|
1344
|
-
if (!Array.isArray(paths)) add("PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS", "planMode.writablePaths", "Expected an array of documentation path strings.");
|
|
1345
|
-
else if (paths.some((value) => !isValidPathPattern(value) || typeof value !== "string" || value.trim().startsWith("/") || !normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/`) || normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/.`) || isSecretLikePathPattern(value))) add("PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS", "planMode.writablePaths", "Expected safe repository-relative paths under documentation.root using exact paths or /** subtree patterns.");
|
|
1346
1352
|
}
|
|
1347
1353
|
}
|
|
1348
1354
|
const referenceExpansion = data.referenceExpansion;
|
|
@@ -3804,7 +3810,6 @@ function formatConfigOutput(payload) {
|
|
|
3804
3810
|
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(", ") || "none"}`);
|
|
3805
3811
|
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(", ") || "none"}`);
|
|
3806
3812
|
lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(", ") || "none"}`);
|
|
3807
|
-
lines.push(`- plan mode writable paths: ${(payload.config.planMode?.writablePaths ?? []).join(", ") || "none"}`);
|
|
3808
3813
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
3809
3814
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(", ") || "none"} (add: ${(lowSignal.add ?? []).join(", ") || "none"}; remove: ${(lowSignal.remove ?? []).join(", ") || "none"})`);
|
|
3810
3815
|
if (errors.length > 0) {
|
|
@@ -3954,7 +3959,12 @@ async function postJson(url, body, profile) {
|
|
|
3954
3959
|
const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...key ? { authorization: `Bearer ${key}` } : {} }, body: JSON.stringify(body), signal: controller.signal });
|
|
3955
3960
|
if (!response.ok) {
|
|
3956
3961
|
const category = response.status === 401 || response.status === 403 ? "authentication" : response.status === 429 ? "rate limit" : `HTTP ${response.status}`;
|
|
3957
|
-
|
|
3962
|
+
const error = new Error(`Remote embedding ${category} failure.`);
|
|
3963
|
+
if (response.status === 400 || response.status === 413) {
|
|
3964
|
+
const body2 = (await response.text()).slice(0, 8192);
|
|
3965
|
+
error.tokenLimit = /(?:exceed\w*[\s\S]{0,120}tokens?|tokens?[\s\S]{0,120}(?:exceed|limit|too (?:long|many))|maximum context length)/i.test(body2);
|
|
3966
|
+
}
|
|
3967
|
+
throw error;
|
|
3958
3968
|
}
|
|
3959
3969
|
try {
|
|
3960
3970
|
return await response.json();
|
|
@@ -3981,20 +3991,35 @@ function remoteEmbedder(profile) {
|
|
|
3981
3991
|
return ordered.map(normalize);
|
|
3982
3992
|
};
|
|
3983
3993
|
}
|
|
3994
|
+
function withInputFormat(embed, profile) {
|
|
3995
|
+
const format = embeddingInputFormat(profile);
|
|
3996
|
+
return (texts, role) => {
|
|
3997
|
+
if (role !== void 0 && role !== "query" && role !== "document") throw new Error("Embedding role must be query or document.");
|
|
3998
|
+
return embed(role === void 0 ? texts : texts.map((text) => format[`${role}Prefix`] + text));
|
|
3999
|
+
};
|
|
4000
|
+
}
|
|
3984
4001
|
async function createEmbedder(profile) {
|
|
3985
4002
|
const embed = profile.provider === "local" ? await localEmbedder(profile) : remoteEmbedder(profile);
|
|
3986
|
-
|
|
4003
|
+
async function embedBatch(texts) {
|
|
3987
4004
|
if (texts.length === 0) return [];
|
|
3988
|
-
|
|
4005
|
+
let rows;
|
|
4006
|
+
try {
|
|
4007
|
+
rows = await embed(texts);
|
|
4008
|
+
} catch (error) {
|
|
4009
|
+
if (!error.tokenLimit || texts.length === 1) throw error;
|
|
4010
|
+
const middle = Math.ceil(texts.length / 2);
|
|
4011
|
+
rows = [...await embedBatch(texts.slice(0, middle)), ...await embedBatch(texts.slice(middle))];
|
|
4012
|
+
}
|
|
3989
4013
|
if (!Array.isArray(rows) || rows.length !== texts.length) throw new Error("Embedding provider returned an unexpected vector count.");
|
|
3990
4014
|
const dimensions = rows[0]?.length;
|
|
3991
4015
|
if (!dimensions || rows.some((row) => row.length !== dimensions)) throw new Error("Embedding provider returned inconsistent vector dimensions.");
|
|
3992
4016
|
return rows;
|
|
3993
|
-
}
|
|
4017
|
+
}
|
|
4018
|
+
return withInputFormat(embedBatch, profile);
|
|
3994
4019
|
}
|
|
3995
4020
|
async function resolveEmbedder(root, options = {}) {
|
|
3996
4021
|
const resolved = options.profile ? { profile: options.profile, source: options.source ?? "injected", path: null } : resolveEmbeddingProfile(root, options);
|
|
3997
|
-
return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed
|
|
4022
|
+
return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed ? withInputFormat(options.embed, resolved.profile) : await createEmbedder(resolved.profile) };
|
|
3998
4023
|
}
|
|
3999
4024
|
|
|
4000
4025
|
// packages/cli/src/query/store.mjs
|
|
@@ -4200,7 +4225,7 @@ async function buildVectorIndex(root, embedOrOptions = {}) {
|
|
|
4200
4225
|
});
|
|
4201
4226
|
for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
|
|
4202
4227
|
const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
|
|
4203
|
-
const embedded = await resolved.embed(batch.map(({ chunk }) =>
|
|
4228
|
+
const embedded = await resolved.embed(batch.map(({ chunk }) => chunk.passage), "document");
|
|
4204
4229
|
embedded.forEach((vector, index) => {
|
|
4205
4230
|
rows[batch[index].index] = vector;
|
|
4206
4231
|
});
|
|
@@ -4230,7 +4255,7 @@ async function queryProject(root, query, options = {}) {
|
|
|
4230
4255
|
try {
|
|
4231
4256
|
assertCodeEmbeddingConsent(root, options, corpus.chunks);
|
|
4232
4257
|
index = await buildVectorIndex(root, { ...options, chunks: corpus.chunks });
|
|
4233
|
-
const [queryVector] = await index.embedding.embed([
|
|
4258
|
+
const [queryVector] = await index.embedding.embed([query], "query");
|
|
4234
4259
|
vector = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * index.manifest.dimensions, index.manifest.dimensions) })).sort((a, b) => b.vectorScore - a.vectorScore || a.id.localeCompare(b.id));
|
|
4235
4260
|
} catch (error) {
|
|
4236
4261
|
if (search === "vector") throw error;
|
|
@@ -6006,7 +6031,7 @@ async function buildVectorImpactOverlay(root, index, changedPaths, options = {})
|
|
|
6006
6031
|
if (!profile) continue;
|
|
6007
6032
|
const embed = vectorIndex.embedding?.embed ?? options.embed;
|
|
6008
6033
|
if (!embed) throw new Error("Vector index did not provide an embedder.");
|
|
6009
|
-
const [queryVector] = await embed([
|
|
6034
|
+
const [queryVector] = await embed([profile.text], "query");
|
|
6010
6035
|
if (!queryVector) throw new Error("Changed-file embedding was empty.");
|
|
6011
6036
|
const matches = rankVectorFiles(queryVector, vectorIndex).filter((match) => match.path !== changed && match.vectorScore >= threshold).slice(0, topK);
|
|
6012
6037
|
for (const match of matches) {
|
package/mcp/server.mjs
CHANGED
|
@@ -3650,49 +3650,49 @@ var require_fast_uri = __commonJS({
|
|
|
3650
3650
|
schemelessOptions.skipEscape = true;
|
|
3651
3651
|
return serialize(resolved, schemelessOptions);
|
|
3652
3652
|
}
|
|
3653
|
-
function resolveComponent(base,
|
|
3653
|
+
function resolveComponent(base, relative3, options, skipNormalization) {
|
|
3654
3654
|
const target = {};
|
|
3655
3655
|
if (!skipNormalization) {
|
|
3656
3656
|
base = parse3(serialize(base, options), options);
|
|
3657
|
-
|
|
3657
|
+
relative3 = parse3(serialize(relative3, options), options);
|
|
3658
3658
|
}
|
|
3659
3659
|
options = options || {};
|
|
3660
|
-
if (!options.tolerant &&
|
|
3661
|
-
target.scheme =
|
|
3662
|
-
target.userinfo =
|
|
3663
|
-
target.host =
|
|
3664
|
-
target.port =
|
|
3665
|
-
target.path = removeDotSegments(
|
|
3666
|
-
target.query =
|
|
3660
|
+
if (!options.tolerant && relative3.scheme) {
|
|
3661
|
+
target.scheme = relative3.scheme;
|
|
3662
|
+
target.userinfo = relative3.userinfo;
|
|
3663
|
+
target.host = relative3.host;
|
|
3664
|
+
target.port = relative3.port;
|
|
3665
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3666
|
+
target.query = relative3.query;
|
|
3667
3667
|
} else {
|
|
3668
|
-
if (
|
|
3669
|
-
target.userinfo =
|
|
3670
|
-
target.host =
|
|
3671
|
-
target.port =
|
|
3672
|
-
target.path = removeDotSegments(
|
|
3673
|
-
target.query =
|
|
3668
|
+
if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
|
|
3669
|
+
target.userinfo = relative3.userinfo;
|
|
3670
|
+
target.host = relative3.host;
|
|
3671
|
+
target.port = relative3.port;
|
|
3672
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3673
|
+
target.query = relative3.query;
|
|
3674
3674
|
} else {
|
|
3675
|
-
if (!
|
|
3675
|
+
if (!relative3.path) {
|
|
3676
3676
|
target.path = base.path;
|
|
3677
|
-
if (
|
|
3678
|
-
target.query =
|
|
3677
|
+
if (relative3.query !== void 0) {
|
|
3678
|
+
target.query = relative3.query;
|
|
3679
3679
|
} else {
|
|
3680
3680
|
target.query = base.query;
|
|
3681
3681
|
}
|
|
3682
3682
|
} else {
|
|
3683
|
-
if (
|
|
3684
|
-
target.path = removeDotSegments(
|
|
3683
|
+
if (relative3.path[0] === "/") {
|
|
3684
|
+
target.path = removeDotSegments(relative3.path);
|
|
3685
3685
|
} else {
|
|
3686
3686
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3687
|
-
target.path = "/" +
|
|
3687
|
+
target.path = "/" + relative3.path;
|
|
3688
3688
|
} else if (!base.path) {
|
|
3689
|
-
target.path =
|
|
3689
|
+
target.path = relative3.path;
|
|
3690
3690
|
} else {
|
|
3691
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3691
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
|
|
3692
3692
|
}
|
|
3693
3693
|
target.path = removeDotSegments(target.path);
|
|
3694
3694
|
}
|
|
3695
|
-
target.query =
|
|
3695
|
+
target.query = relative3.query;
|
|
3696
3696
|
}
|
|
3697
3697
|
target.userinfo = base.userinfo;
|
|
3698
3698
|
target.host = base.host;
|
|
@@ -3700,7 +3700,7 @@ var require_fast_uri = __commonJS({
|
|
|
3700
3700
|
}
|
|
3701
3701
|
target.scheme = base.scheme;
|
|
3702
3702
|
}
|
|
3703
|
-
target.fragment =
|
|
3703
|
+
target.fragment = relative3.fragment;
|
|
3704
3704
|
return target;
|
|
3705
3705
|
}
|
|
3706
3706
|
function equal(uriA, uriB, options) {
|
|
@@ -21959,23 +21959,15 @@ import { finished } from "node:stream/promises";
|
|
|
21959
21959
|
|
|
21960
21960
|
// packages/context/src/paths.mjs
|
|
21961
21961
|
import { realpathSync } from "node:fs";
|
|
21962
|
-
import {
|
|
21963
|
-
function assertWithinRoot(base, target, candidate) {
|
|
21964
|
-
const rel = relative(base, target);
|
|
21965
|
-
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
|
|
21966
|
-
throw new Error(`Path escapes project root: ${candidate}`);
|
|
21967
|
-
}
|
|
21968
|
-
}
|
|
21962
|
+
import { resolve as resolve2 } from "node:path";
|
|
21969
21963
|
function resolveWithinRoot(root2, candidate = ".") {
|
|
21970
21964
|
const base = resolve2(root2 || process.cwd());
|
|
21971
21965
|
const target = resolve2(base, candidate);
|
|
21972
|
-
assertWithinRoot(base, target, candidate);
|
|
21973
21966
|
return target;
|
|
21974
21967
|
}
|
|
21975
21968
|
function resolveExistingWithinRoot(root2, candidate = ".") {
|
|
21976
21969
|
const base = realpathSync(resolve2(root2 || process.cwd()));
|
|
21977
21970
|
const target = realpathSync(resolveWithinRoot(base, candidate));
|
|
21978
|
-
assertWithinRoot(base, target, candidate);
|
|
21979
21971
|
return target;
|
|
21980
21972
|
}
|
|
21981
21973
|
|
|
@@ -22271,7 +22263,7 @@ import { readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
|
|
|
22271
22263
|
|
|
22272
22264
|
// packages/context/src/directory-ingestion.mjs
|
|
22273
22265
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2, statSync as statSync3 } from "node:fs";
|
|
22274
|
-
import {
|
|
22266
|
+
import { relative, resolve as resolve3, sep } from "node:path";
|
|
22275
22267
|
var DEFAULT_LIMITS = Object.freeze({
|
|
22276
22268
|
maxDepth: 16,
|
|
22277
22269
|
maxVisitedEntries: 1e4,
|
|
@@ -22279,12 +22271,8 @@ var DEFAULT_LIMITS = Object.freeze({
|
|
|
22279
22271
|
maxFileBytes: 25 * 1024 * 1024,
|
|
22280
22272
|
maxAggregateBytes: 100 * 1024 * 1024
|
|
22281
22273
|
});
|
|
22282
|
-
function inside(base, target) {
|
|
22283
|
-
const rel = relative2(base, target);
|
|
22284
|
-
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute2(rel);
|
|
22285
|
-
}
|
|
22286
22274
|
function relativePath(base, target) {
|
|
22287
|
-
return
|
|
22275
|
+
return relative(base, target).split(sep).join("/");
|
|
22288
22276
|
}
|
|
22289
22277
|
function boundedInteger(value, fallback, name) {
|
|
22290
22278
|
const selected = value ?? fallback;
|
|
@@ -22335,10 +22323,7 @@ function snapshot(path, target, stat, symlink) {
|
|
|
22335
22323
|
function walkDirectoryManifest(options = {}) {
|
|
22336
22324
|
const root2 = realpathSync2(resolve3(options.root || process.cwd()));
|
|
22337
22325
|
const requested = resolve3(root2, options.path ?? ".");
|
|
22338
|
-
if (!inside(root2, requested)) throw new Error(`Directory escapes project root: ${options.path}`);
|
|
22339
|
-
if (lstatSync(requested).isSymbolicLink()) throw new Error(`Directory symlinks are not followed: ${options.path ?? "."}`);
|
|
22340
22326
|
const directory = realpathSync2(requested);
|
|
22341
|
-
if (!inside(root2, directory)) throw new Error(`Directory escapes project root: ${options.path}`);
|
|
22342
22327
|
if (!statSync3(directory).isDirectory()) throw new Error(`Not a directory: ${options.path ?? "."}`);
|
|
22343
22328
|
const policy = normalizeOptions(options);
|
|
22344
22329
|
const files = [];
|
|
@@ -22395,10 +22380,6 @@ function walkDirectoryManifest(options = {}) {
|
|
|
22395
22380
|
let targetStat;
|
|
22396
22381
|
try {
|
|
22397
22382
|
target = realpathSync2(absolutePath);
|
|
22398
|
-
if (!inside(root2, target)) {
|
|
22399
|
-
skip(path, "symlink-outside-root");
|
|
22400
|
-
continue;
|
|
22401
|
-
}
|
|
22402
22383
|
targetStat = statSync3(target);
|
|
22403
22384
|
} catch {
|
|
22404
22385
|
skip(path, "symlink-unresolvable");
|
|
@@ -22462,9 +22443,7 @@ function matchesIdentity(stat, identity) {
|
|
|
22462
22443
|
return stat.dev === identity.dev && stat.ino === identity.ino && stat.size === identity.size && stat.mtimeMs === identity.mtimeMs;
|
|
22463
22444
|
}
|
|
22464
22445
|
function verifyManifestFile(entry, root2) {
|
|
22465
|
-
const base = realpathSync2(resolve3(root2));
|
|
22466
22446
|
const target = realpathSync2(entry.absolutePath);
|
|
22467
|
-
if (!inside(base, target)) throw new Error(`Manifest file escapes project root: ${entry.path}`);
|
|
22468
22447
|
const stat = statSync3(target);
|
|
22469
22448
|
if (!stat.isFile()) throw new Error(`Manifest entry is not a regular file: ${entry.path}`);
|
|
22470
22449
|
if (!matchesIdentity(stat, entry.identity ?? {})) throw new Error(`Manifest file changed before indexing: ${entry.path}`);
|
|
@@ -23197,7 +23176,7 @@ function runDoctor(options = {}) {
|
|
|
23197
23176
|
// packages/context/src/project.mjs
|
|
23198
23177
|
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
23199
23178
|
import { fileURLToPath } from "node:url";
|
|
23200
|
-
import { relative as
|
|
23179
|
+
import { relative as relative2, resolve as resolve5 } from "node:path";
|
|
23201
23180
|
import { spawn as spawn2 } from "node:child_process";
|
|
23202
23181
|
var cli = fileURLToPath(new URL("./cli.mjs", import.meta.url));
|
|
23203
23182
|
function markdownTree(root2, maxDepth = 5) {
|
|
@@ -23209,9 +23188,9 @@ function markdownTree(root2, maxDepth = 5) {
|
|
|
23209
23188
|
for (const entry of readdirSync2(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
23210
23189
|
const path = resolve5(dir, entry.name);
|
|
23211
23190
|
if (entry.isDirectory()) {
|
|
23212
|
-
output.push(`${" ".repeat(depth - 1)}- ${
|
|
23191
|
+
output.push(`${" ".repeat(depth - 1)}- ${relative2(root2, path)}/`);
|
|
23213
23192
|
walk(path, depth + 1);
|
|
23214
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) output.push(`${" ".repeat(depth - 1)}- ${
|
|
23193
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) output.push(`${" ".repeat(depth - 1)}- ${relative2(root2, path)}`);
|
|
23215
23194
|
}
|
|
23216
23195
|
}
|
|
23217
23196
|
walk(docs, 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdotgod/hermes",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Hermes CLI and messaging-gateway adapter without Plan Mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
15
15
|
"esbuild": "0.27.3",
|
|
16
|
-
"@dotdotgod/context": "0.6.
|
|
16
|
+
"@dotdotgod/context": "0.6.5"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"plugin.yaml",
|
package/plugin.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: dotdotgod
|
|
2
|
-
version: 0.6.
|
|
2
|
+
version: 0.6.5
|
|
3
3
|
description: Project memory, isolated context tools and impact checks for Hermes CLI and messaging gateway; no Plan Mode.
|
|
4
4
|
author: dotdotgod
|
|
5
5
|
license: Elastic-2.0
|
|
@@ -8,10 +8,6 @@ config_schema:
|
|
|
8
8
|
type: dict
|
|
9
9
|
default: {}
|
|
10
10
|
description: Repository labels mapped to absolute local paths, configured by the operator.
|
|
11
|
-
gateway_access:
|
|
12
|
-
type: dict
|
|
13
|
-
default: {}
|
|
14
|
-
description: 'Allowed repository labels per platform:sender_id principal; no wildcard grants.'
|
|
15
11
|
cli_root:
|
|
16
12
|
type: str
|
|
17
13
|
default: ''
|
package/policy.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Project changed-file fingerprints and deliberately limited command gates."""
|
|
2
2
|
import hashlib
|
|
3
3
|
import re
|
|
4
4
|
import subprocess
|
|
@@ -11,10 +11,8 @@ EXCLUDED = (".dotdotgod/", "node_modules/", "dist/", "build/", "coverage/", "doc
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def contained(root, value):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
raise ValueError("Path escapes selected repository")
|
|
17
|
-
return path
|
|
14
|
+
# Legacy helper name: root supplies relative-path defaults only.
|
|
15
|
+
return (root / value).resolve()
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
def tracked(path):
|
|
@@ -66,13 +64,7 @@ def file_arguments(root, name, args):
|
|
|
66
64
|
if name in {"read_file", "write_file", "file_edit", "file_write", "file_read"}:
|
|
67
65
|
for key in ("path", "file_path"):
|
|
68
66
|
if isinstance(args.get(key), str):
|
|
69
|
-
|
|
70
|
-
target = contained(root, args[key])
|
|
71
|
-
except ValueError:
|
|
72
|
-
target = Path(args[key]).resolve()
|
|
73
|
-
if name not in {"read_file", "file_read"} or not target.is_relative_to(Path(__file__).parent.resolve() / "skills"):
|
|
74
|
-
raise
|
|
75
|
-
args[key] = str(target)
|
|
67
|
+
args[key] = str(contained(root, args[key]))
|
|
76
68
|
if name == "terminal":
|
|
77
69
|
args["workdir"] = str(contained(root, args.get("workdir") or "."))
|
|
78
70
|
return args
|
package/runtime.py
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import atexit
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
import subprocess
|
|
7
8
|
import threading
|
|
9
|
+
import uuid
|
|
8
10
|
from dataclasses import dataclass
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
@@ -46,44 +48,28 @@ class Runtime:
|
|
|
46
48
|
roots = self.ctx.get_config("roots", {})
|
|
47
49
|
if not isinstance(roots, dict):
|
|
48
50
|
raise ValueError("roots must be an operator-configured mapping")
|
|
49
|
-
|
|
50
|
-
return roots
|
|
51
|
-
access = self.ctx.get_config("gateway_access", {})
|
|
52
|
-
labels = access.get(session.principal, []) if isinstance(access, dict) else []
|
|
53
|
-
return {label: roots[label] for label in labels if label in roots} if isinstance(labels, list) else {}
|
|
54
|
-
|
|
55
|
-
def trusted_session(self, session_id):
|
|
56
|
-
if not session_id or session_id not in self.sessions:
|
|
57
|
-
raise ValueError("No trusted host session; start a turn before using dotdotgod")
|
|
58
|
-
return self.sessions[session_id]
|
|
51
|
+
return roots
|
|
59
52
|
|
|
60
53
|
def session(self, session_id):
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
raise PermissionError("Delegating parent repository or principal changed")
|
|
68
|
-
if session.label:
|
|
69
|
-
value = self.allowed(session).get(session.label)
|
|
70
|
-
if not isinstance(value, str) or Path(value).resolve(strict=True) != session.root:
|
|
71
|
-
raise PermissionError("Repository grant changed; select an authorized root again")
|
|
72
|
-
return session
|
|
54
|
+
# Anonymous calls use disposable state, never another conversation's route.
|
|
55
|
+
if not session_id:
|
|
56
|
+
return Session(uuid.uuid4().hex, "", "")
|
|
57
|
+
if session_id not in self.sessions:
|
|
58
|
+
self.sessions[session_id] = Session(session_id, "", "")
|
|
59
|
+
return self.sessions[session_id]
|
|
73
60
|
|
|
74
61
|
def select(self, session, label):
|
|
75
|
-
|
|
76
|
-
raise PermissionError("Delegated work cannot select a different repository")
|
|
77
|
-
value = self.allowed(session).get(label)
|
|
62
|
+
value = self.allowed(session).get(label, label)
|
|
78
63
|
if not isinstance(value, str) or not Path(value).is_absolute():
|
|
79
|
-
raise
|
|
64
|
+
raise ValueError("Use a configured repository label or an absolute directory path")
|
|
80
65
|
root = Path(value).resolve(strict=True)
|
|
81
66
|
if not root.is_dir():
|
|
82
67
|
raise ValueError("Repository root must be a directory")
|
|
83
68
|
if session.proxy:
|
|
84
69
|
session.proxy.close()
|
|
85
70
|
session.root, session.label, session.proxy, session.loaded = root, label, None, False
|
|
86
|
-
self.
|
|
71
|
+
if self.sessions.get(session.id) is session:
|
|
72
|
+
self.ctx.state.set("route:" + digest(session.id), {"principal": session.principal, "label": label})
|
|
87
73
|
return envelope({"ok": True, "root": str(root), "label": label})
|
|
88
74
|
|
|
89
75
|
def client(self, session):
|
|
@@ -123,12 +109,18 @@ class Runtime:
|
|
|
123
109
|
data = {"ok": True, "root": str(session.root), "focus": focus, "documentationMap": mapping, "query": query}
|
|
124
110
|
return envelope(data, "Project memory (map/query evidence; read bodies selectively):\n" + json.dumps(data, ensure_ascii=False) + "\nHelp: dotdotgod --help")
|
|
125
111
|
|
|
126
|
-
def call(self, name, args, session_id):
|
|
112
|
+
def call(self, name, args, session_id=""):
|
|
113
|
+
with self.lock:
|
|
114
|
+
session = self.session(session_id)
|
|
115
|
+
try:
|
|
116
|
+
return self.call_with_session(name, args, session)
|
|
117
|
+
finally:
|
|
118
|
+
if not session_id and session.proxy:
|
|
119
|
+
session.proxy.close()
|
|
120
|
+
|
|
121
|
+
def call_with_session(self, name, args, session):
|
|
127
122
|
with self.lock:
|
|
128
|
-
session = self.trusted_session(session_id)
|
|
129
123
|
if name == "dotdotgod_select_root":
|
|
130
|
-
if not self.allowed(session):
|
|
131
|
-
raise PermissionError("No authorized repository labels; only dotdotgod_project_initialize is available")
|
|
132
124
|
return self.select(session, args.get("label"))
|
|
133
125
|
if session.root is None and not session.parent_id and name == "dotdotgod_project_initialize":
|
|
134
126
|
target = args.get("root")
|
|
@@ -142,12 +134,20 @@ class Runtime:
|
|
|
142
134
|
return proxy.call(name, {**args, "root": str(root)})
|
|
143
135
|
finally:
|
|
144
136
|
proxy.close()
|
|
145
|
-
|
|
137
|
+
if name == "dotdotgod_project_load" and args.get("root"):
|
|
138
|
+
base = session.root or self.launch_root
|
|
139
|
+
target = contained(base, args["root"])
|
|
140
|
+
if target != session.root:
|
|
141
|
+
self.select(session, str(target))
|
|
142
|
+
if session.root is None:
|
|
143
|
+
session.root = Path(self.ctx.get_config("cli_root", "") or self.launch_root).resolve(strict=True)
|
|
144
|
+
if not session.root.is_dir():
|
|
145
|
+
raise ValueError("Project root must be a directory")
|
|
146
146
|
if name == "dotdotgod_impact_status":
|
|
147
147
|
return envelope({"ok": True, "pending": list(self.pending(session))})
|
|
148
148
|
if name == "dotdotgod_project_load":
|
|
149
149
|
if contained(session.root, args.get("root") or ".") != session.root:
|
|
150
|
-
|
|
150
|
+
self.select(session, str(contained(session.root, args["root"])))
|
|
151
151
|
return self.load(session, args)
|
|
152
152
|
if name in {"execute", "execute_file"}:
|
|
153
153
|
reason = self.check(session, name, args)
|
|
@@ -155,7 +155,7 @@ class Runtime:
|
|
|
155
155
|
raise PermissionError(reason)
|
|
156
156
|
if name == "dotdotgod_project_impact":
|
|
157
157
|
before = self.pending(session)
|
|
158
|
-
paths = [contained(session.root, path)
|
|
158
|
+
paths = [os.path.relpath(contained(session.root, path), session.root) for path in args.get("paths", [])]
|
|
159
159
|
args = {**args, "paths": paths}
|
|
160
160
|
value = self.client(session).call(name, args)
|
|
161
161
|
if name == "dotdotgod_project_impact" and not value.get("isError") and value.get("structuredContent", {}).get("ok") is True:
|
|
@@ -178,16 +178,13 @@ class Runtime:
|
|
|
178
178
|
|
|
179
179
|
def before_tool(self, tool_name, args, session_id="", **kwargs):
|
|
180
180
|
try:
|
|
181
|
-
session = self.
|
|
182
|
-
if session
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return None
|
|
187
|
-
return {"action": "block", "message": "dotdotgod is inactive without a selected repository; only initialization is available (selection requires authorized labels)"}
|
|
181
|
+
session = self.sessions.get(session_id) if session_id else None
|
|
182
|
+
if session is None:
|
|
183
|
+
return None
|
|
184
|
+
if session.root is None:
|
|
185
|
+
return None
|
|
188
186
|
if tool_name == "dotdotgod_select_root":
|
|
189
187
|
return None
|
|
190
|
-
session = self.session(session_id)
|
|
191
188
|
reason = self.check(session, tool_name, args)
|
|
192
189
|
if reason:
|
|
193
190
|
return {"action": "block", "message": reason}
|
|
@@ -199,20 +196,17 @@ class Runtime:
|
|
|
199
196
|
|
|
200
197
|
def before_llm(self, session_id, user_message, platform="", sender_id="", parent_session_id="", **kwargs):
|
|
201
198
|
with self.lock:
|
|
202
|
-
parent = self.
|
|
199
|
+
parent = self.sessions.get(parent_session_id) if platform == "subagent" else None
|
|
203
200
|
principal = parent.principal if parent else platform + ":" + sender_id
|
|
204
201
|
if parent:
|
|
205
202
|
platform = parent.platform
|
|
206
203
|
session = self.sessions.get(session_id)
|
|
207
|
-
if session and session.principal != principal:
|
|
208
|
-
if session.proxy:
|
|
209
|
-
session.proxy.close()
|
|
210
|
-
session = None
|
|
211
204
|
if session is None:
|
|
212
|
-
session = Session(session_id, principal, platform)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
205
|
+
session = Session(session_id or uuid.uuid4().hex, principal, platform)
|
|
206
|
+
if session_id:
|
|
207
|
+
self.sessions[session_id] = session
|
|
208
|
+
saved = self.ctx.state.get("route:" + digest(session_id), {}) if session_id else {}
|
|
209
|
+
if isinstance(saved.get("label"), str) and (saved["label"] in self.allowed(session) or Path(saved["label"]).is_absolute()):
|
|
216
210
|
self.select(session, saved["label"])
|
|
217
211
|
elif parent:
|
|
218
212
|
session.root, session.label, session.parent_id = parent.root, parent.label, parent.id
|
|
@@ -220,12 +214,11 @@ class Runtime:
|
|
|
220
214
|
session.root = Path(self.ctx.get_config("cli_root", "") or self.launch_root).resolve(strict=True)
|
|
221
215
|
if session.root is None:
|
|
222
216
|
labels = list(self.allowed(session))
|
|
223
|
-
context = "dotdotgod is inactive; ordinary host tools remain available.
|
|
217
|
+
context = "dotdotgod is inactive; ordinary host tools remain available. Select a project using dotdotgod_select_root with a configured label or absolute directory path, or initialize an explicit absolute directory with existing write confirmation. Initialization does not select a project."
|
|
224
218
|
if labels:
|
|
225
|
-
context += "
|
|
219
|
+
context += " Configured project labels: " + json.dumps(labels)
|
|
226
220
|
return {"context": context}
|
|
227
221
|
try:
|
|
228
|
-
self.session(session_id)
|
|
229
222
|
parts = []
|
|
230
223
|
text = user_message if isinstance(user_message, str) else " ".join(p.get("text", "") for p in user_message if isinstance(p, dict))
|
|
231
224
|
if not session.loaded and not re.search(r"(?:^|\s)/?dd:no-load\b|(?:^|\s)/no-load\b", text):
|
|
@@ -247,7 +240,7 @@ class Runtime:
|
|
|
247
240
|
|
|
248
241
|
def finalize(self, session_id="", **kwargs):
|
|
249
242
|
for child in list(self.sessions.values()):
|
|
250
|
-
if child.parent_id == session_id:
|
|
243
|
+
if session_id and child.parent_id == session_id:
|
|
251
244
|
self.finalize(child.id)
|
|
252
245
|
session = self.sessions.pop(session_id, None)
|
|
253
246
|
if session and session.proxy:
|
|
@@ -256,7 +249,7 @@ class Runtime:
|
|
|
256
249
|
def end_turn(self, session_id="", interrupted=False, **kwargs):
|
|
257
250
|
if interrupted:
|
|
258
251
|
for child in list(self.sessions.values()):
|
|
259
|
-
if child.parent_id == session_id:
|
|
252
|
+
if session_id and child.parent_id == session_id:
|
|
260
253
|
self.end_turn(child.id, interrupted=True)
|
|
261
254
|
session = self.sessions.get(session_id)
|
|
262
255
|
if session and session.proxy:
|