opencode-herdr-orchestration 0.2.1 → 0.3.1
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 +312 -5
- package/bin/orchestration.js +6 -1
- package/package.json +4 -3
- package/src/agents.js +275 -4
- package/src/diagnostics.js +117 -0
- package/src/index.js +312 -36
- package/src/installer.js +268 -1
- package/src/prompts.js +213 -6
- package/src/response.js +71 -2
- package/src/state.js +2252 -11
- package/src/steer.js +124 -0
package/src/index.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createAgents,
|
|
5
|
+
DEVELOPER_AGENT,
|
|
6
|
+
mergeAgent,
|
|
7
|
+
OWNERSHIP_TOOL_ACCESS,
|
|
8
|
+
OWNERSHIP_TOOLS,
|
|
9
|
+
RAW_STEERING_TOOL_ACCESS,
|
|
10
|
+
RAW_STEERING_TOOLS,
|
|
11
|
+
SHEPHERD_PHASES,
|
|
12
|
+
STATE_TOOL_ACCESS,
|
|
13
|
+
STATE_TOOLS,
|
|
14
|
+
STEERING_TOOL_ACCESS,
|
|
15
|
+
STEERING_TOOLS,
|
|
16
|
+
} from "./agents.js";
|
|
4
17
|
import { createResponseTool } from "./response.js";
|
|
5
18
|
import { createStateService } from "./state.js";
|
|
19
|
+
import { createSteerCommandHook } from "./steer.js";
|
|
6
20
|
|
|
7
21
|
const SESSION_MODES = new Map();
|
|
8
22
|
|
|
9
23
|
function modeForAgent(agent) {
|
|
24
|
+
if (agent === DEVELOPER_AGENT) return "developer";
|
|
10
25
|
if (agent === "shepherd") return "shepherd";
|
|
11
26
|
if (agent === "shepherd-governor") return "governor";
|
|
12
27
|
if (agent === "sheepdog") return "sheepdog";
|
|
@@ -24,10 +39,14 @@ function stateError(code, message, retryable = false) {
|
|
|
24
39
|
// tools. Enforcement mirrors STATE_TOOL_ACCESS in src/agents.js: the planning
|
|
25
40
|
// shepherd writes and reads plan artifacts; shepherd-governor and sheepdog
|
|
26
41
|
// read the authoritative plan; sheepdog writes and reads execution artifacts.
|
|
27
|
-
// Artifacts are durable Markdown files under the
|
|
28
|
-
// common
|
|
29
|
-
//
|
|
30
|
-
//
|
|
42
|
+
// Artifacts are durable Markdown files under the canonical
|
|
43
|
+
// `<git-common-dir>/flocky` state root in the repository's shared Git common
|
|
44
|
+
// directory, so linked worktrees share state while separate clones never do.
|
|
45
|
+
// Legacy `<git-common-dir>/herdr` artifacts are reconciled into the canonical
|
|
46
|
+
// root before every plan or execution operation — copied, accepted, or failed
|
|
47
|
+
// closed on conflict — and the legacy root is never auto-deleted. The state
|
|
48
|
+
// service invokes only read-only `git rev-parse`; it never writes arbitrary
|
|
49
|
+
// Git metadata.
|
|
31
50
|
export function createStateTools(stateOptions = {}) {
|
|
32
51
|
const state = createStateService(stateOptions);
|
|
33
52
|
|
|
@@ -103,36 +122,293 @@ export function createStateTools(stateOptions = {}) {
|
|
|
103
122
|
return tools;
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
// Developer steering submission (M2, Option A, trusted Developer only).
|
|
126
|
+
// The sole submitter is the explicit non-flock `developer` context. The
|
|
127
|
+
// allowlist holds only Developer; every flock role plus unknown, ambiguous,
|
|
128
|
+
// none, and unset are denied fail-closed with no filesystem write. Developer
|
|
129
|
+
// is never inferred from session mode, directory, environment text, or prompt
|
|
130
|
+
// content: only an exact `context.agent === "developer"` passes. The runtime
|
|
131
|
+
// check stays authoritative over static per-agent permission overrides.
|
|
132
|
+
export function createSteeringTools(stateOptions = {}) {
|
|
133
|
+
const state = createStateService(stateOptions);
|
|
134
|
+
const name = STEERING_TOOLS.submit;
|
|
135
|
+
return {
|
|
136
|
+
[name]: tool({
|
|
137
|
+
description:
|
|
138
|
+
"Submit bounded Developer steering for one Plan ID target. Developer context only; flock roles are denied. Provide explicit planId, or omit it only when exactly one active steering target exists.",
|
|
139
|
+
args: {
|
|
140
|
+
planId: tool.schema
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Explicit steering target Plan ID; required unless exactly one active target exists."),
|
|
144
|
+
content: tool.schema
|
|
145
|
+
.string()
|
|
146
|
+
.min(1)
|
|
147
|
+
.describe("Bounded steering content; at most 8192 UTF-8 bytes."),
|
|
148
|
+
},
|
|
149
|
+
async execute(args, context) {
|
|
150
|
+
const allowed = STEERING_TOOL_ACCESS.get(name);
|
|
151
|
+
if (!allowed?.has(context?.agent)) {
|
|
152
|
+
return JSON.stringify(
|
|
153
|
+
stateError(
|
|
154
|
+
"UNAUTHORIZED_AGENT",
|
|
155
|
+
`Agent ${context?.agent ?? "unknown"} may not use ${name}.`,
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const result = await state.submitSteering(args);
|
|
160
|
+
context.metadata({
|
|
161
|
+
title: result.ok ? `${name}: ${result.entry.planId}#${result.entry.sequence}` : `${name}: ${result.error.code}`,
|
|
162
|
+
metadata: result.ok
|
|
163
|
+
? { planId: result.entry.planId, steeringId: result.entry.id, sequence: result.entry.sequence }
|
|
164
|
+
: { error: result.error.code },
|
|
165
|
+
});
|
|
166
|
+
return JSON.stringify(result);
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Shepherd-only raw steering plus ownership lifecycle tools (M3).
|
|
173
|
+
// Only `shepherd` and `shepherd-governor` pass the runtime allowlist;
|
|
174
|
+
// sheepdog, grazer, sheep, shearers, developer, unknown, and unset are
|
|
175
|
+
// denied fail-closed with no filesystem write. State-level session plus
|
|
176
|
+
// generation fencing stays authoritative inside src/state.js (NOT
|
|
177
|
+
// AUTHORITATIVE PHASE for non-owners). Steering never authorizes push,
|
|
178
|
+
// tag, publish, deploy, merge, or any consequential action; existing
|
|
179
|
+
// approvals still required.
|
|
180
|
+
export function createRawSteeringTools(stateOptions = {}) {
|
|
181
|
+
const state = createStateService(stateOptions);
|
|
182
|
+
const definitions = [
|
|
183
|
+
{
|
|
184
|
+
name: RAW_STEERING_TOOLS.check,
|
|
185
|
+
description:
|
|
186
|
+
"Check unread Developer steering for one Plan ID target without loading bodies. Shepherd phases only with authoritative phase, session, and generation; non-owners receive NOT AUTHORITATIVE PHASE.",
|
|
187
|
+
run: (args) => state.checkSteering(args),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: RAW_STEERING_TOOLS.read,
|
|
191
|
+
description:
|
|
192
|
+
"Read ordered exact unread Developer steering with no mutation. Shepherd phases only with authoritative phase, session, and generation.",
|
|
193
|
+
run: (args) => state.readSteering(args),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: RAW_STEERING_TOOLS.consume,
|
|
197
|
+
description:
|
|
198
|
+
"Consume Developer steering only after the authoritative owner recorded sync disposition for the sync point; idempotent. Shepherd phases only. Steering never authorizes consequential actions.",
|
|
199
|
+
run: (args) => state.consumeSteering(args),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
const tools = {};
|
|
203
|
+
for (const definition of definitions) {
|
|
204
|
+
tools[definition.name] = tool({
|
|
205
|
+
description: definition.description,
|
|
206
|
+
args: {
|
|
207
|
+
planId: tool.schema.string().describe("Explicit steering target Plan ID."),
|
|
208
|
+
phase: tool.schema.string().describe("Owner phase: planning or governance."),
|
|
209
|
+
session: tool.schema.string().describe("Authoritative session fencing the ownership record."),
|
|
210
|
+
generation: tool.schema.number().int().min(1).describe("Authoritative generation fencing the ownership record."),
|
|
211
|
+
...(definition.name === RAW_STEERING_TOOLS.consume
|
|
212
|
+
? {
|
|
213
|
+
ids: tool.schema.array(tool.schema.string()).min(1).max(1000).describe("Steering ids to consume idempotently."),
|
|
214
|
+
syncPoint: tool.schema.string().describe("Closed sync point whose disposition was recorded before consume."),
|
|
215
|
+
disposition: tool.schema.string().describe("Closed disposition recorded before consume."),
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
},
|
|
219
|
+
async execute(args, context) {
|
|
220
|
+
const allowed = RAW_STEERING_TOOL_ACCESS.get(definition.name);
|
|
221
|
+
if (!allowed?.has(context?.agent)) {
|
|
222
|
+
return JSON.stringify(
|
|
223
|
+
stateError(
|
|
224
|
+
"UNAUTHORIZED_AGENT",
|
|
225
|
+
`Agent ${context?.agent ?? "unknown"} may not use ${definition.name}.`,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const result = await definition.run(args);
|
|
230
|
+
context.metadata({
|
|
231
|
+
title: result.ok ? `${definition.name}: ${args.planId}` : `${definition.name}: ${result.error.code}`,
|
|
232
|
+
metadata: result.ok ? { planId: args.planId } : { error: result.error.code },
|
|
233
|
+
});
|
|
234
|
+
return JSON.stringify(result);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return tools;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createOwnershipTools(stateOptions = {}) {
|
|
242
|
+
const state = createStateService(stateOptions);
|
|
243
|
+
const definitions = [
|
|
244
|
+
{
|
|
245
|
+
name: OWNERSHIP_TOOLS.claim,
|
|
246
|
+
description:
|
|
247
|
+
"Claim or hand off validated target lifecycle ownership for one Plan ID with session plus generation fencing. Shepherd phases only; generations must increase and both phases cannot race on the same generation.",
|
|
248
|
+
run: (args) => state.claimOwnership(args),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: OWNERSHIP_TOOLS.read,
|
|
252
|
+
description: "Read the validated lifecycle record for one Plan ID. Shepherd phases only; non-owners receive NOT AUTHORITATIVE PHASE.",
|
|
253
|
+
run: (args) => state.readOwnership(args),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: OWNERSHIP_TOOLS.sync,
|
|
257
|
+
description:
|
|
258
|
+
"Record semantic synchronization disposition for one closed sync point (planning-start, pre-plan, pre-assignment, milestone-executing, result-received, continue, finalize, consequential-preparation). Shepherd phases only; disposition is recorded before consume and consume is idempotent.",
|
|
259
|
+
run: (args) => state.recordSync(args),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: OWNERSHIP_TOOLS.snapshot,
|
|
263
|
+
description:
|
|
264
|
+
"Record a bounded lifecycle snapshot for one stage (planning, executing, result-evaluation, consequential-preparation). Shepherd phases only; pending consequential action must be recorded before the consequential-preparation snapshot and check.",
|
|
265
|
+
run: (args) => state.recordSnapshot(args),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: OWNERSHIP_TOOLS.correct,
|
|
269
|
+
description:
|
|
270
|
+
"Route semantic correction to sheepdog as normal corrective instructions, never raw records. Shepherd phases only; steering never authorizes consequential actions.",
|
|
271
|
+
run: (args) => state.routeCorrection(args),
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
const tools = {};
|
|
275
|
+
for (const definition of definitions) {
|
|
276
|
+
const isClaim = definition.name === OWNERSHIP_TOOLS.claim;
|
|
277
|
+
const isSync = definition.name === OWNERSHIP_TOOLS.sync;
|
|
278
|
+
const isSnapshot = definition.name === OWNERSHIP_TOOLS.snapshot;
|
|
279
|
+
const isCorrect = definition.name === OWNERSHIP_TOOLS.correct;
|
|
280
|
+
tools[definition.name] = tool({
|
|
281
|
+
description: definition.description,
|
|
282
|
+
args: {
|
|
283
|
+
planId: tool.schema.string().describe("Lifecycle target Plan ID."),
|
|
284
|
+
phase: tool.schema.string().describe("Owner phase: planning or governance."),
|
|
285
|
+
session: tool.schema.string().describe("Authoritative session fencing the ownership record."),
|
|
286
|
+
...(isClaim || isSync || isSnapshot || isCorrect
|
|
287
|
+
? { generation: tool.schema.number().int().min(1).describe("Authoritative generation fencing the ownership record.") }
|
|
288
|
+
: {}),
|
|
289
|
+
...(isClaim
|
|
290
|
+
? {
|
|
291
|
+
milestone: tool.schema.string().min(1).describe("Bounded milestone summary."),
|
|
292
|
+
lifecycleState: tool.schema.string().describe("Closed lifecycle state."),
|
|
293
|
+
currentObjective: tool.schema.string().optional().describe("Bounded current objective semantic summary."),
|
|
294
|
+
currentAction: tool.schema.string().optional().describe("Bounded current action semantic summary."),
|
|
295
|
+
activeSheepdogTarget: tool.schema.string().optional().describe("Active sheepdog target or empty when yielded."),
|
|
296
|
+
relevantRevision: tool.schema.string().optional().describe("Relevant revision summary."),
|
|
297
|
+
pendingConsequentialAction: tool.schema.string().optional().describe("Pending consequential action summary."),
|
|
298
|
+
}
|
|
299
|
+
: {}),
|
|
300
|
+
...(isSync
|
|
301
|
+
? {
|
|
302
|
+
syncPoint: tool.schema.string().describe("Closed sync point."),
|
|
303
|
+
disposition: tool.schema.string().describe("Closed disposition recorded before consume."),
|
|
304
|
+
note: tool.schema.string().optional().describe("Bounded semantic note."),
|
|
305
|
+
}
|
|
306
|
+
: {}),
|
|
307
|
+
...(isSnapshot
|
|
308
|
+
? {
|
|
309
|
+
stage: tool.schema.string().describe("Snapshot stage."),
|
|
310
|
+
milestone: tool.schema.string().optional().describe("Bounded milestone override."),
|
|
311
|
+
lifecycleState: tool.schema.string().optional().describe("Closed lifecycle state override."),
|
|
312
|
+
currentObjective: tool.schema.string().optional().describe("Bounded current objective override."),
|
|
313
|
+
currentAction: tool.schema.string().optional().describe("Bounded current action override."),
|
|
314
|
+
activeSheepdogTarget: tool.schema.string().optional().describe("Active sheepdog target override."),
|
|
315
|
+
relevantRevision: tool.schema.string().optional().describe("Relevant revision override."),
|
|
316
|
+
pendingConsequentialAction: tool.schema.string().optional().describe("Pending consequential action override."),
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
319
|
+
...(isCorrect
|
|
320
|
+
? {
|
|
321
|
+
correction: tool.schema.string().min(1).describe("Normal semantic corrective instructions for sheepdog, never raw records."),
|
|
322
|
+
syncPoint: tool.schema.string().optional().describe("Related closed sync point."),
|
|
323
|
+
}
|
|
324
|
+
: {}),
|
|
325
|
+
},
|
|
326
|
+
async execute(args, context) {
|
|
327
|
+
const allowed = OWNERSHIP_TOOL_ACCESS.get(definition.name);
|
|
328
|
+
if (!allowed?.has(context?.agent)) {
|
|
329
|
+
return JSON.stringify(
|
|
330
|
+
stateError(
|
|
331
|
+
"UNAUTHORIZED_AGENT",
|
|
332
|
+
`Agent ${context?.agent ?? "unknown"} may not use ${definition.name}.`,
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const result = await definition.run(args);
|
|
337
|
+
context.metadata({
|
|
338
|
+
title: result.ok ? `${definition.name}: ${args.planId}` : `${definition.name}: ${result.error.code}`,
|
|
339
|
+
metadata: result.ok ? { planId: args.planId } : { error: result.error.code },
|
|
340
|
+
});
|
|
341
|
+
return JSON.stringify(result);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return tools;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export const HerdrOrchestrationPlugin = async (pluginInput = {}, options = {}) => {
|
|
349
|
+
const stateOptions = { ...(options.state ?? {}) };
|
|
350
|
+
if (stateOptions.cwd === undefined) {
|
|
351
|
+
const fallback = pluginInput?.directory ?? pluginInput?.worktree;
|
|
352
|
+
if (typeof fallback === "string" && fallback.length > 0) stateOptions.cwd = fallback;
|
|
353
|
+
}
|
|
354
|
+
const resolveAgent = options.steerResolveAgent ?? options.steer?.resolveAgent;
|
|
355
|
+
const steerBefore = createSteerCommandHook({
|
|
356
|
+
client: pluginInput?.client,
|
|
357
|
+
stateOptions,
|
|
358
|
+
...(resolveAgent ? { resolveAgent } : {}),
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
tool: {
|
|
362
|
+
herdr_agent_response: createResponseTool(options.response),
|
|
363
|
+
...createStateTools(options.state),
|
|
364
|
+
...createSteeringTools(options.state),
|
|
365
|
+
...createRawSteeringTools(options.state),
|
|
366
|
+
...createOwnershipTools(options.state),
|
|
367
|
+
},
|
|
368
|
+
config(config) {
|
|
369
|
+
config.agent ??= {};
|
|
370
|
+
const agents = createAgents(options);
|
|
371
|
+
for (const [name, defaults] of Object.entries(agents)) {
|
|
372
|
+
config.agent[name] = mergeAgent(defaults, config.agent[name]);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async "command.execute.before"(input, output) {
|
|
377
|
+
return steerBefore(input, output);
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
async "chat.message"(input) {
|
|
381
|
+
if (input.sessionID) SESSION_MODES.set(input.sessionID, modeForAgent(input.agent));
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async "shell.env"(input, output) {
|
|
385
|
+
output.env.SHEPHERD_MODE = input.sessionID
|
|
386
|
+
? (SESSION_MODES.get(input.sessionID) ?? "none")
|
|
387
|
+
: "none";
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
async event({ event }) {
|
|
391
|
+
if (event?.type === "session.deleted") {
|
|
392
|
+
const sessionID = event.properties?.info?.id ?? event.properties?.sessionID;
|
|
393
|
+
if (sessionID) SESSION_MODES.delete(sessionID);
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
};
|
|
136
398
|
|
|
137
399
|
export default HerdrOrchestrationPlugin;
|
|
138
|
-
export {
|
|
400
|
+
export {
|
|
401
|
+
createAgents,
|
|
402
|
+
DEVELOPER_AGENT,
|
|
403
|
+
mergeAgent,
|
|
404
|
+
modeForAgent,
|
|
405
|
+
OWNERSHIP_TOOL_ACCESS,
|
|
406
|
+
OWNERSHIP_TOOLS,
|
|
407
|
+
RAW_STEERING_TOOL_ACCESS,
|
|
408
|
+
RAW_STEERING_TOOLS,
|
|
409
|
+
SHEPHERD_PHASES,
|
|
410
|
+
STATE_TOOL_ACCESS,
|
|
411
|
+
STATE_TOOLS,
|
|
412
|
+
STEERING_TOOL_ACCESS,
|
|
413
|
+
STEERING_TOOLS,
|
|
414
|
+
};
|