@carljia/omd-dsh 0.1.5 → 0.1.8

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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { scopeOf } from "@deepseek-ai/dsh-scope";
2
+ import { setModeOverride } from "./shared.js";
3
3
  /**
4
4
  * @module @carljia/omd-dsh
5
5
  *
@@ -31,8 +31,8 @@ import { scopeOf } from "@deepseek-ai/dsh-scope";
31
31
  * - otherwise the user explicitly picked a
32
32
  * different model -> yield: the request and
33
33
  * the persona variables keep the user's selection, and the row
34
- * records it on the scoped context as `omdModeOverride` so the
35
- * omd-task row can route the "deep" tier to the user's model.
34
+ * records it (shared.ts, keyed by the agent) so the omd-task row can
35
+ * route the "deep" tier to the user's model.
36
36
  *
37
37
  * When provider/model are not configured the row passes everything
38
38
  * through and only serves the persona banner variables (inheriting the
@@ -80,10 +80,10 @@ function presetSwitchedAfterLastRequest(session) {
80
80
  return lastSwitch > lastHeader;
81
81
  }
82
82
  function apply(ctx, config) {
83
- if (scopeOf(ctx) === undefined) {
84
- throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
85
- "Mount this row inside an agent preset; a global mount would pin the model for every agent in the process.");
86
- }
83
+ // 无 scope 守卫:本行以自包含 bundle(.omd-vendor/omd-mode.mjs)分发,bundle 内
84
+ // 自带的 dsh-scope 副本永远读不到 harness 实例写入的 kScope Symbol,守卫会误报
85
+ // (曾导致全部 omd preset 挂载失败)。preset 挂载由 harness 的 loader 保证作用域;
86
+ // 若误挂到全局组合,后果(进程级钉模型)立即可见。见 docs/ARCHITECTURE.md。
87
87
  const pinned = config.provider !== undefined && config.model !== undefined
88
88
  ? {
89
89
  provider: config.provider,
@@ -106,7 +106,8 @@ function apply(ctx, config) {
106
106
  }
107
107
  catch { /* 读不到默认时退化为仅 entry == pinned 判定 */ }
108
108
  // 供同 preset 内的 omd-task 行读取:用户显式切换模型后(本行让路),deep tier 沿用用户选择。
