@carljia/omd-dsh 0.1.2 → 0.1.4
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 +16 -6
- package/lib/cli.js +132 -24
- package/lib/index.d.ts +22 -3
- package/lib/index.js +129 -28
- package/lib/mode.d.ts +33 -0
- package/lib/mode.js +145 -0
- package/lib/plan.d.ts +22 -0
- package/lib/plan.js +146 -0
- package/lib/startwork.d.ts +17 -0
- package/lib/startwork.js +147 -0
- package/lib/task.js +8 -1
- package/lib/vendor/omd-mode-switch.mjs +145 -0
- package/lib/vendor/omd-mode.mjs +129 -28
- package/lib/vendor/omd-plan.mjs +146 -0
- package/lib/vendor/omd-start-work.mjs +147 -0
- package/lib/vendor/omd-task.mjs +8 -1
- package/{omd-matrix.json → omd-matrix.default.json} +3 -9
- package/package.json +2 -2
- package/presets/omd-chat/agent.cordis.yml +2 -0
- package/presets/omd-executor/agent.cordis.yml +28 -1
- package/presets/omd-explorer/agent.cordis.yml +2 -0
- package/presets/omd-librarian/agent.cordis.yml +2 -0
- package/presets/omd-planner/agent.cordis.yml +8 -1
- package/presets/omd-reviewer/agent.cordis.yml +2 -0
- package/presets/{omd-architect → omd-ultraworker}/agent.cordis.yml +16 -2
- package/presets/omd-ultraworker/preset.yml +3 -0
- package/presets/omd-architect/preset.yml +0 -3
package/lib/vendor/omd-mode.mjs
CHANGED
|
@@ -11,9 +11,28 @@ import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
|
11
11
|
* model and provider template variables), and overrides provider/model
|
|
12
12
|
* on the agent/request waterfall after next(), dropping any inherited
|
|
13
13
|
* reasoningEffort. Both listeners register with prepend: true so this
|
|
14
|
-
* row sits OUTSIDE the entry point per-session selection listener
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* row sits OUTSIDE the entry point per-session selection listener.
|
|
15
|
+
*
|
|
16
|
+
* Precedence vs. the UI model switch: the matrix model is the preset's
|
|
17
|
+
* DEFAULT route, but an explicit user selection wins for the task.
|
|
18
|
+
* The entry selection (installModelSelection) is invisible to this row
|
|
19
|
+
* (it is owned by the host entry point), so the decision is derived
|
|
20
|
+
* from what the waterfall actually resolved:
|
|
21
|
+
*
|
|
22
|
+
* - entry selection == matrix model -> pin (no-op);
|
|
23
|
+
* - entry selection missing -> pin (claim the mode);
|
|
24
|
+
* - session still blank (no request/header)
|
|
25
|
+
* and entry selection == the deployment
|
|
26
|
+
* default captured at mount -> pin (fallback, no pick);
|
|
27
|
+
* - a preset switch (agent-preset/selected)
|
|
28
|
+
* happened after the last request/header and
|
|
29
|
+
* entry selection == the route the session
|
|
30
|
+
* was running before the switch -> pin (new mode claims);
|
|
31
|
+
* - otherwise the user explicitly picked a
|
|
32
|
+
* different model -> yield: the request and
|
|
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.
|
|
17
36
|
*
|
|
18
37
|
* When provider/model are not configured the row passes everything
|
|
19
38
|
* through and only serves the persona banner variables (inheriting the
|
|
@@ -30,6 +49,36 @@ const Config = z.object({
|
|
|
30
49
|
model: z.string(),
|
|
31
50
|
reasoningEffort: z.string(),
|
|
32
51
|
});
|
|
52
|
+
/**
|
|
53
|
+
* 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
|
|
54
|
+
* 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
|
|
55
|
+
* 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
|
|
56
|
+
*/
|
|
57
|
+
function isSubagent(agent) {
|
|
58
|
+
return agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
|
|
59
|
+
&& typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 最近一次 request/header 之后是否发生过 agent-preset/selected(UI 预设选择或 /mode 切换)。
|
|
63
|
+
* 切换事件由 api-proxy 与 omd-mode-switch 在 recompose 完成之后追加,因此必须每次实时计算——
|
|
64
|
+
* 本行挂载时事件尚未入日志,挂载时快照会漏判。
|
|
65
|
+
*/
|
|
66
|
+
function presetSwitchedAfterLastRequest(session) {
|
|
67
|
+
const events = session === undefined || session === null ? undefined : session.events;
|
|
68
|
+
if (events === undefined)
|
|
69
|
+
return false;
|
|
70
|
+
let lastHeader = -1;
|
|
71
|
+
let lastSwitch = -1;
|
|
72
|
+
for (const event of events) {
|
|
73
|
+
if (event === undefined || event === null || typeof event.seq !== "number")
|
|
74
|
+
continue;
|
|
75
|
+
if (event.type === "request/header")
|
|
76
|
+
lastHeader = event.seq;
|
|
77
|
+
else if (event.type === "agent-preset/selected")
|
|
78
|
+
lastSwitch = event.seq;
|
|
79
|
+
}
|
|
80
|
+
return lastSwitch > lastHeader;
|
|
81
|
+
}
|
|
33
82
|
function apply(ctx, config) {
|
|
34
83
|
if (scopeOf(ctx) === undefined) {
|
|
35
84
|
throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
|
|
@@ -41,42 +90,94 @@ function apply(ctx, config) {
|
|
|
41
90
|
model: config.model,
|
|
42
91
|
}
|
|
43
92
|
: undefined;
|
|
44
|
-
// 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
|
|
45
|
-
// 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
|
|
46
|
-
// 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
|
|
47
|
-
const isSubagent = (agent) => agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
|
|
48
|
-
&& typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
|
|
49
93
|
if (config.reasoningEffort !== undefined && pinned !== undefined) {
|
|
50
94
|
pinned.reasoningEffort = config.reasoningEffort;
|
|
51
95
|
}
|
|
96
|
+
// 挂载时快照部署默认模型(d0)。blank 会话的入口选择 == d0 视为「未显式选择」。
|
|
97
|
+
// 必须静态快照:session.selectModel 每次都会把用户选择写回全局默认,动态读取会把
|
|
98
|
+
// 用户选择误判为默认值。
|
|
99
|
+
let d0;
|
|
100
|
+
try {
|
|
101
|
+
const def = ctx.get("agentDefaultModel");
|
|
102
|
+
const current = def !== undefined && def !== null ? def.currentSelection() : undefined;
|
|
103
|
+
if (current !== undefined && current !== null && typeof current.provider === "string" && typeof current.model === "string") {
|
|
104
|
+
d0 = { provider: current.provider, model: current.model };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* 读不到默认时退化为仅 entry == pinned 判定 */ }
|
|
108
|
+
// 供同 preset 内的 omd-task 行读取:用户显式切换模型后(本行让路),deep tier 沿用用户选择。
|
|
109
|
+
ctx.omdModeOverride = undefined;
|
|
110
|
+
/**
|
|
111
|
+
* 判定一次入口选择是否应钉到模式模型(true),还是让路给用户选择(false)。
|
|
112
|
+
* @param agent - 顶层 agent(子代理已由调用方过滤)。
|
|
113
|
+
* @param entry - 入口选择 { provider, model };provider/model 缺失 = 无入口选择。
|
|
114
|
+
*/
|
|
115
|
+
function shouldPin(agent, entry) {
|
|
116
|
+
if (pinned === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
if (entry === undefined || entry.provider === undefined || entry.model === undefined)
|
|
119
|
+
return true;
|
|
120
|
+
if (entry.provider === pinned.provider && entry.model === pinned.model)
|
|
121
|
+
return true;
|
|
122
|
+
const session = agent !== undefined && agent !== null ? agent.session : undefined;
|
|
123
|
+
const logged = session === undefined ? undefined : session.requestHeader();
|
|
124
|
+
if (logged === undefined) {
|
|
125
|
+
// 会话尚无任何请求:入口选择要么是部署默认(未选择),要么是首请求前的显式选择。
|
|
126
|
+
// 只有默认值视为「未选择」;其余一律视为用户选择。
|
|
127
|
+
if (d0 !== undefined && entry.provider === d0.provider && entry.model === d0.model)
|
|
128
|
+
return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// 会话已跑过请求:刚切换 preset(/mode 或 UI 选择)时,切换前的路由(logged)是
|
|
132
|
+
// 新模式认领矩阵模型的基线;否则入口选择与模式模型不同 = 用户显式切换,让路。
|
|
133
|
+
if (presetSwitchedAfterLastRequest(session)
|
|
134
|
+
&& logged.config !== undefined && logged.config !== null
|
|
135
|
+
&& entry.provider === logged.config.provider && entry.model === logged.config.model) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
52
140
|
ctx.on("system-prompt/assemble", async (assembly, _context, next) => {
|
|
53
141
|
const assembled = await next();
|
|
54
|
-
|
|
142
|
+
const agent = _context && _context.agent;
|
|
143
|
+
if (pinned === undefined || isSubagent(agent))
|
|
55
144
|
return assembled;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...assembled
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
145
|
+
const variables = assembled.variables ?? {};
|
|
146
|
+
if (shouldPin(agent, { provider: variables.provider, model: variables.model })) {
|
|
147
|
+
return {
|
|
148
|
+
...assembled,
|
|
149
|
+
variables: {
|
|
150
|
+
...variables,
|
|
151
|
+
provider: pinned.provider,
|
|
152
|
+
model: pinned.model,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// 让路:保留入口(用户)选择注入的变量,persona 展示实际路由的模型。
|
|
157
|
+
return assembled;
|
|
64
158
|
}, { prepend: true });
|
|
65
159
|
ctx.on("agent/request", async (_payload, next) => {
|
|
66
160
|
const resolved = await next();
|
|
67
|
-
|
|
161
|
+
const agent = _payload && _payload.agent;
|
|
162
|
+
if (pinned === undefined || isSubagent(agent))
|
|
68
163
|
return resolved;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
if (shouldPin(agent, { provider: resolved.provider, model: resolved.model })) {
|
|
165
|
+
ctx.omdModeOverride = undefined;
|
|
166
|
+
const stripped = { ...resolved };
|
|
167
|
+
delete stripped.reasoningEffort;
|
|
168
|
+
const out = {
|
|
169
|
+
...stripped,
|
|
170
|
+
provider: pinned.provider,
|
|
171
|
+
model: pinned.model,
|
|
172
|
+
};
|
|
173
|
+
if (pinned.reasoningEffort !== undefined) {
|
|
174
|
+
out.reasoningEffort = pinned.reasoningEffort;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
78
177
|
}
|
|
79
|
-
|
|
178
|
+
// 用户显式选择了别的模型:本次任务顶层路由用用户选择;deep tier 同步(omd-task 读取)。
|
|
179
|
+
ctx.omdModeOverride = { provider: resolved.provider, model: resolved.model };
|
|
180
|
+
return resolved;
|
|
80
181
|
}, { prepend: true });
|
|
81
182
|
}
|
|
82
183
|
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
4
|
+
/**
|
|
5
|
+
* @module @carljia/omd-dsh/plan
|
|
6
|
+
*
|
|
7
|
+
* omd-plan: plan persistence for the OMD planner mode. It wraps the
|
|
8
|
+
* `tools/post-execute` waterfall and intercepts a successful
|
|
9
|
+
* `exit_plan_mode` approval: the approved plan text is written into the
|
|
10
|
+
* workspace's plan directory (a fixed, code-level convention -- never
|
|
11
|
+
* mentioned in any persona/prompt text), and the tool result content is
|
|
12
|
+
* enriched with the saved file name so the planner's fixed Start Work
|
|
13
|
+
* final step can hand it to the user.
|
|
14
|
+
*
|
|
15
|
+
* Plan directory convention (hardcoded here and in omd-start-work only):
|
|
16
|
+
* <session cwd>/.omd/plans/<slug>-<timestamp>.md
|
|
17
|
+
* The slug derives from the plan's first markdown heading; a timestamp
|
|
18
|
+
* suffix keeps repeated interviews from overwriting each other.
|
|
19
|
+
*/
|
|
20
|
+
/** Cordis plugin name. */
|
|
21
|
+
const name = "omd-plan";
|
|
22
|
+
/** No service injection: this row only registers a scoped event listener. */
|
|
23
|
+
const inject = [];
|
|
24
|
+
/** Plan directory segments relative to the session workspace root (cwd). */
|
|
25
|
+
const PLAN_DIR_SEGMENTS = [".omd", "plans"];
|
|
26
|
+
/** The exit tool whose approved plan we persist. */
|
|
27
|
+
const EXIT_PLAN_MODE = "exit_plan_mode";
|
|
28
|
+
/** Maximum slug length (characters). */
|
|
29
|
+
const SLUG_MAX = 48;
|
|
30
|
+
/** The plan's first markdown heading (any level), or undefined when it has none. */
|
|
31
|
+
function firstHeading(plan) {
|
|
32
|
+
for (const line of plan.split("\n")) {
|
|
33
|
+
const match = /^#{1,6}\s+(.+?)\s*$/.exec(line);
|
|
34
|
+
if (match)
|
|
35
|
+
return match[1];
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Derive a filesystem-safe slug from the plan title. */
|
|
40
|
+
function slugify(title) {
|
|
41
|
+
const slug = String(title)
|
|
42
|
+
.normalize("NFKD")
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
45
|
+
.replace(/^-+|-+$/g, "")
|
|
46
|
+
.slice(0, SLUG_MAX);
|
|
47
|
+
return slug === "" ? "plan" : slug;
|
|
48
|
+
}
|
|
49
|
+
/** Compact local-ish UTC timestamp for the file name: YYYYMMDD-HHmmss. */
|
|
50
|
+
function timestamp() {
|
|
51
|
+
const d = new Date();
|
|
52
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
53
|
+
return (d.getUTCFullYear() +
|
|
54
|
+
pad(d.getUTCMonth() + 1) +
|
|
55
|
+
pad(d.getUTCDate()) +
|
|
56
|
+
"-" +
|
|
57
|
+
pad(d.getUTCHours()) +
|
|
58
|
+
pad(d.getUTCMinutes()) +
|
|
59
|
+
pad(d.getUTCSeconds()));
|
|
60
|
+
}
|
|
61
|
+
/** Write the plan into <cwd>/.omd/plans/ and return the absolute file path. */
|
|
62
|
+
async function savePlan(cwd, plan) {
|
|
63
|
+
const dir = join(cwd, ...PLAN_DIR_SEGMENTS);
|
|
64
|
+
await mkdir(dir, { recursive: true });
|
|
65
|
+
const file = join(dir, slugify(firstHeading(plan) ?? "") + "-" + timestamp() + ".md");
|
|
66
|
+
await writeFile(file, plan, "utf8");
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
/** Display path used in result enrichment and messages (forward slashes). */
|
|
70
|
+
function displayPath(saved) {
|
|
71
|
+
return PLAN_DIR_SEGMENTS.join("/") + "/" + basename(saved);
|
|
72
|
+
}
|
|
73
|
+
/** Subagents never own the plan review -- only the top-level planner does. */
|
|
74
|
+
function isSubagent(agent) {
|
|
75
|
+
return (agent !== undefined &&
|
|
76
|
+
agent !== null &&
|
|
77
|
+
agent.options !== undefined &&
|
|
78
|
+
agent.options !== null &&
|
|
79
|
+
typeof agent.options.subagentDepth === "number" &&
|
|
80
|
+
agent.options.subagentDepth > 0);
|
|
81
|
+
}
|
|
82
|
+
function apply(ctx) {
|
|
83
|
+
if (scopeOf(ctx) === undefined) {
|
|
84
|
+
throw new Error("omd-plan: refusing to mount outside a scoped context; mount this row inside an agent preset");
|
|
85
|
+
}
|
|
86
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
87
|
+
const decision = await next();
|
|
88
|
+
if (decision.kind !== "accept" || decision.value !== undefined)
|
|
89
|
+
return decision;
|
|
90
|
+
if (exec === undefined || exec.name !== EXIT_PLAN_MODE)
|
|
91
|
+
return decision;
|
|
92
|
+
if (result.isError)
|
|
93
|
+
return decision;
|
|
94
|
+
const agent = exec.agent;
|
|
95
|
+
if (agent === undefined || isSubagent(agent))
|
|
96
|
+
return decision;
|
|
97
|
+
const args = exec.arguments;
|
|
98
|
+
const plan = args !== undefined && args !== null && typeof args.plan === "string" ? args.plan : undefined;
|
|
99
|
+
if (plan === undefined)
|
|
100
|
+
return decision;
|
|
101
|
+
const cwd = agent.session !== undefined &&
|
|
102
|
+
agent.session.header !== undefined &&
|
|
103
|
+
typeof agent.session.header.cwd === "string"
|
|
104
|
+
? agent.session.header.cwd
|
|
105
|
+
: "";
|
|
106
|
+
if (cwd === "") {
|
|
107
|
+
// No workspace root to save into: fail closed but tell the model, so
|
|
108
|
+
// the planner does not promise a file name it never produced.
|
|
109
|
+
return withNotice(decision, result, {
|
|
110
|
+
type: "text",
|
|
111
|
+
text: "The approved plan could NOT be saved automatically: this session has no workspace directory. Ask the user how to proceed.",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const saved = await savePlan(cwd, plan);
|
|
116
|
+
return withNotice(decision, result, {
|
|
117
|
+
type: "text",
|
|
118
|
+
text: "Plan saved to " +
|
|
119
|
+
displayPath(saved) +
|
|
120
|
+
". Start work: run /start-work " +
|
|
121
|
+
basename(saved) +
|
|
122
|
+
" in an omd-executor session, or switch this session with /mode omd-executor and continue here.",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return withNotice(decision, result, {
|
|
127
|
+
type: "text",
|
|
128
|
+
text: "The approved plan could NOT be saved automatically: " +
|
|
129
|
+
(error instanceof Error ? error.message : String(error)) +
|
|
130
|
+
". Ask the user how to proceed.",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Keep the accepted decision, appending one text block to its content. */
|
|
136
|
+
function withNotice(decision, result, block) {
|
|
137
|
+
const base = Array.isArray(decision.content) ? decision.content : result.content ?? [];
|
|
138
|
+
return {
|
|
139
|
+
kind: "accept",
|
|
140
|
+
content: [...base, block],
|
|
141
|
+
...(decision.additionalContexts !== undefined
|
|
142
|
+
? { additionalContexts: decision.additionalContexts }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
4
|
+
/**
|
|
5
|
+
* @module @carljia/omd-dsh/startwork
|
|
6
|
+
*
|
|
7
|
+
* omd-start-work: human-facing `/start-work` command -- the "start work"
|
|
8
|
+
* trigger at the end of the OMD planning workflow. It resolves the named
|
|
9
|
+
* plan file inside the workspace's plan directory (a fixed, code-level
|
|
10
|
+
* convention -- never mentioned in any persona/prompt text), arms a goal
|
|
11
|
+
* whose objective references the plan's absolute path, and goal
|
|
12
|
+
* auto-continuation then drives the agent to execute the plan without
|
|
13
|
+
* further input.
|
|
14
|
+
*/
|
|
15
|
+
/** Cordis plugin name. */
|
|
16
|
+
const name = "omd-start-work";
|
|
17
|
+
/** The goal domain is already required by tool-goal in the same preset. */
|
|
18
|
+
const inject = ["goals"];
|
|
19
|
+
/** Plan directory segments relative to the session workspace root (cwd). */
|
|
20
|
+
const PLAN_DIR_SEGMENTS = [".omd", "plans"];
|
|
21
|
+
/** The prefix accepted when a user pastes the full relative plan path. */
|
|
22
|
+
const PLAN_DIR_PREFIX = ".omd/plans/";
|
|
23
|
+
/** Plan directory for one workspace root. */
|
|
24
|
+
function plansDir(cwd) {
|
|
25
|
+
return join(cwd, ...PLAN_DIR_SEGMENTS);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the user's file-name input to a candidate path inside the plan
|
|
29
|
+
* directory. Accepts a bare file name ("foo.md" / "foo") or the full
|
|
30
|
+
* relative path (".omd/plans/foo.md"); rejects absolute paths and anything
|
|
31
|
+
* that would escape the plan directory.
|
|
32
|
+
*/
|
|
33
|
+
function resolveCandidate(cwd, input) {
|
|
34
|
+
const trimmed = String(input).trim().replace(/\\/g, "/");
|
|
35
|
+
if (trimmed === "" || isAbsolute(trimmed))
|
|
36
|
+
return undefined;
|
|
37
|
+
let name = trimmed.replace(/^\.\//, "");
|
|
38
|
+
if (name.includes("/")) {
|
|
39
|
+
if (!name.startsWith(PLAN_DIR_PREFIX))
|
|
40
|
+
return undefined;
|
|
41
|
+
name = name.slice(PLAN_DIR_PREFIX.length);
|
|
42
|
+
}
|
|
43
|
+
if (name === "" || name.includes("/") || name === "." || name === "..")
|
|
44
|
+
return undefined;
|
|
45
|
+
if (name.startsWith("."))
|
|
46
|
+
return undefined; // no hidden-file tricks
|
|
47
|
+
return join(plansDir(cwd), name);
|
|
48
|
+
}
|
|
49
|
+
/** One /start-work invocation through the goal domain. */
|
|
50
|
+
async function executeStartWork(ctx, invocation) {
|
|
51
|
+
const agent = invocation.agent;
|
|
52
|
+
const cwd = agent !== undefined &&
|
|
53
|
+
agent.session !== undefined &&
|
|
54
|
+
agent.session.header !== undefined &&
|
|
55
|
+
typeof agent.session.header.cwd === "string"
|
|
56
|
+
? agent.session.header.cwd
|
|
57
|
+
: "";
|
|
58
|
+
if (cwd === "") {
|
|
59
|
+
return {
|
|
60
|
+
kind: "error",
|
|
61
|
+
text: "This session has no workspace directory; /start-work needs one to find the plan file.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const candidate = resolveCandidate(cwd, invocation.rawInput);
|
|
65
|
+
if (candidate === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "error",
|
|
68
|
+
text: "Usage: /start-work <plan file name> — the file must live inside " + PLAN_DIR_SEGMENTS.join("/") + "/.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let file = candidate;
|
|
72
|
+
const usable = async (path) => {
|
|
73
|
+
try {
|
|
74
|
+
await access(path);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
if (!(await usable(file)) && !file.endsWith(".md") && (await usable(file + ".md"))) {
|
|
82
|
+
file = file + ".md";
|
|
83
|
+
}
|
|
84
|
+
else if (!(await usable(file))) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "error",
|
|
87
|
+
text: "Plan file not found: " + PLAN_DIR_SEGMENTS.join("/") + "/" + file.slice(plansDir(cwd).length + 1),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = await readFile(file, "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
kind: "error",
|
|
97
|
+
text: "Cannot read the plan file: " + (error instanceof Error ? error.message : String(error)),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (text.trim() === "") {
|
|
101
|
+
return { kind: "error", text: "The plan file is empty." };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const current = ctx.goals.get(agent);
|
|
105
|
+
if (current !== undefined && current.phase !== "complete") {
|
|
106
|
+
return {
|
|
107
|
+
kind: "error",
|
|
108
|
+
text: `A goal is already ${current.phase}. Run /goal clear first, then /start-work <plan file name>.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
ctx.goals.create(agent, {
|
|
112
|
+
objective: "Execute the approved plan file at " +
|
|
113
|
+
file +
|
|
114
|
+
". Read the file in full, then carry out every step autonomously: implement, verify, and iterate until the plan's goal and success criteria are met. Work through goal continuation rounds until done.",
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
kind: "success",
|
|
118
|
+
text: "Start work armed — executing the plan to completion.\nPlan: " +
|
|
119
|
+
PLAN_DIR_SEGMENTS.join("/") +
|
|
120
|
+
"/" +
|
|
121
|
+
file.slice(plansDir(cwd).length + 1),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
kind: "error",
|
|
127
|
+
text: "start-work failed: " + (error instanceof Error ? error.message : String(error)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
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
|
+
}
|
|
135
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
136
|
+
commandCtx.commands.register({
|
|
137
|
+
name: "start-work",
|
|
138
|
+
description: "start work: arm a goal that executes the named plan file to completion",
|
|
139
|
+
input: {
|
|
140
|
+
hint: "<plan file name>",
|
|
141
|
+
images: false,
|
|
142
|
+
},
|
|
143
|
+
handler: (invocation) => executeStartWork(ctx, invocation),
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export { apply, inject, name };
|
package/lib/vendor/omd-task.mjs
CHANGED
|
@@ -211,7 +211,14 @@ function apply(ctx, config) {
|
|
|
211
211
|
if (!parent)
|
|
212
212
|
throw new Error("omd_task requires a calling agent (exec.agent was undefined)");
|
|
213
213
|
const tierName = resolveTier(config, args.tier);
|
|
214
|
-
|
|
214
|
+
let tier = config.tiers[tierName];
|
|
215
|
+
// 用户显式切换模型后(omd-mode 在 agent/request 让路并在作用域 ctx 上记录
|
|
216
|
+
// omdModeOverride),deep tier 改用用户选择的模型;其余 tier 保持矩阵配置。
|
|
217
|
+
const override = ctx.omdModeOverride;
|
|
218
|
+
if (tierName === "deep" && override !== undefined
|
|
219
|
+
&& typeof override.provider === "string" && typeof override.model === "string") {
|
|
220
|
+
tier = { ...tier, provider: override.provider, model: override.model };
|
|
221
|
+
}
|
|
215
222
|
const maxDepth = typeof config.maxDepth === "number" ? config.maxDepth : undefined;
|
|
216
223
|
const request = {
|
|
217
224
|
label: args.description,
|
|
@@ -14,10 +14,7 @@
|
|
|
14
14
|
"hint": "cheap and fast — repetitive investigation, searching, summarising, mechanical work",
|
|
15
15
|
"persona": "You are a FAST worker subagent (快速执行子代理): complete the assigned task efficiently with the tools you have; prefer short, focused answers.",
|
|
16
16
|
"toolFilter": {
|
|
17
|
-
"deny": [
|
|
18
|
-
"write",
|
|
19
|
-
"edit"
|
|
20
|
-
],
|
|
17
|
+
"deny": ["write", "edit"],
|
|
21
18
|
"denyShell": true
|
|
22
19
|
}
|
|
23
20
|
},
|
|
@@ -29,7 +26,7 @@
|
|
|
29
26
|
}
|
|
30
27
|
}
|
|
31
28
|
},
|
|
32
|
-
"
|
|
29
|
+
"ultraworker": {
|
|
33
30
|
"provider": "deepseek-official",
|
|
34
31
|
"model": "deepseek-v4-pro",
|
|
35
32
|
"tiers": {
|
|
@@ -39,10 +36,7 @@
|
|
|
39
36
|
"hint": "cheap and fast — repetitive investigation, searching, summarising, mechanical work",
|
|
40
37
|
"persona": "You are a FAST worker subagent (快速执行子代理): complete the assigned task efficiently with the tools you have; prefer short, focused answers.",
|
|
41
38
|
"toolFilter": {
|
|
42
|
-
"deny": [
|
|
43
|
-
"write",
|
|
44
|
-
"edit"
|
|
45
|
-
],
|
|
39
|
+
"deny": ["write", "edit"],
|
|
46
40
|
"denyShell": true
|
|
47
41
|
}
|
|
48
42
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carljia/omd-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "OMD 理念的 DeepSeek Harness 插件:模式能力边界 + 按模式配模型 + tier 差异化子代理委派",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"lib",
|
|
45
45
|
"presets",
|
|
46
|
-
"omd-matrix.json",
|
|
46
|
+
"omd-matrix.default.json",
|
|
47
47
|
"README.md",
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
@@ -4,10 +4,37 @@
|
|
|
4
4
|
name: '@deepseek-ai/dsh-persona'
|
|
5
5
|
config:
|
|
6
6
|
text: >-
|
|
7
|
-
You are in OMD EXECUTOR mode (OMD · 执行者): a full-capability autonomous executor on DeepSeek Harness. Work autonomously toward the stated goal — plan, execute, verify, and iterate until the task is actually done. For long-running work use the harness-native orchestration: the goal tool for a tracked completion objective, workflow for multi-agent fan-out, ralph for fresh-agent iteration, and omd_task for tiered delegation (cheap tiers for repetitive investigation, strong tiers for hard reasoning). Do not stop to ask when the path forward is clear. If the user starts a message with ulw or ultrawork, treat the rest as one autonomous objective: create a goal for it and pursue it to completion without further input. 本模式路由模型:{{model}}(provider: {{provider}})。
|
|
7
|
+
You are in OMD EXECUTOR mode (OMD · 执行者): a full-capability autonomous executor on DeepSeek Harness. Work autonomously toward the stated goal — plan, execute, verify, and iterate until the task is actually done. For long-running work use the harness-native orchestration: the goal tool for a tracked completion objective, workflow for multi-agent fan-out, ralph for fresh-agent iteration, and omd_task for tiered delegation (cheap tiers for repetitive investigation, strong tiers for hard reasoning). Do not stop to ask when the path forward is clear. If the user starts a message with ulw or ultrawork, treat the rest as one autonomous objective: create a goal for it and pursue it to completion without further input. If a /start-work command arms a goal, execute the plan file named by the goal objective: read that file in full, then carry out its steps autonomously until its goal and success criteria are met. 本模式路由模型:{{model}}(provider: {{provider}})。
|
|
8
8
|
|
|
9
9
|
# [omd-dsh:mode:start]
|
|
10
10
|
# [omd-dsh:mode:end]
|
|
11
|
+
- id: omd-start-work
|
|
12
|
+
name: '../.omd-vendor/omd-start-work.mjs'
|
|
13
|
+
|
|
14
|
+
- id: omd-mode-switch
|
|
15
|
+
name: '../.omd-vendor/omd-mode-switch.mjs'
|
|
16
|
+
|
|
17
|
+
- id: planning
|
|
18
|
+
name: cordis:group
|
|
19
|
+
group: true
|
|
20
|
+
isolate:
|
|
21
|
+
planMode: true
|
|
22
|
+
config:
|
|
23
|
+
- id: plan-mode
|
|
24
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
25
|
+
config:
|
|
26
|
+
section: |
|
|
27
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
28
|
+
|
|
29
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
30
|
+
|
|
31
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
32
|
+
|
|
33
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
34
|
+
|
|
35
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
36
|
+
|
|
37
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
11
38
|
- id: agent-instructions
|
|
12
39
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
13
40
|
config:
|
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
name: '@deepseek-ai/dsh-persona'
|
|
5
5
|
config:
|
|
6
6
|
text: >-
|
|
7
|
-
You are in OMD PLANNER mode (OMD · 规划访谈): a planning-and-interview agent on DeepSeek Harness. The harness plan-mode section supplies your planning rules — follow it. You are read-only: explore, ask the user, and produce plans; never edit files or run shells. Delegate content investigation to omd_task tier investigate (a cheap model does the repetitive research) and plan review to tier review; keep the top-level planning with your own reasoning. 本模式路由模型:{{model}}(provider: {{provider}})。
|
|
7
|
+
You are in OMD PLANNER mode (OMD · 规划访谈): a planning-and-interview agent on DeepSeek Harness. The harness plan-mode section supplies your planning rules — follow it. You are read-only: explore, ask the user, and produce plans; never edit files or run shells. Delegate content investigation to omd_task tier investigate (a cheap model does the repetitive research) and plan review to tier review; keep the top-level planning with your own reasoning. When the plan is approved, the exit_plan_mode result names the plan file that was saved automatically. Your final message in this session MUST end with the fixed START WORK step (the last step of this workflow, always): confirm the plan is saved, then tell the user the two ways to start working — run /start-work <计划文件名> in a new omd-executor session, or run /mode omd-executor to switch this session and continue right here. Never begin implementing in this read-only planner session. 本模式路由模型:{{model}}(provider: {{provider}})。
|
|
8
8
|
|
|
9
9
|
# [omd-dsh:mode:start]
|
|
10
10
|
# [omd-dsh:mode:end]
|
|
11
|
+
- id: omd-plan
|
|
12
|
+
name: '../.omd-vendor/omd-plan.mjs'
|
|
13
|
+
|
|
14
|
+
- id: omd-mode-switch
|
|
15
|
+
name: '../.omd-vendor/omd-mode-switch.mjs'
|
|
11
16
|
- id: agent-instructions
|
|
12
17
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
13
18
|
config:
|
|
@@ -35,6 +40,8 @@
|
|
|
35
40
|
|
|
36
41
|
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
37
42
|
|
|
43
|
+
Once the plan is approved, the exit_plan_mode result names the automatically saved plan file. Close the conversation with your persona's fixed Start Work final step — this is the last workflow step, always — and never start implementing in this read-only session.
|
|
44
|
+
|
|
38
45
|
- id: tool-fs
|
|
39
46
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
40
47
|
|