odai-dsh-agent 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -138
  3. package/bin/odai-dsh-agent.mjs +108 -108
  4. package/package.json +69 -69
  5. package/preset/odai/agent.cordis.yml +261 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/compaction-config.mjs +388 -388
  8. package/preset/odai/runtime/governance.mjs +128 -128
  9. package/preset/odai/runtime/index.mjs +2461 -2263
  10. package/preset/odai/runtime/output-config.mjs +360 -304
  11. package/preset/odai/runtime/research-packet.mjs +150 -150
  12. package/preset/odai/runtime/responsibility-scope.mjs +35 -2
  13. package/preset/odai/runtime/role-overlays.mjs +27 -27
  14. package/preset/odai/runtime/route-card.mjs +168 -152
  15. package/preset/odai/runtime/router.mjs +683 -651
  16. package/preset/odai/runtime/routing-config.mjs +480 -436
  17. package/preset/odai/runtime/routing-context.mjs +321 -321
  18. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  19. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  20. package/preset/odai/runtime/session-compat.mjs +389 -389
  21. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  22. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  23. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  24. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  25. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  26. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  27. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  28. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  29. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  30. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  31. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  32. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  33. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  34. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  35. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  36. package/preset/odai/skills/odai/manifest.json +37 -37
  37. package/preset/odai/skills/odai/references/craft.md +54 -54
  38. package/preset/odai/skills/odai/references/dao.md +30 -30
  39. package/preset/odai/skills/odai/references/leverage.md +53 -53
  40. package/preset/odai/skills/odai/references/planning.md +82 -82
  41. package/preset/odai/skills/odai/references/support.md +36 -36
  42. package/preset/odai/skills/odai/references/verification.md +23 -23
  43. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  44. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  45. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  46. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  47. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  48. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  49. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  50. package/scripts/verify-agent-load.mjs +257 -257
  51. package/src/installer.mjs +398 -382
