opencode-herdr-orchestration 0.2.0 → 0.3.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/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,77 @@ 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 stays authoritative over these static
109
+ // entries: even a user override flipping one to "allow" must not bypass the
110
+ // allowlist. No flock role may present as Developer: developer is not a
111
+ // registered orchestration agent and no spawn matrix entry creates it.
112
+ export const DEVELOPER_AGENT = "developer";
113
+ export const ORCHESTRATION_ROLES = Object.freeze([
114
+ "shepherd",
115
+ "shepherd-governor",
116
+ "sheepdog",
117
+ "grazer",
118
+ "sheep",
119
+ "shearer-low",
120
+ "shearer-medium",
121
+ ]);
122
+ export const STEERING_TOOLS = Object.freeze({
123
+ submit: "herdr_steering_submit",
124
+ });
125
+ export const STEERING_TOOL_ACCESS = Object.freeze(
126
+ new Map([[STEERING_TOOLS.submit, new Set([DEVELOPER_AGENT])]]),
127
+ );
128
+ const ALL_STEERING_TOOLS = Object.freeze(Object.values(STEERING_TOOLS));
129
+
130
+ function steeringToolPermissions(allowedTools) {
131
+ return Object.fromEntries(
132
+ ALL_STEERING_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
133
+ );
134
+ }
135
+
136
+ // Shepherd ownership raw steering plus lifecycle tools (M3). Only the two
137
+ // Shepherd phases may check, read, or consume raw steering and only the
138
+ // recorded owner phase plus session plus generation passes the state-level
139
+ // fencing (NOT AUTHORITATIVE PHASE otherwise). Sheepdog and every leaf are
140
+ // explicitly denied in code, not only in prompts; the runtime check in
141
+ // src/index.js stays authoritative over static overrides.
142
+ export const SHEPHERD_PHASES = Object.freeze(["shepherd", "shepherd-governor"]);
143
+ export const RAW_STEERING_TOOLS = Object.freeze({
144
+ check: "herdr_steering_check",
145
+ read: "herdr_steering_read",
146
+ consume: "herdr_steering_consume",
147
+ });
148
+ export const OWNERSHIP_TOOLS = Object.freeze({
149
+ claim: "herdr_ownership_claim",
150
+ read: "herdr_ownership_read",
151
+ sync: "herdr_ownership_sync",
152
+ snapshot: "herdr_ownership_snapshot",
153
+ correct: "herdr_ownership_correct",
154
+ });
155
+ export const RAW_STEERING_TOOL_ACCESS = Object.freeze(
156
+ new Map(Object.values(RAW_STEERING_TOOLS).map((name) => [name, new Set(SHEPHERD_PHASES)])),
157
+ );
158
+ export const OWNERSHIP_TOOL_ACCESS = Object.freeze(
159
+ new Map(Object.values(OWNERSHIP_TOOLS).map((name) => [name, new Set(SHEPHERD_PHASES)])),
160
+ );
161
+ const ALL_RAW_STEERING_TOOLS = Object.freeze(Object.values(RAW_STEERING_TOOLS));
162
+ const ALL_OWNERSHIP_TOOLS = Object.freeze(Object.values(OWNERSHIP_TOOLS));
163
+
164
+ function rawSteeringToolPermissions(allowedTools) {
165
+ return Object.fromEntries(
166
+ ALL_RAW_STEERING_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
167
+ );
168
+ }
169
+
170
+ function ownershipToolPermissions(allowedTools) {
171
+ return Object.fromEntries(
172
+ ALL_OWNERSHIP_TOOLS.map((name) => [name, allowedTools.includes(name) ? "allow" : "deny"]),
173
+ );
174
+ }
175
+
99
176
  const SHEPHERD_STATE_TOOLS = [STATE_TOOLS.planWrite, STATE_TOOLS.planRead];
100
177
  const GOVERNOR_STATE_TOOLS = [STATE_TOOLS.planRead];
