@carljia/omd-dsh 0.1.7 → 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.
@@ -1,199 +1,117 @@
1
+ // lib/plan.js
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  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 + plan-mode activation + write scope for the OMD
8
- * planner mode. Three jobs, all scoped to the planner preset:
9
- * 1. auto-activate plan mode for the top-level agent, so the plan:policy
10
- * section renders and exit_plan_mode works (DSH leaves plan state
11
- * inactive until /plan or a programmatic set);
12
- * 2. enforce a .md-only write guard so the planner stays read-only except
13
- * for markdown files;
14
- * 3. wrap `tools/post-execute` and intercept a successful `exit_plan_mode`
15
- * approval: the approved plan text is written into the workspace's plan
16
- * directory (a fixed, code-level convention -- never mentioned in any
17
- * persona/prompt text), and the tool result content is enriched with the
18
- * saved file name so the planner's fixed Start Work final step can hand
19
- * it to the user.
20
- *
21
- * Plan directory convention (hardcoded here and in omd-start-work only):
22
- * <session cwd>/.omd/plans/<slug>-<timestamp>.md
23
- * The slug derives from the plan's first markdown heading; a timestamp
24
- * suffix keeps repeated interviews from overwriting each other.
25
- */
26
- /** Cordis plugin name. */
27
- const name = "omd-plan";
28
- /** No service injection: this row only registers scoped event listeners. */
29
- const inject = [];
30
- /** Plan directory segments relative to the session workspace root (cwd). */
31
- const PLAN_DIR_SEGMENTS = [".omd", "plans"];
32
- /** The exit tool whose approved plan we persist. */
33
- const EXIT_PLAN_MODE = "exit_plan_mode";
34
- /** Maximum slug length (characters). */
35
- const SLUG_MAX = 48;
36
- /** The plan's first markdown heading (any level), or undefined when it has none. */
4
+ var name = "omd-plan";
5
+ var inject = [];
6
+ var PLAN_DIR_SEGMENTS = [".omd", "plans"];
7
+ var EXIT_PLAN_MODE = "exit_plan_mode";
8
+ var SLUG_MAX = 48;
37
9
  function firstHeading(plan) {
38
- for (const line of plan.split("\n")) {
39
- const match = /^#{1,6}\s+(.+?)\s*$/.exec(line);
40
- if (match)
41
- return match[1];
42
- }
43
- return undefined;
10
+ for (const line of plan.split("\n")) {
11
+ const match = /^#{1,6}\s+(.+?)\s*$/.exec(line);
12
+ if (match)
13
+ return match[1];
14
+ }
15
+ return void 0;
44
16
  }
45
- /** Derive a filesystem-safe slug from the plan title. */
46
17
  function slugify(title) {
47
- const slug = String(title)
48
- .normalize("NFKD")
49
- .toLowerCase()
50
- .replace(/[^\p{L}\p{N}]+/gu, "-")
51
- .replace(/^-+|-+$/g, "")
52
- .slice(0, SLUG_MAX);
53
- return slug === "" ? "plan" : slug;
18
+ const slug = String(title).normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX);
19
+ return slug === "" ? "plan" : slug;
54
20
  }
55
- /** Compact local-ish UTC timestamp for the file name: YYYYMMDD-HHmmss. */
56
21
  function timestamp() {
57
- const d = new Date();
58
- const pad = (n) => String(n).padStart(2, "0");
59
- return (d.getUTCFullYear() +
60
- pad(d.getUTCMonth() + 1) +
61
- pad(d.getUTCDate()) +
62
- "-" +
63
- pad(d.getUTCHours()) +
64
- pad(d.getUTCMinutes()) +
65
- pad(d.getUTCSeconds()));
22
+ const d = /* @__PURE__ */ new Date();
23
+ const pad = (n) => String(n).padStart(2, "0");
24
+ return d.getUTCFullYear() + pad(d.getUTCMonth() + 1) + pad(d.getUTCDate()) + "-" + pad(d.getUTCHours()) + pad(d.getUTCMinutes()) + pad(d.getUTCSeconds());
66
25
  }
67
- /** Write the plan into <cwd>/.omd/plans/ and return the absolute file path. */
68
26
  async function savePlan(cwd, plan) {
69
- const dir = join(cwd, ...PLAN_DIR_SEGMENTS);
70
- await mkdir(dir, { recursive: true });
71
- const file = join(dir, slugify(firstHeading(plan) ?? "") + "-" + timestamp() + ".md");
72
- await writeFile(file, plan, "utf8");
73
- return file;
27
+ const dir = join(cwd, ...PLAN_DIR_SEGMENTS);
28
+ await mkdir(dir, { recursive: true });
29
+ const file = join(dir, slugify(firstHeading(plan) ?? "") + "-" + timestamp() + ".md");
30
+ await writeFile(file, plan, "utf8");
31
+ return file;
74
32
  }