@@ -1,150 +1,150 @@
1
- import { createHash } from "node:crypto";
2
- import { readFileSync, realpathSync, statSync } from "node:fs";
3
- import { isAbsolute, relative, resolve, sep } from "node:path";
4
-
5
- const PACKET_FIELDS = new Set(["schemaVersion", "question", "facts", "conflicts", "unknowns", "stop"]);
6
- const FACT_FIELDS = new Set(["claim", "excerpt", "source", "authority"]);
7
- const SOURCE_FIELDS = new Set(["path", "line"]);
8
- const DEFAULT_MAX_CHARS = 8_000;
9
- const DEFAULT_MAX_FACTS = 12;
10
- const DEFAULT_MAX_ITEMS = 8;
11
-
12
- function plainObject(value, field) {
13
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
- throw new TypeError(`${field} must be an object`);
15
- }
16
- return value;
17
- }
18
-
19
- function exactFields(value, allowed, field) {
20
- const unknown = Object.keys(value).filter((key) => !allowed.has(key));
21
- if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
22
- }
23
-
24
- function text(value, field, maxChars = 1_500) {
25
- if (typeof value !== "string" || value.trim() === "") throw new TypeError(`${field} must be a non-empty string`);
26
- const normalized = value.trim();
27
- if (normalized.length > maxChars) throw new TypeError(`${field} exceeds ${maxChars} characters`);
28
- return normalized;
29
- }
30
-
31
- function stringList(value, field, maxItems) {
32
- if (!Array.isArray(value) || value.length > maxItems) {
33
- throw new TypeError(`${field} must be an array with at most ${maxItems} items`);
34
- }
35
- return Object.freeze(value.map((item, index) => text(item, `${field}[${index}]`, 800)));
36
- }
37
-
38
- function sourcePointer(value, field) {
39
- const source = plainObject(value, field);
40
- exactFields(source, SOURCE_FIELDS, field);
41
- const path = text(source.path, `${field}.path`, 500);
42
- if (path.startsWith("/") || path.startsWith("~") || path.split(/[\\/]/u).includes("..")) {
43
- throw new TypeError(`${field}.path must be repository-relative and may not traverse parent directories`);
44
- }
45
- if (!Number.isSafeInteger(source.line) || source.line <= 0) {
46
- throw new TypeError(`${field}.line must be a positive integer`);
47
- }
48
- return Object.freeze({ path, line: source.line });
49
- }
50
-
51
- function parseJsonText(output) {
52
- const trimmed = String(output ?? "").trim();
53
- const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/iu.exec(trimmed);
54
- return JSON.parse(fenced ? fenced[1] : trimmed);
55
- }
56
-
57
- export function parseResearchPacket(output, options = {}) {
58
- const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
59
- ? options.maxChars
60
- : DEFAULT_MAX_CHARS;
61
- const raw = String(output ?? "");
62
- if (raw.length > maxChars) throw new TypeError(`research packet exceeds ${maxChars} characters`);
63
- const packet = plainObject(parseJsonText(raw), "research packet");
64
- exactFields(packet, PACKET_FIELDS, "research packet");
65
- if (packet.schemaVersion !== 1) throw new TypeError("research packet.schemaVersion must be 1");
66
- if (!Array.isArray(packet.facts) || packet.facts.length < 2 || packet.facts.length > DEFAULT_MAX_FACTS) {
67
- throw new TypeError(`research packet.facts must contain 2-${DEFAULT_MAX_FACTS} source-backed facts`);
68
- }
69
- const facts = Object.freeze(packet.facts.map((value, index) => {
70
- const fact = plainObject(value, `research packet.facts[${index}]`);
71
- exactFields(fact, FACT_FIELDS, `research packet.facts[${index}]`);
72
- return Object.freeze({
73
- claim: text(fact.claim, `research packet.facts[${index}].claim`, 1_200),
74
- excerpt: text(fact.excerpt, `research packet.facts[${index}].excerpt`, 1_200),
75
- source: sourcePointer(fact.source, `research packet.facts[${index}].source`),
76
- authority: text(fact.authority, `research packet.facts[${index}].authority`, 500),
77
- });
78
- }));
79
- if (new Set(facts.map((fact) => fact.source.path)).size < 2) {
80
- throw new TypeError("research packet must cite at least two distinct source paths");
81
- }
82
- const normalized = Object.freeze({
83
- schemaVersion: 1,
84
- question: text(packet.question, "research packet.question", 1_200),
85
- facts,
86
- conflicts: stringList(packet.conflicts, "research packet.conflicts", DEFAULT_MAX_ITEMS),
87
- unknowns: stringList(packet.unknowns, "research packet.unknowns", DEFAULT_MAX_ITEMS),
88
- stop: text(packet.stop, "research packet.stop", 1_000),
89
- });
90
- const digest = createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
91
- return Object.freeze({
92
- ...normalized,
93
- digest,
94
- sourceCount: new Set(facts.map((fact) => fact.source.path)).size,
95
- });
96
- }
97
-
98
- function isInside(root, candidate) {
99
- const fromRoot = relative(root, candidate);
100
- return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
101
- }
102
-
103
- export function verifyResearchPacketSources(packet, projectRoot) {
104
- if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
105
- throw new TypeError("research packet verification requires the controller project root");
106
- }
107
- const root = realpathSync(resolve(projectRoot));
108
- for (const [index, fact] of packet.facts.entries()) {
109
- let sourcePath;
110
- try {
111
- sourcePath = realpathSync(resolve(root, fact.source.path));
112
- } catch {
113
- throw new TypeError(`research packet.facts[${index}].source.path does not exist`);
114
- }
115
- if (!isInside(root, sourcePath)) {
116
- throw new TypeError(`research packet.facts[${index}].source.path resolves outside the project root`);
117
- }
118
- const stats = statSync(sourcePath);
119
- if (!stats.isFile()) throw new TypeError(`research packet.facts[${index}].source.path must be a file`);
120
- if (stats.size > 4 * 1024 * 1024) {
121
- throw new TypeError(`research packet.facts[${index}].source.path exceeds the verification size limit`);
122
- }
123
- const sourceLine = readFileSync(sourcePath, "utf8").split(/\r?\n/u)[fact.source.line - 1];
124
- if (sourceLine === undefined) {
125
- throw new TypeError(`research packet.facts[${index}].source.line is outside the source file`);
126
- }
127
- if (sourceLine.trim() !== fact.excerpt.trim()) {
128
- throw new TypeError(`research packet.facts[${index}].excerpt does not match the cited source line`);
129
- }
130
- }
131
- return Object.freeze({ ...packet, sourcesVerified: true });
132
- }
133
-
134
- export function renderResearchPacket(packet) {
135
- return [
136
- "# Odai bounded researcher evidence packet",
137
- `digest: sha256:${packet.digest}`,
138
- `sources: ${packet.sourceCount}`,
139
- "This packet is a retrieval index, not authority, planning, acceptance, or permission to act. Sample every route-changing source before deciding; reacquire only missing or conflicting evidence.",
140
- "",
141
- JSON.stringify({
142
- schemaVersion: packet.schemaVersion,
143
- question: packet.question,
144
- facts: packet.facts,
145
- conflicts: packet.conflicts,
146
- unknowns: packet.unknowns,
147
- stop: packet.stop,
148
- }, null, 2),
149
- ].join("\n");
150
- }
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+
5
+ const PACKET_FIELDS = new Set(["schemaVersion", "question", "facts", "conflicts", "unknowns", "stop"]);
6
+ const FACT_FIELDS = new Set(["claim", "excerpt", "source", "authority"]);
7
+ const SOURCE_FIELDS = new Set(["path", "line"]);
8
+ const DEFAULT_MAX_CHARS = 8_000;
9
+ const DEFAULT_MAX_FACTS = 12;
10
+ const DEFAULT_MAX_ITEMS = 8;
11
+
12
+ function plainObject(value, field) {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
+ throw new TypeError(`${field} must be an object`);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ function exactFields(value, allowed, field) {
20
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
21
+ if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
22
+ }
23
+
24
+ function text(value, field, maxChars = 1_500) {
25
+ if (typeof value !== "string" || value.trim() === "") throw new TypeError(`${field} must be a non-empty string`);
26
+ const normalized = value.trim();
27
+ if (normalized.length > maxChars) throw new TypeError(`${field} exceeds ${maxChars} characters`);
28
+ return normalized;
29
+ }
30
+
31
+ function stringList(value, field, maxItems) {
32
+ if (!Array.isArray(value) || value.length > maxItems) {
33
+ throw new TypeError(`${field} must be an array with at most ${maxItems} items`);
34
+ }
35
+ return Object.freeze(value.map((item, index) => text(item, `${field}[${index}]`, 800)));
36
+ }
37
+
38
+ function sourcePointer(value, field) {
39
+ const source = plainObject(value, field);
40
+ exactFields(source, SOURCE_FIELDS, field);
41
+ const path = text(source.path, `${field}.path`, 500);
42
+ if (path.startsWith("/") || path.startsWith("~") || path.split(/[\\/]/u).includes("..")) {
43
+ throw new TypeError(`${field}.path must be repository-relative and may not traverse parent directories`);
44
+ }
45
+ if (!Number.isSafeInteger(source.line) || source.line <= 0) {
46
+ throw new TypeError(`${field}.line must be a positive integer`);
47
+ }
48
+ return Object.freeze({ path, line: source.line });
49
+ }
50
+
51
+ function parseJsonText(output) {
52
+ const trimmed = String(output ?? "").trim();
53
+ const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/iu.exec(trimmed);
54
+ return JSON.parse(fenced ? fenced[1] : trimmed);
55
+ }
56
+
57
+ export function parseResearchPacket(output, options = {}) {
58
+ const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
59
+ ? options.maxChars
60
+ : DEFAULT_MAX_CHARS;
61
+ const raw = String(output ?? "");
62
+ if (raw.length > maxChars) throw new TypeError(`research packet exceeds ${maxChars} characters`);
63
+ const packet = plainObject(parseJsonText(raw), "research packet");
64
+ exactFields(packet, PACKET_FIELDS, "research packet");
65
+ if (packet.schemaVersion !== 1) throw new TypeError("research packet.schemaVersion must be 1");
66
+ if (!Array.isArray(packet.facts) || packet.facts.length < 2 || packet.facts.length > DEFAULT_MAX_FACTS) {
67
+ throw new TypeError(`research packet.facts must contain 2-${DEFAULT_MAX_FACTS} source-backed facts`);
68
+ }
69
+ const facts = Object.freeze(packet.facts.map((value, index) => {
70
+ const fact = plainObject(value, `research packet.facts[${index}]`);
71
+ exactFields(fact, FACT_FIELDS, `research packet.facts[${index}]`);
72
+ return Object.freeze({
73
+ claim: text(fact.claim, `research packet.facts[${index}].claim`, 1_200),
74
+ excerpt: text(fact.excerpt, `research packet.facts[${index}].excerpt`, 1_200),
75
+ source: sourcePointer(fact.source, `research packet.facts[${index}].source`),
76
+ authority: text(fact.authority, `research packet.facts[${index}].authority`, 500),
77
+ });
78
+ }));
79
+ if (new Set(facts.map((fact) => fact.source.path)).size < 2) {
80
+ throw new TypeError("research packet must cite at least two distinct source paths");
81
+ }
82
+ const normalized = Object.freeze({
83
+ schemaVersion: 1,
84
+ question: text(packet.question, "research packet.question", 1_200),
85
+ facts,
86
+ conflicts: stringList(packet.conflicts, "research packet.conflicts", DEFAULT_MAX_ITEMS),
87
+ unknowns: stringList(packet.unknowns, "research packet.unknowns", DEFAULT_MAX_ITEMS),
88
+ stop: text(packet.stop, "research packet.stop", 1_000),
89
+ });
90
+ const digest = createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
91
+ return Object.freeze({
92
+ ...normalized,
93
+ digest,
94
+ sourceCount: new Set(facts.map((fact) => fact.source.path)).size,
95
+ });
96
+ }
97
+
98
+ function isInside(root, candidate) {
99
+ const fromRoot = relative(root, candidate);
100
+ return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
101
+ }
102
+
103
+ export function verifyResearchPacketSources(packet, projectRoot) {
104
+ if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
105
+ throw new TypeError("research packet verification requires the controller project root");
106
+ }
107
+ const root = realpathSync(resolve(projectRoot));
108
+ for (const [index, fact] of packet.facts.entries()) {
109
+ let sourcePath;
110
+ try {
111
+ sourcePath = realpathSync(resolve(root, fact.source.path));
112
+ } catch {
113
+ throw new TypeError(`research packet.facts[${index}].source.path does not exist`);
114
+ }
115
+ if (!isInside(root, sourcePath)) {
116
+ throw new TypeError(`research packet.facts[${index}].source.path resolves outside the project root`);
117
+ }
118
+ const stats = statSync(sourcePath);
119
+ if (!stats.isFile()) throw new TypeError(`research packet.facts[${index}].source.path must be a file`);
120
+ if (stats.size > 4 * 1024 * 1024) {
121
+ throw new TypeError(`research packet.facts[${index}].source.path exceeds the verification size limit`);
122
+ }
123
+ const sourceLine = readFileSync(sourcePath, "utf8").split(/\r?\n/u)[fact.source.line - 1];
124
+ if (sourceLine === undefined) {
125
+ throw new TypeError(`research packet.facts[${index}].source.line is outside the source file`);
126
+ }
127
+ if (sourceLine.trim() !== fact.excerpt.trim()) {
128
+ throw new TypeError(`research packet.facts[${index}].excerpt does not match the cited source line`);
129
+ }
130
+ }
131
+ return Object.freeze({ ...packet, sourcesVerified: true });
132
+ }
133
+
134
+ export function renderResearchPacket(packet) {
135
+ return [
136
+ "# Odai bounded researcher evidence packet",
137
+ `digest: sha256:${packet.digest}`,
138
+ `sources: ${packet.sourceCount}`,
139
+ "This packet is a retrieval index, not authority, planning, acceptance, or permission to act. Sample every route-changing source before deciding; reacquire only missing or conflicting evidence.",
140
+ "",
141
+ JSON.stringify({
142
+ schemaVersion: packet.schemaVersion,
143
+ question: packet.question,
144
+ facts: packet.facts,
145
+ conflicts: packet.conflicts,
146
+ unknowns: packet.unknowns,
147
+ stop: packet.stop,
148
+ }, null, 2),
149
+ ].join("\n");
150
+ }
@@ -28,11 +28,15 @@ export function createResponsibilityScope({
28
28
  decision,
29
29
  routeValidated = false,
30
30
  cardId,
31
+ resumeOfScopeId,
31
32
  }) {
32
33
  if (!Number.isSafeInteger(turn) || turn < 1) throw new TypeError("scope turn must be a positive integer");
33
34
  if (!Number.isSafeInteger(startStep) || startStep < 1) throw new TypeError("scope startStep must be a positive integer");
34
35
  const requestedRoute = routeSnapshot(route);
35
36
  if (!requestedRoute) throw new TypeError("scope route must contain provider and model");
37
+ if (resumeOfScopeId !== undefined && (typeof resumeOfScopeId !== "string" || resumeOfScopeId === "")) {
38
+ throw new TypeError("resumeOfScopeId must be a non-empty string");
39
+ }
36
40
  return Object.freeze({
37
41
  id: randomUUID(),
38
42
  state: "pending",
@@ -46,6 +50,7 @@ export function createResponsibilityScope({
46
50
  stopPolicy: "terminal-response-or-ownership-boundary",
47
51
  routeValidated: routeValidated === true,
48
52
  ...(cardId ? { cardId } : {}),
53
+ ...(resumeOfScopeId ? { resumeOfScopeId } : {}),
49
54
  });
50
55
  }
51
56
 
@@ -113,6 +118,7 @@ function scopeEventData(scope) {
113
118
  stopPolicy: scope.stopPolicy,
114
119
  ...(scope.source ? { routeSource: scope.source } : {}),
115
120
  ...(scope.cardId ? { routeCardId: scope.cardId } : {}),
121
+ ...(scope.resumeOfScopeId ? { resumeOfScopeId: scope.resumeOfScopeId } : {}),
116
122
  ...(scope.baseRoute ? { baseRoute: scope.baseRoute } : {}),
117
123
  ...(scope.temporaryRoute ? { temporaryRoute: scope.temporaryRoute } : {}),
118
124
  ...(scope.routeMode ? { routeMode: scope.routeMode } : {}),
@@ -148,6 +154,33 @@ export function latestDanglingResponsibilityScope(events) {
148
154
  return [...states.values()].findLast((entry) => entry.state === "claimed")?.data;
149
155
  }
150
156
 
157
+ export function latestStoppedResponsibilityScope(events, turn) {
158
+ return (Array.isArray(events) ? events : []).findLast((event) => (
159
+ event?.type === "odai/responsibility-scope-stopped"
160
+ && event.data?.turn === turn
161
+ && typeof event.data?.scopeId === "string"
162
+ ))?.data;
163
+ }
164
+
165
+ export function pendingResponsibilityInterruption(events) {
166
+ const settled = new Set();
167
+ for (let index = (Array.isArray(events) ? events.length : 0) - 1; index >= 0; index -= 1) {
168
+ const event = events[index];
169
+ const scopeId = event?.data?.scopeId;
170
+ if (typeof scopeId !== "string") continue;
171
+ if (["odai/responsibility-interruption-consumed", "odai/responsibility-interruption-cleared"].includes(event.type)) {
172
+ settled.add(scopeId);
173
+ continue;
174
+ }
175
+ if (event.type === "odai/responsibility-interrupted") {
176
+ return event.data?.reason === "max-tokens" && !settled.has(scopeId)
177
+ ? event.data
178
+ : undefined;
179
+ }
180
+ }
181
+ return undefined;
182
+ }
183
+
151
184
  export function pendingResponsibilityScopeRestoration(events) {
152
185
  let candidate;
153
186
  for (const event of Array.isArray(events) ? events : []) {
@@ -157,10 +190,10 @@ export function pendingResponsibilityScopeRestoration(events) {
157
190
  candidate = event.data;
158
191
  continue;
159
192
  }
160
- if (candidate && event?.type === "request/header") candidate = undefined;
161
193
  if (candidate
162
194
  && event?.type === "odai/responsibility-scope-restored"
163
- && event.data?.scopeId === candidate.scopeId) candidate = undefined;
195
+ && event.data?.scopeId === candidate.scopeId
196
+ && event.data?.status === "applied") candidate = undefined;
164
197
  }
165
198
  return candidate;
166
199
  }
@@ -1,27 +1,27 @@
1
- const DSH_ROLE_OVERLAYS = Object.freeze({
2
- researcher: `## DSH researcher execution boundary
3
-
4
- This responsibility runs as one read-only child before the primary routing decision. Use only read, glob, grep, and other non-mutating source tools; do not run shell commands, edit, plan, recommend, approve, or delegate. Return JSON only, with no fence and no fields beyond this exact shape: {"schemaVersion":1,"question":"...","facts":[{"claim":"...","excerpt":"exact complete cited line","source":{"path":"repository/relative/path","line":1},"authority":"source role and freshness boundary"}],"conflicts":[],"unknowns":[],"stop":"..."}. Return the smallest useful 2-6 facts from at least two distinct files. DSH validates every cited file, line, and excerpt inside the project root before hashing and exposing this packet to the controller or planner.`,
5
- planner: `## DSH planner execution boundary
6
-
7
- In auto mode this responsibility runs by replacing the controller route only inside one explicit responsibility scope, not by starting a child. The scope may continue through read-only tool calls, but ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. Retain the current conversation and project context, use only read-only evidence, and do not implement or edit. If the result is mode: planned, execute: executor, and separation has a concrete observable benefit, use odai_route_card to freeze exactly one structured card with non-empty evidence, scope, acceptance conditions, and stop condition. When the original current task already authorizes implementation, immediately submit one executor gap through odai_responsibility_gap so the same task continues without another user prompt. For plan-only work, a new task, expanded scope, or missing user-owned authorization, stop for the minimal user decision instead. Otherwise do not create a route card.`,
8
- executor: `## DSH executor execution boundary
9
-
10
- In auto mode this responsibility is reachable when an active frozen route card proves observable separation benefit and either the original current task already authorizes implementation or the user explicitly continues it. It runs in one bounded controller responsibility scope because DSH routed children are read-only. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, card release, or turn boundary. Follow the claimed card exactly; do not reinterpret the request, broaden scope, or claim final acceptance. The card is consumed after the effective executor request route is verified; that route receipt admits one executor attempt but does not keep the responsibility scope alive by itself.`,
11
- reviewer: `## DSH reviewer execution boundary
12
-
13
- Start an independent child only from a bounded, hash-addressed packet that contains verified tool evidence. The child's effective request header must match its configured route before its output is accepted. Without such a packet, auto mode keeps the current controller route and may perform only a controller-local read-only check while it gathers project-available acceptance evidence. That local check is not independent acceptance, must not claim reviewer approval, and must not stop the authorized task solely to ask the user for artifacts the project can produce.`,
14
- frontend: `## DSH frontend execution boundary
15
-
16
- This responsibility runs in one bounded controller responsibility scope so it retains the active conversation, workspace, dev-server, and browser context. It is not an independent child. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. DSH may use a user-persisted frontend provider/model/reasoning route and, only when that mapping explicitly includes maxTokens, apply that responsibility ceiling inside this scope. The runtime compares the effective DSH request header with that mapping and records an applied, mismatch, or unverified receipt; configuration and self-report are not routing evidence.`,
17
- });
18
-
19
- export function dshRoleContract(role, canonicalContract, referenceContracts = {}) {
20
- const canonical = typeof canonicalContract === "string" ? canonicalContract.trim() : "";
21
- const overlay = DSH_ROLE_OVERLAYS[role];
22
- if (!canonical) throw new Error(`canonical ${role} responsibility contract is unavailable`);
23
- const craft = role === "frontend" && typeof referenceContracts.craft === "string"
24
- ? `## Canonical craft reference\n\n${referenceContracts.craft.trim()}`
25
- : "";
26
- return [canonical, craft, overlay].filter(Boolean).join("\n\n");
27
- }
1
+ const DSH_ROLE_OVERLAYS = Object.freeze({
2
+ researcher: `## DSH researcher execution boundary
3
+
4
+ This responsibility runs as one read-only child before the primary routing decision. Use only read, glob, grep, and other non-mutating source tools; do not run shell commands, edit, plan, recommend, approve, or delegate. Return JSON only, with no fence and no fields beyond this exact shape: {"schemaVersion":1,"question":"...","facts":[{"claim":"...","excerpt":"exact complete cited line","source":{"path":"repository/relative/path","line":1},"authority":"source role and freshness boundary"}],"conflicts":[],"unknowns":[],"stop":"..."}. Return the smallest useful 2-6 facts from at least two distinct files. DSH validates every cited file, line, and excerpt inside the project root before hashing and exposing this packet to the controller or planner.`,
5
+ planner: `## DSH planner execution boundary
6
+
7
+ In auto mode this responsibility runs by replacing the controller route only inside one explicit responsibility scope, not by starting a child. The scope may continue through read-only tool calls, but ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. When the user-persisted planner mapping explicitly includes maxTokens, that responsibility ceiling overrides the controller ceiling only inside this scope. Retain the current conversation and project context, use only read-only evidence, and do not implement or edit. If the result is mode: planned, execute: executor, and separation has a concrete observable benefit, use odai_route_card to freeze exactly one structured card with non-empty evidence, scope, acceptance conditions, and stop condition. When the original current task already authorizes implementation, immediately submit one executor gap through odai_responsibility_gap so the same task continues without another user prompt. For plan-only work, a new task, expanded scope, or missing user-owned authorization, stop for the minimal user decision instead. Otherwise do not create a route card.`,
8
+ executor: `## DSH executor execution boundary
9
+
10
+ In auto mode this responsibility is reachable when an active frozen route card proves observable separation benefit and either the original current task already authorizes implementation or the user explicitly continues it. It runs in one bounded controller responsibility scope because DSH routed children are read-only. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, card release, or turn boundary. When the user-persisted executor mapping explicitly includes maxTokens, that responsibility ceiling overrides the controller ceiling only inside this scope. Follow the claimed card exactly; do not reinterpret the request, broaden scope, or claim final acceptance. The card is consumed after the effective executor request route is verified; that route receipt admits one executor attempt but does not keep the responsibility scope alive by itself.`,
11
+ reviewer: `## DSH reviewer execution boundary
12
+
13
+ Start an independent child only from a bounded, hash-addressed packet that contains verified tool evidence. The child's effective request header must match its configured route before its output is accepted. Without such a packet, auto mode keeps the current controller route and may perform only a controller-local read-only check while it gathers project-available acceptance evidence. That local check is not independent acceptance, must not claim reviewer approval, and must not stop the authorized task solely to ask the user for artifacts the project can produce.`,
14
+ frontend: `## DSH frontend execution boundary
15
+
16
+ This responsibility runs in one bounded controller responsibility scope so it retains the active conversation, workspace, dev-server, and browser context. It is not an independent child. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. DSH may use a user-persisted frontend provider/model/reasoning route and, only when that mapping explicitly includes maxTokens, apply that responsibility ceiling inside this scope. The runtime compares the effective DSH request header with that mapping and records an applied, mismatch, or unverified receipt; configuration and self-report are not routing evidence.`,
17
+ });
18
+
19
+ export function dshRoleContract(role, canonicalContract, referenceContracts = {}) {
20
+ const canonical = typeof canonicalContract === "string" ? canonicalContract.trim() : "";
21
+ const overlay = DSH_ROLE_OVERLAYS[role];
22
+ if (!canonical) throw new Error(`canonical ${role} responsibility contract is unavailable`);
23
+ const craft = role === "frontend" && typeof referenceContracts.craft === "string"
24
+ ? `## Canonical craft reference\n\n${referenceContracts.craft.trim()}`
25
+ : "";
26
+ return [canonical, craft, overlay].filter(Boolean).join("\n\n");
27
+ }