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/src/agents.js CHANGED
@@ -38,6 +38,12 @@ const safeGitInspection = {
38
38
  "git ls-files": "allow",
39
39
  };
40
40
 
41
+ // 20-M1 responsive wait: bounded `herdr agent get` polling uses only the
42
+ // already-permitted prompt plus wait plus get plus read plus list surfaces.
43
+ // No event stream command is evidenced, so no new Herdr command is invented;
44
+ // `herdr agent wait` stays a bounded sleep between `get` state checks and the
45
+ // safety timeout stays the final bound only. Sheepdog owns routine flock waits
46
+ // with no per-transition Shepherd wakeups; governor leaf bans stay untouched.
41
47
  const herdrInspection = {
42
48
  "Get-Item Env:HERDR_ENV": "allow",
43
49
  "herdr --help": "allow",
@@ -96,6 +102,81 @@ function stateToolPermissions(allowedTools) {
96
102
  );
97
103
  }
98
104
 
105
+ // Developer steering submission (M2, Option A, trusted Developer only).
106
+ // The explicit non-flock `developer` context is the sole submitter; all
107
+ // seven orchestration roles are denied as defense in depth. The runtime
108
+ // context-agent check in src/index.js plus the /steer hook in src/steer.js
109
+ // stays authoritative over these static entries: even a user override
110
+ // flipping one to "allow" must not bypass the allowlist. No flock role may
111
+ // present as Developer: the developer profile below holds only the submit
112
+ // tool and no spawn matrix entry creates or targets it.
113
+ export const DEVELOPER_AGENT = "developer";
114
+ export const DEVELOPER_PROMPT = String.raw`
115
+ You are developer, the trusted steering submitter. Submit bounded steering via /steer as <content> or <planId> :: <content>; omit planId only when exactly one active steering target exists. You hold only herdr_steering_submit; you never implement flock work, never spawn workers, and never read raw steering.
116
+ `.trim();
117
+ export const ORCHESTRATION_ROLES = Object.freeze([
118
+ "shepherd",
119
+ "shepherd-governor",
120
+ "sheepdog",
121
+ "grazer",
122
+ "sheep",
123
+ "shearer-low",
124
+ "shearer-medium",
125
+ ]);
126
+ export const STEERING_TOOLS = Object.freeze({
127
+ submit: "herdr_steering_submit",
128
+ });
129
+ export const STEERING_TOOL_ACCESS = Object.freeze(
130
+ new Map([[STEERING_TOOLS.submit, new Set([DEVELOPER_AGENT])]]),
131
+ );
132
+ const ALL_STEERING_TOOLS = Object.freeze(Object.values(STEERING_TOOLS));
133
+
134
+ function steeringToolPermissions(allowedTools) {
135
+ return Object.fromEntries(
136
+ ALL_STEERING_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
137
+ );
138
+ }
139
+
140
+ // Shepherd ownership raw steering plus lifecycle tools (M3). Only the two
141
+ // Shepherd phases may check, read, or consume raw steering and only the
142
+ // recorded owner phase plus session plus generation passes the state-level
143
+ // fencing (NOT AUTHORITATIVE PHASE otherwise). Sheepdog and every leaf are
144
+ // explicitly denied in code, not only in prompts; the runtime check in
145
+ // src/index.js stays authoritative over static overrides.
146
+ export const SHEPHERD_PHASES = Object.freeze(["shepherd", "shepherd-governor"]);
147
+ export const RAW_STEERING_TOOLS = Object.freeze({
148
+ check: "herdr_steering_check",
149
+ read: "herdr_steering_read",
150
+ consume: "herdr_steering_consume",
151
+ });
152
+ export const OWNERSHIP_TOOLS = Object.freeze({
153
+ claim: "herdr_ownership_claim",
154
+ read: "herdr_ownership_read",
155
+ sync: "herdr_ownership_sync",
156
+ snapshot: "herdr_ownership_snapshot",
157
+ correct: "herdr_ownership_correct",
158
+ });
159
+ export const RAW_STEERING_TOOL_ACCESS = Object.freeze(
160
+ new Map(Object.values(RAW_STEERING_TOOLS).map((name) => [name, new Set(SHEPHERD_PHASES)])),
161
+ );
162
+ export const OWNERSHIP_TOOL_ACCESS = Object.freeze(
163
+ new Map(Object.values(OWNERSHIP_TOOLS).map((name) => [name, new Set(SHEPHERD_PHASES)])),
164
+ );
165
+ const ALL_RAW_STEERING_TOOLS = Object.freeze(Object.values(RAW_STEERING_TOOLS));
166
+ const ALL_OWNERSHIP_TOOLS = Object.freeze(Object.values(OWNERSHIP_TOOLS));
167
+
168
+ function rawSteeringToolPermissions(allowedTools) {
169
+ return Object.fromEntries(
170
+ ALL_RAW_STEERING_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
171
+ );
172
+ }
173
+
174
+ function ownershipToolPermissions(allowedTools) {
175
+ return Object.fromEntries(
176
+ ALL_OWNERSHIP_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
177
+ );
178
+ }
179
+
99
180
  const SHEPHERD_STATE_TOOLS = [STATE_TOOLS.planWrite, STATE_TOOLS.planRead];
