@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/mode.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
3
|
+
/**
|
|
4
|
+
* @module @carljia/omd-dsh/mode
|
|
5
|
+
*
|
|
6
|
+
* omd-mode-switch: human-facing `/mode` command -- switch the CURRENT
|
|
7
|
+
* session to another OMD agent preset, including mid-conversation.
|
|
8
|
+
*
|
|
9
|
+
* DSH's native `agentPreset.select` host API refuses to recompose a
|
|
10
|
+
* session that has already started (its preset is fixed at the UI level),
|
|
11
|
+
* and the AgentPresets.recompose method itself performs no history check
|
|
12
|
+
* ("the CALLER owns that check"). This row deliberately performs the
|
|
13
|
+
* in-session recompose, then keeps the log honest:
|
|
14
|
+
* - appends `agent-preset/selected`, so resume/fork rebuild the same
|
|
15
|
+
* composition ("model-visible <-> logged" rule);
|
|
16
|
+
* - appends `plan/mode { active: false }` when plan mode is still
|
|
17
|
+
* active, since the switch itself is a mode decision;
|
|
18
|
+
* - steers a notice message so the model knows the tool set changed.
|
|
19
|
+
*
|
|
20
|
+
* Mitigations for the swapped tool catalog: the omd-planner catalog is a
|
|
21
|
+
* subset of omd-executor's (the executor preset also mounts the plan-mode
|
|
22
|
+
* row), so logged planner tool calls stay renderable under the executor
|
|
23
|
+
* composition. Switching between other omd presets follows the same rule
|
|
24
|
+
* and the model simply receives the new catalog on the next request.
|
|
25
|
+
*/
|
|
26
|
+
/** Cordis plugin name. */
|
|
27
|
+
const name = "omd-mode-switch";
|
|
28
|
+
/**
|
|
29
|
+
* No mount-time injection: the roster service is resolved at runtime so a
|
|
30
|
+
* rosterless deployment fails only the /mode command, never the preset
|
|
31
|
+
* mount itself.
|
|
32
|
+
*/
|
|
33
|
+
const inject = [];
|
|
34
|
+
/** The OMD presets /mode may switch to. */
|
|
35
|
+
const OMD_PRESET_IDS = [
|
|
36
|
+
"omd-executor",
|
|
37
|
+
"omd-ultraworker",
|
|
38
|
+
"omd-planner",
|
|
39
|
+
"omd-reviewer",
|
|
40
|
+
"omd-explorer",
|
|
41
|
+
"omd-librarian",
|
|
42
|
+
"omd-chat",
|
|
43
|
+
];
|
|
44
|
+
/** Normalize the command input to a valid omd preset id, or undefined. */
|
|
45
|
+
function normalizeTarget(rawInput) {
|
|
46
|
+
const trimmed = String(rawInput).trim().toLowerCase();
|
|
47
|
+
if (trimmed === "")
|
|
48
|
+
return undefined;
|
|
49
|
+
const candidate = trimmed.startsWith("omd-") ? trimmed : "omd-" + trimmed;
|
|
50
|
+
return OMD_PRESET_IDS.includes(candidate) ? candidate : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Fold the session's plan/mode events (last one wins). */
|
|
53
|
+
function planModeActive(events) {
|
|
54
|
+
let active = false;
|
|
55
|
+
for (const event of events ?? []) {
|
|
56
|
+
if (event !== undefined && event.type === "plan/mode") {
|
|
57
|
+
active = event.data !== undefined && event.data !== null && event.data.active === true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return active;
|
|
61
|
+
}
|
|
62
|
+
/** Execute one /mode invocation through the roster service. */
|
|
63
|
+
async function executeSwitch(ctx, invocation) {
|
|
64
|
+
const agent = invocation.agent;
|
|
65
|
+
const target = normalizeTarget(invocation.rawInput);
|
|
66
|
+
if (target === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "error",
|
|
69
|
+
text: "Usage: /mode <preset> — valid: " + OMD_PRESET_IDS.join(", "),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let presets;
|
|
73
|
+
try {
|
|
74
|
+
presets = ctx.get("agentPresets");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
presets = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (presets === undefined || presets === null) {
|
|
80
|
+
return {
|
|
81
|
+
kind: "error",
|
|
82
|
+
text: "/mode is unavailable: this deployment composes no agent presets.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let current;
|
|
86
|
+
try {
|
|
87
|
+
current = presets.composedPreset(agent.ctx);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
current = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (current === target) {
|
|
93
|
+
return { kind: "success", text: "Already running " + target + "." };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const preset = await presets.recompose(agent.ctx, target);
|
|
97
|
+
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
98
|
+
if (planModeActive(agent.session.events)) {
|
|
99
|
+
agent.session.append("plan/mode", { active: false });
|
|
100
|
+
}
|
|
101
|
+
agent.steer(createUserMessage({
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: "The session switched to the " +
|
|
106
|
+
preset.id +
|
|
107
|
+
" agent preset. Continue in this mode with its tool set, persona, and model routing.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
source: {
|
|
111
|
+
kind: "plugin",
|
|
112
|
+
plugin: "omd-mode-switch",
|
|
113
|
+
form: "notice",
|
|
114
|
+
summary: "Session mode switched to " + preset.id,
|
|
115
|
+
},
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
kind: "success",
|
|
119
|
+
text: "Session preset switched to " + preset.id + " — the next turn runs with that mode's tools and model.",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "error",
|
|
125
|
+
text: "/mode failed: " + (error instanceof Error ? error.message : String(error)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
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
|
+
}
|
|
133
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
134
|
+
commandCtx.commands.register({
|
|
135
|
+
name: "mode",
|
|
136
|
+
description: "switch this session to another omd agent preset (tool set + model)",
|
|
137
|
+
input: {
|
|
138
|
+
hint: "<omd-* preset id>",
|
|
139
|
+
images: false,
|
|
140
|
+
},
|
|
141
|
+
handler: (invocation) => executeSwitch(ctx, invocation),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export { apply, inject, name };
|
package/lib/plan.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @carljia/omd-dsh/plan
|
|
3
|
+
*
|
|
4
|
+
* omd-plan: plan persistence for the OMD planner mode. It wraps the
|
|
5
|
+
* `tools/post-execute` waterfall and intercepts a successful
|
|
6
|
+
* `exit_plan_mode` approval: the approved plan text is written into the
|
|
7
|
+
* workspace's plan directory (a fixed, code-level convention -- never
|
|
8
|
+
* mentioned in any persona/prompt text), and the tool result content is
|
|
9
|
+
* enriched with the saved file name so the planner's fixed Start Work
|
|
10
|
+
* final step can hand it to the user.
|
|
11
|
+
*
|
|
12
|
+
* Plan directory convention (hardcoded here and in omd-start-work only):
|
|
13
|
+
* <session cwd>/.omd/plans/<slug>-<timestamp>.md
|
|
14
|
+
* The slug derives from the plan's first markdown heading; a timestamp
|
|
15
|
+
* suffix keeps repeated interviews from overwriting each other.
|
|
16
|
+
*/
|
|
17
|
+
/** Cordis plugin name. */
|
|
18
|
+
declare const name = "omd-plan";
|
|
19
|
+
/** No service injection: this row only registers a scoped event listener. */
|
|
20
|
+
declare const inject: never[];
|
|
21
|
+
declare function apply(ctx: any): void;
|
|
22
|
+
export { apply, inject, name };
|
package/lib/plan.js
ADDED
|
@@ -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,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @carljia/omd-dsh/startwork
|
|
3
|
+
*
|
|
4
|
+
* omd-start-work: human-facing `/start-work` command -- the "start work"
|
|
5
|
+
* trigger at the end of the OMD planning workflow. It resolves the named
|
|
6
|
+
* plan file inside the workspace's plan directory (a fixed, code-level
|
|
7
|
+
* convention -- never mentioned in any persona/prompt text), arms a goal
|
|
8
|
+
* whose objective references the plan's absolute path, and goal
|
|
9
|
+
* auto-continuation then drives the agent to execute the plan without
|
|
10
|
+
* further input.
|
|
11
|
+
*/
|
|
12
|
+
/** Cordis plugin name. */
|
|
13
|
+
declare const name = "omd-start-work";
|
|
14
|
+
/** The goal domain is already required by tool-goal in the same preset. */
|
|
15
|
+
declare const inject: string[];
|
|
16
|
+
declare function apply(ctx: any): void;
|
|
17
|
+
export { apply, inject, name };
|
package/lib/startwork.js
ADDED
|
@@ -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/task.js
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,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
3
|
+
/**
|
|
4
|
+
* @module @carljia/omd-dsh/mode
|
|
5
|
+
*
|
|
6
|
+
* omd-mode-switch: human-facing `/mode` command -- switch the CURRENT
|
|
7
|
+
* session to another OMD agent preset, including mid-conversation.
|
|
8
|
+
*
|
|
9
|
+
* DSH's native `agentPreset.select` host API refuses to recompose a
|
|
10
|
+
* session that has already started (its preset is fixed at the UI level),
|
|
11
|
+
* and the AgentPresets.recompose method itself performs no history check
|
|
12
|
+
* ("the CALLER owns that check"). This row deliberately performs the
|
|
13
|
+
* in-session recompose, then keeps the log honest:
|
|
14
|
+
* - appends `agent-preset/selected`, so resume/fork rebuild the same
|
|
15
|
+
* composition ("model-visible <-> logged" rule);
|
|
16
|
+
* - appends `plan/mode { active: false }` when plan mode is still
|
|
17
|
+
* active, since the switch itself is a mode decision;
|
|
18
|
+
* - steers a notice message so the model knows the tool set changed.
|
|
19
|
+
*
|
|
20
|
+
* Mitigations for the swapped tool catalog: the omd-planner catalog is a
|
|
21
|
+
* subset of omd-executor's (the executor preset also mounts the plan-mode
|
|
22
|
+
* row), so logged planner tool calls stay renderable under the executor
|
|
23
|
+
* composition. Switching between other omd presets follows the same rule
|
|
24
|
+
* and the model simply receives the new catalog on the next request.
|
|
25
|
+
*/
|
|
26
|
+
/** Cordis plugin name. */
|
|
27
|
+
const name = "omd-mode-switch";
|
|
28
|
+
/**
|
|
29
|
+
* No mount-time injection: the roster service is resolved at runtime so a
|
|
30
|
+
* rosterless deployment fails only the /mode command, never the preset
|
|
31
|
+
* mount itself.
|
|
32
|
+
*/
|
|
33
|
+
const inject = [];
|
|
34
|
+
/** The OMD presets /mode may switch to. */
|
|
35
|
+
const OMD_PRESET_IDS = [
|
|
36
|
+
"omd-executor",
|
|
37
|
+
"omd-ultraworker",
|
|
38
|
+
"omd-planner",
|
|
39
|
+
"omd-reviewer",
|
|
40
|
+
"omd-explorer",
|
|
41
|
+
"omd-librarian",
|
|
42
|
+
"omd-chat",
|
|
43
|
+
];
|
|
44
|
+
/** Normalize the command input to a valid omd preset id, or undefined. */
|
|
45
|
+
function normalizeTarget(rawInput) {
|
|
46
|
+
const trimmed = String(rawInput).trim().toLowerCase();
|
|
47
|
+
if (trimmed === "")
|
|
48
|
+
return undefined;
|
|
49
|
+
const candidate = trimmed.startsWith("omd-") ? trimmed : "omd-" + trimmed;
|
|
50
|
+
return OMD_PRESET_IDS.includes(candidate) ? candidate : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Fold the session's plan/mode events (last one wins). */
|
|
53
|
+
function planModeActive(events) {
|
|
54
|
+
let active = false;
|
|
55
|
+
for (const event of events ?? []) {
|
|
56
|
+
if (event !== undefined && event.type === "plan/mode") {
|
|
57
|
+
active = event.data !== undefined && event.data !== null && event.data.active === true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return active;
|
|
61
|
+
}
|
|
62
|
+
/** Execute one /mode invocation through the roster service. */
|
|
63
|
+
async function executeSwitch(ctx, invocation) {
|
|
64
|
+
const agent = invocation.agent;
|
|
65
|
+
const target = normalizeTarget(invocation.rawInput);
|
|
66
|
+
if (target === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "error",
|
|
69
|
+
text: "Usage: /mode <preset> — valid: " + OMD_PRESET_IDS.join(", "),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let presets;
|
|
73
|
+
try {
|
|
74
|
+
presets = ctx.get("agentPresets");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
presets = undefined;
|
|
78
|
+
}
|
|
79
|
+
if (presets === undefined || presets === null) {
|
|
80
|
+
return {
|
|
81
|
+
kind: "error",
|
|
82
|
+
text: "/mode is unavailable: this deployment composes no agent presets.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let current;
|
|
86
|
+
try {
|
|
87
|
+
current = presets.composedPreset(agent.ctx);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
current = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (current === target) {
|
|
93
|
+
return { kind: "success", text: "Already running " + target + "." };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const preset = await presets.recompose(agent.ctx, target);
|
|
97
|
+
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
98
|
+
if (planModeActive(agent.session.events)) {
|
|
99
|
+
agent.session.append("plan/mode", { active: false });
|
|
100
|
+
}
|
|
101
|
+
agent.steer(createUserMessage({
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: "The session switched to the " +
|
|
106
|
+
preset.id +
|
|
107
|
+
" agent preset. Continue in this mode with its tool set, persona, and model routing.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
source: {
|
|
111
|
+
kind: "plugin",
|
|
112
|
+
plugin: "omd-mode-switch",
|
|
113
|
+
form: "notice",
|
|
114
|
+
summary: "Session mode switched to " + preset.id,
|
|
115
|
+
},
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
kind: "success",
|
|
119
|
+
text: "Session preset switched to " + preset.id + " — the next turn runs with that mode's tools and model.",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "error",
|
|
125
|
+
text: "/mode failed: " + (error instanceof Error ? error.message : String(error)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
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
|
+
}
|
|
133
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
134
|
+
commandCtx.commands.register({
|
|
135
|
+
name: "mode",
|
|
136
|
+
description: "switch this session to another omd agent preset (tool set + model)",
|
|
137
|
+
input: {
|
|
138
|
+
hint: "<omd-* preset id>",
|
|
139
|
+
images: false,
|
|
140
|
+
},
|
|
141
|
+
handler: (invocation) => executeSwitch(ctx, invocation),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export { apply, inject, name };
|