75
- /** Display path used in result enrichment and messages (forward slashes). */
76
33
  function displayPath(saved) {
77
- return PLAN_DIR_SEGMENTS.join("/") + "/" + basename(saved);
34
+ return PLAN_DIR_SEGMENTS.join("/") + "/" + basename(saved);
78
35
  }
79
- /** Subagents never own the plan review -- only the top-level planner does. */
80
36
  function isSubagent(agent) {
81
- return (agent !== undefined &&
82
- agent !== null &&
83
- agent.options !== undefined &&
84
- agent.options !== null &&
85
- typeof agent.options.subagentDepth === "number" &&
86
- agent.options.subagentDepth > 0);
37
+ return agent !== void 0 && agent !== null && agent.options !== void 0 && agent.options !== null && typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
87
38
  }
88
- /** Fold the session's plan/mode events (last one wins); mirror mode.ts. */
89
39
  function planModeActive(events) {
90
- let active = false;
91
- for (const event of events ?? []) {
92
- if (event !== undefined && event.type === "plan/mode") {
93
- active = event.data !== undefined && event.data !== null && event.data.active === true;
94
- }
40
+ let active = false;
41
+ for (const event of events ?? []) {
42
+ if (event !== void 0 && event.type === "plan/mode") {
43
+ active = event.data !== void 0 && event.data !== null && event.data.active === true;
95
44
  }
96
- return active;
45
+ }
46
+ return active;
97
47
  }
98
48
  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");
49
+ ctx.on("tools/pre-execute", async (exec, next) => {
50
+ const toolName = exec !== void 0 && exec !== null ? exec.name : void 0;
51
+ if (toolName !== "write" && toolName !== "edit")
52
+ return await next();
53
+ const filePath = exec.arguments !== void 0 && exec.arguments !== null && typeof exec.arguments.file_path === "string" ? exec.arguments.file_path : void 0;
54
+ if (filePath !== void 0 && filePath.toLowerCase().endsWith(".md"))
55
+ return await next();
56
+ return {
57
+ kind: "deny",
58
+ reason: "omd-plan: the planner preset may write or edit only .md files (refusing " + toolName + " on a non-.md path)"
59
+ };
60
+ });
61
+ ctx.on("agent/pre-step", async ({ agent }, next) => {
62
+ if (agent !== void 0 && agent !== null && !isSubagent(agent) && agent.session !== void 0 && agent.session !== null) {
63
+ if (!planModeActive(agent.session.events)) {
64
+ agent.session.append("plan/mode", { active: true });
65
+ }
66
+ }
67
+ return await next();
68
+ });
69
+ ctx.on("tools/post-execute", async (exec, result, next) => {
70
+ const decision = await next();
71
+ if (decision.kind !== "accept" || decision.value !== void 0)
72
+ return decision;
73
+ if (exec === void 0 || exec.name !== EXIT_PLAN_MODE)
74
+ return decision;
75
+ if (result.isError)
76
+ return decision;
77
+ const agent = exec.agent;
78
+ if (agent === void 0 || isSubagent(agent))
79
+ return decision;
80
+ const args = exec.arguments;
81
+ const plan = args !== void 0 && args !== null && typeof args.plan === "string" ? args.plan : void 0;
82
+ if (plan === void 0)
83
+ return decision;
84
+ const cwd = agent.session !== void 0 && agent.session.header !== void 0 && typeof agent.session.header.cwd === "string" ? agent.session.header.cwd : "";
85
+ if (cwd === "") {
86
+ return withNotice(decision, result, {
87
+ type: "text",
88
+ text: "The approved plan could NOT be saved automatically: this session has no workspace directory. Ask the user how to proceed."
89
+ });
101
90
  }
102
- // Scoped .md-only write guard: the planner may write/edit only markdown
103
- // files, keeping it read-only for every other path. The plan file itself is
104
- // written by this row via node:fs (not through the model's write/edit tools),
105
- // so plan persistence is unaffected by the guard. Implemented on the
106
- // tools/pre-execute gate (the row's existing ctx.on style) so no service
107
- // injection is required.
108
- ctx.on("tools/pre-execute", async (exec, next) => {
109
- const toolName = exec !== undefined && exec !== null ? exec.name : undefined;
110
- if (toolName !== "write" && toolName !== "edit")
111
- return await next();
112
- const filePath = exec.arguments !== undefined &&
113
- exec.arguments !== null &&
114
- typeof exec.arguments.file_path === "string"
115
- ? exec.arguments.file_path
116
- : undefined;
117
- if (filePath !== undefined && filePath.toLowerCase().endsWith(".md"))
118
- return await next();
119
- return {
120
- kind: "deny",
121
- reason: "omd-plan: the planner preset may write or edit only .md files (refusing " +
122
- toolName +
123
- " on a non-.md path)",
124
- };
125
- });
126
- // Auto-activate plan mode for the top-level planner agent. DSH leaves plan
127
- // state inactive until /plan or a programmatic set, so without this the
128
- // plan:policy section never renders and exit_plan_mode fails with "only
129
- // available in plan mode". Mirror mode.ts's direct log append (no narration)
130
- // so the planner session is in plan mode from its first request onward.
131
- ctx.on("agent/pre-step", async ({ agent }, next) => {
132
- if (agent !== undefined && agent !== null && !isSubagent(agent) && agent.session !== undefined && agent.session !== null) {
133
- if (!planModeActive(agent.session.events)) {
134
- agent.session.append("plan/mode", { active: true });
135
- }
136
- }
137
- return await next();
138
- });
139
- ctx.on("tools/post-execute", async (exec, result, next) => {
140
- const decision = await next();
141
- if (decision.kind !== "accept" || decision.value !== undefined)
142
- return decision;
143
- if (exec === undefined || exec.name !== EXIT_PLAN_MODE)
144
- return decision;
145
- if (result.isError)
146
- return decision;
147
- const agent = exec.agent;
148
- if (agent === undefined || isSubagent(agent))
149
- return decision;
150
- const args = exec.arguments;
151
- const plan = args !== undefined && args !== null && typeof args.plan === "string" ? args.plan : undefined;
152
- if (plan === undefined)
153
- return decision;
154
- const cwd = agent.session !== undefined &&
155
- agent.session.header !== undefined &&
156
- typeof agent.session.header.cwd === "string"
157
- ? agent.session.header.cwd
158
- : "";
159
- if (cwd === "") {
160
- // No workspace root to save into: fail closed but tell the model, so
161
- // the planner does not promise a file name it never produced.
162
- return withNotice(decision, result, {
163
- type: "text",
164
- text: "The approved plan could NOT be saved automatically: this session has no workspace directory. Ask the user how to proceed.",
165
- });
166
- }
167
- try {
168
- const saved = await savePlan(cwd, plan);
169
- return withNotice(decision, result, {
170
- type: "text",
171
- text: "Plan saved to " +
172
- displayPath(saved) +
173
- ". Start work: run /start-work " +
174
- basename(saved) +
175
- " in an omd-executor session, or switch this session with /mode omd-executor and continue here.",
176
- });
177
- }
178
- catch (error) {
179
- return withNotice(decision, result, {
180
- type: "text",
181
- text: "The approved plan could NOT be saved automatically: " +
182
- (error instanceof Error ? error.message : String(error)) +
183
- ". Ask the user how to proceed.",
184
- });
185
- }
186
- });
91
+ try {
92
+ const saved = await savePlan(cwd, plan);
93
+ return withNotice(decision, result, {
94
+ type: "text",
95
+ text: "Plan saved to " + displayPath(saved) + ". Start work: run /start-work " + basename(saved) + " in an omd-executor session, or switch this session with /mode omd-executor and continue here."
96
+ });
97
+ } catch (error) {
98
+ return withNotice(decision, result, {
99
+ type: "text",
100
+ text: "The approved plan could NOT be saved automatically: " + (error instanceof Error ? error.message : String(error)) + ". Ask the user how to proceed."
101
+ });
102
+ }
103
+ });
187
104
  }