101
178
  const SHEEPDOG_STATE_TOOLS = [
@@ -103,6 +180,8 @@ const SHEEPDOG_STATE_TOOLS = [
103
180
  STATE_TOOLS.executionWrite,
104
181
  STATE_TOOLS.executionRead,
105
182
  ];
183
+ const SHEPHERD_RAW_STEERING_TOOLS = Object.values(RAW_STEERING_TOOLS);
184
+ const SHEPHERD_OWNERSHIP_TOOLS = Object.values(OWNERSHIP_TOOLS);
106
185
 
107
186
  const SHEPHERD_SPAWNABLE_AGENTS = ["grazer"];
108
187
  const GOVERNOR_SPAWNABLE_AGENTS = ["grazer", "sheepdog"];
@@ -122,6 +201,148 @@ const SHEEPDOG_LIFECYCLE_ALLOWS = {
122
201
  "git commit*": "allow",
123
202
  };
124
203
 
204
+ // Sheepdog Herdr lifecycle (21-M1): explicit prompt plus wait plus get plus
205
+ // read allows. These re-assert the shared herdrInspection entries so the four
206
+ // lifecycle operations stay evaluation-effective under OpenCode last-match
207
+ // glob semantics: "*" deny is the fallback first, these allows sit in the
208
+ // middle, separator denies stay global last. A prompt whose task text carries
209
+ // raw "; && || | > <" matches a separator deny after the prompt allow and
210
+ // fails closed to deny even inside quotes, so task text must avoid raw
211
+ // separators (see SHEEPDOG_PROMPT safe rule).
212
+ const SHEEPDOG_HERDR_LIFECYCLE_ALLOWS = {
213
+ "herdr agent prompt*": "allow",
214
+ "herdr agent wait*": "allow",
215
+ "herdr agent get*": "allow",
216
+ "herdr agent read*": "allow",
217
+ };
218
+
219
+ // Sheepdog interrupt bound (21-M1): replacement for the broad
220
+ // "herdr agent send-keys*" allow in shared herdrInspection. The broad pattern
221
+ // is explicitly denied first so only the narrow Ctrl-C spellings below are
222
+ // evaluation-effective. Matchers are string globs and cannot prove semantic
223
+ // intent, so the prompt inspect-first plus never-type rule stays primary; see
224
+ // SHEEPDOG_PROMPT and README Worker interruption residual.
225
+ const SHEEPDOG_SEND_KEYS_DENY = {
226
+ "herdr agent send-keys*": "deny",
227
+ };
228
+ const SHEEPDOG_SEND_KEYS_CTRL_C_ALLOWS = {
229
+ "herdr agent send-keys * --keys C-c*": "allow",
230
+ "herdr agent send-keys * --keys c-c*": "allow",
231
+ "herdr agent send-keys * --keys ctrl+c*": "allow",
232
+ "herdr agent send-keys * --keys Ctrl+C*": "allow",
233
+ "herdr agent send-keys * C-c*": "allow",
234
+ "herdr agent send-keys * ctrl+c*": "allow",
235
+ };
236
+
237
+ // Governor Herdr prompt residual (21-M2): `herdr agent prompt` plus wait plus
238
+ // get plus read patterns are name-based and cannot encode worker role, so they
239
+ // stay broad as far as text matchers permit. Prompt bans plus start denial
240
+ // (spawn matrix allows only grazer and sheepdog) plus the response matrix
241
+ // (retrieval allows only grazer and sheepdog) are the load-bearing layers;
242
+ // see SHEPHERD_GOVERNOR_PROMPT and README Governor prompt scoping residual.
243
+ // Governor interrupt bound (21-M2): replacement for the broad
244
+ // "herdr agent send-keys*" allow in shared herdrInspection, mirroring the
245
+ // sheepdog M1 bound. The broad pattern is explicitly denied first so only the
246
+ // narrow Ctrl-C spellings below are evaluation-effective. Matchers are string
247
+ // globs and cannot prove semantic intent, so the prompt inspect-first plus
248
+ // never-type plus never-bypass rule stays primary.
249
+ const GOVERNOR_SEND_KEYS_DENY = {
250
+ "herdr agent send-keys*": "deny",
251
+ };
252
+ const GOVERNOR_SEND_KEYS_CTRL_C_ALLOWS = {
253
+ "herdr agent send-keys * --keys C-c*": "allow",
254
+ "herdr agent send-keys * --keys c-c*": "allow",
255
+ "herdr agent send-keys * --keys ctrl+c*": "allow",
256
+ "herdr agent send-keys * --keys Ctrl+C*": "allow",
257
+ "herdr agent send-keys * C-c*": "allow",
258
+ "herdr agent send-keys * ctrl+c*": "allow",
259
+ };
260
+
261
+ // Pane layout (14-18-M1): live discovery on installed 0.8.2 via full-path
262
+ // `herdr --help` plus `herdr --skill` plus scoped read-only queries
263
+ // (`pane list --workspace w1K`, `tab list --workspace w1K`,
264
+ // `pane current --current`, `pane layout --current`, `agent get <name>`,
265
+ // `pane get w1K:p999` error sampling). Direct `herdr ...` spelling stays
266
+ // denied for leaves; discovery used only read-only help plus list plus get
267
+ // plus current plus layout, never split plus rename plus close plus start.
268
+ // Tab evidenced as list plus create plus get plus focus plus rename plus
269
+ // close; list takes `--workspace <ID>`, create takes
270
+ // `--workspace/--cwd/--label/--env/--focus/--no-focus`, rename takes
271
+ // `<TAB_ID> <LABEL>...`, close plus get plus focus take `<tab_id>`.
272
+ // Pane placement evidenced as split plus move plus focus plus resize plus
273
+ // swap plus layout plus get plus list plus current plus read plus rename plus
274
+ // close; split is `[PANE_ID] --pane/--current --direction right/down --ratio
275
+ // --cwd --env --focus/--no-focus`, move is `<PANE_ID>
276
+ // --tab/--split/--target-pane/--ratio/--new-tab/--workspace/--new-workspace/--label/--tab-label/--focus/--no-focus`,
277
+ // focus and neighbor take `--direction left/right/up/down --pane/--current`,
278
+ // resize takes `--direction --amount --pane/--current`, swap takes
279
+ // `--direction/--pane/--current/--source-pane/--target-pane`, layout takes
280
+ // `--pane/--current`, get takes `<pane_id>`, list takes
281
+ // `--workspace <ID>`, current takes `--pane/--current`, read takes
282
+ // `<PANE_ID> --source visible/recent/recent-unwrapped/detection
283
+ // --lines/--format/--ansi/--raw`, rename takes `<PANE_ID> [LABEL]...
284
+ // --clear`, close takes `<pane_id>`.
285
+ // Agent rename evidenced as `herdr agent rename <TARGET> <NAME>|--clear`
286
+ // (live `agent list` shows `name` such as `issue1418m1sheep` and `agent get`
287
+ // shows `name` plus `pane_id`); pane rename plus tab rename helps evidenced
288
+ // but only pane plus agent rename are enabled, tab rename stays denied to
289
+ // keep the single-tab 4-pane cap.
290
+ // Count queries evidenced as `herdr tab list --workspace <ID>` returning
291
+ // `{"id":"cli:tab:list","result":{"tabs":[{"pane_count":1,"tab_id":"w1K:t1",...}]},"type":"tab_list"}`,
292
+ // `herdr pane list --workspace <ID>` returning
293
+ // `{"id":"cli:pane:list","result":{"panes":[{...,"pane_id":"w1K:p1","tab_id":"w1K:t1","workspace_id":"w1K",...}]},"type":"pane_list"}`
294
+ // (count is length filtered by workspace plus tab), `herdr pane layout
295
+ // --current` returning
296
+ // `{"id":"cli:pane:layout","result":{"layout":{"panes":[...],"splits":[],"focused_pane_id":"w1K:p1",...}},"type":"pane_layout"}`,
297
+ // `herdr pane current --current` returning
298
+ // `{"id":"cli:pane:current","result":{"pane":{...}},"type":"pane_current"}`.
299
+ // Creation JSON per `herdr --skill`: `tab create` returns
300
+ // `.result.tab` plus `.result.root_pane`, `pane split` returns the new pane
301
+ // as `.result.pane` (skill geometry is `pane split --current --direction
302
+ // right/down --cwd "$PWD" --no-focus`, wide to the right and narrow or tall
303
+ // down, reading `.result.pane.pane_id` and never deriving from sidebar
304
+ // order), `pane move` would return
305
+ // `.result.move_result.pane.pane_id` plus `.result.move_result.previous_pane_id`
306
+ // but move stays denied and is documented only.
307
+ // Exits per `herdr --skill` plus live sampling: most controls return JSON on
308
+ // stdout with `id` plus `result` plus `type`; server errors are JSON such as
309
+ // `{"error":{"code":"pane_not_found","message":"pane w1K:p999 not found"},"id":"cli:pane:get"}`
310
+ // on stderr with exit 1; syntax plus validation such as `pane split
311
+ // --direction invalid` returning `invalid split direction` exits with 2.
312
+ // IDs are opaque stable handles (`w1K`, `w1K:t1`, `w1K:p1` from
313
+ // `HERDR_WORKSPACE_ID` plus `HERDR_TAB_ID` plus `HERDR_PANE_ID`); closed IDs
314
+ // are not reused; prefer `--current` and never rely on the UI-focused pane.
315
+ // Fallback if any of tab list plus pane get plus rename plus close plus
316
+ // split are missing: reuse the current pane via `--pane` plus `--current`,
317
+ // report STOP naming the missing capability, never invent `herdr pane
318
+ // create*` plus `herdr tab split*` plus `herdr agent events*` plus
319
+ // `herdr pane move*` plus `herdr pane resize*` plus `herdr workspace
320
+ // create*` behavior.
321
+ // Protected Dev Developer Terminal exclusion cannot be matcher-enforced:
322
+ // pane IDs are opaque and labels are absent from scan plus split plus close
323
+ // command strings, while a `*Dev*` glob would overmatch legitimate
324
+ // `--cwd C:\Dev\...` values, so no such glob is added; the prompt plus
325
+ // README Pane layout policy exclusion stays primary; see README residual.
326
+ const SHEPHERD_PANE_ALLOWS = {
327
+ "herdr tab list*": "allow",
328
+ "herdr pane get*": "allow",
329
+ "herdr pane rename*": "allow",
330
+ "herdr agent rename*": "allow",
331
+ };
332
+ const GOVERNOR_PANE_ALLOWS = {
333
+ "herdr tab list*": "allow",
334
+ "herdr pane get*": "allow",
335
+ "herdr pane rename*": "allow",
336
+ "herdr agent rename*": "allow",
337
+ };
338
+ const SHEEPDOG_PANE_ALLOWS = {
339
+ "herdr tab list*": "allow",
340
+ "herdr pane get*": "allow",
341
+ "herdr pane rename*": "allow",
342
+ "herdr agent rename*": "allow",
343
+ "herdr pane close*": "allow",
344
+ };
345
+
125
346
  const SHEEPDOG_DENIALS = {
126
347
  "git push*": "deny",
127
348
  "git pull*": "deny",
@@ -197,11 +418,14 @@ export function createAgents(options = {}) {
197
418
  const shepherdModel = options.shepherdModel;
198
419
  const workerModel = options.workerModel ?? "litellm/glm-5.3-flash";
199
420
  const workerVariant = options.workerVariant;
421
+ const grazerVariant = options.grazerVariant ?? workerVariant;
200
422
  const sheepdogModel = options.sheepdogModel ?? "litellm/glm-5.3-flash";
201
423
  const sheepdogVariant = options.sheepdogVariant;
202
424
  const reviewerModel = options.reviewerModel ?? "litellm-responses/gpt-5.6-terra";
203
425
  const shepherdPermissions = options.shepherdPermissions ?? {};
204
- const shepherdPrompt = appendPrompt(SHEPHERD_PROMPT, options.shepherdPromptAppend);
426
+ const shepherdPrompt = appendPrompt(SHEPHERD_PROMPT, options.shepherdPromptAppend, "shepherdPromptAppend");
427
+ const sheepdogPermissions = options.sheepdogPermissions ?? {};
428
+ const sheepdogPrompt = appendPrompt(SHEEPDOG_PROMPT, options.sheepdogPromptAppend, "sheepdogPromptAppend");
205
429
 
206
430
  return {
207
431
  shepherd: {
@@ -217,9 +441,13 @@ export function createAgents(options = {}) {
217
441
  apply_patch: markdownOnly,
218
442
  herdr_agent_response: "allow",
219
443
  ...stateToolPermissions(SHEPHERD_STATE_TOOLS),
444
+ ...steeringToolPermissions([]),
445
+ ...rawSteeringToolPermissions(SHEPHERD_RAW_STEERING_TOOLS),
446
+ ...ownershipToolPermissions(SHEPHERD_OWNERSHIP_TOOLS),
220
447
  bash: {
221
448
  "*": "deny",
222
449
  ...herdrInspection,
450
+ ...SHEPHERD_PANE_ALLOWS,
223
451
  ...spawnMatrix(SHEPHERD_SPAWNABLE_AGENTS),
224
452
  "git status*": "allow",
225
453
  "git diff*": "allow",
@@ -260,9 +488,13 @@ export function createAgents(options = {}) {
260
488
  apply_patch: markdownOnly,
261
489
  herdr_agent_response: "allow",
262
490
  ...stateToolPermissions(GOVERNOR_STATE_TOOLS),
491
+ ...steeringToolPermissions([]),
492
+ ...rawSteeringToolPermissions(SHEPHERD_RAW_STEERING_TOOLS),
493
+ ...ownershipToolPermissions(SHEPHERD_OWNERSHIP_TOOLS),
263
494
  bash: {
264
495
  "*": "deny",
265
496
  ...herdrInspection,
497
+ ...GOVERNOR_PANE_ALLOWS,
266
498
  ...spawnMatrix(GOVERNOR_SPAWNABLE_AGENTS),
267
499
  "git status*": "allow",
268
500
  "git diff*": "allow",
@@ -302,6 +534,8 @@ export function createAgents(options = {}) {
302
534
  "gh pr create*": "allow",
303
535
  "gh pr view*": "allow",
304
536
  "gh pr checks*": "allow",
537
+ ...GOVERNOR_SEND_KEYS_DENY,
538
+ ...GOVERNOR_SEND_KEYS_CTRL_C_ALLOWS,
305
539
  ...separatorDenials,
306
540
  },
307
541
  task: "deny",
@@ -314,7 +548,7 @@ export function createAgents(options = {}) {
314
548
  ...(sheepdogVariant ? { variant: sheepdogVariant } : {}),
315
549
  description:
316
550
  "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.",
317
- prompt: SHEEPDOG_PROMPT,
551
+ prompt: sheepdogPrompt,
318
552
  permission: {
319
553
  read: "allow",
320
554
  glob: "allow",
@@ -325,23 +559,31 @@ export function createAgents(options = {}) {
325
559
  apply_patch: "deny",
326
560
  herdr_agent_response: "allow",
327
561
  ...stateToolPermissions(SHEEPDOG_STATE_TOOLS),
562
+ ...steeringToolPermissions([]),
563
+ ...rawSteeringToolPermissions([]),
564
+ ...ownershipToolPermissions([]),
328
565
  bash: {
329
566
  "*": "deny",
330
567
  ...safeGitInspection,
331
568
  ...herdrInspection,
569
+ ...SHEEPDOG_PANE_ALLOWS,
332
570
  ...spawnMatrix(SHEEPDOG_SPAWNABLE_AGENTS),
571
+ ...SHEEPDOG_HERDR_LIFECYCLE_ALLOWS,
333
572
  ...SHEEPDOG_LIFECYCLE_ALLOWS,
334
573
  ...SHEEPDOG_DENIALS,
574
+ ...SHEEPDOG_SEND_KEYS_DENY,
575
+ ...SHEEPDOG_SEND_KEYS_CTRL_C_ALLOWS,
335
576
  ...separatorDenials,
336
577
  },
337
578
  task: "deny",
579
+ ...sheepdogPermissions,
338
580
  },
339
581
  },
340
582
 
341
583
  grazer: {
342
584
  mode: "primary",
343
585
  model: workerModel,
344
- ...(workerVariant ? { variant: workerVariant } : {}),
586
+ ...(grazerVariant ? { variant: grazerVariant } : {}),
345
587
  description: "Performs read-only repository research for the shepherd, shepherd-governor, or sheepdog.",
346
588
  prompt: GRAZER_PROMPT,
347
589
  permission: {
@@ -354,6 +596,9 @@ export function createAgents(options = {}) {
354
596
  apply_patch: "deny",
355
597
  herdr_agent_response: "deny",
356
598
  ...stateToolPermissions([]),
599
+ ...steeringToolPermissions([]),
600
+ ...rawSteeringToolPermissions([]),
601
+ ...ownershipToolPermissions([]),
357
602
  bash: { "*": "deny", ...safeGitInspection, ...separatorDenials },
358
603
  task: "deny",
359
604
  },
@@ -370,6 +615,9 @@ export function createAgents(options = {}) {
370
615
  grep: "allow",
371
616
  herdr_agent_response: "deny",
372
617
  ...stateToolPermissions([]),
618
+ ...steeringToolPermissions([]),
619
+ ...rawSteeringToolPermissions([]),
620
+ ...ownershipToolPermissions([]),
373
621
  bash: {
374
622
  "*": "allow",
375
623
  ...SHEEP_DENIALS,
@@ -384,10 +632,10 @@ export function createAgents(options = {}) {
384
632
  };
385
633
  }
386
634
 
387
- function appendPrompt(prompt, addition) {
635
+ function appendPrompt(prompt, addition, optionName = "shepherdPromptAppend") {
388
636
  if (addition === undefined || addition === "") return prompt;
389
637
  if (typeof addition !== "string") {
390
- throw new TypeError("shepherdPromptAppend must be a string.");
638
+ throw new TypeError(`${optionName} must be a string.`);
391
639
  }
392
640
  return `${prompt.trimEnd()}\n\n${addition.trim()}`;
393
641
  }
@@ -409,6 +657,9 @@ function reviewerAgent(model, variant) {
409
657
  todowrite: "deny",
410
658
  herdr_agent_response: "deny",
411
659
  ...stateToolPermissions([]),
660
+ ...steeringToolPermissions([]),
661
+ ...rawSteeringToolPermissions([]),
662
+ ...ownershipToolPermissions([]),
412
663
  bash: { "*": "deny", ...safeGitInspection, ...separatorDenials },
413
664
  task: "deny",
414
665
  },
@@ -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
+ }