@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.
- package/.claude-plugin/marketplace.json +11 -14
- package/.claude-plugin/plugin.json +9 -32
- package/CHANGELOG.md +138 -0
- package/README.md +54 -1
- package/agents/design-reflector.md +13 -0
- package/bin/gdd-sdk +55 -0
- package/connections/connections.md +3 -0
- package/connections/figma.md +2 -0
- package/connections/gdd-state.md +186 -0
- package/hooks/budget-enforcer.ts +716 -0
- package/hooks/context-exhaustion.ts +251 -0
- package/hooks/gdd-read-injection-scanner.ts +172 -0
- package/hooks/hooks.json +3 -3
- package/package.json +32 -51
- package/reference/codex-tools.md +53 -0
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +21 -0
- package/reference/schemas/budget.schema.json +42 -0
- package/reference/schemas/events.schema.json +55 -0
- package/reference/schemas/generated.d.ts +419 -0
- package/reference/schemas/iteration-budget.schema.json +36 -0
- package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
- package/reference/schemas/rate-limits.schema.json +31 -0
- package/scripts/aggregate-agent-metrics.ts +282 -0
- package/scripts/codegen-schema-types.ts +149 -0
- package/scripts/e2e/run-headless.ts +514 -0
- package/scripts/lib/cli/commands/audit.ts +382 -0
- package/scripts/lib/cli/commands/init.ts +217 -0
- package/scripts/lib/cli/commands/query.ts +329 -0
- package/scripts/lib/cli/commands/run.ts +656 -0
- package/scripts/lib/cli/commands/stage.ts +468 -0
- package/scripts/lib/cli/index.ts +167 -0
- package/scripts/lib/cli/parse-args.ts +336 -0
- package/scripts/lib/context-engine/index.ts +116 -0
- package/scripts/lib/context-engine/manifest.ts +69 -0
- package/scripts/lib/context-engine/truncate.ts +282 -0
- package/scripts/lib/context-engine/types.ts +59 -0
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
- package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
- package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
- package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
- package/scripts/lib/error-classifier.cjs +232 -0
- package/scripts/lib/error-classifier.d.cts +44 -0
- package/scripts/lib/event-stream/emitter.ts +88 -0
- package/scripts/lib/event-stream/index.ts +164 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- package/scripts/lib/explore-parallel-runner/index.ts +294 -0
- package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
- package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
- package/scripts/lib/explore-parallel-runner/types.ts +139 -0
- package/scripts/lib/gdd-errors/classification.ts +124 -0
- package/scripts/lib/gdd-errors/index.ts +218 -0
- package/scripts/lib/gdd-state/gates.ts +216 -0
- package/scripts/lib/gdd-state/index.ts +167 -0
- package/scripts/lib/gdd-state/lockfile.ts +232 -0
- package/scripts/lib/gdd-state/mutator.ts +574 -0
- package/scripts/lib/gdd-state/parser.ts +523 -0
- package/scripts/lib/gdd-state/types.ts +179 -0
- package/scripts/lib/harness/detect.ts +90 -0
- package/scripts/lib/harness/index.ts +64 -0
- package/scripts/lib/harness/tool-map.ts +142 -0
- package/scripts/lib/init-runner/index.ts +396 -0
- package/scripts/lib/init-runner/researchers.ts +245 -0
- package/scripts/lib/init-runner/scaffold.ts +224 -0
- package/scripts/lib/init-runner/synthesizer.ts +224 -0
- package/scripts/lib/init-runner/types.ts +143 -0
- package/scripts/lib/iteration-budget.cjs +205 -0
- package/scripts/lib/iteration-budget.d.cts +32 -0
- package/scripts/lib/jittered-backoff.cjs +112 -0
- package/scripts/lib/jittered-backoff.d.cts +38 -0
- package/scripts/lib/lockfile.cjs +177 -0
- package/scripts/lib/lockfile.d.cts +21 -0
- package/scripts/lib/logger/index.ts +251 -0
- package/scripts/lib/logger/sinks.ts +269 -0
- package/scripts/lib/logger/types.ts +110 -0
- package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
- package/scripts/lib/pipeline-runner/index.ts +527 -0
- package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
- package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
- package/scripts/lib/pipeline-runner/types.ts +183 -0
- package/scripts/lib/prompt-sanitizer/index.ts +435 -0
- package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
- package/scripts/lib/rate-guard.cjs +365 -0
- package/scripts/lib/rate-guard.d.cts +38 -0
- package/scripts/lib/session-runner/errors.ts +406 -0
- package/scripts/lib/session-runner/index.ts +715 -0
- package/scripts/lib/session-runner/transcript.ts +189 -0
- package/scripts/lib/session-runner/types.ts +144 -0
- package/scripts/lib/tool-scoping/index.ts +219 -0
- package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
- package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
- package/scripts/lib/tool-scoping/types.ts +77 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
- package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
- package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
- package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
- package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
- package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
- package/scripts/mcp-servers/gdd-state/server.ts +288 -0
- package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
- package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
- package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
- package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
- package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
- package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
- package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
- package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
- package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
- package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
- package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
- package/scripts/validate-frontmatter.ts +114 -0
- package/scripts/validate-schemas.ts +401 -0
- package/skills/brief/SKILL.md +15 -6
- package/skills/design/SKILL.md +31 -13
- package/skills/explore/SKILL.md +41 -17
- package/skills/health/SKILL.md +15 -4
- package/skills/optimize/SKILL.md +3 -3
- package/skills/pause/SKILL.md +16 -10
- package/skills/plan/SKILL.md +33 -17
- package/skills/progress/SKILL.md +15 -11
- package/skills/resume/SKILL.md +19 -10
- package/skills/settings/SKILL.md +11 -3
- package/skills/todo/SKILL.md +12 -3
- package/skills/verify/SKILL.md +65 -29
- package/hooks/budget-enforcer.js +0 -329
- package/hooks/context-exhaustion.js +0 -127
- package/hooks/gdd-read-injection-scanner.js +0 -39
- package/scripts/aggregate-agent-metrics.js +0 -173
- package/scripts/validate-frontmatter.cjs +0 -68
- 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
|
+
}
|