188
- /** Keep the accepted decision, appending one text block to its content. */
189
105
  function withNotice(decision, result, block) {
190
- const base = Array.isArray(decision.content) ? decision.content : result.content ?? [];
191
- return {
192
- kind: "accept",
193
- content: [...base, block],
194
- ...(decision.additionalContexts !== undefined
195
- ? { additionalContexts: decision.additionalContexts }
196
- : {}),
197
- };
106
+ const base = Array.isArray(decision.content) ? decision.content : result.content ?? [];
107
+ return {
108
+ kind: "accept",
109
+ content: [...base, block],
110
+ ...decision.additionalContexts !== void 0 ? { additionalContexts: decision.additionalContexts } : {}
111
+ };
198
112
  }
199
- export { apply, inject, name };
113
+ export {
114
+ apply,
115
+ inject,
116
+ name
117
+ };
@@ -1,147 +1,111 @@
1
+ // lib/startwork.js
1
2
  import { access, readFile } from "node:fs/promises";
2
3
  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. */
4
+ var name = "omd-start-work";
5
+ var inject = ["goals"];
6
+ var PLAN_DIR_SEGMENTS = [".omd", "plans"];
7
+ var PLAN_DIR_PREFIX = ".omd/plans/";
24
8
  function plansDir(cwd) {
25
- return join(cwd, ...PLAN_DIR_SEGMENTS);
9
+ return join(cwd, ...PLAN_DIR_SEGMENTS);
26
10
  }
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
11
  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);