100
181
  const GOVERNOR_STATE_TOOLS = [STATE_TOOLS.planRead];
101
182
  const SHEEPDOG_STATE_TOOLS = [
@@ -103,6 +184,8 @@ const SHEEPDOG_STATE_TOOLS = [
103
184
  STATE_TOOLS.executionWrite,
104
185
  STATE_TOOLS.executionRead,
105
186
  ];
187
+ const SHEPHERD_RAW_STEERING_TOOLS = Object.values(RAW_STEERING_TOOLS);
188
+ const SHEPHERD_OWNERSHIP_TOOLS = Object.values(OWNERSHIP_TOOLS);
106
189
 
107
190
  const SHEPHERD_SPAWNABLE_AGENTS = ["grazer"];
108
191
  const GOVERNOR_SPAWNABLE_AGENTS = ["grazer", "sheepdog"];
@@ -122,6 +205,148 @@ const SHEEPDOG_LIFECYCLE_ALLOWS = {
122
205
  "git commit*": "allow",
123
206
  };
124
207
 
208
+ // Sheepdog Herdr lifecycle (21-M1): explicit prompt plus wait plus get plus
209
+ // read allows. These re-assert the shared herdrInspection entries so the four
210
+ // lifecycle operations stay evaluation-effective under OpenCode last-match
211
+ // glob semantics: "*" deny is the fallback first, these allows sit in the
212
+ // middle, separator denies stay global last. A prompt whose task text carries
213
+ // raw "; && || | > <" matches a separator deny after the prompt allow and
214
+ // fails closed to deny even inside quotes, so task text must avoid raw
215
+ // separators (see SHEEPDOG_PROMPT safe rule).
216
+ const SHEEPDOG_HERDR_LIFECYCLE_ALLOWS = {
217
+ "herdr agent prompt*": "allow",
218
+ "herdr agent wait*": "allow",
219
+ "herdr agent get*": "allow",
220
+ "herdr agent read*": "allow",
221
+ };
222
+
223
+ // Sheepdog interrupt bound (21-M1): replacement for the broad
224
+ // "herdr agent send-keys*" allow in shared herdrInspection. The broad pattern
225
+ // is explicitly denied first so only the narrow Ctrl-C spellings below are
226
+ // evaluation-effective. Matchers are string globs and cannot prove semantic
227
+ // intent, so the prompt inspect-first plus never-type rule stays primary; see
228
+ // SHEEPDOG_PROMPT and README Worker interruption residual.
229
+ const SHEEPDOG_SEND_KEYS_DENY = {
230
+ "herdr agent send-keys*": "deny",
231
+ };
232
+ const SHEEPDOG_SEND_KEYS_CTRL_C_ALLOWS = {
233
+ "herdr agent send-keys * --keys C-c*": "allow",
234
+ "herdr agent send-keys * --keys c-c*": "allow",
235
+ "herdr agent send-keys * --keys ctrl+c*": "allow",
236
+ "herdr agent send-keys * --keys Ctrl+C*": "allow",
237
+ "herdr agent send-keys * C-c*": "allow",
238
+ "herdr agent send-keys * ctrl+c*": "allow",
239
+ };
240
+
241
+ // Governor Herdr prompt residual (21-M2): `herdr agent prompt` plus wait plus
242
+ // get plus read patterns are name-based and cannot encode worker role, so they
243
+ // stay broad as far as text matchers permit. Prompt bans plus start denial
244
+ // (spawn matrix allows only grazer and sheepdog) plus the response matrix
245
+ // (retrieval allows only grazer and sheepdog) are the load-bearing layers;
246
+ // see SHEPHERD_GOVERNOR_PROMPT and README Governor prompt scoping residual.
247
+ // Governor interrupt bound (21-M2): replacement for the broad
248
+ // "herdr agent send-keys*" allow in shared herdrInspection, mirroring the
249
+ // sheepdog M1 bound. The broad pattern is explicitly denied first so only the
250
+ // narrow Ctrl-C spellings below are evaluation-effective. Matchers are string
251
+ // globs and cannot prove semantic intent, so the prompt inspect-first plus
252
+ // never-type plus never-bypass rule stays primary.
253
+ const GOVERNOR_SEND_KEYS_DENY = {
254
+ "herdr agent send-keys*": "deny",
255
+ };
256
+ const GOVERNOR_SEND_KEYS_CTRL_C_ALLOWS = {
257
+ "herdr agent send-keys * --keys C-c*": "allow",
258
+ "herdr agent send-keys * --keys c-c*": "allow",
259
+ "herdr agent send-keys * --keys ctrl+c*": "allow",
260
+ "herdr agent send-keys * --keys Ctrl+C*": "allow",
261
+ "herdr agent send-keys * C-c*": "allow",
262
+ "herdr agent send-keys * ctrl+c*": "allow",
263
+ };
264
+
265
+ // Pane layout (14-18-M1): live discovery on installed 0.8.2 via full-path
266
+ // `herdr --help` plus `herdr --skill` plus scoped read-only queries
267
+ // (`pane list --workspace w1K`, `tab list --workspace w1K`,
268
+ // `pane current --current`, `pane layout --current`, `agent get <name>`,
269
+ // `pane get w1K:p999` error sampling). Direct `herdr ...` spelling stays
270
+ // denied for leaves; discovery used only read-only help plus list plus get
271
+ // plus current plus layout, never split plus rename plus close plus start.
272
+ // Tab evidenced as list plus create plus get plus focus plus rename plus
273
+ // close; list takes `--workspace <ID>`, create takes
274
+ // `--workspace/--cwd/--label/--env/--focus/--no-focus`, rename takes
275
+ // `<TAB_ID> <LABEL>...`, close plus get plus focus take `<tab_id>`.
276
+ // Pane placement evidenced as split plus move plus focus plus resize plus
277
+ // swap plus layout plus get plus list plus current plus read plus rename plus
278
+ // close; split is `[PANE_ID] --pane/--current --direction right/down --ratio
279
+ // --cwd --env --focus/--no-focus`, move is `<PANE_ID>
280
+ // --tab/--split/--target-pane/--ratio/--new-tab/--workspace/--new-workspace/--label/--tab-label/--focus/--no-focus`,
281
+ // focus and neighbor take `--direction left/right/up/down --pane/--current`,
282
+ // resize takes `--direction --amount --pane/--current`, swap takes
283
+ // `--direction/--pane/--current/--source-pane/--target-pane`, layout takes
284
+ // `--pane/--current`, get takes `<pane_id>`, list takes
285
+ // `--workspace <ID>`, current takes `--pane/--current`, read takes
286
+ // `<PANE_ID> --source visible/recent/recent-unwrapped/detection
287
+ // --lines/--format/--ansi/--raw`, rename takes `<PANE_ID> [LABEL]...
288
+ // --clear`, close takes `<pane_id>`.
289
+ // Agent rename evidenced as `herdr agent rename <TARGET> <NAME>|--clear`
290
+ // (live `agent list` shows `name` such as `issue1418m1sheep` and `agent get`
291
+ // shows `name` plus `pane_id`); pane rename plus tab rename helps evidenced
292
+ // but only pane plus agent rename are enabled, tab rename stays denied to
293
+ // keep the single-tab 4-pane cap.
294
+ // Count queries evidenced as `herdr tab list --workspace <ID>` returning
295
+ // `{"id":"cli:tab:list","result":{"tabs":[{"pane_count":1,"tab_id":"w1K:t1",...}]},"type":"tab_list"}`,
296
+ // `herdr pane list --workspace <ID>` returning
297
+ // `{"id":"cli:pane:list","result":{"panes":[{...,"pane_id":"w1K:p1","tab_id":"w1K:t1","workspace_id":"w1K",...}]},"type":"pane_list"}`
298
+ // (count is length filtered by workspace plus tab), `herdr pane layout
299
+ // --current` returning
300
+ // `{"id":"cli:pane:layout","result":{"layout":{"panes":[...],"splits":[],"focused_pane_id":"w1K:p1",...}},"type":"pane_layout"}`,
301
+ // `herdr pane current --current` returning
302
+ // `{"id":"cli:pane:current","result":{"pane":{...}},"type":"pane_current"}`.
303
+ // Creation JSON per `herdr --skill`: `tab create` returns
304
+ // `.result.tab` plus `.result.root_pane`, `pane split` returns the new pane
305
+ // as `.result.pane` (skill geometry is `pane split --current --direction
306
+ // right/down --cwd "$PWD" --no-focus`, wide to the right and narrow or tall
307
+ // down, reading `.result.pane.pane_id` and never deriving from sidebar
308
+ // order), `pane move` would return
309
+ // `.result.move_result.pane.pane_id` plus `.result.move_result.previous_pane_id`
310
+ // but move stays denied and is documented only.
311
+ // Exits per `herdr --skill` plus live sampling: most controls return JSON on
312
+ // stdout with `id` plus `result` plus `type`; server errors are JSON such as
313
+ // `{"error":{"code":"pane_not_found","message":"pane w1K:p999 not found"},"id":"cli:pane:get"}`
314
+ // on stderr with exit 1; syntax plus validation such as `pane split
315
+ // --direction invalid` returning `invalid split direction` exits with 2.
316
+ // IDs are opaque stable handles (`w1K`, `w1K:t1`, `w1K:p1` from
317
+ // `HERDR_WORKSPACE_ID` plus `HERDR_TAB_ID` plus `HERDR_PANE_ID`); closed IDs
318
+ // are not reused; prefer `--current` and never rely on the UI-focused pane.
319
+ // Fallback if any of tab list plus pane get plus rename plus close plus
320
+ // split are missing: reuse the current pane via `--pane` plus `--current`,
321
+ // report STOP naming the missing capability, never invent `herdr pane
322
+ // create*` plus `herdr tab split*` plus `herdr agent events*` plus
323
+ // `herdr pane move*` plus `herdr pane resize*` plus `herdr workspace
324
+ // create*` behavior.
325
+ // Protected Dev Developer Terminal exclusion cannot be matcher-enforced:
326
+ // pane IDs are opaque and labels are absent from scan plus split plus close
327
+ // command strings, while a `*Dev*` glob would overmatch legitimate
328
+ // `--cwd C:\Dev\...` values, so no such glob is added; the prompt plus
329
+ // README Pane layout policy exclusion stays primary; see README residual.
330
+ const SHEPHERD_PANE_ALLOWS = {
331
+ "herdr tab list*": "allow",
332
+ "herdr pane get*": "allow",
333
+ "herdr pane rename*": "allow",
334
+ "herdr agent rename*": "allow",
335
+ };
336
+ const GOVERNOR_PANE_ALLOWS = {
337
+ "herdr tab list*": "allow",
338
+ "herdr pane get*": "allow",
339
+ "herdr pane rename*": "allow",
340
+ "herdr agent rename*": "allow",
341
+ };
342
+ const SHEEPDOG_PANE_ALLOWS = {
343
+ "herdr tab list*": "allow",
344
+ "herdr pane get*": "allow",
345
+ "herdr pane rename*": "allow",
346
+ "herdr agent rename*": "allow",
347
+ "herdr pane close*": "allow",
348
+ };
349
+
125
350
  const SHEEPDOG_DENIALS = {
126
351
  "git push*": "deny",
127
352
  "git pull*": "deny",
@@ -202,7 +427,9 @@ export function createAgents(options = {}) {
202
427
  const sheepdogVariant = options.sheepdogVariant;
203
428
  const reviewerModel = options.reviewerModel ?? "litellm-responses/gpt-5.6-terra";
204
429
  const shepherdPermissions = options.shepherdPermissions ?? {};
205
- const shepherdPrompt = appendPrompt(SHEPHERD_PROMPT, options.shepherdPromptAppend);
430
+ const shepherdPrompt = appendPrompt(SHEPHERD_PROMPT, options.shepherdPromptAppend, "shepherdPromptAppend");
431
+ const sheepdogPermissions = options.sheepdogPermissions ?? {};
432
+ const sheepdogPrompt = appendPrompt(SHEEPDOG_PROMPT, options.sheepdogPromptAppend, "sheepdogPromptAppend");
206
433
 
207
434
  return {
208
435
  shepherd: {
@@ -218,9 +445,13 @@ export function createAgents(options = {}) {
218
445
  apply_patch: markdownOnly,
219
446
  herdr_agent_response: "allow",
220
447
  ...stateToolPermissions(SHEPHERD_STATE_TOOLS),
448
+ ...steeringToolPermissions([]),
449
+ ...rawSteeringToolPermissions(SHEPHERD_RAW_STEERING_TOOLS),
450
+ ...ownershipToolPermissions(SHEPHERD_OWNERSHIP_TOOLS),
221
451
  bash: {
222
452
  "*": "deny",
223
453
  ...herdrInspection,
454
+ ...SHEPHERD_PANE_ALLOWS,
224
455
  ...spawnMatrix(SHEPHERD_SPAWNABLE_AGENTS),
225
456
  "git status*": "allow",
226
457
  "git diff*": "allow",
@@ -261,9 +492,13 @@ export function createAgents(options = {}) {
261
492
  apply_patch: markdownOnly,
262
493
  herdr_agent_response: "allow",
263
494
  ...stateToolPermissions(GOVERNOR_STATE_TOOLS),
495
+ ...steeringToolPermissions([]),
496
+ ...rawSteeringToolPermissions(SHEPHERD_RAW_STEERING_TOOLS),
497
+ ...ownershipToolPermissions(SHEPHERD_OWNERSHIP_TOOLS),
264
498
  bash: {
265
499
  "*": "deny",
266
500
  ...herdrInspection,
501
+ ...GOVERNOR_PANE_ALLOWS,
267
502
  ...spawnMatrix(GOVERNOR_SPAWNABLE_AGENTS),
268
503
  "git status*": "allow",
269
504
  "git diff*": "allow",
@@ -303,6 +538,8 @@ export function createAgents(options = {}) {
303
538
  "gh pr create*": "allow",
304
539
  "gh pr view*": "allow",
305
540
  "gh pr checks*": "allow",
541
+ ...GOVERNOR_SEND_KEYS_DENY,
542
+ ...GOVERNOR_SEND_KEYS_CTRL_C_ALLOWS,
306
543
  ...separatorDenials,
307
544
  },
308
545
  task: "deny",
@@ -315,7 +552,7 @@ export function createAgents(options = {}) {
315
552
  ...(sheepdogVariant ? { variant: sheepdogVariant } : {}),
316
553
  description:
317
554
  "Leads execution squads of grazer, sheep, and shearers, prepares worker branches and worktrees, owns validation, review tiers, retries, and conflict recovery, and performs clean local integration with merge and cherry-pick lifecycle commands only.",
318
- prompt: SHEEPDOG_PROMPT,
555
+ prompt: sheepdogPrompt,
319
556
  permission: {
320
557
  read: "allow",
321
558
  glob: "allow",
@@ -326,16 +563,24 @@ export function createAgents(options = {}) {
326
563
  apply_patch: "deny",
327
564
  herdr_agent_response: "allow",
328
565
  ...stateToolPermissions(SHEEPDOG_STATE_TOOLS),
566
+ ...steeringToolPermissions([]),
567
+ ...rawSteeringToolPermissions([]),
568
+ ...ownershipToolPermissions([]),
329
569
  bash: {
330
570
  "*": "deny",
331
571
  ...safeGitInspection,
332
572
  ...herdrInspection,
573
+ ...SHEEPDOG_PANE_ALLOWS,
333
574
  ...spawnMatrix(SHEEPDOG_SPAWNABLE_AGENTS),
575
+ ...SHEEPDOG_HERDR_LIFECYCLE_ALLOWS,
334
576
  ...SHEEPDOG_LIFECYCLE_ALLOWS,
335
577
  ...SHEEPDOG_DENIALS,
578
+ ...SHEEPDOG_SEND_KEYS_DENY,
579
+ ...SHEEPDOG_SEND_KEYS_CTRL_C_ALLOWS,
336
580
  ...separatorDenials,
337
581
  },
338
582
  task: "deny",
583
+ ...sheepdogPermissions,
339
584
  },
340
585
  },
341
586
 
@@ -355,6 +600,9 @@ export function createAgents(options = {}) {
355
600
  apply_patch: "deny",
356
601
  herdr_agent_response: "deny",
357
602
  ...stateToolPermissions([]),
603
+ ...steeringToolPermissions([]),
604
+ ...rawSteeringToolPermissions([]),
605
+ ...ownershipToolPermissions([]),
358
606
  bash: { "*": "deny", ...safeGitInspection, ...separatorDenials },
359
607
  task: "deny",
360
608
  },
@@ -371,6 +619,9 @@ export function createAgents(options = {}) {
371
619
  grep: "allow",
372
620
  herdr_agent_response: "deny",
373
621
  ...stateToolPermissions([]),
622
+ ...steeringToolPermissions([]),
623
+ ...rawSteeringToolPermissions([]),
624
+ ...ownershipToolPermissions([]),
374
625
  bash: {
375
626
  "*": "allow",
376
627
  ...SHEEP_DENIALS,
@@ -382,13 +633,30 @@ export function createAgents(options = {}) {
382
633
 
383
634
  "shearer-low": reviewerAgent(reviewerModel, "low"),
384
635
  "shearer-medium": reviewerAgent(reviewerModel, "medium"),
636
+
637
+ developer: {
638
+ mode: "primary",
639
+ description: "Trusted Developer steering submitter; submits bounded steering via /steer; never implements flock work.",
640
+ prompt: DEVELOPER_PROMPT,
641
+ permission: {
642
+ ...stateToolPermissions([]),
643
+ ...steeringToolPermissions([STEERING_TOOLS.submit]),
644
+ ...rawSteeringToolPermissions([]),
645
+ ...ownershipToolPermissions([]),
646
+ herdr_agent_response: "deny",
647
+ task: "deny",
648
+ edit: "deny",
649
+ apply_patch: "deny",
650
+ bash: { "*": "deny", ...separatorDenials },
651
+ },
652
+ },
385
653
  };
386
654
  }
387
655
 
388
- function appendPrompt(prompt, addition) {
656
+ function appendPrompt(prompt, addition, optionName = "shepherdPromptAppend") {
389
657
  if (addition === undefined || addition === "") return prompt;
390
658
  if (typeof addition !== "string") {
391
- throw new TypeError("shepherdPromptAppend must be a string.");
659
+ throw new TypeError(`${optionName} must be a string.`);
392
660
  }
393
661
  return `${prompt.trimEnd()}\n\n${addition.trim()}`;
394
662
  }
@@ -410,6 +678,9 @@ function reviewerAgent(model, variant) {
410
678
  todowrite: "deny",
411
679
  herdr_agent_response: "deny",
412
680
  ...stateToolPermissions([]),
681
+ ...steeringToolPermissions([]),
682
+ ...rawSteeringToolPermissions([]),
683
+ ...ownershipToolPermissions([]),
413
684
  bash: { "*": "deny", ...safeGitInspection, ...separatorDenials },
414
685
  task: "deny",
415
686
  },
@@ -0,0 +1,117 @@
1
+ // 20-M2 operational diagnostics log (diagnostics only, never results).
2
+ // Strategy: process-local bounded in-memory ring buffer with no filesystem,
3
+ // no Git, no Herdr commands, no plugin tools, and no persistence. Entries are
4
+ // ephemeral like SHEPHERD_MODE and response cursors: they do not survive a
5
+ // plugin restart and must never substitute for authoritative retrieval via
6
+ // herdr_agent_response until complete is true.
7
+ // Guardrails: no chain of thought, no transcripts, no scrollback, bounded
8
+ // size per field, bounded retention per log, never read as results.
9
+ export const DIAGNOSTIC_EVENT_TYPES = Object.freeze([
10
+ "worker-started",
11
+ "prompt-submitted",
12
+ "state-changed",
13
+ "command-failed",
14
+ "settled",
15
+ "disappeared",
16
+ "timed-out",
17
+ "recovery-started",
18
+ ]);
19
+ const DIAGNOSTIC_EVENT_SET = new Set(DIAGNOSTIC_EVENT_TYPES);
20
+ export const MAX_DIAGNOSTIC_EVENTS_DEFAULT = 100;
21
+ export const MAX_DIAGNOSTIC_EVENTS_LIMIT = 1000;
22
+ export const MAX_DIAGNOSTIC_TARGET_CHARS = 64;
23
+ export const MAX_DIAGNOSTIC_CODE_CHARS = 64;
24
+ export const MAX_DIAGNOSTIC_DETAIL_CHARS = 512;
25
+ const SENSITIVE_DIAGNOSTIC_PATTERN = /(transcript|scrollback|chain\s*of\s*thought)/i;
26
+ function diagnosticError(code, message) {
27
+ return { ok: false, error: { code, message, retryable: false } };
28
+ }
29
+ function validateDiagnosticType(type) {
30
+ if (typeof type !== "string" || !DIAGNOSTIC_EVENT_SET.has(type)) {
31
+ return diagnosticError(
32
+ "INVALID_DIAGNOSTIC_TYPE",
33
+ `Diagnostic type must be one of: ${DIAGNOSTIC_EVENT_TYPES.join(", ")}.`,
34
+ );
35
+ }
36
+ return null;
37
+ }
38
+ function validateDiagnosticTarget(target) {
39
+ if (typeof target !== "string" || target.length === 0 || target.length > MAX_DIAGNOSTIC_TARGET_CHARS) {
40
+ return diagnosticError(
41
+ "INVALID_DIAGNOSTIC_TARGET",
42
+ `Diagnostic target must be a non-empty string of at most ${MAX_DIAGNOSTIC_TARGET_CHARS} characters.`,
43
+ );
44
+ }
45
+ return null;
46
+ }
47
+ function validateDiagnosticCode(code) {
48
+ if (code === undefined) return null;
49
+ if (typeof code !== "string" || code.length === 0 || code.length > MAX_DIAGNOSTIC_CODE_CHARS) {
50
+ return diagnosticError(
51
+ "INVALID_DIAGNOSTIC_CODE",
52
+ `Diagnostic code must be a non-empty string of at most ${MAX_DIAGNOSTIC_CODE_CHARS} characters.`,
53
+ );
54
+ }
55
+ if (SENSITIVE_DIAGNOSTIC_PATTERN.test(code)) {
56
+ return diagnosticError(
57
+ "SENSITIVE_CONTENT_EXCLUDED",
58
+ "Diagnostic code must not contain transcript, scrollback, or chain of thought content.",
59
+ );
60
+ }
61
+ return null;
62
+ }
63
+ function validateDiagnosticDetail(detail) {
64
+ if (detail === undefined) return null;
65
+ if (typeof detail !== "string" || detail.length > MAX_DIAGNOSTIC_DETAIL_CHARS) {
66
+ return diagnosticError(
67
+ "INVALID_DIAGNOSTIC_DETAIL",
68
+ `Diagnostic detail must be a string of at most ${MAX_DIAGNOSTIC_DETAIL_CHARS} characters; response text is never stored.`,
69
+ );
70
+ }
71
+ if (SENSITIVE_DIAGNOSTIC_PATTERN.test(detail)) {
72
+ return diagnosticError(
73
+ "SENSITIVE_CONTENT_EXCLUDED",
74
+ "Diagnostic detail must not contain transcript, scrollback, or chain of thought content; store only bounded operational summaries.",
75
+ );
76
+ }
77
+ return null;
78
+ }
79
+ export function createDiagnosticsLog(options = {}) {
80
+ const maxEvents = options.maxEvents ?? MAX_DIAGNOSTIC_EVENTS_DEFAULT;
81
+ if (!Number.isSafeInteger(maxEvents) || maxEvents < 1 || maxEvents > MAX_DIAGNOSTIC_EVENTS_LIMIT) {
82
+ throw new TypeError(`maxEvents must be an integer from 1 through ${MAX_DIAGNOSTIC_EVENTS_LIMIT}.`);
83
+ }
84
+ const now = options.now ?? Date.now;
85
+ let sequence = 0;
86
+ const events = [];
87
+ function record(type, fields = {}) {
88
+ const typeFailure = validateDiagnosticType(type);
89
+ if (typeFailure) return typeFailure;
90
+ const targetFailure = validateDiagnosticTarget(fields.target);
91
+ if (targetFailure) return targetFailure;
92
+ const codeFailure = validateDiagnosticCode(fields.code);
93
+ if (codeFailure) return codeFailure;
94
+ const detailFailure = validateDiagnosticDetail(fields.detail);
95
+ if (detailFailure) return detailFailure;
96
+ sequence += 1;
97
+ const event = {
98
+ sequence,
99
+ type,
100
+ target: fields.target,
101
+ ...(fields.code === undefined ? {} : { code: fields.code }),
102
+ ...(fields.detail === undefined ? {} : { detail: fields.detail }),
103
+ at: new Date(now()).toISOString(),
104
+ };
105
+ events.push(event);
106
+ while (events.length > maxEvents) events.shift();
107
+ return { ok: true, event: { ...event } };
108
+ }
109
+ function list() {
110
+ return { ok: true, events: events.map((event) => ({ ...event })), dropped: sequence - events.length, maxEvents };
111
+ }
112
+ function clear() {
113
+ events.length = 0;
114
+ return { ok: true, dropped: sequence, maxEvents };
115
+ }
116
+ return { record, list, clear, maxEvents };
117
+ }