109
- ctx.omdModeOverride = undefined;
109
+ // 注意:不能直接往 cordis 作用域 ctx 上写属性(Proxy 会抛 "without provide",且行间
110
+ // ctx 互不可见),因此共享状态放在 shared.ts 的 WeakMap 里,按顶层 agent 对象为键。
110
111
  /**
111
112
  * 判定一次入口选择是否应钉到模式模型(true),还是让路给用户选择(false)。
112
113
  * @param agent - 顶层 agent(子代理已由调用方过滤)。
@@ -162,7 +163,7 @@ function apply(ctx, config) {
162
163
  if (pinned === undefined || isSubagent(agent))
163
164
  return resolved;
164
165
  if (shouldPin(agent, { provider: resolved.provider, model: resolved.model })) {
165
- ctx.omdModeOverride = undefined;
166
+ setModeOverride(agent, undefined);
166
167
  const stripped = { ...resolved };
167
168
  delete stripped.reasoningEffort;
168
169
  const out = {
@@ -176,7 +177,7 @@ function apply(ctx, config) {
176
177
  return out;
177
178
  }
178
179
  // 用户显式选择了别的模型:本次任务顶层路由用用户选择;deep tier 同步(omd-task 读取)。
179
- ctx.omdModeOverride = { provider: resolved.provider, model: resolved.model };
180
+ setModeOverride(agent, { provider: resolved.provider, model: resolved.model });
180
181
  return resolved;
181
182
  }, { prepend: true });
182
183
  }
package/lib/mode.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
- import { scopeOf } from "@deepseek-ai/dsh-scope";
3
2
  /**
4
3
  * @module @carljia/omd-dsh/mode
5
4
  *
@@ -127,9 +126,7 @@ async function executeSwitch(ctx, invocation) {
127
126
  }
128
127
  }
129
128
  function apply(ctx) {
130
- if (scopeOf(ctx) === undefined) {
131
- throw new Error("omd-mode-switch: refusing to mount outside a scoped context; mount this row inside an agent preset");
132
- }
129
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
133
130
  ctx.inject(["commands"], (commandCtx) => {
134
131
  commandCtx.commands.register({
135
132
  name: "mode",
package/lib/plan.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
- import { scopeOf } from "@deepseek-ai/dsh-scope";
4
3
  /**
5
4
  * @module @carljia/omd-dsh/plan
6
5
  *
@@ -96,9 +95,7 @@ function planModeActive(events) {
96
95
  return active;
97
96
  }
98
97
  function apply(ctx) {
99
- if (scopeOf(ctx) === undefined) {
100
- throw new Error("omd-plan: refusing to mount outside a scoped context; mount this row inside an agent preset");
101
- }
98
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
102
99
  // Scoped .md-only write guard: the planner may write/edit only markdown
103
100
  // files, keeping it read-only for every other path. The plan file itself is
104
101
  // written by this row via node:fs (not through the model's write/edit tools),
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @module @carljia/omd-dsh/shared
3
+ *
4
+ * Per-agent transient state shared between the omd-mode and omd-task rows.
5
+ *
6
+ * Why this module exists: cordis scoped contexts are proxies — assigning an
7
+ * undeclared property throws ("cannot set property ... without provide"), and
8
+ * two rows in one preset are sibling contexts that cannot see each other's
9
+ * declared properties either. Both rows therefore import this module, and the
10
+ * sync ships it next to them (`.omd-vendor/shared.js`), so the two vendored
11
+ * rows resolve the SAME module instance and share one WeakMap. The override is
12
+ * keyed by the top-level agent object (stable across a session's turns; a
13
+ * resumed session mints a new agent and starts clean; subagents are distinct
14
+ * objects and simply miss the map, which is exactly the documented passthrough
15
+ * semantics).
16
+ */
17
+ /** One user model pick, recorded when omd-mode yields to it. */
18
+ export interface ModeOverride {
19
+ provider: string;
20
+ model: string;
21
+ }
22
+ /** Record (or clear, with `undefined`) the user's model pick for one agent. */
23
+ export declare function setModeOverride(agent: object, override: ModeOverride | undefined): void;
24
+ /** The user's recorded model pick for one agent, or undefined. */
25
+ export declare function modeOverrideFor(agent: object): ModeOverride | undefined;
package/lib/shared.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @module @carljia/omd-dsh/shared
3
+ *
4
+ * Per-agent transient state shared between the omd-mode and omd-task rows.
5
+ *
6
+ * Why this module exists: cordis scoped contexts are proxies — assigning an
7
+ * undeclared property throws ("cannot set property ... without provide"), and
8
+ * two rows in one preset are sibling contexts that cannot see each other's
9
+ * declared properties either. Both rows therefore import this module, and the
10
+ * sync ships it next to them (`.omd-vendor/shared.js`), so the two vendored
11
+ * rows resolve the SAME module instance and share one WeakMap. The override is
12
+ * keyed by the top-level agent object (stable across a session's turns; a
13
+ * resumed session mints a new agent and starts clean; subagents are distinct
14
+ * objects and simply miss the map, which is exactly the documented passthrough
15
+ * semantics).
16
+ */
17
+ const modeOverrides = new WeakMap();
18
+ /** Record (or clear, with `undefined`) the user's model pick for one agent. */
19
+ export function setModeOverride(agent, override) {
20
+ if (override === undefined)
21
+ modeOverrides.delete(agent);
22
+ else
23
+ modeOverrides.set(agent, override);
24
+ }
25
+ /** The user's recorded model pick for one agent, or undefined. */
26
+ export function modeOverrideFor(agent) {
27
+ return modeOverrides.get(agent);
28
+ }
package/lib/startwork.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { isAbsolute, join } from "node:path";
3
- import { scopeOf } from "@deepseek-ai/dsh-scope";
4
3
  /**
5
4
  * @module @carljia/omd-dsh/startwork
6
5
  *
@@ -129,9 +128,7 @@ async function executeStartWork(ctx, invocation) {
129
128
  }
130
129
  }
131
130
  function apply(ctx) {
132
- if (scopeOf(ctx) === undefined) {
133
- throw new Error("omd-start-work: refusing to mount outside a scoped context; mount this row inside an agent preset");
134
- }
131
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
135
132
  ctx.inject(["commands"], (commandCtx) => {
136
133
  commandCtx.commands.register({
137
134
  name: "start-work",
package/lib/sync.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * omd-dsh sync core — materialize the OMD presets into <DSH_HOME>/.agent-presets.
3
+ *
4
+ * Shared by the CLI (`omd-dsh sync`) and the bundle boot row (`dsh plugin add`
5
+ * + restart): render each preset's omd-mode / omd-task rows from the user's
6
+ * model matrix, and copy the presets plus the vendored row modules into
7
+ * .agent-presets.
8
+ *
9
+ * The vendored rows are SELF-CONTAINED bundles (see scripts/postbuild.mjs):
10
+ * every dependency is bundled in and they carry no @deepseek-ai imports, so
11
+ * the sync needs NO harness tree knowledge — the rows work regardless of
12
+ * whether the harness's agent machinery loads from an npx cache, a profile,
13
+ * or a global npm install, and no `--harness` configuration is ever needed.
14
+ * Bare package rows in the preset compositions (e.g. @deepseek-ai/dsh-persona)
15
+ * are resolved by the harness's own loader at runtime against the host base.
16
+ */
17
+ export type SyncFlags = {
18
+ dryRun: boolean;
19
+ verbose: boolean;
20
+ };
21
+ export interface TierConfig {
22
+ provider: string;
23
+ model: string;
24
+ hint?: string;
25
+ persona?: string;
26
+ maxTokens?: number;
27
+ toolFilter?: {
28
+ allow?: string[];
29
+ deny?: string[];
30
+ denyShell?: boolean;
31
+ };
32
+ }
33
+ export interface ModeConfig {
34
+ provider?: string;
35
+ model?: string;
36
+ reasoningEffort?: string;
37
+ tiers?: Record<string, TierConfig>;
38
+ }
39
+ export interface Matrix {
40
+ version: number;
41
+ defaults?: {
42
+ provider?: string;
43
+ };
44
+ modes: Record<string, ModeConfig>;
45
+ }
46
+ export declare const PACKAGE_ROOT: string;
47
+ export declare const VENDOR_SOURCES: string[];
48
+ /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
49
+ export declare const MATRIX_PATH: string;
50
+ export declare function dshHome(): string;
51
+ export declare function loadMatrix(flags: SyncFlags, log?: (msg: string) => void): Matrix;
52
+ export declare function saveMatrix(m: Matrix): void;
53
+ export declare function runSync(flags: SyncFlags, log?: (msg: string) => void): Promise<void>;
package/lib/sync.js ADDED
@@ -0,0 +1,345 @@
1
+ import { promises as fs, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
+ export const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs", "omd-plan.mjs", "omd-start-work.mjs", "omd-mode-switch.mjs", "shared.js"];
8
+ /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
9
+ export const MATRIX_PATH = join(dshHome(), "omd-matrix.json");
10
+ /** Pre-migration location (package root) — migrated to MATRIX_PATH once when present. */
11
+ const LEGACY_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.json");
12
+ const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
13
+ const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]" };
14
+ /** Presets that were renamed: old directory name -> new preset name. */
15
+ const RENAMED_FROM = { "omd-architect": "omd-ultraworker" };
16
+ function sha256(text) { return createHash("sha256").update(text).digest("hex"); }
17
+ export function dshHome() { return process.env.DSH_HOME !== undefined && process.env.DSH_HOME !== "" ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh"); }
18
+ function readMeta(dir) {
19
+ const metaPath = join(dir, ".omd-meta.json");
20
+ if (!existsSync(metaPath))
21
+ return undefined;
22
+ try {
23
+ const p = JSON.parse(readFileSync(metaPath, "utf8"));
24
+ if (p !== null && typeof p === "object" && p.files !== null && typeof p.files === "object")
25
+ return p;
26
+ return undefined;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ async function collectSourceFiles(rootDir) {
33
+ const out = [];
34
+ const walk = async (dir) => {
35
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
36
+ const full = join(dir, entry.name);
37
+ if (entry.isDirectory())
38
+ await walk(full);
39
+ else
40
+ out.push(full);
41
+ }
42
+ };
43
+ await walk(rootDir);
44
+ return out;
45
+ }
46
+ // ── matrix ──
47
+ const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
48
+ function parseMatrix(text) {
49
+ try {
50
+ const parsed = JSON.parse(text);
51
+ if (parsed !== null && typeof parsed === "object" && parsed.modes !== null && typeof parsed.modes === "object")
52
+ return parsed;
53
+ }
54
+ catch { /* fall through */ }
55
+ return undefined;
56
+ }
57
+ function readDefaultMatrix() {
58
+ if (!existsSync(DEFAULT_MATRIX_PATH))
59
+ throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
60
+ const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
61
+ if (parsed === undefined)
62
+ throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
63
+ return parsed;
64
+ }
65
+ function writeMatrixFile(path, text) {
66
+ mkdirSync(dirname(path), { recursive: true });
67
+ writeFileSync(path, text, "utf8");
68
+ }
69
+ export function loadMatrix(flags, log = console.log) {
70
+ if (!existsSync(MATRIX_PATH)) {
71
+ if (flags.dryRun)
72
+ return JSON.parse(JSON.stringify(readDefaultMatrix()));
73
+ const legacyText = existsSync(LEGACY_MATRIX_PATH) ? readFileSync(LEGACY_MATRIX_PATH, "utf8") : undefined;
74
+ if (legacyText !== undefined && parseMatrix(legacyText) !== undefined) {
75
+ writeMatrixFile(MATRIX_PATH, legacyText);
76
+ log("omd-dsh: migrated omd-matrix.json: " + LEGACY_MATRIX_PATH + " -> " + MATRIX_PATH);
77
+ log("omd-dsh: customize the model matrix any time with \`omd-dsh setup\`.");
78
+ }
79
+ else {
80
+ const defaults = readDefaultMatrix();
81
+ writeMatrixFile(MATRIX_PATH, JSON.stringify(defaults, null, 2) + "\n");
82
+ log("omd-dsh: generated " + MATRIX_PATH + " from the shipped deepseek default matrix; customize it any time with \`omd-dsh setup\`.");
83
+ }
84
+ }
85
+ const text = readFileSync(MATRIX_PATH, "utf8");
86
+ const parsed = parseMatrix(text);
87
+ if (parsed === undefined)
88
+ throw new Error("omd-dsh: malformed " + MATRIX_PATH + " (restore it, or delete it and run omd-dsh setup)");
89
+ return parsed;
90
+ }
91
+ export function saveMatrix(m) { writeMatrixFile(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n"); }
92
+ // ── row rendering ──
93
+ function q(s) { return JSON.stringify(s); }
94
+ function renderModeRow(modeId, cfg) {
95
+ const out = ["- id: omd-mode", " name: '../.omd-vendor/omd-mode.mjs'", " config:", " mode: " + modeId];
96
+ if (cfg.provider !== undefined)
97
+ out.push(" provider: " + cfg.provider);
98
+ if (cfg.model !== undefined)
99
+ out.push(" model: " + cfg.model);
100
+ if (cfg.reasoningEffort !== undefined)
101
+ out.push(" reasoningEffort: " + cfg.reasoningEffort);
102
+ return out;
103
+ }
104
+ function renderToolFilter(tf) {
105
+ const deny = tf.deny ?? [];
106
+ const allow = tf.allow ?? [];
107
+ const out = [];
108
+ if (tf.denyShell === true) {
109
+ const arr = (names) => "[" + names.map((n) => "'" + n + "'").join(", ") + "]";
110
+ const expr = "(process.platform === 'win32') ? " + arr([...deny, "pwsh"]) + " : " + arr([...deny, "bash"]);
111
+ out.push("toolFilter:", " deny: !!js " + q(expr));
112
+ }
113
+ else {
114
+ if (allow.length > 0)
115
+ out.push("toolFilter:", " allow: [" + allow.join(", ") + "]");
116
+ if (deny.length > 0) {
117
+ if (out.length === 0)
118
+ out.push("toolFilter:");
119
+ out.push(" deny: [" + deny.join(", ") + "]");
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function renderTaskRow(cfg) {
125
+ const tiers = cfg.tiers ?? {};
126
+ if (Object.keys(tiers).length === 0)
127
+ return [];
128
+ const out = ["- id: omd-task", " name: '../.omd-vendor/omd-task.mjs'", " config:", " provider: spawn", " toolName: omd_task", " backgroundMode: continuable", " tiers:"];
129
+ for (const [name, t] of Object.entries(tiers)) {
130
+ out.push(" " + name + ":");
131
+ out.push(" provider: " + t.provider);
132
+ out.push(" model: " + t.model);
133
+ if (t.hint !== undefined)
134
+ out.push(" hint: " + q(t.hint));
135
+ if (t.persona !== undefined)
136
+ out.push(" persona: " + q(t.persona));
137
+ if (t.maxTokens !== undefined)
138
+ out.push(" maxTokens: " + t.maxTokens);
139
+ if (t.toolFilter !== undefined)
140
+ for (const l of renderToolFilter(t.toolFilter))
141
+ out.push(" " + l);
142
+ }
143
+ return out;
144
+ }
145
+ function spliceFence(lines, fence, rendered) {
146
+ const start = lines.findIndex((l) => l.trim() === fence.start);
147
+ const end = lines.findIndex((l) => l.trim() === fence.end);
148
+ if (start === -1 || end === -1 || end < start)
149
+ throw new Error("omd-dsh: preset is missing the " + fence.start + " / " + fence.end + " markers; regenerate the preset from source");
150
+ const indent = (lines[start].match(/^ */) || [""])[0];
151
+ const renderedIndented = rendered.map((l) => indent + l);
152
+ lines.splice(start, end - start + 1, indent + fence.start, ...renderedIndented, indent + fence.end);
153
+ }
154
+ function applyMatrix(text, modeId, cfg) {
155
+ const lines = text.split("\n");
156
+ spliceFence(lines, MODE_FENCE, renderModeRow(modeId, cfg));
157
+ const taskRendered = renderTaskRow(cfg);
158
+ const hasTaskFence = lines.some((l) => l.trim() === TASK_FENCE.start);
159
+ if (taskRendered.length > 0 && !hasTaskFence) {
160
+ throw new Error("omd-dsh: mode \"" + modeId + "\" has tiers in omd-matrix.json but its preset is missing the task fence");
161
+ }
162
+ if (hasTaskFence)
163
+ spliceFence(lines, TASK_FENCE, taskRendered);
164
+ return lines.join("\n");
165
+ }
166
+ /**
167
+ * Whether one omd-dsh-managed preset directory differs from the hashes its
168
+ * .omd-meta.json recorded. Returns a description of the first discrepancy,
169
+ * or undefined when every recorded file is present and unmodified.
170
+ */
171
+ async function locallyModified(dir, meta) {
172
+ const recorded = meta.files ?? {};
173
+ const current = {};
174
+ const walk = async (d) => {
175
+ for (const entry of await fs.readdir(d, { withFileTypes: true })) {
176
+ const full = join(d, entry.name);
177
+ if (entry.isDirectory())
178
+ await walk(full);
179
+ else if (entry.name !== ".omd-meta.json") {
180
+ const rel = relative(dir, full).split("\\").join("/");
181
+ current[rel] = sha256(await fs.readFile(full, "utf8"));
182
+ }
183
+ }
184
+ };
185
+ await walk(dir);
186
+ for (const rel of new Set([...Object.keys(recorded), ...Object.keys(current)])) {
187
+ if (current[rel] === undefined)
188
+ return "missing file " + rel;
189
+ const recordedHash = recorded[rel] !== undefined && typeof recorded[rel] === "object" && recorded[rel] !== null ? recorded[rel].sha256 : undefined;
190
+ if (typeof recordedHash !== "string" || recordedHash !== current[rel])
191
+ return "modified file " + rel;
192
+ }
193
+ return undefined;
194
+ }
195
+ export async function runSync(flags, log = console.log) {
196
+ const matrix = loadMatrix(flags, log);
197
+ const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
198
+ const sourceVersion = manifest.version;
199
+ const presetsSourceDir = join(PACKAGE_ROOT, "presets");
200
+ const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
201
+ const agentPresetsRoot = join(dshHome(), ".agent-presets");
202
+ const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
203
+ const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [], removed: [] };
204
+ const record = (kind, text) => report[kind].push(text);
205
+ for (const presetName of presetNames) {
206
+ const modeId = presetName.replace(/^omd-/, "");
207
+ const cfg = matrix.modes[modeId];
208
+ if (cfg === undefined)
209
+ throw new Error("omd-dsh sync: omd-matrix.json has no entry for mode \"" + modeId + "\" (preset " + presetName + ")");
210
+ const sourceDir = join(presetsSourceDir, presetName);
211
+ const targetDir = join(agentPresetsRoot, presetName);
212
+ const sourceFiles = await collectSourceFiles(sourceDir);
213
+ const existingMeta = existsSync(targetDir) ? readMeta(targetDir) : undefined;
214
+ if (existsSync(targetDir) && existingMeta === undefined) {
215
+ record("skipped", presetName + "/ (directory exists but is not managed by omd-dsh -- left untouched)");
216
+ continue;
217
+ }
218
+ const nextFiles = {};
219
+ for (const sourceFile of sourceFiles) {
220
+ const rel = relative(sourceDir, sourceFile).split("\\").join("/");
221
+ let sourceText = await fs.readFile(sourceFile, "utf8");
222
+ if (rel === "agent.cordis.yml")
223
+ sourceText = applyMatrix(sourceText, modeId, cfg);
224
+ const sourceHash = sha256(sourceText);
225
+ const destFile = join(targetDir, rel);
226
+ let action = "synced";
227
+ if (existsSync(destFile)) {
228
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
229
+ const prevHash = existingMeta?.files?.[rel]?.sha256;
230
+ if (destHash === sourceHash)
231
+ action = "up-to-date";
232
+ else if (prevHash !== undefined && destHash === prevHash)
233
+ action = "updated";
234
+ else
235
+ action = "conflict";
236
+ }
237
+ if (action === "conflict") {
238
+ record("conflicts", presetName + "/" + rel + " (locally modified -- keeping your version)");
239
+ if (existingMeta?.files?.[rel]?.sha256 !== undefined)
240
+ nextFiles[rel] = { sha256: existingMeta.files[rel].sha256 };
241
+ continue;
242
+ }
243
+ if (action === "up-to-date")
244
+ record("skipped", presetName + "/" + rel + " (up to date)");
245
+ else {
246
+ if (!flags.dryRun) {
247
+ await fs.mkdir(dirname(destFile), { recursive: true });
248
+ await fs.writeFile(destFile, sourceText, "utf8");
249
+ }
250
+ record(action === "synced" ? "synced" : "updated", presetName + "/" + rel + (flags.dryRun ? " (dry-run)" : ""));
251
+ }
252
+ nextFiles[rel] = { sha256: sourceHash };
253
+ }
254
+ if (!flags.dryRun) {
255
+ await fs.mkdir(targetDir, { recursive: true });
256
+ await fs.writeFile(join(targetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextFiles }, null, 2) + "\n", "utf8");
257
+ }
258
+ }
259
+ const vendorTargetDir = join(agentPresetsRoot, ".omd-vendor");
260
+ const vendorMeta = existsSync(vendorTargetDir) ? readMeta(vendorTargetDir) : undefined;
261
+ if (existsSync(vendorTargetDir) && vendorMeta === undefined) {
262
+ record("skipped", ".omd-vendor/ (directory exists but is not managed by omd-dsh -- left untouched)");
263
+ }
264
+ else {
265
+ const nextVendorFiles = {};
266
+ for (const vendorName of VENDOR_SOURCES) {
267
+ const sourceFile = join(vendorSourceDir, vendorName);
268
+ if (!existsSync(sourceFile)) {
269
+ throw new Error("omd-dsh sync: missing vendored source " + sourceFile + " -- run \`npm run build\` first");
270
+ }
271
+ // Self-contained bundles: copied verbatim, no import rewriting, no
272
+ // harness tree knowledge (see the module doc comment).
273
+ const sourceText = await fs.readFile(sourceFile, "utf8");
274
+ const sourceHash = sha256(sourceText);
275
+ const destFile = join(vendorTargetDir, vendorName);
276
+ let action = "synced";
277
+ if (existsSync(destFile)) {
278
+ const destHash = sha256(await fs.readFile(destFile, "utf8"));
279
+ const prevHash = vendorMeta?.files?.[vendorName]?.sha256;
280
+ if (destHash === sourceHash)
281
+ action = "up-to-date";
282
+ else if (prevHash !== undefined && destHash === prevHash)
283
+ action = "updated";
284
+ else
285
+ action = "conflict";
286
+ }
287
+ if (action === "conflict") {
288
+ record("conflicts", ".omd-vendor/" + vendorName + " (locally modified -- keeping your version)");
289
+ if (vendorMeta?.files?.[vendorName]?.sha256 !== undefined)
290
+ nextVendorFiles[vendorName] = { sha256: vendorMeta.files[vendorName].sha256 };
291
+ continue;
292
+ }
293
+ if (action === "up-to-date")
294
+ record("skipped", ".omd-vendor/" + vendorName + " (up to date)");
295
+ else {
296
+ if (!flags.dryRun) {
297
+ await fs.mkdir(vendorTargetDir, { recursive: true });
298
+ await fs.writeFile(destFile, sourceText, "utf8");
299
+ }
300
+ record(action === "synced" ? "synced" : "updated", ".omd-vendor/" + vendorName + (flags.dryRun ? " (dry-run)" : ""));
301
+ }
302
+ nextVendorFiles[vendorName] = { sha256: sourceHash };
303
+ }
304
+ if (!flags.dryRun && VENDOR_SOURCES.length > 0) {
305
+ await fs.mkdir(vendorTargetDir, { recursive: true });
306
+ await fs.writeFile(join(vendorTargetDir, ".omd-meta.json"), JSON.stringify({ source: "omd-dsh", sourceVersion, files: nextVendorFiles }, null, 2) + "\n", "utf8");
307
+ }
308
+ }
309
+ if (existsSync(agentPresetsRoot)) {
310
+ for (const entry of await fs.readdir(agentPresetsRoot, { withFileTypes: true })) {
311
+ if (!entry.isDirectory() || !entry.name.startsWith("omd-"))
312
+ continue;
313
+ if (presetNames.includes(entry.name))
314
+ continue;
315
+ const orphanDir = join(agentPresetsRoot, entry.name);
316
+ const meta = readMeta(orphanDir);
317
+ if (meta === undefined)
318
+ continue;
319
+ const renamedTo = RENAMED_FROM[entry.name];
320
+ if (renamedTo !== undefined && presetNames.includes(renamedTo)) {
321
+ const dirty = await locallyModified(orphanDir, meta);
322
+ if (dirty === undefined) {
323
+ if (!flags.dryRun)
324
+ await fs.rm(orphanDir, { recursive: true, force: true });
325
+ record("removed", entry.name + "/ (renamed to " + renamedTo + " and unmodified -- removed" + (flags.dryRun ? ", dry-run" : "") + ")");
326
+ }
327
+ else {
328
+ record("conflicts", entry.name + "/ (renamed to " + renamedTo + " but locally modified -- keeping your version: " + dirty + ")");
329
+ }
330
+ }
331
+ else {
332
+ record("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
333
+ }
334
+ }
335
+ }
336
+ log("omd-dsh sync: DSH_HOME=" + dshHome());
337
+ log("omd-dsh sync: matrix=" + MATRIX_PATH + " (customize the model matrix any time with \`omd-dsh setup\`)");
338
+ log("omd-dsh sync: vendored rows are self-contained bundles (no harness tree dependency)");
339
+ log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
340
+ for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
341
+ for (const line of report[key])
342
+ log(" [" + key + "] " + line);
343
+ const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
344
+ log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
345
+ }
package/lib/task.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import { assertSubagentMaxDepth } from "@deepseek-ai/dsh-subagent";
4
- import { scopeOf } from "@deepseek-ai/dsh-scope";
4
+ import { modeOverrideFor } from "./shared.js";
5
5
  /**
6
6
  * @module @carljia/omd-dsh/task
7
7
  *
@@ -135,9 +135,7 @@ function resolveTier(config, requested) {
135
135
  throw new Error("omd_task: choose a tier for this task -- valid tiers: " + tierNames.join(", "));
136
136
  }
137
137
  function apply(ctx, config) {
138
- if (scopeOf(ctx) === undefined) {
139
- throw new Error("omd-task: refusing to mount outside a scoped context; mount this row inside an agent preset");
140
- }
138
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
141
139
  const invalid = configError(config);
142
140
  if (invalid !== undefined)
143
141
  throw new Error(invalid);
@@ -212,9 +210,9 @@ function apply(ctx, config) {
212
210
  throw new Error("omd_task requires a calling agent (exec.agent was undefined)");
213
211
  const tierName = resolveTier(config, args.tier);
214
212
  let tier = config.tiers[tierName];
215
- // 用户显式切换模型后(omd-mode 在 agent/request 让路并在作用域 ctx 上记录
216
- // omdModeOverride),deep tier 改用用户选择的模型;其余 tier 保持矩阵配置。
217
- const override = ctx.omdModeOverride;
213
+ // 用户显式切换模型后(omd-mode 在 agent/request 让路并把用户选择记入 shared.ts,
214
+ // 键为顶层 agent 对象),deep tier 改用用户选择的模型;其余 tier 保持矩阵配置。
215
+ const override = modeOverrideFor(parent);
218
216
  if (tierName === "deep" && override !== undefined
219
217
  && typeof override.provider === "string" && typeof override.model === "string") {
220
218
  tier = { ...tier, provider: override.provider, model: override.model };