12
+ const trimmed = String(input).trim().replace(/\\/g, "/");
13
+ if (trimmed === "" || isAbsolute(trimmed))
14
+ return void 0;
15
+ let name2 = trimmed.replace(/^\.\//, "");
16
+ if (name2.includes("/")) {
17
+ if (!name2.startsWith(PLAN_DIR_PREFIX))
18
+ return void 0;
19
+ name2 = name2.slice(PLAN_DIR_PREFIX.length);
20
+ }
21
+ if (name2 === "" || name2.includes("/") || name2 === "." || name2 === "..")
22
+ return void 0;
23
+ if (name2.startsWith("."))
24
+ return void 0;
25
+ return join(plansDir(cwd), name2);
48
26
  }
49
- /** One /start-work invocation through the goal domain. */
50
27
  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
- }
28
+ const agent = invocation.agent;
29
+ const cwd = agent !== void 0 && agent.session !== void 0 && agent.session.header !== void 0 && typeof agent.session.header.cwd === "string" ? agent.session.header.cwd : "";
30
+ if (cwd === "") {
31
+ return {
32
+ kind: "error",
33
+ text: "This session has no workspace directory; /start-work needs one to find the plan file."
80
34
  };
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
- }
35
+ }
36
+ const candidate = resolveCandidate(cwd, invocation.rawInput);
37
+ if (candidate === void 0) {
38
+ return {
39
+ kind: "error",
40
+ text: "Usage: /start-work <plan file name> \u2014 the file must live inside " + PLAN_DIR_SEGMENTS.join("/") + "/."
41
+ };
42
+ }
43
+ let file = candidate;
44
+ const usable = async (path) => {
103
45
  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
- };
46
+ await access(path);
47
+ return true;
48
+ } catch {
49
+ return false;
123
50
  }
124
- catch (error) {
125
- return {
126
- kind: "error",
127
- text: "start-work failed: " + (error instanceof Error ? error.message : String(error)),
128
- };
51
+ };
52
+ if (!await usable(file) && !file.endsWith(".md") && await usable(file + ".md")) {
53
+ file = file + ".md";
54
+ } else if (!await usable(file)) {
55
+ return {
56
+ kind: "error",
57
+ text: "Plan file not found: " + PLAN_DIR_SEGMENTS.join("/") + "/" + file.slice(plansDir(cwd).length + 1)
58
+ };
59
+ }
60
+ let text;
61
+ try {
62
+ text = await readFile(file, "utf8");
63
+ } catch (error) {
64
+ return {
65
+ kind: "error",
66
+ text: "Cannot read the plan file: " + (error instanceof Error ? error.message : String(error))
67
+ };
68
+ }
69
+ if (text.trim() === "") {
70
+ return { kind: "error", text: "The plan file is empty." };
71
+ }
72
+ try {
73
+ const current = ctx.goals.get(agent);
74
+ if (current !== void 0 && current.phase !== "complete") {
75
+ return {
76
+ kind: "error",
77
+ text: `A goal is already ${current.phase}. Run /goal clear first, then /start-work <plan file name>.`
78
+ };
129
79
  }
80
+ ctx.goals.create(agent, {
81
+ objective: "Execute the approved plan file at " + file + ". 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."
82
+ });
83
+ return {
84
+ kind: "success",
85
+ text: "Start work armed \u2014 executing the plan to completion.\nPlan: " + PLAN_DIR_SEGMENTS.join("/") + "/" + file.slice(plansDir(cwd).length + 1)
86
+ };
87
+ } catch (error) {
88
+ return {
89
+ kind: "error",
90
+ text: "start-work failed: " + (error instanceof Error ? error.message : String(error))
91
+ };
92
+ }
130
93
  }
131
94
  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
- });
95
+ ctx.inject(["commands"], (commandCtx) => {
96
+ commandCtx.commands.register({
97
+ name: "start-work",
98
+ description: "start work: arm a goal that executes the named plan file to completion",
99
+ input: {
100
+ hint: "<plan file name>",
101
+ images: false
102
+ },
103
+ handler: (invocation) => executeStartWork(ctx, invocation)
145
104
  });
105
+ });
146
106
  }
147
- export { apply, inject, name };
107
+ export {
108
+ apply,
109
+ inject,
110
+ name
111
+ };