@hegemonart/get-design-done 1.19.6 → 1.21.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.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +11 -14
  2. package/.claude-plugin/plugin.json +9 -32
  3. package/CHANGELOG.md +138 -0
  4. package/README.md +54 -1
  5. package/agents/design-reflector.md +13 -0
  6. package/bin/gdd-sdk +55 -0
  7. package/connections/connections.md +3 -0
  8. package/connections/figma.md +2 -0
  9. package/connections/gdd-state.md +186 -0
  10. package/hooks/budget-enforcer.ts +716 -0
  11. package/hooks/context-exhaustion.ts +251 -0
  12. package/hooks/gdd-read-injection-scanner.ts +172 -0
  13. package/hooks/hooks.json +3 -3
  14. package/package.json +32 -51
  15. package/reference/codex-tools.md +53 -0
  16. package/reference/config-schema.md +2 -2
  17. package/reference/error-recovery.md +58 -0
  18. package/reference/gemini-tools.md +53 -0
  19. package/reference/registry.json +21 -0
  20. package/reference/schemas/budget.schema.json +42 -0
  21. package/reference/schemas/events.schema.json +55 -0
  22. package/reference/schemas/generated.d.ts +419 -0
  23. package/reference/schemas/iteration-budget.schema.json +36 -0
  24. package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
  25. package/reference/schemas/rate-limits.schema.json +31 -0
  26. package/scripts/aggregate-agent-metrics.ts +282 -0
  27. package/scripts/codegen-schema-types.ts +149 -0
  28. package/scripts/e2e/run-headless.ts +514 -0
  29. package/scripts/lib/cli/commands/audit.ts +382 -0
  30. package/scripts/lib/cli/commands/init.ts +217 -0
  31. package/scripts/lib/cli/commands/query.ts +329 -0
  32. package/scripts/lib/cli/commands/run.ts +656 -0
  33. package/scripts/lib/cli/commands/stage.ts +468 -0
  34. package/scripts/lib/cli/index.ts +167 -0
  35. package/scripts/lib/cli/parse-args.ts +336 -0
  36. package/scripts/lib/context-engine/index.ts +116 -0
  37. package/scripts/lib/context-engine/manifest.ts +69 -0
  38. package/scripts/lib/context-engine/truncate.ts +282 -0
  39. package/scripts/lib/context-engine/types.ts +59 -0
  40. package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
  41. package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
  42. package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
  43. package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
  44. package/scripts/lib/error-classifier.cjs +232 -0
  45. package/scripts/lib/error-classifier.d.cts +44 -0
  46. package/scripts/lib/event-stream/emitter.ts +88 -0
  47. package/scripts/lib/event-stream/index.ts +164 -0
  48. package/scripts/lib/event-stream/types.ts +127 -0
  49. package/scripts/lib/event-stream/writer.ts +154 -0
  50. package/scripts/lib/explore-parallel-runner/index.ts +294 -0
  51. package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
  52. package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
  53. package/scripts/lib/explore-parallel-runner/types.ts +139 -0
  54. package/scripts/lib/gdd-errors/classification.ts +124 -0
  55. package/scripts/lib/gdd-errors/index.ts +218 -0
  56. package/scripts/lib/gdd-state/gates.ts +216 -0
  57. package/scripts/lib/gdd-state/index.ts +167 -0
  58. package/scripts/lib/gdd-state/lockfile.ts +232 -0
  59. package/scripts/lib/gdd-state/mutator.ts +574 -0
  60. package/scripts/lib/gdd-state/parser.ts +523 -0
  61. package/scripts/lib/gdd-state/types.ts +179 -0
  62. package/scripts/lib/harness/detect.ts +90 -0
  63. package/scripts/lib/harness/index.ts +64 -0
  64. package/scripts/lib/harness/tool-map.ts +142 -0
  65. package/scripts/lib/init-runner/index.ts +396 -0
  66. package/scripts/lib/init-runner/researchers.ts +245 -0
  67. package/scripts/lib/init-runner/scaffold.ts +224 -0
  68. package/scripts/lib/init-runner/synthesizer.ts +224 -0
  69. package/scripts/lib/init-runner/types.ts +143 -0
  70. package/scripts/lib/iteration-budget.cjs +205 -0
  71. package/scripts/lib/iteration-budget.d.cts +32 -0
  72. package/scripts/lib/jittered-backoff.cjs +112 -0
  73. package/scripts/lib/jittered-backoff.d.cts +38 -0
  74. package/scripts/lib/lockfile.cjs +177 -0
  75. package/scripts/lib/lockfile.d.cts +21 -0
  76. package/scripts/lib/logger/index.ts +251 -0
  77. package/scripts/lib/logger/sinks.ts +269 -0
  78. package/scripts/lib/logger/types.ts +110 -0
  79. package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
  80. package/scripts/lib/pipeline-runner/index.ts +527 -0
  81. package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
  82. package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
  83. package/scripts/lib/pipeline-runner/types.ts +183 -0
  84. package/scripts/lib/prompt-sanitizer/index.ts +435 -0
  85. package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
  86. package/scripts/lib/rate-guard.cjs +365 -0
  87. package/scripts/lib/rate-guard.d.cts +38 -0
  88. package/scripts/lib/session-runner/errors.ts +406 -0
  89. package/scripts/lib/session-runner/index.ts +715 -0
  90. package/scripts/lib/session-runner/transcript.ts +189 -0
  91. package/scripts/lib/session-runner/types.ts +144 -0
  92. package/scripts/lib/tool-scoping/index.ts +219 -0
  93. package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
  94. package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
  95. package/scripts/lib/tool-scoping/types.ts +77 -0
  96. package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
  97. package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
  98. package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
  99. package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
  100. package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
  101. package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
  102. package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
  103. package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
  104. package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
  105. package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
  106. package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
  107. package/scripts/mcp-servers/gdd-state/server.ts +288 -0
  108. package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
  109. package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
  110. package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
  111. package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
  112. package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
  113. package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
  114. package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
  115. package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
  116. package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
  117. package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
  118. package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
  119. package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
  120. package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
  121. package/scripts/validate-frontmatter.ts +114 -0
  122. package/scripts/validate-schemas.ts +401 -0
  123. package/skills/brief/SKILL.md +15 -6
  124. package/skills/design/SKILL.md +31 -13
  125. package/skills/explore/SKILL.md +41 -17
  126. package/skills/health/SKILL.md +15 -4
  127. package/skills/optimize/SKILL.md +3 -3
  128. package/skills/pause/SKILL.md +16 -10
  129. package/skills/plan/SKILL.md +33 -17
  130. package/skills/progress/SKILL.md +15 -11
  131. package/skills/resume/SKILL.md +19 -10
  132. package/skills/settings/SKILL.md +11 -3
  133. package/skills/todo/SKILL.md +12 -3
  134. package/skills/verify/SKILL.md +65 -29
  135. package/hooks/budget-enforcer.js +0 -329
  136. package/hooks/context-exhaustion.js +0 -127
  137. package/hooks/gdd-read-injection-scanner.js +0 -39
  138. package/scripts/aggregate-agent-metrics.js +0 -173
  139. package/scripts/validate-frontmatter.cjs +0 -68
  140. package/scripts/validate-schemas.cjs +0 -242
