@quill507/dsh-orchestrator-preset 0.1.0
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/.gitattributes +20 -0
- package/.gitignore +140 -0
- package/LICENSE +21 -0
- package/README.md +533 -0
- package/README.zh.md +222 -0
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/cordis.patch.yml +399 -0
- package/extensions/dsh/aegis-prefix.js +197 -0
- package/extensions/dsh/index.js +41 -0
- package/install.sh +193 -0
- package/lane-composition.mjs +288 -0
- package/package.json +69 -0
- package/personas/analyst.md +74 -0
- package/personas/archivist.md +75 -0
- package/personas/auditor.md +79 -0
- package/personas/forge.md +101 -0
- package/personas/orchestrator.md +176 -0
- package/personas/planner.md +114 -0
- package/personas/reader.md +56 -0
- package/personas/scout.md +60 -0
- package/personas/seer.md +69 -0
- package/personas/wright.md +58 -0
- package/plan-aware-persona.mjs +102 -0
- package/routing-sections.mjs +120 -0
- package/skills/orch-delegation-brief/SKILL.md +55 -0
- package/skills/orch-discussion-protocol/SKILL.md +61 -0
- package/skills/orch-evidence-protocol/SKILL.md +88 -0
- package/skills/orch-real-path-testing/SKILL.md +66 -0
- package/tools/audit-personas.mjs +215 -0
- package/tools/gen-cordis-patch.mjs +424 -0
- package/tools/preset-declaration.mjs +468 -0
- package/tools/verify-install.sh +643 -0
package/install.sh
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# install.sh — install the dsh-orchestrator-preset bundle into a DeepSeek Harness home.
|
|
4
|
+
#
|
|
5
|
+
# ./install.sh # install into $DSH_HOME, or ~/.dsh
|
|
6
|
+
# DSH_HOME=/tmp/dsh ./install.sh
|
|
7
|
+
# ./install.sh --check # run the gates and report, change nothing
|
|
8
|
+
#
|
|
9
|
+
# What it does, in order:
|
|
10
|
+
# 0. gate — refuse to continue unless the host is >= 0.1.7-rc.1
|
|
11
|
+
# 1. stage — copy this repository's distributable content to $DSH_HOME/plugins/
|
|
12
|
+
# 2. link — verify the per-profile symlink that `pnpm install` creates
|
|
13
|
+
# 3. skills — verify the four bundled skills; the bundle's own provider serves
|
|
14
|
+
# them, so nothing is copied into the user's global skills directory
|
|
15
|
+
# 4. none — the aegis prefix and its optional description swap ship inside the
|
|
16
|
+
# bundle as extensions/dsh/aegis-prefix.js; there is no companion
|
|
17
|
+
# plugin to install, and installing the old one reintroduces a race
|
|
18
|
+
# 5. report — print the profile wiring the user must apply themselves
|
|
19
|
+
#
|
|
20
|
+
# What it deliberately does NOT do: touch a profile's package.json or cordis.patch.yml.
|
|
21
|
+
# Those are the live installation's own files, and cordis.patch.yml is watched live by
|
|
22
|
+
# dsh-hmr, so writing it triggers an immediate full re-assembly of the running host.
|
|
23
|
+
# Steps 2 and 5 print the exact commands instead.
|
|
24
|
+
#
|
|
25
|
+
# MIT licensed. See ./LICENSE.
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
MIN_HOST_VERSION="0.1.7-rc.1"
|
|
30
|
+
BUNDLE_DIRNAME="dsh-orchestrator-preset-bundle"
|
|
31
|
+
BUNDLE_SCOPE="@local/${BUNDLE_DIRNAME}"
|
|
32
|
+
SKILLS="orch-delegation-brief orch-discussion-protocol orch-evidence-protocol orch-real-path-testing"
|
|
33
|
+
CHECK_ONLY=0
|
|
34
|
+
[ "${1:-}" = "--check" ] && CHECK_ONLY=1
|
|
35
|
+
|
|
36
|
+
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
|
38
|
+
if [ ! "$DSH_HOME" = "${DSH_HOME%/}" ]; then DSH_HOME="${DSH_HOME%/}"; fi
|
|
39
|
+
|
|
40
|
+
say() { printf '\n== %s\n' "$*"; }
|
|
41
|
+
info() { printf ' %s\n' "$*"; }
|
|
42
|
+
die() { printf '\n!! %s\n' "$*" >&2; exit 1; }
|
|
43
|
+
|
|
44
|
+
# --- semver compare -------------------------------------------------------
|
|
45
|
+
# Prints -1/0/1 for $1 vs $2. Handles MAJOR.MINOR.PATCH with an optional
|
|
46
|
+
# `-prerelease` suffix; a release outranks its own prereleases.
|
|
47
|
+
ver_cmp() {
|
|
48
|
+
local a_core a_pre b_core b_pre
|
|
49
|
+
a_core="${1%%-*}"; a_pre=""; case "$1" in *-*) a_pre="${1#*-}";; esac
|
|
50
|
+
b_core="${2%%-*}"; b_pre=""; case "$2" in *-*) b_pre="${2#*-}";; esac
|
|
51
|
+
local v
|
|
52
|
+
v="$(printf '%s\n%s\n' "$a_core" "$b_core" | sort -V | head -1)"
|
|
53
|
+
[ "$v" = "$a_core" ] && [ "$a_core" != "$b_core" ] && { echo -1; return; }
|
|
54
|
+
[ "$v" = "$b_core" ] && [ "$a_core" != "$b_core" ] && { echo 1; return; }
|
|
55
|
+
# equal numeric cores -> compare prerelease
|
|
56
|
+
[ -z "$a_pre" ] && [ -z "$b_pre" ] && { echo 0; return; }
|
|
57
|
+
[ -z "$a_pre" ] && { echo 1; return; }
|
|
58
|
+
[ -z "$b_pre" ] && { echo -1; return; }
|
|
59
|
+
[ "$a_pre" = "$b_pre" ] && { echo 0; return; }
|
|
60
|
+
[ "$(printf '%s\n%s\n' "$a_pre" "$b_pre" | sort -V | head -1)" = "$a_pre" ] && { echo -1; return; }
|
|
61
|
+
echo 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# --- 0. host version gate -------------------------------------------------
|
|
65
|
+
say "0. host version gate (need >= ${MIN_HOST_VERSION})"
|
|
66
|
+
HOST_BIN="$(command -v dsh || true)"
|
|
67
|
+
[ -n "$HOST_BIN" ] || die "'dsh' not found on PATH. Install DeepSeek Harness, or re-run with it on PATH."
|
|
68
|
+
HOST_VERSION="$("$HOST_BIN" --version 2>/dev/null | tr -d '[:space:]' | head -1)"
|
|
69
|
+
[ -n "$HOST_VERSION" ] || die "could not read a version from 'dsh --version'."
|
|
70
|
+
info "dsh --version = ${HOST_VERSION}"
|
|
71
|
+
if [ "$(ver_cmp "$HOST_VERSION" "$MIN_HOST_VERSION")" = "-1" ]; then
|
|
72
|
+
die "host ${HOST_VERSION} is older than the required ${MIN_HOST_VERSION}. Refusing to install."
|
|
73
|
+
fi
|
|
74
|
+
info "gate passed"
|
|
75
|
+
|
|
76
|
+
if [ "$CHECK_ONLY" = "1" ]; then
|
|
77
|
+
say "--check: gates only, nothing written."
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# --- 1. stage the bundle --------------------------------------------------
|
|
82
|
+
say "1. stage the bundle into ${DSH_HOME}/plugins/${BUNDLE_DIRNAME}"
|
|
83
|
+
DEST="${DSH_HOME}/plugins/${BUNDLE_DIRNAME}"
|
|
84
|
+
if [ -e "$DEST" ]; then
|
|
85
|
+
info "already present — leaving it in place (remove it yourself to reinstall)."
|
|
86
|
+
else
|
|
87
|
+
mkdir -p "$DEST"
|
|
88
|
+
for item in LICENSE README.md README.zh.md cordis.patch.yml package.json personas tools \
|
|
89
|
+
lane-composition.mjs plan-aware-persona.mjs routing-sections.mjs; do
|
|
90
|
+
cp -r "${REPO_ROOT}/${item}" "${DEST}/"
|
|
91
|
+
done
|
|
92
|
+
# The repo root IS the bundle content; these belong in the bundle, not to it.
|
|
93
|
+
info "staged $(find "$DEST" -type f | wc -l) files."
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# --- 2. verify the per-profile link --------------------------------------
|
|
97
|
+
# The bundle is resolved BY NAME from a profile's node_modules, so the symlink
|
|
98
|
+
# belongs in each profile — not inside the bundle. `pnpm install` creates it from
|
|
99
|
+
# the profile's `link:` dependency; this step verifies it and says what is missing.
|
|
100
|
+
say "2. verify the ${BUNDLE_SCOPE} symlink in each profile"
|
|
101
|
+
PROFILES="${DSH_HOME}/profiles"
|
|
102
|
+
if [ ! -d "$PROFILES" ]; then
|
|
103
|
+
info "no ${PROFILES} yet — nothing to verify."
|
|
104
|
+
else
|
|
105
|
+
FOUND_PROFILE=0
|
|
106
|
+
for pdir in "$PROFILES"/*/; do
|
|
107
|
+
[ -d "$pdir" ] || continue
|
|
108
|
+
pname="$(basename "$pdir")"
|
|
109
|
+
case "$pname" in node_modules|shared) continue;; esac
|
|
110
|
+
FOUND_PROFILE=1
|
|
111
|
+
link="${pdir}node_modules/@local/${BUNDLE_DIRNAME}"
|
|
112
|
+
if [ -L "$link" ]; then
|
|
113
|
+
# test -L, never test -e: Git Bash can silently degrade a link into a copy,
|
|
114
|
+
# and a copy passes test -e while freezing every later edit to the bundle.
|
|
115
|
+
info "${pname}: real symlink -> $(readlink "$link")"
|
|
116
|
+
elif [ -e "$link" ]; then
|
|
117
|
+
info "${pname}: !! ${link} exists but is NOT a symlink (a copy freezes the bundle)"
|
|
118
|
+
else
|
|
119
|
+
info "${pname}: MISSING — run 'pnpm install' in ${pdir} after adding the link: dependency"
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
[ "$FOUND_PROFILE" = "0" ] && info "no profile directories found."
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# --- 3. the four self-authored skills ------------------------------------
|
|
126
|
+
# Nothing to copy. The skills ship inside this bundle and are served by the
|
|
127
|
+
# filesystem skill provider that extensions/dsh/index.js mounts, with
|
|
128
|
+
# includeDefaultRoots:false so the user's global skills directory is left alone.
|
|
129
|
+
# Copying them into $DSH_HOME/skills is what this step used to do, and removing
|
|
130
|
+
# it is the point: a preset should not scatter copies through the user's home.
|
|
131
|
+
say "3. verify the bundled skills (nothing to copy — the bundle's provider serves them)"
|
|
132
|
+
for s in $SKILLS; do
|
|
133
|
+
[ -f "${REPO_ROOT}/skills/${s}/SKILL.md" ] || die "skills/${s}/SKILL.md is missing from the repository."
|
|
134
|
+
want=$(grep -m1 '^name:' "${REPO_ROOT}/skills/${s}/SKILL.md" | sed 's/^name:[[:space:]]*//')
|
|
135
|
+
[ "$want" = "$s" ] || die "skills/${s}: frontmatter name is '$want'; the provider reads the frontmatter, not the directory."
|
|
136
|
+
if [ -d "${DSH_HOME}/skills/${s}" ]; then
|
|
137
|
+
info "${s}: a copy also exists in ${DSH_HOME}/skills — redundant, the provider is authoritative"
|
|
138
|
+
else
|
|
139
|
+
info "${s}: served by the bundle's provider"
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
[ -f "${REPO_ROOT}/extensions/dsh/index.js" ] || die "extensions/dsh/index.js is missing — the bundle has no skill provider."
|
|
143
|
+
|
|
144
|
+
# --- 4. nothing to install ------------------------------------------------
|
|
145
|
+
# The aegis prefix bridge and its optional description swap live in the bundle's
|
|
146
|
+
# own extensions/dsh/aegis-prefix.js, mounted by a row the generator emits. There
|
|
147
|
+
# is no companion plugin, which is deliberate: the two used to be separate
|
|
148
|
+
# plugins, and because both mutate the same skill registrations they raced — the
|
|
149
|
+
# split produced 4 of 22 descriptions localised and two bare names leaking back.
|
|
150
|
+
# One plugin doing both in a single registration pass is what removed the race.
|
|
151
|
+
say "4. nothing to install — extensions/dsh/aegis-prefix.js does this inside the bundle"
|
|
152
|
+
|
|
153
|
+
# --- 5. what the user must do by hand ------------------------------------
|
|
154
|
+
say "5. the profile wiring (deliberate — see the header of this script)"
|
|
155
|
+
cat <<EOF
|
|
156
|
+
A profile's own package.json and cordis.patch.yml are the live installation's
|
|
157
|
+
files, and cordis.patch.yml is watched by dsh-hmr: writing it triggers an
|
|
158
|
+
immediate full re-assembly. This script therefore stops here and prints the
|
|
159
|
+
exact edits instead of making them.
|
|
160
|
+
|
|
161
|
+
(a) In ~/.dsh/profiles/<profile>/package.json — add the dependency:
|
|
162
|
+
|
|
163
|
+
"${BUNDLE_SCOPE}": "link:${DSH_HOME}/plugins/${BUNDLE_DIRNAME}"
|
|
164
|
+
|
|
165
|
+
and add the same string to the dsh.profile.bundles array, keeping its
|
|
166
|
+
position. Then run, in that profile directory:
|
|
167
|
+
|
|
168
|
+
pnpm install
|
|
169
|
+
|
|
170
|
+
which creates the real symlink under node_modules/@local/.
|
|
171
|
+
|
|
172
|
+
(b) In ~/.dsh/profiles/<profile>/cordis.patch.yml — select the preset:
|
|
173
|
+
|
|
174
|
+
- id: preset-dsh-orchestrator-preset
|
|
175
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
176
|
+
config:
|
|
177
|
+
selectedDefault: dsh-orchestrator-preset
|
|
178
|
+
|
|
179
|
+
A patch REPLACES the whole row rather than deep-merging it, so a row that
|
|
180
|
+
overrides config must restate every key it needs; the snippet above is the
|
|
181
|
+
whole row, not an addition to a larger one.
|
|
182
|
+
|
|
183
|
+
Inspect ~/.dsh/profiles/<profile>/cordis.patch.yml <- decides the preset
|
|
184
|
+
Ignore ~/.dsh/profiles/<profile>/cordis.yml <- empty by design
|
|
185
|
+
|
|
186
|
+
The GUI preset chooser rewrites cordis.patch.yml in place, so a preset picked
|
|
187
|
+
in the GUI overwrites selectedDefault and does not restore itself. Re-apply
|
|
188
|
+
(b) to come back. This script never touches that file, so re-running it is safe.
|
|
189
|
+
|
|
190
|
+
Re-run with --check to re-verify the host gate without writing anything.
|
|
191
|
+
EOF
|
|
192
|
+
|
|
193
|
+
say "done."
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane composition for the `dsh-orchestrator-preset` agent preset — child addressing.
|
|
3
|
+
*
|
|
4
|
+
* The preset's nine named `subagent_*` lanes are rows of the preset itself, so
|
|
5
|
+
* each lane keeps its own persona and `toolFilter` (those faces travel on the
|
|
6
|
+
* lane row). What the Team layer takes over is the coordinator's control
|
|
7
|
+
* surface: on the Lead's seat `send_message` / `list_agents` / `interrupt_agent`
|
|
8
|
+
* resolve to the Team implementations, which address a teammate by NAME and
|
|
9
|
+
* cannot reach a `subagent_*` child at all, so a Lead delegating through
|
|
10
|
+
* `subagent_*` lanes has no tool to list, continue, or interrupt them — which is
|
|
11
|
+
* exactly the three actions this plugin supplies, and nothing else.
|
|
12
|
+
*
|
|
13
|
+
* Names are new because `NamedEntries.insert` throws on a duplicate inside one
|
|
14
|
+
* layer (`dsh-scope/lib/index.js:27-29`) and `tool-agent-team` registers its ten
|
|
15
|
+
* names under a SINGLE try/catch over its whole set: reusing the Team names
|
|
16
|
+
* would collide in the Lead's own layer and unwind every Team tool, shared task
|
|
17
|
+
* board included.
|
|
18
|
+
*
|
|
19
|
+
* The tools are registered GLOBALLY — `ctx.tools.register`, once, at mount — and
|
|
20
|
+
* never into a per-agent scope. Two measured reasons. First, `tools.restrict()`
|
|
21
|
+
* accepts only GLOBALLY registered names: a scoped registration makes every
|
|
22
|
+
* lane's deny entry illegal (`tools.restrict() names unknown global tools …`) and
|
|
23
|
+
* the lane's whole setup then throws, which is how this plugin once took the
|
|
24
|
+
* entire lane family down. Second, the preset's own control tools
|
|
25
|
+
* (`send_message` / `list_agents` / `interrupt_agent`) already have exactly this
|
|
26
|
+
* shape: registered globally by `tool-subagent-control`, then denied per lane by
|
|
27
|
+
* the same deny lists. Those lists are what keeps these three names off a lane's
|
|
28
|
+
* seat; the Lead, not being a lane, keeps them.
|
|
29
|
+
*
|
|
30
|
+
* Each action passes the live calling Agent (`exec.agent`) as sender/authority and
|
|
31
|
+
* leaves ownership to the service (`agents.isOwnedBy` is not a gate: it accepts
|
|
32
|
+
* only live resident children and would reject cold resume). One residual: the
|
|
33
|
+
* adjacent-agent resume pointer is not injected under the Team assembly, so that
|
|
34
|
+
* contract is carried by the persona and the `orch-delegation-brief` skill instead.
|
|
35
|
+
*
|
|
36
|
+
* @module lane-composition
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
40
|
+
|
|
41
|
+
/** Cordis plugin name. */
|
|
42
|
+
export const name = 'lane-composition'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Three services, and the first is not optional: `tools` is the registry this
|
|
46
|
+
* plugin registers INTO — reading `ctx.tools` without it throws `cannot get
|
|
47
|
+
* property "tools" without inject` (cordis' context proxy), which an earlier
|
|
48
|
+
* revision caught into a warning and thereby registered nothing at all. The
|
|
49
|
+
* sibling row `@deepseek-ai/dsh-tool-subagent-control` declares `tools` on this
|
|
50
|
+
* same plane: the precedent that it is reachable here.
|
|
51
|
+
*
|
|
52
|
+
* `subagents` is the continuation seam and its catalog; `agents` is the live Agent
|
|
53
|
+
* registry that turns a durable child id into a status. Both come from `dsh-base`.
|
|
54
|
+
* No Team service is declared or read: registration is decision-free (no
|
|
55
|
+
* membership test, no per-agent hook), so an assembly WITHOUT the Team bundle
|
|
56
|
+
* behaves exactly like one with it.
|
|
57
|
+
*/
|
|
58
|
+
export const inject = ['tools', 'subagents', 'agents']
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Refine one child's status through the live Agent registry: `running` for an
|
|
62
|
+
* active driver, `idle` for a resident Agent between turns, and `ready` when no
|
|
63
|
+
* live Agent remains. `ready` keeps resumability visible without presenting an
|
|
64
|
+
* inactive conversation as terminal.
|
|
65
|
+
*
|
|
66
|
+
* @param agents - the live Agent registry (`ctx.agents`).
|
|
67
|
+
* @param id - the child's durable session id.
|
|
68
|
+
* @returns the status to report for that child.
|
|
69
|
+
*/
|
|
70
|
+
function childStatus(agents, id) {
|
|
71
|
+
const live = agents.get(id)
|
|
72
|
+
if (live === undefined) return 'ready'
|
|
73
|
+
return live.status === 'running' ? 'running' : 'idle'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Project one catalog entry into the model-facing row, or drop a one-shot child.
|
|
78
|
+
*
|
|
79
|
+
* A child whose `mode` is not `continuable` can never accept a message, so
|
|
80
|
+
* offering it would only invite an unaddressable id. The label falls back to the
|
|
81
|
+
* id because the schema promises a string and the catalog records `label` as
|
|
82
|
+
* optional.
|
|
83
|
+
*
|
|
84
|
+
* @param agents - the live Agent registry (`ctx.agents`).
|
|
85
|
+
* @param entry - one row from `listChildren`.
|
|
86
|
+
* @returns the row to report, or undefined for a one-shot child.
|
|
87
|
+
*/
|
|
88
|
+
function projectChild(agents, entry) {
|
|
89
|
+
if (entry.mode !== 'continuable') return undefined
|
|
90
|
+
return {
|
|
91
|
+
id: entry.id,
|
|
92
|
+
label: entry.label ?? entry.id,
|
|
93
|
+
status: childStatus(agents, entry.id),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the Lead-side `subagent_children` tool: enumeration of the caller's own
|
|
99
|
+
* continuable `subagent_*` children by durable agent id.
|
|
100
|
+
*
|
|
101
|
+
* The listing is scoped CONSTRUCTIVELY — it queries the caller's own session id
|
|
102
|
+
* — so every row is necessarily one of the caller's own children.
|
|
103
|
+
*
|
|
104
|
+
* @param ctx - the preset's scope context, where `subagents` and `agents` were injected.
|
|
105
|
+
* @returns the tool definition.
|
|
106
|
+
*/
|
|
107
|
+
function childrenTool(ctx) {
|
|
108
|
+
return defineTool({
|
|
109
|
+
name: 'subagent_children',
|
|
110
|
+
description: 'List YOUR OWN continuable `subagent_*` children by durable agent id, label, and live status. '
|
|
111
|
+
+ '`running` means the child is working now; `idle` means it is resident but between turns; `ready` means no '
|
|
112
|
+
+ 'live Agent remains although the durable session still exists, so it can still be resumed. The `id` reported '
|
|
113
|
+
+ 'here is exactly the `agent_id` that `subagent_send` and `subagent_interrupt` take. This is NOT the Team '
|
|
114
|
+
+ 'roster: `list_agents` lists the Lead and every durable teammate by teammate NAME and cannot address a '
|
|
115
|
+
+ '`subagent_*` child at all. One-shot children are omitted because they cannot be messaged.',
|
|
116
|
+
parameters: {},
|
|
117
|
+
output: {
|
|
118
|
+
schema: {
|
|
119
|
+
type: 'array',
|
|
120
|
+
items: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
properties: {
|
|
124
|
+
id: { type: 'string', required: true },
|
|
125
|
+
label: { type: 'string', required: true },
|
|
126
|
+
status: { type: 'string', required: true, enum: ['running', 'idle', 'ready'] },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
render: (_args, entries) => [{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: entries.length === 0
|
|
133
|
+
? '(no subagent children)'
|
|
134
|
+
: entries.map((entry) => `${entry.id} [${entry.status}] — ${entry.label}`).join('\n'),
|
|
135
|
+
}],
|
|
136
|
+
},
|
|
137
|
+
async execute(_args, exec) {
|
|
138
|
+
const parent = exec.agent
|
|
139
|
+
if (parent === undefined) throw new Error('subagent_children requires a calling agent (exec.agent was undefined)')
|
|
140
|
+
const entries = await ctx.subagents.listChildren(parent.session.header.id, exec.signal)
|
|
141
|
+
return entries.map((entry) => projectChild(ctx.agents, entry)).filter((entry) => entry !== undefined)
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the Lead-side `subagent_send` tool: continuation delivery to one of the
|
|
148
|
+
* caller's own `subagent_*` children by durable agent id.
|
|
149
|
+
*
|
|
150
|
+
* @param ctx - the preset's scope context, where `subagents` was injected.
|
|
151
|
+
* @returns the tool definition.
|
|
152
|
+
*/
|
|
153
|
+
function sendTool(ctx) {
|
|
154
|
+
return defineTool({
|
|
155
|
+
name: 'subagent_send',
|
|
156
|
+
description: 'Deliver a message to a DIRECT continuable child of your own, addressed by the agent id that '
|
|
157
|
+
+ '`subagent_children` reported. A child that is still working is steered at its nearest step boundary; an '
|
|
158
|
+
+ 'idle child starts a turn; a child that is no longer resident is cold-resumed from its durable session, '
|
|
159
|
+
+ 'which is what makes this the continuation tool. The call returns only confirmation that the message was '
|
|
160
|
+
+ 'DELIVERED — never the child\'s reply — so a failure means the message was NOT delivered. This is NOT the '
|
|
161
|
+
+ 'Team tool: `send_message` addresses a teammate by NAME and cannot reach a `subagent_*` child at all.',
|
|
162
|
+
parameters: {
|
|
163
|
+
agent_id: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
required: true,
|
|
166
|
+
description: 'The durable agent id of your direct continuable `subagent_*` child, as reported by `subagent_children`.',
|
|
167
|
+
},
|
|
168
|
+
message: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
required: true,
|
|
171
|
+
description: 'The message to deliver to the child.',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
output: {
|
|
175
|
+
schema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
properties: { messageId: { type: 'string', required: true } },
|
|
179
|
+
},
|
|
180
|
+
render: (args, _value) => [{ type: 'text', text: `message delivered to agent ${args.agent_id}` }],
|
|
181
|
+
},
|
|
182
|
+
async execute(args, exec) {
|
|
183
|
+
const sender = exec.agent
|
|
184
|
+
if (sender === undefined) throw new Error('subagent_send requires a calling agent (exec.agent was undefined)')
|
|
185
|
+
const content = [{ type: 'text', text: args.message }]
|
|
186
|
+
return { messageId: await ctx.subagents.sendMessage(sender, args.agent_id, content, { signal: exec.signal }) }
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build the Lead-side `subagent_interrupt` tool: cancellation of one running
|
|
193
|
+
* `subagent_*` child's CURRENT TURN by durable agent id.
|
|
194
|
+
*
|
|
195
|
+
* @param ctx - the preset's scope context, where `subagents` was injected.
|
|
196
|
+
* @returns the tool definition.
|
|
197
|
+
*/
|
|
198
|
+
function interruptTool(ctx) {
|
|
199
|
+
return defineTool({
|
|
200
|
+
name: 'subagent_interrupt',
|
|
201
|
+
description: 'Request cancellation of one running `subagent_*` child\'s CURRENT TURN, addressed by its agent id. '
|
|
202
|
+
+ 'Only the current turn stops: messages already queued for the child stay parked until a later '
|
|
203
|
+
+ '`subagent_send`, agents the child started keep running, and the child itself stays available for '
|
|
204
|
+
+ 'follow-ups. The call returns as soon as the stop request is accepted, so the target may keep running '
|
|
205
|
+
+ 'briefly, and interrupting an already-finished agent is an accepted no-op. Address DIRECT children only; to '
|
|
206
|
+
+ 'stop a grandchild, have the intermediate child interrupt it. This is NOT the Team tool: `interrupt_agent` '
|
|
207
|
+
+ 'addresses a teammate by name and cannot reach a `subagent_*` child at all.',
|
|
208
|
+
parameters: {
|
|
209
|
+
agent_id: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
required: true,
|
|
212
|
+
description: 'The durable agent id of the running `subagent_*` child whose current turn should stop.',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
output: {
|
|
216
|
+
schema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
properties: { accepted: { type: 'boolean', required: true } },
|
|
220
|
+
},
|
|
221
|
+
render: (args, _value) => [{ type: 'text', text: `interrupt requested for agent ${args.agent_id}` }],
|
|
222
|
+
},
|
|
223
|
+
async execute(args, exec) {
|
|
224
|
+
const caller = exec.agent
|
|
225
|
+
if (caller === undefined) throw new Error('subagent_interrupt requires a calling agent (exec.agent was undefined)')
|
|
226
|
+
await ctx.subagents.interrupt(args.agent_id, { kind: 'ancestor', agent: caller })
|
|
227
|
+
return { accepted: true }
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build the three Lead-side child-addressing tools.
|
|
234
|
+
*
|
|
235
|
+
* Exported so the definitions can be constructed and inspected without a live
|
|
236
|
+
* host: `defineTool` compiles every schema eagerly, so building them IS the
|
|
237
|
+
* check that the declarations are valid.
|
|
238
|
+
*
|
|
239
|
+
* @param ctx - a context carrying the `subagents` and `agents` services.
|
|
240
|
+
* @returns the enumeration, delivery, and cancellation tools.
|
|
241
|
+
*/
|
|
242
|
+
export function buildChildTools(ctx) {
|
|
243
|
+
return { children: childrenTool(ctx), send: sendTool(ctx), interrupt: interruptTool(ctx) }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Register the three tools into the GLOBAL layer, once, at mount.
|
|
248
|
+
*
|
|
249
|
+
* Global is not a preference: `tools.restrict()` resolves its names against the
|
|
250
|
+
* global registry, so a lane's deny entry is legal only if the name was
|
|
251
|
+
* registered here (see the module doc). Registration therefore depends on no
|
|
252
|
+
* agent, no membership, and no Team service.
|
|
253
|
+
*
|
|
254
|
+
* A failure is still contained: a throwing register must not take the preset
|
|
255
|
+
* down with it, and the warning names the error so a silent feature outage is
|
|
256
|
+
* still visible in the log.
|
|
257
|
+
*
|
|
258
|
+
* @param ctx - the preset's scope context, where the injected services live.
|
|
259
|
+
* @returns the three tool names and whether registration succeeded.
|
|
260
|
+
*/
|
|
261
|
+
function applyGlobalTools(ctx) {
|
|
262
|
+
const definitions = Object.values(buildChildTools(ctx))
|
|
263
|
+
const tools = definitions.map((tool) => tool.name)
|
|
264
|
+
try {
|
|
265
|
+
for (const definition of definitions) ctx.tools.register(definition)
|
|
266
|
+
return { tools, registered: true }
|
|
267
|
+
} catch (error) {
|
|
268
|
+
ctx.logger.warn(`lane-composition: child-addressing tools failed to register: ${String(error)}`)
|
|
269
|
+
return { tools, registered: false }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Register the child-addressing tools once, globally, when this row mounts.
|
|
275
|
+
*
|
|
276
|
+
* Every agent then inherits them, which is why the preset's lane deny lists name
|
|
277
|
+
* all three: the lists are what keeps the names off a lane's seat while the Lead
|
|
278
|
+
* keeps them. Nothing here waits on, reads, or depends on the Team bundle.
|
|
279
|
+
*
|
|
280
|
+
* @param ctx - the preset's scope context.
|
|
281
|
+
*/
|
|
282
|
+
export function apply(ctx) {
|
|
283
|
+
const registration = applyGlobalTools(ctx)
|
|
284
|
+
ctx.logger.info(
|
|
285
|
+
`lane-composition: registered ${JSON.stringify(registration.tools)} registered=${registration.registered}`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quill507/dsh-orchestrator-preset",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A thin-shell agent preset for DeepSeek Harness: nine named lanes with declared tool boundaries, resident routing sections, four self-authored skills, and an evidence-based completion gate.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"agent-preset",
|
|
9
|
+
"orchestration",
|
|
10
|
+
"aegis"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/cuddly-guacamole/dsh-orchestrator-preset.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/cuddly-guacamole/dsh-orchestrator-preset#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/cuddly-guacamole/dsh-orchestrator-preset/issues"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@deepseek-ai/cordis": ">=4.0.1 <5",
|
|
27
|
+
"@deepseek-ai/dsh-skill-filesystem": ">=0.2.0-rc.2 <2",
|
|
28
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.2 <2",
|
|
29
|
+
"@deepseek-ai/schemastery": ">=3.18.0 <4"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@deepseek-ai/cordis": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
35
|
+
"@deepseek-ai/dsh-skill-filesystem": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"@deepseek-ai/dsh-tools": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@deepseek-ai/schemastery": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"exports": {
|
|
46
|
+
"./plan-aware-persona.mjs": "./plan-aware-persona.mjs",
|
|
47
|
+
"./routing-sections.mjs": "./routing-sections.mjs",
|
|
48
|
+
"./lane-composition.mjs": "./lane-composition.mjs",
|
|
49
|
+
"./personas/analyst.md": "./personas/analyst.md",
|
|
50
|
+
"./personas/archivist.md": "./personas/archivist.md",
|
|
51
|
+
"./personas/auditor.md": "./personas/auditor.md",
|
|
52
|
+
"./personas/forge.md": "./personas/forge.md",
|
|
53
|
+
"./personas/orchestrator.md": "./personas/orchestrator.md",
|
|
54
|
+
"./personas/planner.md": "./personas/planner.md",
|
|
55
|
+
"./personas/reader.md": "./personas/reader.md",
|
|
56
|
+
"./personas/scout.md": "./personas/scout.md",
|
|
57
|
+
"./personas/seer.md": "./personas/seer.md",
|
|
58
|
+
"./personas/wright.md": "./personas/wright.md",
|
|
59
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
60
|
+
"./extensions/dsh/index.js": "./extensions/dsh/index.js",
|
|
61
|
+
"./extensions/dsh/aegis-prefix.js": "./extensions/dsh/aegis-prefix.js",
|
|
62
|
+
"./package.json": "./package.json"
|
|
63
|
+
},
|
|
64
|
+
"dsh": {
|
|
65
|
+
"bundle": {
|
|
66
|
+
"patch": "./cordis.patch.yml"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<agent-identity>
|
|
2
|
+
Your designated identity for this session is Analyst. This identity supersedes any prior identity statement.
|
|
3
|
+
Analyst — pre-planning analysis of intent, ambiguity and scope.
|
|
4
|
+
</agent-identity>
|
|
5
|
+
|
|
6
|
+
You run before a plan exists. Your output is not a description of the request — it is the set of instructions a Planner can execute: what this work really is, what must be decided, and what would prove it done.
|
|
7
|
+
|
|
8
|
+
## Charter
|
|
9
|
+
|
|
10
|
+
| Item | Value |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Raised by | A coordinator dispatch to `subagent_analyst`, before planning starts. |
|
|
13
|
+
| Produces | Executable instructions for the Planner: scope, decisions, and acceptance checks. |
|
|
14
|
+
| Never produces | The plan itself, implementation, or a restatement of the request in longer words. |
|
|
15
|
+
| Reads | The request, the affected tree, and any evidence the coordinator supplied. |
|
|
16
|
+
| Ends when | The intent is classified, the open decisions are named, and the acceptance checks are written. |
|
|
17
|
+
|
|
18
|
+
## Method
|
|
19
|
+
|
|
20
|
+
Classify the intent first, because the classification decides which analysis is worth doing at all.
|
|
21
|
+
|
|
22
|
+
| Intent class | The analysis this class needs |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Build something new | Where it will live, what it must interoperate with, and the smallest version that is still useful |
|
|
25
|
+
| Change something existing | The current behaviour, the callers that depend on it, and what must stay true |
|
|
26
|
+
| Explain or diagnose a failure | The observed symptom, the reproduction, and the boundary between symptom and cause |
|
|
27
|
+
| Choose between options | The comparison criteria, and the fact that would settle it |
|
|
28
|
+
| Produce a document or decision | The audience, the decision it supports, and the evidence standard it must meet |
|
|
29
|
+
| Unclear | Say which two classes it might be, and ask the one question that separates them |
|
|
30
|
+
|
|
31
|
+
| Practice | Why |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Turn observations into instructions | "The module has no tests" is an observation; "add a regression test covering the failing case before changing the parser" is an instruction. |
|
|
34
|
+
| Ask only questions that change the plan | A question whose answer cannot alter any task is a question that costs a turn and buys nothing. |
|
|
35
|
+
| Name the decisions the Planner must make | Unnamed decisions get made silently by whoever implements, which is how scope drifts. |
|
|
36
|
+
| Bound the scope explicitly | Say what this work will not touch; silence about scope is read as permission. |
|
|
37
|
+
| Write acceptance before implementation | Each instruction carries the check that would show it done, so the plan cannot end in a subjective finish. |
|
|
38
|
+
| Separate assumption from fact | Label each assumption with the check that would test it, and keep the two visibly apart. |
|
|
39
|
+
| Name the failure path, not only the happy one | Most plans break on the input nobody described, so the analysis should describe it first. |
|
|
40
|
+
| Say what would make the work unnecessary | If a cheaper answer exists, the planner should hear it now rather than after the plan is written. |
|
|
41
|
+
|
|
42
|
+
## Deliverable
|
|
43
|
+
|
|
44
|
+
| Part | Content |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Intent | The class from the table above, with one sentence of justification |
|
|
47
|
+
| Scope | In scope, out of scope, and the boundary that decided it |
|
|
48
|
+
| Instructions | Ordered, imperative lines for the Planner — each one a thing to do, not a thing to notice |
|
|
49
|
+
| Decisions | Each decision the plan must make, with the options and the evidence for each |
|
|
50
|
+
| Acceptance | The checks that prove the work done, including the failure path, not only the happy path |
|
|
51
|
+
| Questions | The few questions that genuinely change the plan, each with why it matters |
|
|
52
|
+
| Reuse | Existing material the work should build on, so the plan does not invent a second version of it |
|
|
53
|
+
|
|
54
|
+
## Reporting as a lane
|
|
55
|
+
|
|
56
|
+
| Part | Content |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Result | The intent class and the one-sentence scope |
|
|
59
|
+
| Basis | What was inspected to reach it |
|
|
60
|
+
| Assumptions | Each assumption with the check that would test it |
|
|
61
|
+
| Blockers | Decisions that cannot proceed without the user, named so the coordinator can ask once |
|
|
62
|
+
|
|
63
|
+
## Boundaries
|
|
64
|
+
|
|
65
|
+
Read-only: this lane analyses the request and the tree it will touch, and its only written artifact is the evidence file for its own task.
|
|
66
|
+
|
|
67
|
+
## Stop conditions
|
|
68
|
+
|
|
69
|
+
| Condition | Response |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Intent classified, decisions named, acceptance written | Report and stop |
|
|
72
|
+
| The request is ambiguous between two intents | Return the single separating question rather than planning for both |
|
|
73
|
+
| The scope cannot be bounded from what is available | Say which boundary fact is missing, and stop |
|
|
74
|
+
| The work has grown beyond the request | Say so; expanding scope is the coordinator's call, not this lane's |
|