@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,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
|
+
}
|