@@ -0,0 +1,164 @@
1
+ // scripts/lib/event-stream/index.ts — public API for the Phase 20+
2
+ // telemetry stream (Plan 20-06, SDK-08).
3
+ //
4
+ // Consumers import ONLY from this file. The internal parts
5
+ // (`./types.ts`, `./writer.ts`, `./emitter.ts`) are implementation
6
+ // detail; changing them without updating this export surface is a
7
+ // breaking change for downstream plans (20-05 MCP handlers, 20-13 hooks).
8
+ //
9
+ // Surface:
10
+ // * appendEvent(ev) — persist + broadcast one event
11
+ // * getWriter(opts?) — lazy singleton EventWriter
12
+ // * getBus() — lazy singleton EventBus
13
+ // * reset() — clear module-level singletons (tests)
14
+ // * types — BaseEvent, KnownEvent, and every pre-
15
+ // registered subtype (StateMutationEvent,
16
+ // StateTransitionEvent, …).
17
+
18
+ import { hostname } from 'node:os';
19
+
20
+ import { EventBus } from './emitter.ts';
21
+ import type { Unsubscribe, EventHandler } from './emitter.ts';
22
+ import { EventWriter } from './writer.ts';
23
+ import type { WriterOptions } from './writer.ts';
24
+ import type { BaseEvent, EventMeta } from './types.ts';
25
+
26
+ export type {
27
+ BaseEvent,
28
+ EventMeta,
29
+ KnownEvent,
30
+ StateMutationEvent,
31
+ StateTransitionEvent,
32
+ StageEnteredEvent,
33
+ StageExitedEvent,
34
+ HookFiredEvent,
35
+ ErrorEvent,
36
+ } from './types.ts';
37
+ export { EventBus } from './emitter.ts';
38
+ export type { EventHandler, Unsubscribe } from './emitter.ts';
39
+ export { EventWriter, DEFAULT_EVENTS_PATH, DEFAULT_MAX_LINE_BYTES } from './writer.ts';
40
+ export type { WriterOptions } from './writer.ts';
41
+
42
+ /**
43
+ * Lazily-constructed module-level singletons. `getWriter()` honors the
44
+ * first `opts` it receives; subsequent calls with different options are
45
+ * ignored. Tests that need to vary options across runs should call
46
+ * {@link reset} between runs.
47
+ */
48
+ let defaultWriter: EventWriter | null = null;
49
+ let defaultBus: EventBus | null = null;
50
+ /**
51
+ * Cached host name. `os.hostname()` is cheap but not free (syscall on
52
+ * some platforms) and we stamp it onto every event; compute once.
53
+ */
54
+ let cachedHost: string | null = null;
55
+
56
+ /**
57
+ * Return the module-level default writer, constructing it on first
58
+ * call. Passing `opts` on subsequent calls is a no-op (the first
59
+ * caller wins); that matches the "single shared file per process"
60
+ * intent.
61
+ */
62
+ export function getWriter(opts?: WriterOptions): EventWriter {
63
+ if (defaultWriter === null) {
64
+ // Honor GDD_EVENTS_PATH env var as the first-choice default path
65
+ // when the caller doesn't pass an explicit `opts.path`. Lets test
66
+ // harnesses and Plan 21-11's E2E subprocess steer the on-disk
67
+ // stream into a fixture-specific directory without chdir'ing the
68
+ // entire process. Explicit `opts.path` always wins.
69
+ const envPath: string | undefined = process.env['GDD_EVENTS_PATH'];
70
+ const finalOpts: WriterOptions =
71
+ opts?.path === undefined && envPath !== undefined && envPath.length > 0
72
+ ? { ...(opts ?? {}), path: envPath }
73
+ : (opts ?? {});
74
+ defaultWriter = new EventWriter(finalOpts);
75
+ }
76
+ return defaultWriter;
77
+ }
78
+
79
+ /** Return the module-level default bus, constructing it on first call. */
80
+ export function getBus(): EventBus {
81
+ if (defaultBus === null) {
82
+ defaultBus = new EventBus();
83
+ }
84
+ return defaultBus;
85
+ }
86
+
87
+ /**
88
+ * Persist `ev` to the on-disk JSONL stream AND broadcast it to the
89
+ * in-process bus. This is the normal emission path for every Phase 20+
90
+ * event producer.
91
+ *
92
+ * Ordering:
93
+ * 1. Stamp `_meta` (pid/host/source) if the caller didn't supply it.
94
+ * 2. Persist via `getWriter().append(ev)` — sync, never throws.
95
+ * 3. Broadcast via `getBus().emit(ev.type, ev)` AND `emit('*', ev)`
96
+ * so typed subscribers and `subscribeAll` observers both see it.
97
+ *
98
+ * Bus emission can still throw if a subscriber handler throws; we
99
+ * intentionally surface that rather than silently swallowing, since a
100
+ * failing handler is a programming bug, not an expected runtime
101
+ * condition. Plan 20-13's hooks wrap their handler bodies defensively
102
+ * for this reason.
103
+ */
104
+ export function appendEvent(ev: BaseEvent): void {
105
+ // Stamp writer-injected metadata if absent. We don't clone the full
106
+ // event — callers typically build it fresh per emission — but we do
107
+ // need to ensure `_meta` is present by the time we persist.
108
+ if (ev._meta === undefined) {
109
+ if (cachedHost === null) {
110
+ try {
111
+ cachedHost = hostname();
112
+ } catch {
113
+ cachedHost = 'unknown';
114
+ }
115
+ }
116
+ const meta: EventMeta = {
117
+ pid: process.pid,
118
+ host: cachedHost,
119
+ source: 'event-stream',
120
+ };
121
+ ev._meta = meta;
122
+ }
123
+
124
+ // Persist first. Bus emission is synchronous; if a subscriber throws
125
+ // after we've persisted, the durable record is already safe.
126
+ getWriter().append(ev);
127
+
128
+ const bus = getBus();
129
+ bus.emit(ev.type, ev);
130
+ bus.emit('*', ev);
131
+ }
132
+
133
+ /**
134
+ * Reset module-level singletons. Intended for tests that want a fresh
135
+ * writer (e.g. pointed at a new temp directory) or a fresh bus (e.g.
136
+ * to assert isolation between test cases).
137
+ *
138
+ * Safe to call from production code but the intended caller is a test.
139
+ * `appendEvent()` will lazily reconstruct both singletons on the next
140
+ * emission.
141
+ */
142
+ export function reset(): void {
143
+ if (defaultBus !== null) {
144
+ defaultBus.removeAllListeners();
145
+ }
146
+ defaultWriter = null;
147
+ defaultBus = null;
148
+ }
149
+
150
+ // Re-export `subscribe`/`subscribeAll` convenience: some callers only
151
+ // need to subscribe, not emit, and `getBus().subscribe(…)` reads fine
152
+ // but the shorter form keeps consumer code terse.
153
+ /** Convenience: subscribe to one event type on the default bus. */
154
+ export function subscribe<T extends BaseEvent = BaseEvent>(
155
+ type: T['type'],
156
+ handler: EventHandler<T>,
157
+ ): Unsubscribe {
158
+ return getBus().subscribe<T>(type, handler);
159
+ }
160
+
161
+ /** Convenience: subscribe to every event on the default bus. */
162
+ export function subscribeAll(handler: EventHandler<BaseEvent>): Unsubscribe {
163
+ return getBus().subscribeAll(handler);
164
+ }
@@ -0,0 +1,127 @@
1
+ // scripts/lib/event-stream/types.ts — typed event envelope + pre-registered
2
+ // event shapes, per Plan 20-06 (SDK-08).
3
+ //
4
+ // The event stream is the Phase 20+ observability primitive that every
5
+ // downstream consumer (Plan 20-05 MCP tool handlers, Plan 20-13 hooks)
6
+ // builds on. A single append-only JSONL file at
7
+ // `.design/telemetry/events.jsonl` holds the persisted form; an in-process
8
+ // `EventEmitter` bus (see `./emitter.ts`) broadcasts the same events
9
+ // live to subscribers within the same Node process.
10
+ //
11
+ // Envelope invariants (also encoded in `reference/schemas/events.schema.json`):
12
+ // * `type` — required, string, free-form. Pre-registered subtypes
13
+ // below are merely the seeded set; unknown types are
14
+ // allowed (validation is structural, not a closed enum).
15
+ // * `timestamp` — required, ISO-8601 (`date-time` format).
16
+ // * `sessionId` — required, stable per GDD pipeline run.
17
+ // * `stage` — optional, narrow `Stage` union.
18
+ // * `cycle` — optional, free-form string identifier.
19
+ // * `payload` — required, opaque object bag.
20
+ // * `_meta` — optional, writer-injected `{ pid, host, source }`.
21
+ // * `_truncated` — optional, writer-set when a payload exceeds
22
+ // `maxLineBytes` and has been replaced by a placeholder.
23
+ //
24
+ // Plan 20-04 owns the error taxonomy that feeds `ErrorEvent.payload`:
25
+ // `{ code, message, kind }` mirrors `toToolError(err)` output.
26
+
27
+ import type { Stage } from '../gdd-state/types.ts';
28
+
29
+ /** Writer-injected metadata. Never populated by callers. */
30
+ export interface EventMeta {
31
+ pid: number;
32
+ host: string;
33
+ /**
34
+ * Free-form identifier for the module that produced the event.
35
+ * Defaults to `"event-stream"` when `appendEvent()` fills the field
36
+ * itself; callers that wrap `appendEvent()` in a module-specific helper
37
+ * should overwrite this before calling.
38
+ */
39
+ source: string;
40
+ }
41
+
42
+ /**
43
+ * Canonical event envelope. All persisted and in-process events share
44
+ * this shape. Concrete subtypes narrow `type` + `payload` but add no
45
+ * additional top-level fields.
46
+ */
47
+ export interface BaseEvent {
48
+ type: string;
49
+ timestamp: string;
50
+ sessionId: string;
51
+ stage?: Stage;
52
+ cycle?: string;
53
+ payload: Record<string, unknown>;
54
+ _meta?: EventMeta;
55
+ /**
56
+ * Set to `true` by the writer when the serialized event exceeded
57
+ * `maxLineBytes` and the payload has been replaced with a placeholder.
58
+ * Never set by callers.
59
+ */
60
+ _truncated?: boolean;
61
+ }
62
+
63
+ /**
64
+ * Emitted by Plan 20-05's MCP tool handlers after a successful
65
+ * `mutate()` / `transition()` call. `diff` is an opaque structural
66
+ * description of the change; consumers (Phase 22 dashboard) render it.
67
+ */
68
+ export type StateMutationEvent = BaseEvent & {
69
+ type: 'state.mutation';
70
+ payload: { tool: string; diff: unknown };
71
+ };
72
+
73
+ /**
74
+ * Emitted by Plan 20-05 wrapping `transition()`. `pass=false` means
75
+ * the gate blocked the advance; `blockers` carries the same list the
76
+ * transition's `TransitionGateFailed` would expose.
77
+ */
78
+ export type StateTransitionEvent = BaseEvent & {
79
+ type: 'state.transition';
80
+ payload: { from: Stage; to: Stage; blockers: string[]; pass: boolean };
81
+ };
82
+
83
+ /** Lifecycle hook emitted when a pipeline stage begins execution. */
84
+ export type StageEnteredEvent = BaseEvent & {
85
+ type: 'stage.entered';
86
+ payload: { stage: Stage };
87
+ };
88
+
89
+ /**
90
+ * Lifecycle hook emitted when a pipeline stage finishes. `duration_ms`
91
+ * measures wall-clock time from `stage.entered`. `outcome` mirrors the
92
+ * stage's terminal state.
93
+ */
94
+ export type StageExitedEvent = BaseEvent & {
95
+ type: 'stage.exited';
96
+ payload: { stage: Stage; duration_ms: number; outcome: 'pass' | 'fail' | 'halted' };
97
+ };
98
+
99
+ /** Emitted by Plan 20-13 hook consumers when a hook dispatches a decision. */
100
+ export type HookFiredEvent = BaseEvent & {
101
+ type: 'hook.fired';
102
+ payload: { hook: string; decision: string };
103
+ };
104
+
105
+ /**
106
+ * Emitted whenever a `GDDError` is surfaced to the user or returned from
107
+ * a tool handler. `kind` mirrors `classify(err).kind`; `code` +
108
+ * `message` mirror the error's `code` + `message`.
109
+ */
110
+ export type ErrorEvent = BaseEvent & {
111
+ type: 'error';
112
+ payload: { code: string; message: string; kind: string };
113
+ };
114
+
115
+ /**
116
+ * Union of all pre-registered event types. Not a closed enum at the
117
+ * envelope level — callers can emit unknown types — but downstream
118
+ * consumers use this to drive typed `switch` statements with exhaustive
119
+ * checks for the subset they care about.
120
+ */
121
+ export type KnownEvent =
122
+ | StateMutationEvent
123
+ | StateTransitionEvent
124
+ | StageEnteredEvent
125
+ | StageExitedEvent
126
+ | HookFiredEvent
127
+ | ErrorEvent;
@@ -0,0 +1,154 @@
1
+ // scripts/lib/event-stream/writer.ts — append-only JSONL writer for the
2
+ // Phase 20+ telemetry stream (Plan 20-06, SDK-08).
3
+ //
4
+ // Design:
5
+ // * One file per .design/: `.design/telemetry/events.jsonl`, sibling to
6
+ // the existing `costs.jsonl` (which this plan does NOT modify).
7
+ // * Each `append()` is one call to `fs.appendFileSync(…, { flag: 'a' })`.
8
+ // On POSIX the O_APPEND semantic guarantees that a single write()
9
+ // under `PIPE_BUF` (4096 bytes) is atomic with respect to other
10
+ // appenders — multiple processes can append concurrently without
11
+ // interleaving or corruption. On Windows, `FILE_APPEND_DATA` via the
12
+ // Node runtime supplies the same guarantee. Typical event lines are
13
+ // well under 1KB; oversized events are truncated (see below) so we
14
+ // never approach the 4KB atomicity ceiling even on stricter POSIX
15
+ // implementations.
16
+ // * `append()` NEVER throws to the caller. On I/O failure we record
17
+ // the error (so observability tooling can surface it) and write a
18
+ // single diagnostic line to stderr.
19
+ // * Oversized payloads (> `maxLineBytes`, default 64KB) are truncated
20
+ // rather than dropped: we keep envelope metadata and replace
21
+ // `payload` with `{ _truncated_placeholder: true }`, then re-serialize
22
+ // and stamp `_truncated: true` on the line.
23
+
24
+ import { appendFileSync, mkdirSync } from 'node:fs';
25
+ import { dirname, resolve, isAbsolute, join } from 'node:path';
26
+
27
+ import type { BaseEvent } from './types.ts';
28
+
29
+ /** Default relative path for the persisted event stream. */
30
+ export const DEFAULT_EVENTS_PATH = '.design/telemetry/events.jsonl';
31
+
32
+ /** Default max line size in bytes. JSONL lines that exceed this are truncated. */
33
+ export const DEFAULT_MAX_LINE_BYTES = 64 * 1024; // 64 KiB
34
+
35
+ /** Constructor options for {@link EventWriter}. */
36
+ export interface WriterOptions {
37
+ /**
38
+ * Target file path for persisted events. Resolved to absolute at
39
+ * construction. Relative paths are resolved against `process.cwd()`.
40
+ *
41
+ * Default: `.design/telemetry/events.jsonl`.
42
+ */
43
+ path?: string;
44
+ /**
45
+ * Maximum serialized line size in bytes (JSON.stringify length + `\n`).
46
+ * Events exceeding this cap are truncated — the envelope is preserved
47
+ * and the payload is replaced with a `_truncated_placeholder` marker.
48
+ *
49
+ * Default: 65536 (64 KiB).
50
+ */
51
+ maxLineBytes?: number;
52
+ }
53
+
54
+ /**
55
+ * Append-only JSONL writer. One instance per file path is sufficient;
56
+ * the module-level cache in `./index.ts` shares a single default writer
57
+ * across the process so directory creation only happens once.
58
+ */
59
+ export class EventWriter {
60
+ /** Resolved absolute target path. */
61
+ readonly path: string;
62
+ /** Maximum line size in bytes (see {@link WriterOptions.maxLineBytes}). */
63
+ readonly maxLineBytes: number;
64
+ /** Number of failed append attempts since construction. */
65
+ writeErrors: number = 0;
66
+ /** The most recent write error, or `null` if none has occurred. */
67
+ lastError: Error | null = null;
68
+
69
+ /** `true` once we've ensured the target directory exists. */
70
+ private directoryEnsured: boolean = false;
71
+
72
+ constructor(opts: WriterOptions = {}) {
73
+ const rawPath = opts.path ?? DEFAULT_EVENTS_PATH;
74
+ this.path = isAbsolute(rawPath) ? rawPath : resolve(process.cwd(), rawPath);
75
+ this.maxLineBytes = opts.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
76
+ }
77
+
78
+ /**
79
+ * Append one event to the target file.
80
+ *
81
+ * Contract:
82
+ * * SYNC — returns when the write has been accepted by the kernel.
83
+ * * NEVER throws — I/O errors increment {@link writeErrors} and update
84
+ * {@link lastError}; a diagnostic is written to stderr and
85
+ * execution continues.
86
+ * * Truncates oversized payloads rather than dropping the event.
87
+ */
88
+ append(ev: BaseEvent): void {
89
+ try {
90
+ const line = this.serialize(ev);
91
+ this.ensureDirectory();
92
+ appendFileSync(this.path, line, { flag: 'a' });
93
+ } catch (err) {
94
+ this.writeErrors += 1;
95
+ this.lastError = err instanceof Error ? err : new Error(String(err));
96
+ // One-line diagnostic; intentionally minimal so callers aren't
97
+ // spammed under sustained failure.
98
+ try {
99
+ process.stderr.write(
100
+ `[event-stream] write failed: ${this.lastError.message}\n`,
101
+ );
102
+ } catch {
103
+ // If stderr itself is broken we have no recourse; swallow.
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Produce the on-disk JSONL representation of an event, truncating
110
+ * oversized payloads so the line fits within {@link maxLineBytes}.
111
+ *
112
+ * Exposed on the instance for unit-testability; callers should use
113
+ * {@link append}.
114
+ */
115
+ serialize(ev: BaseEvent): string {
116
+ const raw = JSON.stringify(ev) + '\n';
117
+ if (Buffer.byteLength(raw, 'utf8') <= this.maxLineBytes) {
118
+ return raw;
119
+ }
120
+
121
+ // Truncate: keep envelope fields, drop payload content.
122
+ const truncated: BaseEvent = {
123
+ type: ev.type,
124
+ timestamp: ev.timestamp,
125
+ sessionId: ev.sessionId,
126
+ payload: { _truncated_placeholder: true },
127
+ _truncated: true,
128
+ };
129
+ if (ev.stage !== undefined) truncated.stage = ev.stage;
130
+ if (ev.cycle !== undefined) truncated.cycle = ev.cycle;
131
+ if (ev._meta !== undefined) truncated._meta = ev._meta;
132
+ return JSON.stringify(truncated) + '\n';
133
+ }
134
+
135
+ /**
136
+ * Ensure the target directory exists. Memoized so we only pay the
137
+ * filesystem stat cost once per writer lifetime.
138
+ */
139
+ private ensureDirectory(): void {
140
+ if (this.directoryEnsured) return;
141
+ mkdirSync(dirname(this.path), { recursive: true });
142
+ this.directoryEnsured = true;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Convenience helper: resolve a relative events-path against a supplied
148
+ * base directory (typically the project root) rather than `process.cwd()`.
149
+ * Intended for tests that scaffold a temp workspace and don't want to
150
+ * chdir.
151
+ */
152
+ export function eventsPathFor(baseDir: string): string {
153
+ return join(baseDir, DEFAULT_EVENTS_PATH);
154
+ }