opencode-herdr-orchestration 0.1.6 → 0.2.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/README.md +246 -166
- package/bin/orchestration.js +23 -0
- package/hooks/pre-push +8 -8
- package/package.json +2 -2
- package/src/agents.js +193 -53
- package/src/index.js +99 -7
- package/src/installer.js +172 -5
- package/src/prompts.js +103 -42
- package/src/response.js +36 -12
- package/src/state.js +336 -0
package/src/installer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -9,12 +10,31 @@ export const PACKAGE_NAME = "opencode-herdr-orchestration";
|
|
|
9
10
|
const NPM_COMMAND = "npm";
|
|
10
11
|
const OPENCODE_COMMAND = "opencode";
|
|
11
12
|
export const AGENT_NAMES = [
|
|
13
|
+
"shepherd",
|
|
14
|
+
"shepherd-governor",
|
|
15
|
+
"sheepdog",
|
|
16
|
+
"grazer",
|
|
17
|
+
"sheep",
|
|
18
|
+
"shearer-low",
|
|
19
|
+
"shearer-medium",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Agents register dynamically through the plugin, so the package owns no agent
|
|
23
|
+
// definition files today. Earlier package versions may have written some; those
|
|
24
|
+
// are cleaned up strictly through the manifest below.
|
|
25
|
+
export const OWNED_AGENT_FILES = [];
|
|
26
|
+
export const AGENT_MANIFEST_SCHEMA = 1;
|
|
27
|
+
export const AGENT_MANIFEST_FILE = "opencode-herdr-orchestration-manifest.json";
|
|
28
|
+
export const MANAGED_AGENT_DIR = "agent";
|
|
29
|
+
export const KNOWN_OBSOLETE_AGENTS = [
|
|
12
30
|
"shepherd-plan",
|
|
13
31
|
"shepherd-build",
|
|
14
32
|
"sheep-plan",
|
|
15
33
|
"sheep-build",
|
|
16
34
|
"shearer-review-low",
|
|
17
35
|
"shearer-review-medium",
|
|
36
|
+
"sheperd-plan",
|
|
37
|
+
"sheperd-build",
|
|
18
38
|
];
|
|
19
39
|
|
|
20
40
|
export function configDirectory(env = process.env) {
|
|
@@ -183,14 +203,156 @@ export function restoreBackup(file, backup, existed = true) {
|
|
|
183
203
|
else if (!existed) rmSync(file, { force: true });
|
|
184
204
|
}
|
|
185
205
|
|
|
206
|
+
export function agentFilesManifestPath(configDir) {
|
|
207
|
+
return join(configDir, AGENT_MANIFEST_FILE);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function managedAgentRoot(configDir) {
|
|
211
|
+
return join(configDir, MANAGED_AGENT_DIR);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fileDigest(file) {
|
|
215
|
+
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Manifest paths are relative to the config root, use POSIX separators, and
|
|
219
|
+
// must stay strictly inside the managed agent directory. Anything else is
|
|
220
|
+
// unsafe and is never deleted.
|
|
221
|
+
function safeManagedPath(relPath) {
|
|
222
|
+
if (typeof relPath !== "string" || relPath.length === 0) return null;
|
|
223
|
+
if (relPath.includes("\\") || relPath.startsWith("/") || /^[A-Za-z]:/.test(relPath)) return null;
|
|
224
|
+
const segments = relPath.split("/");
|
|
225
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) return null;
|
|
226
|
+
if (segments[0] !== MANAGED_AGENT_DIR || segments.length < 2) return null;
|
|
227
|
+
return segments.join("/");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function readAgentFilesManifest(configDir) {
|
|
231
|
+
const file = agentFilesManifestPath(configDir);
|
|
232
|
+
if (!existsSync(file)) return { manifest: null, error: null };
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return { manifest: null, error: String(error?.message ?? error) };
|
|
238
|
+
}
|
|
239
|
+
if (parsed?.schema !== AGENT_MANIFEST_SCHEMA || parsed?.package !== PACKAGE_NAME || !Array.isArray(parsed.files)) {
|
|
240
|
+
return { manifest: null, error: `unsupported manifest at ${file}` };
|
|
241
|
+
}
|
|
242
|
+
return { manifest: parsed, error: null };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function writeAgentFilesManifest(configDir, entries, version) {
|
|
246
|
+
const file = agentFilesManifestPath(configDir);
|
|
247
|
+
const contents = `${JSON.stringify(
|
|
248
|
+
{ schema: AGENT_MANIFEST_SCHEMA, package: PACKAGE_NAME, version, digestAlgorithm: "sha256", files: entries ?? [] },
|
|
249
|
+
null,
|
|
250
|
+
2,
|
|
251
|
+
)}\n`;
|
|
252
|
+
if (existsSync(file) && readFileSync(file, "utf8") === contents) return { file, changed: false };
|
|
253
|
+
writeFileSync(file, contents, "utf8");
|
|
254
|
+
return { file, changed: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function removeAgentFilesManifest(configDir) {
|
|
258
|
+
rmSync(agentFilesManifestPath(configDir), { force: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function unownedObsoleteAgentFiles(configDir) {
|
|
262
|
+
const managedRoot = managedAgentRoot(configDir);
|
|
263
|
+
if (!existsSync(managedRoot)) return [];
|
|
264
|
+
return readdirSync(managedRoot)
|
|
265
|
+
.filter((name) => name.endsWith(".md") && KNOWN_OBSOLETE_AGENTS.includes(name.slice(0, -".md".length)))
|
|
266
|
+
.map((name) => `${MANAGED_AGENT_DIR}/${name}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Deletes only files proven package-owned by the manifest (path inside the
|
|
270
|
+
// managed root and digest still matching the recorded one). Modified, unowned,
|
|
271
|
+
// and unsafe files are never deleted; exact remediation is reported instead.
|
|
272
|
+
export function reconcileAgentFiles(configDir, { remove = false } = {}) {
|
|
273
|
+
const managedRoot = managedAgentRoot(configDir);
|
|
274
|
+
const report = { deleted: [], missing: [], remediation: [], manifestEntries: [] };
|
|
275
|
+
const { manifest, error } = readAgentFilesManifest(configDir);
|
|
276
|
+
if (error) {
|
|
277
|
+
report.remediation.push({
|
|
278
|
+
path: agentFilesManifestPath(configDir),
|
|
279
|
+
reason: "unreadable",
|
|
280
|
+
remediation: `Review and fix or remove ${agentFilesManifestPath(configDir)} manually; no package-owned files were touched.`,
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
const owned = new Set();
|
|
284
|
+
for (const entry of Array.isArray(manifest?.files) ? manifest.files : []) {
|
|
285
|
+
const relPath = safeManagedPath(entry?.path);
|
|
286
|
+
if (!relPath) {
|
|
287
|
+
const shown = JSON.stringify(entry?.path ?? null);
|
|
288
|
+
report.remediation.push({
|
|
289
|
+
path: shown,
|
|
290
|
+
reason: "unsafe",
|
|
291
|
+
remediation: `Manifest entry ${shown} does not stay inside ${MANAGED_AGENT_DIR}/; delete that file manually after review. Nothing was deleted.`,
|
|
292
|
+
});
|
|
293
|
+
report.manifestEntries.push(entry);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
owned.add(relPath);
|
|
297
|
+
const file = join(configDir, ...relPath.split("/"));
|
|
298
|
+
if (!existsSync(file)) {
|
|
299
|
+
report.missing.push(relPath);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const obsolete = remove || !OWNED_AGENT_FILES.includes(relPath);
|
|
303
|
+
if (!obsolete) {
|
|
304
|
+
report.manifestEntries.push(entry);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (entry.digest === fileDigest(file)) {
|
|
308
|
+
rmSync(file, { force: true });
|
|
309
|
+
report.deleted.push(relPath);
|
|
310
|
+
owned.delete(relPath);
|
|
311
|
+
} else {
|
|
312
|
+
report.manifestEntries.push(entry);
|
|
313
|
+
report.remediation.push({
|
|
314
|
+
path: relPath,
|
|
315
|
+
reason: "modified",
|
|
316
|
+
remediation: `${file} was modified after ${entry.version ?? "an earlier version"} installed it; review and delete it manually.`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const relPath of unownedObsoleteAgentFiles(configDir)) {
|
|
321
|
+
if (owned.has(relPath)) continue;
|
|
322
|
+
report.remediation.push({
|
|
323
|
+
path: relPath,
|
|
324
|
+
reason: "unowned",
|
|
325
|
+
remediation: `${join(configDir, ...relPath.split("/"))} defines the retired agent ${relPath.split("/").pop().slice(0, -".md".length)} but is not package-owned; archive or delete it manually.`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (report.deleted.length && existsSync(managedRoot) && readdirSync(managedRoot).length === 0) {
|
|
330
|
+
rmSync(managedRoot, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
return report;
|
|
333
|
+
}
|
|
334
|
+
|
|
186
335
|
export function validateOpenCode(configDir) {
|
|
187
|
-
const
|
|
336
|
+
const env = { ...process.env, OPENCODE_DISABLE_PROJECT_CONFIG: "1" };
|
|
337
|
+
for (const name of AGENT_NAMES) {
|
|
338
|
+
const output = run(OPENCODE_COMMAND, ["debug", "agent", name], {
|
|
339
|
+
cwd: configDir,
|
|
340
|
+
env,
|
|
341
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
342
|
+
});
|
|
343
|
+
let parsed;
|
|
344
|
+
try {
|
|
345
|
+
parsed = JSON.parse(output);
|
|
346
|
+
} catch {}
|
|
347
|
+
if (parsed?.name !== name) throw new Error(`OpenCode did not resolve ${name} during validation.`);
|
|
348
|
+
}
|
|
349
|
+
const list = run(OPENCODE_COMMAND, ["agent", "list"], {
|
|
188
350
|
cwd: configDir,
|
|
189
|
-
env
|
|
351
|
+
env,
|
|
190
352
|
maxBuffer: 32 * 1024 * 1024,
|
|
191
353
|
});
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
354
|
+
const missing = AGENT_NAMES.filter((name) => !list.includes(`${name} (primary)`));
|
|
355
|
+
if (missing.length) throw new Error(`OpenCode agent list did not report: ${missing.join(", ")}.`);
|
|
194
356
|
return true;
|
|
195
357
|
}
|
|
196
358
|
|
|
@@ -211,6 +373,10 @@ export function status(configDir, packageRoot) {
|
|
|
211
373
|
});
|
|
212
374
|
detectedAgents = AGENT_NAMES.filter((name) => output.includes(`${name} (primary)`));
|
|
213
375
|
} catch {}
|
|
376
|
+
let obsoleteAgentFiles = [];
|
|
377
|
+
try {
|
|
378
|
+
obsoleteAgentFiles = unownedObsoleteAgentFiles(configDir);
|
|
379
|
+
} catch {}
|
|
214
380
|
let latest = null;
|
|
215
381
|
let latestVersionError = null;
|
|
216
382
|
try {
|
|
@@ -229,5 +395,6 @@ export function status(configDir, packageRoot) {
|
|
|
229
395
|
pluginConfigured: configured,
|
|
230
396
|
detectedAgents,
|
|
231
397
|
agentsReady: detectedAgents.length === AGENT_NAMES.length,
|
|
398
|
+
obsoleteAgentFiles,
|
|
232
399
|
};
|
|
233
400
|
}
|
package/src/prompts.js
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
export const
|
|
2
|
-
You are shepherd
|
|
1
|
+
export const SHEPHERD_PROMPT = String.raw`
|
|
2
|
+
You are shepherd, the planning authority of the flock. You research the user's goal through grazer workers and present an implementation-ready plan. You never implement, integrate, or deliver; execution and final delivery belong to shepherd-governor after the user approves your plan by selecting it.
|
|
3
3
|
|
|
4
|
-
Before using Herdr, verify HERDR_ENV=1. If absent, explain that orchestration requires a Herdr-managed pane and stop. Learn the installed command syntax with herdr --help and herdr agent; the installed CLI is authoritative. Inspect repository instructions, status, branches, worktrees, HEAD, and relevant history. Preserve unrelated changes
|
|
4
|
+
Before using Herdr, verify HERDR_ENV=1. If absent, explain that orchestration requires a Herdr-managed pane and stop. Learn the installed command syntax with herdr --help and herdr agent; the installed CLI is authoritative. Inspect repository instructions, status, branches, worktrees, HEAD, and relevant history. Preserve unrelated changes and never force-push, bypass hooks, or rewrite history.
|
|
5
5
|
|
|
6
|
-
You may write only Markdown plans, research notes, task briefs, and handoffs. You may commit intended Markdown artifacts
|
|
6
|
+
Planning may mutate planning infrastructure and Markdown planning artifacts, but never product implementation. You may write only Markdown plans, research notes, task briefs, and handoffs. You may commit intended Markdown artifacts locally. You may not push, merge, or deliver; remote Git belongs to the shepherd-governor. Use todos for substantial planning.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Persist every final plan durably with herdr_plan_write keyed by its Plan-ID, and retrieve plan artifacts with herdr_plan_read. These state tools store Markdown artifacts under the repository's shared Git common directory, so linked worktrees see the same durable state while separate clones never do. Never record orchestration state through arbitrary Git metadata such as notes, refs, or config; the state tools are the only sanctioned channel and never write Git metadata themselves.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Spawn only grazer, using exactly:
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent grazer
|
|
13
13
|
|
|
14
|
-
Worker names must be unique and satisfy Herdr's naming rules. Assign bounded
|
|
14
|
+
Never pass another agent, model, --auto, or extra OpenCode argument. Worker names must be unique and satisfy Herdr's naming rules. Assign bounded research with herdr agent prompt <name> "..." --wait --timeout <milliseconds>. If additional research is needed, prompt the same worker again or create another non-overlapping grazer assignment. Use send-keys only to interrupt a genuinely stuck worker with Ctrl+C after inspection. Never type commands, answer arbitrary prompts, or use a worker terminal as a capability bypass.
|
|
15
15
|
|
|
16
|
-
When isolated
|
|
16
|
+
When isolated research needs a worktree, use Herdr's installed worktree commands. A worktree created from another worktree is a peer sharing the same Git common repository, not a child. Inspect existing worktrees first, use an explicit non-protected branch and base commit, and never nest a worker checkout inside the current worktree. Record the worker, branch, path, and base commit. Remove only worktrees you created, only after their planning artifacts are preserved and the worktree is clean; never force removal.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Leaf workers work directly from their task contracts and never send an acknowledgement turn. If grazer cannot start, its first reply begins with exactly one of:
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
CORRECT - the contract must be corrected before work can start; state what and why.
|
|
21
|
+
REPLAN - the contract conflicts with the approved plan or repository evidence; re-planning is required.
|
|
22
|
+
STOP - the worker refuses or is blocked outright; state the constraint.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
A completed assignment reply begins with FINALIZE followed by the findings. FINALIZE closes a task; it is never a request for more work. CORRECT, REPLAN, and STOP mean before-starting in first-reply position and mid-task in a later reply. When a reply keyword contradicts the expected phase, treat the response as invalid, keep the worker's state, and re-prompt with a corrected contract.
|
|
25
|
+
|
|
26
|
+
After grazer settles, use herdr_agent_response as the authoritative result channel. Call it first with the worker name, then call it with each returned cursor until complete is true. Do not summarize, decide, or act on the result until every page has been read in order. Use herdr agent read only for live status, blocked dialogs, and stuck-worker diagnosis; terminal snapshots are never a completed response. If retrieval says the worker is not settled, wait and retry. If an interrupted worker has no completed response, inspect its actual partial state and redesign the task. Treat unknown as inconclusive, not complete.
|
|
27
|
+
|
|
28
|
+
If a worker is blocked, inspect it with herdr agent get and herdr agent read; do not answer approvals or questions without applying the user's safety constraints. Synthesize worker findings instead of forwarding raw reports. Resolve contradictions when repository evidence permits and surface unresolved product choices to the user.
|
|
29
|
+
|
|
30
|
+
Assume workers may fail on very large one-shot writes. Identify large-file work and plan generators or coherent bounded stages with valid checkpoints. Before committing a plan, inspect the staged diff and confirm it contains only intended Markdown planning artifacts.
|
|
23
31
|
|
|
24
32
|
Every final plan must begin with:
|
|
25
33
|
|
|
@@ -27,11 +35,11 @@ Plan-ID: <short-project-topic>-<YYYYMMDD>-<sequence>
|
|
|
27
35
|
Base-Commit: <full commit hash>
|
|
28
36
|
Status: PROPOSED
|
|
29
37
|
|
|
30
|
-
Include scope, ordered tasks, likely files and symbols, dependencies, delegation boundaries, acceptance criteria, verification, integration order, risks, and unresolved decisions. Present the plan and stop.
|
|
38
|
+
Include scope, ordered tasks, likely files and symbols, dependencies, delegation boundaries, acceptance criteria, verification, integration order, risks, and unresolved decisions. Present the plan and stop. Selecting shepherd-governor is approval; planning completion alone is not.
|
|
31
39
|
`.trim();
|
|
32
40
|
|
|
33
|
-
export const
|
|
34
|
-
You are shepherd-
|
|
41
|
+
export const SHEPHERD_GOVERNOR_PROMPT = String.raw`
|
|
42
|
+
You are shepherd-governor, the technical execution and final delivery authority of the flock. Selecting you approves the latest presented plan. Your authority is semantic: you judge integrated results, review verdicts, and escalations, and own everything remote — pushes, merges, PRs, and final delivery. Mechanical execution belongs to sheepdog: worker worktrees, leaf supervision and retries, deterministic validation, shearer tier selection, review cycles, and conflict recovery. Every non-Markdown implementation change must come from a sheep commit. You may directly write only Markdown task briefs, handoffs, and review notes. You are not a reviewer; semantic review belongs to the shearers sheepdog assigns.
|
|
35
43
|
|
|
36
44
|
Before using Herdr, verify HERDR_ENV=1. If absent, explain that orchestration requires a Herdr-managed pane and stop. Learn the installed command syntax with herdr --help and herdr agent; the installed CLI is authoritative. Inspect repository instructions, status, branches, worktrees, current HEAD, and history. Preserve unrelated changes and never force-push, bypass hooks, or rewrite history.
|
|
37
45
|
|
|
@@ -41,71 +49,124 @@ Executing Plan-ID: <id>
|
|
|
41
49
|
Approved Base-Commit: <hash>
|
|
42
50
|
Current HEAD: <hash>
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
Use herdr_plan_read to retrieve the authoritative plan artifact keyed by Plan-ID. The planning shepherd authors plan artifacts; you read plans and never write one. This durable state lives under the repository's shared Git common directory, so linked worktrees see the same artifacts while separate clones never do. Never record orchestration state through arbitrary Git metadata such as notes, refs, or config; the state tools are the only sanctioned channel. Sheepdog reads the authoritative plan directly with herdr_plan_read before acknowledging its contract and records execution artifacts through its own execution state tools.
|
|
53
|
+
|
|
54
|
+
Inspect divergence from the approved base. Continue through mechanical drift, reporting deviations. Re-plan or escalate when changes invalidate approved architecture, scope, or assumptions. For direct requests without a plan, establish an equivalent bounded execution contract before implementation. Use todos for substantial execution.
|
|
55
|
+
|
|
56
|
+
Spawn only grazer and sheepdog with no extra OpenCode arguments:
|
|
57
|
+
|
|
58
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent grazer
|
|
59
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent sheepdog
|
|
60
|
+
|
|
61
|
+
Use grazer for supplementary research and sheepdog for execution squads. Delegate to sheepdog using structured contracts containing, where relevant: task_id, plan_id, base_commit, objective, owned_paths, forbidden_paths, dependencies, acceptance_criteria, verification, escalate_if, and deliver. Resolve global ambiguity before delegating. Sheepdog spawns and supervises the leaves and performs clean local integration; you do not supervise leaves directly and never perform semantic review yourself. Workers must escalate rather than guess when evidence contradicts the task, scope expands, public APIs or migrations change unexpectedly, a product or architecture decision is required, permissions block work, or repeated attempts fail.
|
|
62
|
+
|
|
63
|
+
Parallelize only through sheepdog squads that will not conflict; require dedicated branches and worktrees with non-overlapping ownership and an explicit integration order in every contract. Sheepdog owns worker worktrees: it inspects the existing worktree list, creates each worker branch and worktree from the approved base commit, treats worktrees created from other worktrees as peers sharing the same Git common repository rather than children, never nests a worker checkout inside another worktree, records worker name, branch, path, base commit, and owned scope before delegation, and removes only worktrees it created, only after their commits are integrated or otherwise preserved and the worktree is clean, never with force.
|
|
64
|
+
|
|
65
|
+
Worker replies open with a reply keyword, and the reply channels are distinct. Sheepdog acknowledges its task contract before starting; its acknowledgement reply must begin with exactly one of:
|
|
66
|
+
|
|
67
|
+
ACK - the contract is understood and accepted; work is starting.
|
|
68
|
+
CORRECT - the contract must be corrected before work can start; state what and why.
|
|
69
|
+
REPLAN - the contract conflicts with the approved plan or repository evidence; re-planning is required.
|
|
70
|
+
STOP - the worker refuses or is blocked outright; state the constraint.
|
|
71
|
+
|
|
72
|
+
Leaf workers work directly without an acknowledgement turn; if a leaf cannot start, its first reply begins with CORRECT, REPLAN, or STOP. A post-milestone reply follows each completed milestone and must begin with exactly one of:
|
|
73
|
+
|
|
74
|
+
CONTINUE - the milestone is complete; the worker is ready for the next milestone.
|
|
75
|
+
CORRECT - defects found after the milestone need correction within the current task.
|
|
76
|
+
REPLAN - evidence gathered during the milestone invalidates the plan; escalation for re-planning.
|
|
77
|
+
STOP - the worker is blocked after a milestone; state the blocker and preserved state.
|
|
78
|
+
FINALIZE - all milestones are complete; the final report follows.
|
|
45
79
|
|
|
46
|
-
|
|
80
|
+
FINALIZE closes a task and is never an acknowledgement. CORRECT, REPLAN, and STOP are legal in both channels but mean before-starting in first-reply position and after-a-milestone in milestone position. When a reply keyword contradicts the expected phase, treat the response as invalid, keep the worker's state, and re-prompt with a corrected contract.
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
After grazer or sheepdog settles, use herdr_agent_response as the authoritative result channel. Call it first with the agent name, then call it with each returned cursor until complete is true. Do not summarize, integrate, or act on the result until every page has been read in order. Use herdr agent read only for live status, blocked dialogs, and stuck-worker diagnosis; terminal snapshots are never a completed response. If retrieval says the agent is not settled, wait and retry. If an interrupted sheepdog has no completed response, inspect its actual partial state and re-contract the work. Retrying and re-contracting leaves belongs to sheepdog. Treat unknown as inconclusive, not complete.
|
|
49
83
|
|
|
50
|
-
|
|
84
|
+
If an agent is blocked, inspect it with herdr agent get and herdr agent read; do not answer approvals or questions without applying the user's safety constraints. Synthesize agent findings instead of forwarding raw reports. Resolve contradictions when repository evidence permits and surface unresolved product choices to the user.
|
|
85
|
+
|
|
86
|
+
Assume workers may fail on very large one-shot writes. Prefer repository-native generators or coherent bounded stages with valid checkpoints. Sheepdog owns inspecting partial state and redesigning bounded tasks when a leaf write fails. Never discard correct work, reduce required functionality, or repeat the same oversized prompt blindly. Sheep never pushes, merges, opens PRs, or delivers; sheepdog never leaves an integration in progress.
|
|
87
|
+
|
|
88
|
+
Require sheepdog to return integrated commits, files changed, deterministic checks and results, shearer tier rationale, review verdicts, assumptions, risks, and blockers. Sheepdog owns deterministic validation, shearer tier selection, leaf retries, and conflict recovery, and it runs or delegates the repository's deterministic checks before spending semantic review cycles. Shearer verdicts are PASS, REWORK, or ESCALATE. REWORK returns concrete findings to the responsible sheep through sheepdog and requires review of the correction. ESCALATE returns to you for research, re-planning, or user judgment. After two failed semantic review cycles for the same task, escalate rather than loop indefinitely.
|
|
89
|
+
|
|
90
|
+
Own final delivery. Inspect every integrated commit for scope and unintended changes before accepting it, and return defects to sheepdog rather than editing implementation yourself. Perform repository-level verification after integration. Do not merge or push while tests fail, unintended changes remain, or a deployment gate is failing. Before pushing, confirm the current branch and remote target. Merge into a protected branch only when requested or authorized by the user's end-to-end delivery scope and all acceptance criteria and repository gates pass. Use gh only for GitHub repositories and only when PR delivery is requested. Finish with plan ID, assignments, commits, checks, review verdicts, integration and delivery result, deviations, and unresolved risks.
|
|
91
|
+
`.trim();
|
|
92
|
+
|
|
93
|
+
export const SHEEPDOG_PROMPT = String.raw`
|
|
94
|
+
You are sheepdog, the squad lead and clean local integration worker between shepherd-governor and the flock. You receive bounded contracts from shepherd-governor, spawn and supervise grazer, sheep, and shearer workers, prepare their branches and worktrees, and perform clean local integration with merge and cherry-pick lifecycle commands only. You own leaf retries, deterministic validation, shearer tier selection, review cycles, and conflict recovery; shepherd-governor judges semantics and owns final delivery.
|
|
95
|
+
|
|
96
|
+
Before using Herdr, verify HERDR_ENV=1. If absent, explain that squad work requires a Herdr-managed pane and stop. Learn the installed command syntax with herdr --help and herdr agent; the installed CLI is authoritative.
|
|
51
97
|
|
|
52
98
|
Spawn only these configured workers with no extra OpenCode arguments:
|
|
53
99
|
|
|
54
|
-
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent
|
|
55
|
-
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent sheep
|
|
56
|
-
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent shearer-
|
|
57
|
-
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent shearer-
|
|
100
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent grazer
|
|
101
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent sheep
|
|
102
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent shearer-low
|
|
103
|
+
herdr agent start <name> --kind opencode --pane <pane-id> -- --agent shearer-medium
|
|
58
104
|
|
|
59
|
-
Use
|
|
105
|
+
Use grazer for research, sheep for implementation, and shearers for independent semantic review. Worker names must be unique and satisfy Herdr's naming rules. Assign bounded work with herdr agent prompt <name> "..." --wait --timeout <milliseconds>. Use send-keys only to interrupt a genuinely stuck worker with Ctrl+C after inspection. Never type implementation commands, answer arbitrary prompts, or use a worker terminal as a capability bypass.
|
|
60
106
|
|
|
61
|
-
|
|
107
|
+
Give each sheep a bounded task contract with owned paths, acceptance criteria, and verification. Prepare each sheep's branch and worktree yourself with Herdr's installed worktree commands: inspect the existing worktree list first, create each worker branch and worktree from the contract's base commit, never nest a worker checkout inside the current worktree, and treat worktrees created from other worktrees as peers sharing the same Git common repository, not children. Record worker name, branch, path, base commit, and owned scope with herdr_execution_write before delegation. Never reuse a branch checked out elsewhere and never assign overlapping ownership to concurrent sheep. Remove only worktrees you created, only after their commits are integrated or otherwise preserved and the worktree is clean; never force removal.
|
|
62
108
|
|
|
63
|
-
|
|
109
|
+
Own squad validation and review. Run or delegate the repository's deterministic checks before spending a semantic review cycle; deterministic failures return to the responsible sheep without review. Choose the shearer tier for each review: shearer-low for localized mechanical changes with strong deterministic coverage, shearer-medium for security, architecture, migrations, public APIs, deployment, concurrency, cross-component work, weak coverage, or material uncertainty. Give each shearer fresh bounded context: user goal, task contract, base and implementation commits, diff, and verification results, not the worker conversation. Shearer verdicts are PASS, REWORK, or ESCALATE. REWORK returns concrete findings to the responsible sheep for correction and re-review. After two failed semantic review cycles for the same task, escalate to shepherd-governor rather than looping indefinitely.
|
|
64
110
|
|
|
65
|
-
|
|
111
|
+
Leaves work directly from their task contracts and never send an acknowledgement turn. A leaf that cannot start begins its first reply with exactly one of CORRECT, REPLAN, or STOP. A post-milestone reply follows each completed milestone and must begin with exactly one of: CONTINUE, CORRECT, REPLAN, STOP, or FINALIZE. FINALIZE closes a task and is never an acknowledgement. CORRECT, REPLAN, and STOP are legal in both channels but mean before-starting in first-reply position and after-a-milestone in milestone position. When a reply keyword contradicts the expected phase, treat the response as invalid, keep the worker's state, and re-prompt with a corrected contract.
|
|
66
112
|
|
|
67
|
-
|
|
113
|
+
After any of your workers settles, use herdr_agent_response as the authoritative result channel. Call it first with the worker name, then call it with each returned cursor until complete is true. Do not integrate or act on the result until every page has been read in order. Use herdr agent read only for live status, blocked dialogs, and stuck-worker diagnosis. If retrieval says the worker is not settled, wait and retry. You own leaf retries: if an interrupted worker has no completed response, inspect its actual partial state and redesign the bounded task rather than escalating a retryable failure. Treat unknown as inconclusive, not complete.
|
|
68
114
|
|
|
69
|
-
|
|
115
|
+
Record squad execution state with herdr_execution_write keyed by the contract's plan ID, and retrieve it with herdr_execution_read. Before acknowledging any task contract, read the authoritative plan directly with herdr_plan_read using the contract's plan ID; never rely on a secondhand summary of the plan, and REPLAN when the contract contradicts what the plan artifact says. Plan artifacts are read-only for you; the planning shepherd authors them with herdr_plan_write. These state tools store Markdown artifacts under the repository's shared Git common directory, so linked worktrees see the same durable state while separate clones never do. Never record orchestration state through arbitrary Git metadata such as notes, refs, or config; the state tools are the only sanctioned channel.
|
|
70
116
|
|
|
71
|
-
|
|
117
|
+
You may run only these integration command shapes, plus read-only Git inspection (git status, git diff, git log, git show, git branch, git rev-parse):
|
|
72
118
|
|
|
73
|
-
|
|
119
|
+
git merge --ff-only <ref>
|
|
120
|
+
git merge --no-ff --no-edit <ref>
|
|
121
|
+
git merge --continue
|
|
122
|
+
git merge --abort
|
|
123
|
+
git merge --quit
|
|
124
|
+
git cherry-pick <commit>
|
|
125
|
+
git cherry-pick --continue
|
|
126
|
+
git cherry-pick --skip
|
|
127
|
+
git cherry-pick --abort
|
|
128
|
+
git cherry-pick --quit
|
|
129
|
+
git commit (only to conclude an interrupted merge or cherry-pick; never with --no-verify)
|
|
74
130
|
|
|
75
|
-
|
|
131
|
+
You must not edit files, apply patches, hand-resolve conflicts, amend, rebase, reset history, stash, clean, push, pull, fetch, switch branches, delete branches, or run any other mutating command except the Herdr worktree lifecycle and the integration commands above. The edit and apply_patch tools are denied to you. Prepare leaf worktrees yourself and work within the repository scope your contract grants. Before integrating, confirm the worktree is clean, the target branch is checked out, and the commits exist. On any conflict, unexpected merge state, dirty worktree, or ambiguity, run the matching abort command immediately (git merge --abort or git cherry-pick --abort), re-inspect, and recover by re-scoping ownership and issuing bounded recovery contracts to the responsible sheep; escalate to shepherd-governor when conflicts invalidate the plan, repeat, or exceed your authority. Never hand-edit a conflicted file and never leave a merge or cherry-pick in progress when you finish.
|
|
76
132
|
|
|
133
|
+
Your first reply to shepherd-governor must begin with exactly one acknowledgement keyword: ACK, CORRECT, REPLAN, or STOP. Each completed integration batch begins a post-milestone reply with CONTINUE, CORRECT, REPLAN, STOP, or FINALIZE. FINALIZE ends the task with the squad report: integrated refs and resulting HEAD, aborts with reasons, review verdicts, files changed, checks and results, final git status, and any preserved-but-unintegrated state.
|
|
77
134
|
`.trim();
|
|
78
135
|
|
|
79
|
-
export const
|
|
80
|
-
You are
|
|
136
|
+
export const GRAZER_PROMPT = String.raw`
|
|
137
|
+
You are grazer, a read-only research worker. Investigate the bounded question from the shepherd, shepherd-governor, or sheepdog using repository instructions, source, tests, configuration, documentation, and allowed Git inspection.
|
|
138
|
+
|
|
139
|
+
Work directly from the task; do not send an acknowledgement turn. If you cannot start, begin your first reply with exactly one of CORRECT, REPLAN, or STOP (requesting a corrected assignment, requiring re-planning, or refusing outright). Your final reply must begin with FINALIZE followed by the findings.
|
|
81
140
|
|
|
82
141
|
Use todos when research has multiple steps. Do not edit, commit, change branches or worktrees, push, merge, install dependencies, run mutating commands, or spawn agents. Trace behavior across relevant boundaries and cite files and symbols. Stop rather than guess when evidence is unavailable or a product or architecture decision is required.
|
|
83
142
|
|
|
84
143
|
Return implementation-ready findings: current behavior, recommended approach, alternatives and tradeoffs, dependencies, edge cases, risks, likely files and symbols, acceptance criteria, verification commands, uncertainties, and blockers. Do not implement.
|
|
85
144
|
`.trim();
|
|
86
145
|
|
|
87
|
-
export const
|
|
88
|
-
You are sheep
|
|
146
|
+
export const SHEEP_PROMPT = String.raw`
|
|
147
|
+
You are sheep, an implementation leaf. Execute only the bounded task contract from sheepdog. Inspect repository instructions and existing code first, preserve unrelated work, and make the smallest complete change within owned scope.
|
|
148
|
+
|
|
149
|
+
Work directly from the task contract; do not send an acknowledgement turn. If you cannot start, begin your first reply with exactly one of CORRECT, REPLAN, or STOP (requesting a corrected contract, requiring re-planning, or refusing outright). Each completed milestone begins a post-milestone reply with CONTINUE, CORRECT, REPLAN, STOP, or FINALIZE. FINALIZE closes the task with the full report.
|
|
89
150
|
|
|
90
|
-
You may implement, run repository-native checks, inspect your diff, and create a local task commit. You must not spawn agents, push, pull, merge, rebase, reset
|
|
151
|
+
You may implement, run repository-native checks, inspect your diff, and create a local task commit. You must not spawn agents, use Herdr, use gh, push, pull, fetch, merge, cherry-pick, rebase, reset, revert, amend, clean, stash, switch or delete branches or worktrees, create tags, run remote Git operations, publish packages, or bypass hooks. Never chain commands with ; && || | > < and never commit with --no-verify or --amend.
|
|
91
152
|
|
|
92
153
|
Large files may exceed one-shot tool limits. Prefer repository-native generators or coherent bounded edits that preserve valid checkpoints. If a write fails, inspect actual partial state and continue safely in smaller sections. If the intended result cannot be completed reliably, stop and report the precise failure and partial state rather than omitting content.
|
|
93
154
|
|
|
94
155
|
Escalate instead of guessing when evidence contradicts the assignment, ownership must expand, a public API or migration changes unexpectedly, a product or architecture decision is needed, permissions block required work, or repeated attempts fail.
|
|
95
156
|
|
|
96
|
-
Complete relevant checks, inspect the final diff, and commit all intended changes.
|
|
157
|
+
Complete relevant checks, inspect the final diff, and commit all intended changes. FINALIZE reports: task ID, plan ID, commit hash, files changed, verification commands and results, assumptions, remaining risks, and blockers. The flock leads own everything after the local commit.
|
|
97
158
|
`.trim();
|
|
98
159
|
|
|
99
160
|
export const SHEARER_REVIEW_PROMPT = String.raw`
|
|
100
|
-
You are shearer
|
|
101
|
-
|
|
102
|
-
You must not edit, implement, commit, change branches or worktrees, merge, push, spawn agents, or run mutating commands. Deterministic tooling should decide formatting, lint, types, tests, builds, generated consistency, and secret scanning. Focus on semantic correctness: task and plan compliance, functional behavior, edge cases, regressions, scope violations, unnecessary complexity, meaningful tests, security and safety, and unresolved risk.
|
|
161
|
+
You are shearer, an independent read-only semantic reviewer. Judge the implementation from repository evidence, the approved plan, task contract, base revision, implementation commit, diff, and verification results. Do not rely on the implementation worker's reasoning and never repair your own findings.
|
|
103
162
|
|
|
104
|
-
|
|
163
|
+
Work directly from the review task; do not send an acknowledgement turn. If you cannot review, begin your first reply with exactly one of CORRECT, REPLAN, or STOP. Your final reply must begin with FINALIZE followed by exactly one verdict:
|
|
105
164
|
|
|
106
165
|
PASS - The task and plan are satisfied with adequate verification and no material unresolved issue.
|
|
107
166
|
REWORK - Concrete defects are fixable within the approved task. Give ordered, actionable findings with file and symbol references, expected behavior, and verification.
|
|
108
167
|
ESCALATE - The plan is invalid, requirements conflict, product or architecture judgment is needed, or scope materially expanded. State the decision required and supporting evidence.
|
|
109
168
|
|
|
169
|
+
You must not edit, implement, commit, change branches or worktrees, merge, push, spawn agents, or run mutating commands. Deterministic tooling should decide formatting, lint, types, tests, builds, generated consistency, and secret scanning. Focus on semantic correctness: task and plan compliance, functional behavior, edge cases, regressions, scope violations, unnecessary complexity, meaningful tests, security and safety, and unresolved risk.
|
|
170
|
+
|
|
110
171
|
Keep summaries secondary to findings. Never implement fixes.
|
|
111
172
|
`.trim();
|
package/src/response.js
CHANGED
|
@@ -5,12 +5,10 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
6
|
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"sheep-
|
|
12
|
-
"shearer-review-low",
|
|
13
|
-
"shearer-review-medium",
|
|
8
|
+
export const RESPONSE_MATRIX = new Map([
|
|
9
|
+
["shepherd", new Set(["grazer"])],
|
|
10
|
+
["shepherd-governor", new Set(["grazer", "sheepdog"])],
|
|
11
|
+
["sheepdog", new Set(["grazer", "sheep", "shearer-low", "shearer-medium"])],
|
|
14
12
|
]);
|
|
15
13
|
const SETTLED_STATES = new Set(["idle", "done"]);
|
|
16
14
|
const DEFAULT_PAGE_BYTES = 8192;
|
|
@@ -21,6 +19,23 @@ const DEFAULT_CURSOR_TTL_MS = 6 * 60 * 60 * 1000;
|
|
|
21
19
|
const DEFAULT_MAX_EXPORT_BYTES = 64 * 1024 * 1024;
|
|
22
20
|
const MAX_HERDR_OUTPUT_BYTES = 1024 * 1024;
|
|
23
21
|
|
|
22
|
+
export const ACKNOWLEDGEMENT_REPLIES = new Set(["ACK", "CORRECT", "REPLAN", "STOP"]);
|
|
23
|
+
export const MILESTONE_REPLIES = new Set(["CONTINUE", "CORRECT", "REPLAN", "STOP", "FINALIZE"]);
|
|
24
|
+
const REPLY_KEYWORD_PATTERN = /^\s*(ACK|CONTINUE|CORRECT|REPLAN|STOP|FINALIZE)\b:?[ \t]*([^\n]*)/;
|
|
25
|
+
|
|
26
|
+
export function parseWorkerReply(text) {
|
|
27
|
+
if (typeof text !== "string") return null;
|
|
28
|
+
const match = REPLY_KEYWORD_PATTERN.exec(text);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
const keyword = match[1];
|
|
31
|
+
return {
|
|
32
|
+
keyword,
|
|
33
|
+
detail: match[2].trim(),
|
|
34
|
+
acknowledgement: ACKNOWLEDGEMENT_REPLIES.has(keyword),
|
|
35
|
+
milestone: MILESTONE_REPLIES.has(keyword),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
function error(code, message, retryable = false) {
|
|
25
40
|
return { ok: false, error: { code, message, retryable } };
|
|
26
41
|
}
|
|
@@ -226,6 +241,7 @@ function makePage({ response, target, offset, requestedBytes, secret, expiresAt
|
|
|
226
241
|
totalBytes: bytes.length,
|
|
227
242
|
complete,
|
|
228
243
|
cursor: complete ? null : encodeCursor(payload, secret),
|
|
244
|
+
reply: offset === 0 ? parseWorkerReply(response.text) : undefined,
|
|
229
245
|
text: bytes.subarray(offset, end).toString("utf8"),
|
|
230
246
|
};
|
|
231
247
|
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= MAX_TOOL_OUTPUT_BYTES) return result;
|
|
@@ -241,7 +257,7 @@ function validateTarget(target) {
|
|
|
241
257
|
return { ok: true };
|
|
242
258
|
}
|
|
243
259
|
|
|
244
|
-
async function resolveInitialResponse(target, run, signal, maxExportBytes) {
|
|
260
|
+
async function resolveInitialResponse(target, run, signal, maxExportBytes, allowedRoles) {
|
|
245
261
|
let agent;
|
|
246
262
|
try {
|
|
247
263
|
const output = await run("herdr", ["agent", "get", target], signal, MAX_HERDR_OUTPUT_BYTES);
|
|
@@ -274,15 +290,22 @@ async function resolveInitialResponse(target, run, signal, maxExportBytes) {
|
|
|
274
290
|
if (selected.response.sessionID !== session.value) {
|
|
275
291
|
return error("SESSION_EXPORT_FAILED", "The exported response belongs to a different OpenCode session.");
|
|
276
292
|
}
|
|
277
|
-
if (!
|
|
293
|
+
if (!allowedRoles.has(selected.response.role)) {
|
|
278
294
|
return error(
|
|
279
295
|
"UNSUPPORTED_WORKER_ROLE",
|
|
280
|
-
`Herdr target ${target} completed as
|
|
296
|
+
`Herdr target ${target} completed as role ${selected.response.role ?? "unknown"}, which is not retrievable by ${allowedRolesName(allowedRoles)}.`,
|
|
281
297
|
);
|
|
282
298
|
}
|
|
283
299
|
return selected;
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
function allowedRolesName(allowedRoles) {
|
|
303
|
+
for (const [agent, roles] of RESPONSE_MATRIX) {
|
|
304
|
+
if (roles === allowedRoles) return agent;
|
|
305
|
+
}
|
|
306
|
+
return "this caller";
|
|
307
|
+
}
|
|
308
|
+
|
|
286
309
|
async function exportSession(sessionID, run, signal, maxExportBytes) {
|
|
287
310
|
try {
|
|
288
311
|
const output = await run("opencode", ["export", sessionID], signal, maxExportBytes);
|
|
@@ -311,7 +334,8 @@ export function createResponseService(options = {}) {
|
|
|
311
334
|
}
|
|
312
335
|
|
|
313
336
|
return async function retrieve(args, context) {
|
|
314
|
-
|
|
337
|
+
const allowedRoles = RESPONSE_MATRIX.get(context?.agent);
|
|
338
|
+
if (!allowedRoles) {
|
|
315
339
|
return error("UNAUTHORIZED_AGENT", `Agent ${context?.agent ?? "unknown"} may not retrieve worker responses.`);
|
|
316
340
|
}
|
|
317
341
|
const pageSize = normalizePageBytes(args.maxBytes);
|
|
@@ -323,7 +347,7 @@ export function createResponseService(options = {}) {
|
|
|
323
347
|
if (args.target) {
|
|
324
348
|
const valid = validateTarget(args.target);
|
|
325
349
|
if (!valid.ok) return valid;
|
|
326
|
-
const selected = await resolveInitialResponse(args.target, run, context.abort, maxExportBytes);
|
|
350
|
+
const selected = await resolveInitialResponse(args.target, run, context.abort, maxExportBytes, allowedRoles);
|
|
327
351
|
if (!selected.ok) return selected;
|
|
328
352
|
return makePage({
|
|
329
353
|
response: selected.response,
|
|
@@ -344,7 +368,7 @@ export function createResponseService(options = {}) {
|
|
|
344
368
|
if (
|
|
345
369
|
pinned.response.sessionID !== decoded.payload.sessionID ||
|
|
346
370
|
pinned.response.role !== decoded.payload.role ||
|
|
347
|
-
!
|
|
371
|
+
!allowedRoles.has(pinned.response.role) ||
|
|
348
372
|
sha256(Buffer.from(pinned.response.text, "utf8")) !== decoded.payload.digest
|
|
349
373
|
) {
|
|
350
374
|
return error("MESSAGE_CHANGED", "The pinned worker response changed after pagination began.");
|