dsh-trellis 0.1.0 → 0.1.2
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 +92 -80
- package/lib/client.js +475 -0
- package/lib/client.js.map +1 -0
- package/lib/commands.js +215 -195
- package/lib/config.js +77 -45
- package/lib/index.js +186 -146
- package/lib/wait-tool.js +192 -149
- package/package.json +114 -79
package/lib/index.js
CHANGED
|
@@ -1,146 +1,186 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-trellis: Trellis workflow integration for DeepSeek Harness.
|
|
3
|
-
*
|
|
4
|
-
* - Per-turn workflow-state breadcrumb: at every eligible `agent/pre-step`,
|
|
5
|
-
* resolves the project's Trellis state (.trellis/workflow.md blocks +
|
|
6
|
-
* active task) and injects the matching [workflow-state:*] body into the
|
|
7
|
-
* session, exactly where the workspace-instruction baseline lands.
|
|
8
|
-
* - `/trellis-status` and read-only `/trellis-finish` utility commands.
|
|
9
|
-
* Session rituals remain skills (`/trellis-start`, `/trellis-continue`,
|
|
10
|
-
* `/trellis-finish-work`) so command registration cannot shadow them.
|
|
11
|
-
* - `trellis_wait`, an event-driven synchronization tool for a continuable
|
|
12
|
-
* role agent after the main session has exhausted independent work.
|
|
13
|
-
* - A managed DSH_TRELLIS_CONTEXT_ID derived per shell execution from the
|
|
14
|
-
* native DSH session, so an outer host's inherited TRELLIS_CONTEXT_ID cannot
|
|
15
|
-
* silently claim the inner session. Utility commands set the same identity.
|
|
16
|
-
*
|
|
17
|
-
* In directories without a Trellis project it injects no breadcrumb; utility
|
|
18
|
-
* commands validate the project only when a user explicitly invokes them.
|
|
19
|
-
*
|
|
20
|
-
* @module dsh-trellis
|
|
21
|
-
*/
|
|
22
|
-
import { isDeepStrictEqual } from "node:util";
|
|
23
|
-
import { Config, resolveConfig } from "./config.js";
|
|
24
|
-
import { createBreadcrumbComposer, isBreadcrumbMessage } from "./breadcrumb.js";
|
|
25
|
-
import { registerCommands } from "./commands.js";
|
|
26
|
-
import { registerSessionEnv } from "./session-env.js";
|
|
27
|
-
import { registerWaitTool } from "./wait-tool.js";
|
|
28
|
-
|
|
29
|
-
export const name = "dsh-trellis";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
) {
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-trellis: Trellis workflow integration for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* - Per-turn workflow-state breadcrumb: at every eligible `agent/pre-step`,
|
|
5
|
+
* resolves the project's Trellis state (.trellis/workflow.md blocks +
|
|
6
|
+
* active task) and injects the matching [workflow-state:*] body into the
|
|
7
|
+
* session, exactly where the workspace-instruction baseline lands.
|
|
8
|
+
* - `/trellis-status` and read-only `/trellis-finish` utility commands.
|
|
9
|
+
* Session rituals remain skills (`/trellis-start`, `/trellis-continue`,
|
|
10
|
+
* `/trellis-finish-work`) so command registration cannot shadow them.
|
|
11
|
+
* - `trellis_wait`, an event-driven synchronization tool for a continuable
|
|
12
|
+
* role agent after the main session has exhausted independent work.
|
|
13
|
+
* - A managed DSH_TRELLIS_CONTEXT_ID derived per shell execution from the
|
|
14
|
+
* native DSH session, so an outer host's inherited TRELLIS_CONTEXT_ID cannot
|
|
15
|
+
* silently claim the inner session. Utility commands set the same identity.
|
|
16
|
+
*
|
|
17
|
+
* In directories without a Trellis project it injects no breadcrumb; utility
|
|
18
|
+
* commands validate the project only when a user explicitly invokes them.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-trellis
|
|
21
|
+
*/
|
|
22
|
+
import { isDeepStrictEqual } from "node:util";
|
|
23
|
+
import { Config, resolveConfig } from "./config.js";
|
|
24
|
+
import { createBreadcrumbComposer, isBreadcrumbMessage } from "./breadcrumb.js";
|
|
25
|
+
import { registerCommands } from "./commands.js";
|
|
26
|
+
import { registerSessionEnv } from "./session-env.js";
|
|
27
|
+
import { registerWaitTool } from "./wait-tool.js";
|
|
28
|
+
|
|
29
|
+
export const name = "dsh-trellis";
|
|
30
|
+
export const SETTINGS_NAMESPACE = "dsh-trellis";
|
|
31
|
+
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
export const inject = ["commands", "shellEnv", "subagents", "tools"];
|
|
34
|
+
|
|
35
|
+
export { Config };
|
|
36
|
+
|
|
37
|
+
function stopAll(disposers) {
|
|
38
|
+
for (const dispose of disposers.toReversed()) dispose();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sameBreadcrumbPayload(left, right) {
|
|
42
|
+
return (
|
|
43
|
+
isDeepStrictEqual(left.content, right.content) &&
|
|
44
|
+
isDeepStrictEqual(left.source, right.source)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {import("@deepseek-ai/cordis").Context} ctx */
|
|
49
|
+
export function apply(ctx, config = {}) {
|
|
50
|
+
let stopped = false;
|
|
51
|
+
let disposeRuntime = () => {};
|
|
52
|
+
|
|
53
|
+
const mountRuntime = (nextConfig) => {
|
|
54
|
+
if (stopped) return;
|
|
55
|
+
disposeRuntime();
|
|
56
|
+
const resolved = resolveConfig(nextConfig);
|
|
57
|
+
if (!resolved.enabled) {
|
|
58
|
+
disposeRuntime = () => {};
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const disposers = [];
|
|
63
|
+
try {
|
|
64
|
+
if (resolved.commandsEnabled) {
|
|
65
|
+
disposers.push(registerCommands(ctx, resolved));
|
|
66
|
+
}
|
|
67
|
+
disposers.push(registerWaitTool(ctx));
|
|
68
|
+
disposers.push(registerSessionEnv(ctx));
|
|
69
|
+
disposers.push(registerBreadcrumb(ctx, resolved));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
stopAll(disposers);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
disposeRuntime = () => stopAll(disposers);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Keep the composition entry working when no settings provider is mounted.
|
|
78
|
+
// Once rc.8's settings service appears, its resolved section becomes the
|
|
79
|
+
// live source and a saved Web card edit remounts only this plugin's runtime.
|
|
80
|
+
ctx.effect(() => {
|
|
81
|
+
mountRuntime(config);
|
|
82
|
+
return () => {
|
|
83
|
+
stopped = true;
|
|
84
|
+
disposeRuntime();
|
|
85
|
+
};
|
|
86
|
+
}, "dsh-trellis.runtime");
|
|
87
|
+
|
|
88
|
+
// Optional by design: rc.6 profiles have no settings service and retain the
|
|
89
|
+
// loader-config behavior, while rc.8 Web profiles expose this namespace.
|
|
90
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
91
|
+
const scope = settingsCtx.settings.register(
|
|
92
|
+
SETTINGS_NAMESPACE,
|
|
93
|
+
Config,
|
|
94
|
+
{ base: config, applies: "live" },
|
|
95
|
+
);
|
|
96
|
+
const adopt = () => mountRuntime(scope.get());
|
|
97
|
+
adopt();
|
|
98
|
+
const unwatch = scope.watch(adopt);
|
|
99
|
+
settingsCtx.effect(
|
|
100
|
+
() => () => {
|
|
101
|
+
unwatch();
|
|
102
|
+
mountRuntime(config);
|
|
103
|
+
},
|
|
104
|
+
"dsh-trellis.settings",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function registerBreadcrumb(ctx, resolved) {
|
|
110
|
+
const { compose } = createBreadcrumbComposer(resolved, ctx);
|
|
111
|
+
|
|
112
|
+
const syncInbox = (agent, desired) => {
|
|
113
|
+
const pending = agent.inbox.nextStep.filter(isBreadcrumbMessage);
|
|
114
|
+
const alreadySupplied =
|
|
115
|
+
desired === undefined ||
|
|
116
|
+
agent.session.surface.nodes.some((seq) => {
|
|
117
|
+
const event = agent.session.events[seq];
|
|
118
|
+
return (
|
|
119
|
+
event?.type === "user/message" &&
|
|
120
|
+
event.data !== undefined &&
|
|
121
|
+
isBreadcrumbMessage(event.data) &&
|
|
122
|
+
sameBreadcrumbPayload(event.data, desired)
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
if (alreadySupplied) {
|
|
126
|
+
for (const message of pending) agent.inbox.remove(message.id);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const reusable = pending.find((message) =>
|
|
130
|
+
sameBreadcrumbPayload(message, desired),
|
|
131
|
+
);
|
|
132
|
+
if (reusable !== undefined) {
|
|
133
|
+
for (const message of pending)
|
|
134
|
+
if (message !== reusable) agent.inbox.remove(message.id);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const replaced = pending[0];
|
|
138
|
+
if (replaced === undefined) agent.inbox.prepend("next-step", desired);
|
|
139
|
+
else agent.inbox.replace(replaced.id, desired);
|
|
140
|
+
for (const message of pending.slice(1)) agent.inbox.remove(message.id);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return ctx.on("agent/pre-step", async ({ agent, messages, step, signal }, next) => {
|
|
144
|
+
const decision = await next();
|
|
145
|
+
let desired;
|
|
146
|
+
try {
|
|
147
|
+
desired = await compose(agent, messages, signal);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (signal !== undefined && signal.aborted) throw error;
|
|
150
|
+
ctx.logger?.warn?.(
|
|
151
|
+
"dsh-trellis breadcrumb composition failed: %o",
|
|
152
|
+
error,
|
|
153
|
+
);
|
|
154
|
+
return decision;
|
|
155
|
+
}
|
|
156
|
+
signal?.throwIfAborted();
|
|
157
|
+
if (desired === undefined) {
|
|
158
|
+
const pending = agent.inbox.nextStep.filter(isBreadcrumbMessage);
|
|
159
|
+
for (const message of pending) agent.inbox.remove(message.id);
|
|
160
|
+
return decision;
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
decision.kind === "reject" ||
|
|
164
|
+
(step === 1 && decision.messages.length === 0)
|
|
165
|
+
) {
|
|
166
|
+
syncInbox(agent, desired);
|
|
167
|
+
return decision;
|
|
168
|
+
}
|
|
169
|
+
const pending = agent.inbox.nextStep.filter(isBreadcrumbMessage);
|
|
170
|
+
for (const message of pending) agent.inbox.remove(message.id);
|
|
171
|
+
if (
|
|
172
|
+
decision.messages.some((message) =>
|
|
173
|
+
sameBreadcrumbPayload(message, desired),
|
|
174
|
+
)
|
|
175
|
+
) {
|
|
176
|
+
return decision;
|
|
177
|
+
}
|
|
178
|
+
const lastClaimedIndex = decision.messages.findLastIndex((message) =>
|
|
179
|
+
messages.includes(message),
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
kind: "enter",
|
|
183
|
+
messages: decision.messages.toSpliced(lastClaimedIndex + 1, 0, desired),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|