@carljia/omd-dsh 0.1.4 → 0.1.7

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/plan.d.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * @module @carljia/omd-dsh/plan
3
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.
4
+ * omd-plan: plan persistence + plan-mode activation + write scope for the OMD
5
+ * planner mode. Three jobs, all scoped to the planner preset:
6
+ * 1. auto-activate plan mode for the top-level agent, so the plan:policy
7
+ * section renders and exit_plan_mode works (DSH leaves plan state
8
+ * inactive until /plan or a programmatic set);
9
+ * 2. enforce a .md-only write guard so the planner stays read-only except
10
+ * for markdown files;
11
+ * 3. wrap `tools/post-execute` and intercept a successful `exit_plan_mode`
12
+ * approval: the approved plan text is written into the workspace's plan
13
+ * directory (a fixed, code-level convention -- never mentioned in any
14
+ * persona/prompt text), and the tool result content is enriched with the
15
+ * saved file name so the planner's fixed Start Work final step can hand
16
+ * it to the user.
11
17
  *
12
18
  * Plan directory convention (hardcoded here and in omd-start-work only):
13
19
  * <session cwd>/.omd/plans/<slug>-<timestamp>.md
@@ -16,7 +22,7 @@
16
22
  */
17
23
  /** Cordis plugin name. */
18
24
  declare const name = "omd-plan";
19
- /** No service injection: this row only registers a scoped event listener. */
25
+ /** No service injection: this row only registers scoped event listeners. */
20
26
  declare const inject: never[];
21
27
  declare function apply(ctx: any): void;
22
28
  export { apply, inject, name };
package/lib/plan.js CHANGED
@@ -4,13 +4,19 @@ import { scopeOf } from "@deepseek-ai/dsh-scope";
4
4
  /**
5
5
  * @module @carljia/omd-dsh/plan
6
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.
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.
14
20
  *
15
21
  * Plan directory convention (hardcoded here and in omd-start-work only):
16
22
  * <session cwd>/.omd/plans/<slug>-<timestamp>.md
@@ -19,7 +25,7 @@ import { scopeOf } from "@deepseek-ai/dsh-scope";
19
25
  */
20
26
  /** Cordis plugin name. */
21
27
  const name = "omd-plan";
22
- /** No service injection: this row only registers a scoped event listener. */
28
+ /** No service injection: this row only registers scoped event listeners. */
23
29
  const inject = [];
24
30
  /** Plan directory segments relative to the session workspace root (cwd). */
25
31
  const PLAN_DIR_SEGMENTS = [".omd", "plans"];
@@ -79,10 +85,57 @@ function isSubagent(agent) {
79
85
  typeof agent.options.subagentDepth === "number" &&
80
86
  agent.options.subagentDepth > 0);
81
87
  }
88
+ /** Fold the session's plan/mode events (last one wins); mirror mode.ts. */
89
+ 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
+ }
95
+ }
96
+ return active;
97
+ }
82
98
  function apply(ctx) {
83
99
  if (scopeOf(ctx) === undefined) {
84
100
  throw new Error("omd-plan: refusing to mount outside a scoped context; mount this row inside an agent preset");
85
101
  }
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
+ });
86
139
  ctx.on("tools/post-execute", async (exec, result, next) => {
87
140
  const decision = await next();
88
141
  if (decision.kind !== "accept" || decision.value !== undefined)
@@ -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/sync.d.ts ADDED
@@ -0,0 +1,62 @@
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): locate the DSH harness node_modules, render each preset's
6
+ * omd-mode / omd-task rows from the user's model matrix, copy the presets and
7
+ * vendored row modules into .agent-presets, and rewrite the vendored modules'
8
+ * bare @deepseek-ai/* imports to absolute file:// URLs into the harness tree so
9
+ * the rows share ONE module instance with the harness (scope symbols etc.).
10
+ */
11
+ export type SyncFlags = {
12
+ harness?: string;
13
+ dryRun: boolean;
14
+ verbose: boolean;
15
+ };
16
+ export interface TierConfig {
17
+ provider: string;
18
+ model: string;
19
+ hint?: string;
20
+ persona?: string;
21
+ maxTokens?: number;
22
+ toolFilter?: {
23
+ allow?: string[];
24
+ deny?: string[];
25
+ denyShell?: boolean;
26
+ };
27
+ }
28
+ export interface ModeConfig {
29
+ provider?: string;
30
+ model?: string;
31
+ reasoningEffort?: string;
32
+ tiers?: Record<string, TierConfig>;
33
+ }
34
+ export interface Matrix {
35
+ version: number;
36
+ defaults?: {
37
+ provider?: string;
38
+ };
39
+ modes: Record<string, ModeConfig>;
40
+ }
41
+ export declare const PACKAGE_ROOT: string;
42
+ export declare const VENDOR_SOURCES: string[];
43
+ /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
44
+ export declare const MATRIX_PATH: string;
45
+ export declare function dshHome(): string;
46
+ /**
47
+ * Resolve the DSH harness node_modules:
48
+ * 1. --harness flag (and cache it for later);
49
+ * 2. auto-detect via the dsh executable on PATH;
50
+ * 3. fall back to the locally cached value.
51
+ */
52
+ export declare function resolveHarness(flags: SyncFlags): string | undefined;
53
+ /**
54
+ * Resolve the harness node_modules from THIS module's own location, walking up
55
+ * the Node resolution path for @deepseek-ai/dsh-scope. This is the reliable
56
+ * anchor when the package runs as a bundle inside a DSH profile: the profile's
57
+ * flat module fallback (or its hoisted node_modules) exposes the harness tree.
58
+ */
59
+ export declare function resolveHarnessFromSelf(): string | undefined;
60
+ export declare function loadMatrix(flags: SyncFlags, log?: (msg: string) => void): Matrix;
61
+ export declare function saveMatrix(m: Matrix): void;
62
+ export declare function runSync(flags: SyncFlags, harnessNodeModules: string, log?: (msg: string) => void): Promise<void>;