@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,269 @@
1
+ // scripts/lib/logger/sinks.ts — Plan 21-04 (SDK-16).
2
+ //
3
+ // Sink implementations for the Phase-21 structured logger:
4
+ // * ConsoleSink — pretty stderr output with ANSI colors when TTY.
5
+ // * JsonlSink — crash-safe append-only JSONL to .design/logs/<file>.
6
+ // * MultiSink — fan-out to N sinks (e.g., console + JSONL simultaneously
7
+ // under a test harness).
8
+ //
9
+ // Design rules:
10
+ // * Sinks MUST NOT throw from `.write()`. IO failures are swallowed
11
+ // (they'd otherwise break the caller's happy path). A one-shot
12
+ // `process.stderr.write` of the error is acceptable as a tripwire.
13
+ // * `safeStringify()` walks the entry with a WeakSet for circular
14
+ // detection and replaces non-JSON-serializable values with
15
+ // `"<unserializable: <reason>>"` so a bad caller payload never
16
+ // crashes the process.
17
+ // * File paths encode the ISO timestamp with `:` → `-` (Windows won't
18
+ // allow colons in filenames).
19
+
20
+ import { appendFileSync, mkdirSync } from 'node:fs';
21
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
22
+
23
+ import type { LogEntry, Sink } from './types.ts';
24
+
25
+ /**
26
+ * Default directory for JSONL logs when neither `opts.dir` nor
27
+ * `GDD_LOG_DIR` is set. Resolved relative to `process.cwd()`.
28
+ */
29
+ export const DEFAULT_LOG_DIR = '.design/logs';
30
+
31
+ /**
32
+ * ANSI color codes keyed by log level. Empty string when color disabled.
33
+ */
34
+ const ANSI_RESET = '\u001b[0m';
35
+ const ANSI_BY_LEVEL: Record<LogEntry['level'], string> = {
36
+ debug: '\u001b[90m', // gray
37
+ info: '\u001b[36m', // cyan
38
+ warn: '\u001b[33m', // yellow
39
+ error: '\u001b[31m', // red
40
+ };
41
+
42
+ /**
43
+ * Reserved keys that the logger controls directly. The sinks rely on
44
+ * these being present; extraction helpers skip them when rendering
45
+ * "extra fields".
46
+ */
47
+ const RESERVED_KEYS = new Set(['ts', 'level', 'msg', 'pid', 'scope']);
48
+
49
+ /**
50
+ * Replacer that handles non-JSON-serializable values. Uses a single
51
+ * WeakSet across the whole walk to catch circular refs. Returns
52
+ * `"<unserializable: <reason>>"` for:
53
+ * * circular references (object already seen on the ancestor path)
54
+ * * BigInt values
55
+ * * Function values
56
+ * * anything else that `JSON.stringify` would otherwise throw on.
57
+ */
58
+ function buildSafeReplacer(): (key: string, value: unknown) => unknown {
59
+ const seen = new WeakSet<object>();
60
+ return (_key: string, value: unknown): unknown => {
61
+ if (typeof value === 'bigint') return '<unserializable: bigint>';
62
+ if (typeof value === 'function') return '<unserializable: function>';
63
+ if (typeof value === 'symbol') return '<unserializable: symbol>';
64
+ if (value !== null && typeof value === 'object') {
65
+ if (seen.has(value)) return '<unserializable: circular>';
66
+ seen.add(value);
67
+ }
68
+ return value;
69
+ };
70
+ }
71
+
72
+ /**
73
+ * JSON.stringify that never throws. All unserializable leaves become
74
+ * `<unserializable: ...>` placeholders.
75
+ */
76
+ export function safeStringify(value: unknown): string {
77
+ try {
78
+ return JSON.stringify(value, buildSafeReplacer()) ?? 'null';
79
+ } catch {
80
+ // Extremely pathological cases (e.g., `toJSON` throws after our
81
+ // replacer) still fall through here. Return a tagged sentinel
82
+ // rather than propagate.
83
+ return '"<unserializable: stringify-failed>"';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Split a LogEntry into (reserved header, caller fields) for rendering.
89
+ * The reserved header controls output ordering; caller fields are
90
+ * rendered as an inline JSON object suffix.
91
+ */
92
+ function splitEntry(entry: LogEntry): {
93
+ header: { ts: string; level: string; msg: string; pid: number; scope?: string };
94
+ extras: Record<string, unknown>;
95
+ } {
96
+ const extras: Record<string, unknown> = {};
97
+ for (const [k, v] of Object.entries(entry)) {
98
+ if (!RESERVED_KEYS.has(k)) extras[k] = v;
99
+ }
100
+ const header: { ts: string; level: string; msg: string; pid: number; scope?: string } = {
101
+ ts: entry.ts,
102
+ level: entry.level,
103
+ msg: entry.msg,
104
+ pid: entry.pid,
105
+ };
106
+ if (entry.scope !== undefined) header.scope = entry.scope;
107
+ return { header, extras };
108
+ }
109
+
110
+ export interface ConsoleSinkOptions {
111
+ /**
112
+ * Enable ANSI color output. When `undefined` (default), autodetected
113
+ * from `process.stderr.isTTY`. Explicit `false` disables coloring;
114
+ * explicit `true` forces it even for non-TTY streams (useful for
115
+ * tests that assert the colorized form).
116
+ */
117
+ color?: boolean;
118
+ /**
119
+ * Write target. Defaults to `process.stderr.write`. Tests inject a
120
+ * capturing function here.
121
+ */
122
+ write?: (chunk: string) => void;
123
+ }
124
+
125
+ /**
126
+ * Pretty-printed stderr sink used in interactive mode. Format:
127
+ * `<ts> [<LEVEL>] <scope?> <msg> <json-fields>`
128
+ * Fields JSON is omitted when the entry has no caller fields.
129
+ */
130
+ export class ConsoleSink implements Sink {
131
+ readonly colorEnabled: boolean;
132
+ private readonly writer: (chunk: string) => void;
133
+
134
+ constructor(opts: ConsoleSinkOptions = {}) {
135
+ this.colorEnabled =
136
+ opts.color !== undefined ? opts.color : Boolean(process.stderr.isTTY);
137
+ this.writer = opts.write ?? ((chunk: string) => {
138
+ process.stderr.write(chunk);
139
+ });
140
+ }
141
+
142
+ write(entry: LogEntry): void {
143
+ const { header, extras } = splitEntry(entry);
144
+ const levelToken = header.level.toUpperCase();
145
+ const coloredLevel = this.colorEnabled
146
+ ? `${ANSI_BY_LEVEL[entry.level]}${levelToken}${ANSI_RESET}`
147
+ : levelToken;
148
+ const scopePart = header.scope !== undefined ? ` ${header.scope}` : '';
149
+ const extrasKeys = Object.keys(extras);
150
+ const fieldsPart = extrasKeys.length > 0 ? ` ${safeStringify(extras)}` : '';
151
+ const line = `${header.ts} [${coloredLevel}]${scopePart} ${header.msg}${fieldsPart}\n`;
152
+ try {
153
+ this.writer(line);
154
+ } catch {
155
+ // Swallow: writing to stderr failed (very rare — detached stdio,
156
+ // pipe closed). We intentionally do nothing to honor the "sinks
157
+ // never throw" contract.
158
+ }
159
+ }
160
+
161
+ close(): void {
162
+ // No fd to release; stderr is process-owned.
163
+ }
164
+ }
165
+
166
+ export interface JsonlSinkOptions {
167
+ /**
168
+ * Output directory. Resolved relative to `process.cwd()` when relative.
169
+ * Priority: `opts.dir` → `process.env.GDD_LOG_DIR` → `DEFAULT_LOG_DIR`.
170
+ */
171
+ dir?: string;
172
+ /**
173
+ * Override the ISO timestamp used when composing the filename. Only
174
+ * affects the filename itself — entry timestamps come from the Logger.
175
+ * Allows tests to produce a deterministic file path.
176
+ */
177
+ nowOverride?: () => string;
178
+ /**
179
+ * Override pid used in the filename suffix. Tests use this to pin
180
+ * the filename when `process.pid` would otherwise vary.
181
+ */
182
+ pidOverride?: number;
183
+ }
184
+
185
+ /**
186
+ * Append-only JSONL sink. One entry per line, UTF-8, newline-terminated.
187
+ * File path: `<dir>/<ISO-with-dashes>-<pid>.jsonl`.
188
+ *
189
+ * Uses `appendFileSync` on every write — crash-safe (kernel-level atomic
190
+ * append for small writes), no in-memory buffering. Writes are sync so
191
+ * the logger caller can return immediately; async buffering would
192
+ * complicate ordering guarantees under concurrent pipeline stages.
193
+ */
194
+ export class JsonlSink implements Sink {
195
+ readonly path: string;
196
+ private dirCreated = false;
197
+
198
+ constructor(opts: JsonlSinkOptions = {}) {
199
+ const dirOpt =
200
+ opts.dir ?? process.env['GDD_LOG_DIR'] ?? DEFAULT_LOG_DIR;
201
+ const dir = isAbsolute(dirOpt) ? dirOpt : resolve(process.cwd(), dirOpt);
202
+ const iso = opts.nowOverride ? opts.nowOverride() : new Date().toISOString();
203
+ const pid = opts.pidOverride ?? process.pid;
204
+ // Replace colons (invalid on Windows filesystems) with dashes.
205
+ const safeIso = iso.replace(/:/g, '-');
206
+ this.path = join(dir, `${safeIso}-${pid}.jsonl`);
207
+ }
208
+
209
+ private ensureDir(): void {
210
+ if (this.dirCreated) return;
211
+ try {
212
+ mkdirSync(dirname(this.path), { recursive: true });
213
+ this.dirCreated = true;
214
+ } catch {
215
+ // If mkdir fails (e.g., permission denied), subsequent appendFileSync
216
+ // will also fail and we'll swallow there. Keep dirCreated=false so
217
+ // a later write attempts mkdir again (the condition may clear).
218
+ }
219
+ }
220
+
221
+ write(entry: LogEntry): void {
222
+ this.ensureDir();
223
+ const line = `${safeStringify(entry)}\n`;
224
+ try {
225
+ appendFileSync(this.path, line, { encoding: 'utf8' });
226
+ } catch {
227
+ // Swallow. The logger contract forbids throwing from sinks.
228
+ // A sustained IO failure is visible via missing log file.
229
+ }
230
+ }
231
+
232
+ close(): void {
233
+ // `appendFileSync` opens+closes per call; nothing to release.
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Fan-out sink: every `.write()` is forwarded to every child sink.
239
+ * Construction validates that the input is a non-null array; null/undefined
240
+ * entries are rejected defensively (they'd NPE on write otherwise).
241
+ */
242
+ export class MultiSink implements Sink {
243
+ private readonly sinks: readonly Sink[];
244
+
245
+ constructor(sinks: readonly Sink[]) {
246
+ this.sinks = sinks.filter((s): s is Sink => s !== null && s !== undefined);
247
+ }
248
+
249
+ write(entry: LogEntry): void {
250
+ for (const s of this.sinks) {
251
+ try {
252
+ s.write(entry);
253
+ } catch {
254
+ // Defense in depth: child sinks shouldn't throw, but if one does,
255
+ // it must not block the others. Swallow per-sink.
256
+ }
257
+ }
258
+ }
259
+
260
+ close(): void {
261
+ for (const s of this.sinks) {
262
+ try {
263
+ s.close();
264
+ } catch {
265
+ // Same defense — close of one sink must not block another.
266
+ }
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,110 @@
1
+ // scripts/lib/logger/types.ts — Plan 21-04 (SDK-16).
2
+ //
3
+ // Public type surface for the structured logger. Consumers import from
4
+ // `./index.ts` (the module barrel); this file exists to keep the type
5
+ // graph clean for `./sinks.ts` and `./index.ts` which both depend on the
6
+ // same `LogEntry`/`Sink`/`Logger` contracts.
7
+ //
8
+ // Contracts:
9
+ // * LogLevel — closed union of the four levels supported in v1.
10
+ // * LEVEL_ORDER — numeric map used for min-level filtering. Higher
11
+ // numbers are more severe. Frozen so callers cannot mutate.
12
+ // * LoggerOptions — construction-time options. All fields optional so
13
+ // `createLogger()` can be called with zero args.
14
+ // * LogEntry — the on-wire shape (both JSONL and console). `ts`,
15
+ // `level`, `msg`, `pid` are reserved keys: the logger overwrites
16
+ // caller fields of the same name to preserve the guarantee that a
17
+ // JSONL line always has a valid timestamp/level/msg/pid.
18
+ // * Logger — the public API: 4 level methods, `child()`, `flush()`.
19
+ // * Sink — the sink interface used by ConsoleSink/JsonlSink/MultiSink
20
+ // in `./sinks.ts`.
21
+
22
+ /** Closed set of log levels. */
23
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
24
+
25
+ /**
26
+ * Numeric ordering for level filtering. Higher = more severe.
27
+ * `LEVEL_ORDER[entry.level] >= LEVEL_ORDER[opts.level]` → emit; otherwise drop.
28
+ * Frozen to prevent accidental mutation by consumers.
29
+ */
30
+ export const LEVEL_ORDER: Readonly<Record<LogLevel, number>> = Object.freeze({
31
+ debug: 10,
32
+ info: 20,
33
+ warn: 30,
34
+ error: 40,
35
+ });
36
+
37
+ export interface LoggerOptions {
38
+ /** Minimum level to emit. Levels below this are dropped. Default: `'info'`. */
39
+ level?: LogLevel;
40
+ /**
41
+ * Force headless mode regardless of env / TTY detection. When `undefined`
42
+ * (the default), mode is auto-detected: `GDD_HEADLESS=1` OR
43
+ * `!process.stdout.isTTY` → headless. `GDD_HEADLESS=0` pins interactive.
44
+ */
45
+ headless?: boolean;
46
+ /**
47
+ * JSONL output directory (headless mode only). Resolved at construction
48
+ * time; falls back to `process.env.GDD_LOG_DIR` then `'.design/logs'`.
49
+ */
50
+ logDir?: string;
51
+ /**
52
+ * Module / subsystem tag merged into every entry (e.g., `'session-runner'`).
53
+ * `child(scope)` concatenates onto this dot-joined.
54
+ */
55
+ scope?: string;
56
+ /**
57
+ * Test-only override: replaces the `new Date().toISOString()` source so
58
+ * test fixtures can be byte-identical across runs.
59
+ */
60
+ nowOverride?: () => string;
61
+ /**
62
+ * Test-only override: when explicitly `false`, `warn`/`error` do NOT
63
+ * emit an `ErrorEvent` via event-stream. Any other value behaves as
64
+ * "emit normally" (the default).
65
+ */
66
+ emitEventsOverride?: false;
67
+ }
68
+
69
+ /**
70
+ * Shape written to JSONL sinks and passed to ConsoleSink.write().
71
+ * Reserved keys (`ts`, `level`, `msg`, `pid`) are always present; caller
72
+ * fields are merged shallow but never override reserved keys.
73
+ */
74
+ export interface LogEntry {
75
+ ts: string;
76
+ level: LogLevel;
77
+ msg: string;
78
+ pid: number;
79
+ scope?: string;
80
+ [field: string]: unknown;
81
+ }
82
+
83
+ export interface Logger {
84
+ debug(msg: string, fields?: Record<string, unknown>): void;
85
+ info(msg: string, fields?: Record<string, unknown>): void;
86
+ warn(msg: string, fields?: Record<string, unknown>): void;
87
+ error(msg: string, fields?: Record<string, unknown>): void;
88
+ /**
89
+ * Returns a child logger inheriting options, with additional scope
90
+ * appended (dot-joined if a parent scope exists) and additional fields
91
+ * merged into every entry. Child loggers share the parent's sink(s).
92
+ */
93
+ child(scope: string, fields?: Record<string, unknown>): Logger;
94
+ /**
95
+ * Flush buffered writes. JSONL and Console sinks in v1 are synchronous,
96
+ * so this is a no-op — reserved for async sinks in a future wave.
97
+ */
98
+ flush(): void;
99
+ }
100
+
101
+ /**
102
+ * Sink interface implemented by ConsoleSink/JsonlSink/MultiSink in
103
+ * `./sinks.ts`. Logger.write() is expected to never throw; sinks SHOULD
104
+ * swallow IO errors and either retry or drop the line rather than
105
+ * surface failures to the caller.
106
+ */
107
+ export interface Sink {
108
+ write(entry: LogEntry): void;
109
+ close(): void;
110
+ }
@@ -0,0 +1,134 @@
1
+ // scripts/lib/pipeline-runner/human-gate.ts — Plan 21-05 Task 4.
2
+ //
3
+ // Human-gate extraction + dispatch. The pipeline recognizes HTML-comment
4
+ // markers of the form:
5
+ //
6
+ // <!-- AWAIT_USER_GATE: name="..." -->
7
+ //
8
+ // emitted by skills that want to pause mid-session. When the
9
+ // session-runner's `final_text` contains such a marker, the stage
10
+ // handler maps the session's terminal status to `halted-human-gate`
11
+ // and surfaces the gate name plus stdout tail.
12
+ //
13
+ // The pipeline driver (index.ts) then invokes `dispatchHumanGate`:
14
+ // * With `config.onHumanGate` → call it; its decision drives the
15
+ // driver.
16
+ // * Without a callback → default `{decision: 'stop'}` (safe default
17
+ // for headless operation — never proceed past a gate on autopilot).
18
+ // * Callback throws → caught; logged as warn; default `stop`.
19
+
20
+ import { getLogger } from '../logger/index.ts';
21
+ import type {
22
+ HumanGateDecision,
23
+ HumanGateInfo,
24
+ PipelineConfig,
25
+ } from './types.ts';
26
+
27
+ /**
28
+ * Regex for the canonical gate marker. Name is the first capture group;
29
+ * whitespace around `:` and inside the double quotes is tolerated.
30
+ *
31
+ * Intentionally LENIENT about surrounding whitespace (sanitizers may
32
+ * normalize around the comment), but STRICT about the core token
33
+ * shape so false positives (e.g., docs discussing AWAIT_USER_GATE)
34
+ * don't trip it — the marker must be inside an HTML comment AND
35
+ * carry a double-quoted `name`.
36
+ */
37
+ const GATE_MARKER_RE =
38
+ /<!--\s*AWAIT_USER_GATE\s*:\s*name\s*=\s*"([^"]+)"\s*-->/;
39
+
40
+ /**
41
+ * Extract the first `AWAIT_USER_GATE` marker from a session's stdout /
42
+ * final text. Returns `null` when no marker is present.
43
+ *
44
+ * Only the FIRST marker is returned — subsequent gates in the same
45
+ * session's output are ignored by design (one pause per stage).
46
+ */
47
+ export function extractGateMarker(
48
+ stdout: string,
49
+ ): { readonly name: string } | null {
50
+ if (typeof stdout !== 'string' || stdout.length === 0) return null;
51
+ const m = GATE_MARKER_RE.exec(stdout);
52
+ if (m === null) return null;
53
+ const name: string | undefined = m[1];
54
+ if (name === undefined || name === '') return null;
55
+ return { name };
56
+ }
57
+
58
+ /**
59
+ * Dispatch a single human gate. Calls `config.onHumanGate` when
60
+ * supplied; otherwise returns `{decision: 'stop'}`.
61
+ *
62
+ * Never throws — callback exceptions are caught and converted into a
63
+ * `stop` decision, with a warn-level log entry for observability.
64
+ */
65
+ export async function dispatchHumanGate(
66
+ info: HumanGateInfo,
67
+ config: PipelineConfig,
68
+ ): Promise<HumanGateDecision> {
69
+ if (config.onHumanGate === undefined) {
70
+ // No callback — default stop. We log this at debug (not warn)
71
+ // because it's a normal headless flow: the operator wanted to
72
+ // pause, and the orchestrator will resume via a fresh `run()`
73
+ // invocation with `resumeFrom` set.
74
+ try {
75
+ getLogger().debug('human-gate: no callback; default stop', {
76
+ stage: info.stage,
77
+ gateName: info.gateName,
78
+ });
79
+ } catch {
80
+ // Logger failures must not propagate.
81
+ }
82
+ return { decision: 'stop' };
83
+ }
84
+
85
+ try {
86
+ const decision = await config.onHumanGate(info);
87
+ // Validate the decision shape — callbacks may return partial
88
+ // objects from user code. Fall back to `stop` on anything invalid.
89
+ if (
90
+ decision === null ||
91
+ decision === undefined ||
92
+ typeof decision !== 'object'
93
+ ) {
94
+ try {
95
+ getLogger().warn('human-gate: callback returned non-object; defaulting to stop', {
96
+ stage: info.stage,
97
+ gateName: info.gateName,
98
+ });
99
+ } catch {
100
+ // Logger failures must not propagate.
101
+ }
102
+ return { decision: 'stop' };
103
+ }
104
+ if (decision.decision !== 'resume' && decision.decision !== 'stop') {
105
+ try {
106
+ getLogger().warn('human-gate: callback returned unknown decision; defaulting to stop', {
107
+ stage: info.stage,
108
+ gateName: info.gateName,
109
+ received: String(decision.decision),
110
+ });
111
+ } catch {
112
+ // Logger failures must not propagate.
113
+ }
114
+ return { decision: 'stop' };
115
+ }
116
+ // The decision's `payload` is optional; pass it through verbatim
117
+ // when present.
118
+ if (decision.decision === 'resume' && decision.payload !== undefined) {
119
+ return { decision: 'resume', payload: decision.payload };
120
+ }
121
+ return { decision: decision.decision };
122
+ } catch (err) {
123
+ try {
124
+ getLogger().warn('human-gate: callback threw; defaulting to stop', {
125
+ stage: info.stage,
126
+ gateName: info.gateName,
127
+ error: err instanceof Error ? err.message : String(err),
128
+ });
129
+ } catch {
130
+ // Logger failures must not propagate.
131
+ }
132
+ return { decision: 'stop' };
133
+ }
134
+ }