@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,177 @@
|
|
|
1
|
+
// scripts/lib/lockfile.cjs
|
|
2
|
+
//
|
|
3
|
+
// Plan 20-14 — PID+timestamp sibling lockfile for `.cjs` consumers.
|
|
4
|
+
//
|
|
5
|
+
// Algorithm mirrors scripts/lib/gdd-state/lockfile.ts (Plan 20-01):
|
|
6
|
+
// Lock path: `${target}.lock`
|
|
7
|
+
// Payload: { pid: number, host: string, acquired_at: ISO8601 }
|
|
8
|
+
// Acquire: atomic `writeFileSync(..., { flag: 'wx' })`
|
|
9
|
+
// Stale rule: pid dead (ESRCH via `kill(pid, 0)`) OR `acquired_at` older
|
|
10
|
+
// than `staleMs` OR unparseable payload
|
|
11
|
+
// Release: unlink; ENOENT is not an error; idempotent
|
|
12
|
+
//
|
|
13
|
+
// Windows: AV scanners and file-indexers can hold a file briefly after
|
|
14
|
+
// close. `wx` create may fail with EPERM/EBUSY even when the target is
|
|
15
|
+
// free; we treat these as transient and loop (same code path as EEXIST).
|
|
16
|
+
//
|
|
17
|
+
// Dependency-cycle note: Plan 20-14's rate-guard + iteration-budget
|
|
18
|
+
// consume this module, and both are required to stay dependency-light so
|
|
19
|
+
// that hooks/budget-enforcer.ts can import them without dragging the
|
|
20
|
+
// gdd-state MCP graph along. Hence this standalone .cjs port instead of
|
|
21
|
+
// calling the .ts version.
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
|
|
28
|
+
const DEFAULT_STALE_MS = 60_000;
|
|
29
|
+
const DEFAULT_MAX_WAIT_MS = 5_000;
|
|
30
|
+
const DEFAULT_POLL_MS = 50;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Acquire an advisory lock at `${path}.lock`. Returns an idempotent
|
|
34
|
+
* async release function.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} path path being locked (we append `.lock`)
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
* @param {number} [opts.staleMs] ms after which an existing lock is stale. Default 60_000.
|
|
39
|
+
* @param {number} [opts.maxWaitMs] total ms to wait before throwing. Default 5_000.
|
|
40
|
+
* @param {number} [opts.pollMs] ms between retry attempts. Default 50.
|
|
41
|
+
* @returns {Promise<() => Promise<void>>} release function
|
|
42
|
+
* @throws {Error} with name === 'LockAcquisitionError' when maxWaitMs elapses
|
|
43
|
+
*/
|
|
44
|
+
async function acquire(path, opts) {
|
|
45
|
+
const o = opts || {};
|
|
46
|
+
const staleMs = Number.isFinite(o.staleMs) ? o.staleMs : DEFAULT_STALE_MS;
|
|
47
|
+
const maxWaitMs = Number.isFinite(o.maxWaitMs) ? o.maxWaitMs : DEFAULT_MAX_WAIT_MS;
|
|
48
|
+
const pollMs = Number.isFinite(o.pollMs) ? o.pollMs : DEFAULT_POLL_MS;
|
|
49
|
+
|
|
50
|
+
if (staleMs < 0 || maxWaitMs < 0 || pollMs < 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`lockfile.acquire: invalid options (staleMs=${staleMs}, maxWaitMs=${maxWaitMs}, pollMs=${pollMs})`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lockPath = `${path}.lock`;
|
|
57
|
+
const payload = JSON.stringify({
|
|
58
|
+
pid: process.pid,
|
|
59
|
+
host: os.hostname(),
|
|
60
|
+
acquired_at: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(lockPath, payload, { flag: 'wx', encoding: 'utf8' });
|
|
67
|
+
return makeRelease(lockPath);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
70
|
+
if (code !== 'EEXIST' && code !== 'EPERM' && code !== 'EBUSY') {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
// Try to read the current holder; if it vanished between EEXIST and
|
|
74
|
+
// read, loop immediately.
|
|
75
|
+
const existing = readLockSafe(lockPath);
|
|
76
|
+
if (existing === null) continue;
|
|
77
|
+
|
|
78
|
+
const parsed = parseLock(existing);
|
|
79
|
+
if (parsed === null || isStale(parsed, staleMs)) {
|
|
80
|
+
// Clear stale/garbage lock; race-tolerant — if it's already gone
|
|
81
|
+
// we'll just get ENOENT, no-op.
|
|
82
|
+
try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
87
|
+
const e = new Error(
|
|
88
|
+
`lockfile: failed to acquire ${lockPath} within ${maxWaitMs}ms (held by ${existing})`,
|
|
89
|
+
);
|
|
90
|
+
e.name = 'LockAcquisitionError';
|
|
91
|
+
e.lockPath = lockPath;
|
|
92
|
+
e.holder = existing;
|
|
93
|
+
e.waitedMs = Date.now() - startedAt;
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
await sleep(pollMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function makeRelease(lockPath) {
|
|
102
|
+
let released = false;
|
|
103
|
+
return async function release() {
|
|
104
|
+
if (released) return;
|
|
105
|
+
released = true;
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(lockPath);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
110
|
+
if (code === 'ENOENT') return; // idempotent — already gone
|
|
111
|
+
if (code === 'EPERM' || code === 'EBUSY') {
|
|
112
|
+
// Windows AV/indexer: retry once.
|
|
113
|
+
await sleep(50);
|
|
114
|
+
try {
|
|
115
|
+
if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
|
|
116
|
+
} catch { /* give up; stale-detection will reclaim */ }
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Any other errno: swallow. Best-effort cleanup; stale-age check
|
|
120
|
+
// will eventually reclaim the lock.
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readLockSafe(p) {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readFileSync(p, 'utf8');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
130
|
+
if (code === 'ENOENT') return null;
|
|
131
|
+
return '<unreadable>';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseLock(raw) {
|
|
136
|
+
try {
|
|
137
|
+
const obj = JSON.parse(raw);
|
|
138
|
+
if (
|
|
139
|
+
obj && typeof obj === 'object' &&
|
|
140
|
+
typeof obj.pid === 'number' &&
|
|
141
|
+
typeof obj.host === 'string' &&
|
|
142
|
+
typeof obj.acquired_at === 'string'
|
|
143
|
+
) {
|
|
144
|
+
return obj;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isStale(payload, staleMs) {
|
|
153
|
+
if (!isPidAlive(payload.pid, payload.host)) return true;
|
|
154
|
+
const t = Date.parse(payload.acquired_at);
|
|
155
|
+
if (!Number.isFinite(t)) return true;
|
|
156
|
+
return Date.now() - t > staleMs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isPidAlive(pid, host) {
|
|
160
|
+
if (host !== os.hostname()) return true; // can't introspect other hosts
|
|
161
|
+
if (pid === process.pid) return true;
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 0); // signal 0 = validate, don't deliver
|
|
164
|
+
return true;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
167
|
+
if (code === 'ESRCH') return false;
|
|
168
|
+
// EPERM / EACCES: process exists but is unsignalable; treat as alive.
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sleep(ms) {
|
|
174
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { acquire };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// scripts/lib/lockfile.d.cts — types for lockfile.cjs.
|
|
2
|
+
|
|
3
|
+
export interface AcquireOptions {
|
|
4
|
+
/** ms after which an existing lock is considered stale. Default 60_000. */
|
|
5
|
+
staleMs?: number;
|
|
6
|
+
/** total ms to wait before throwing LockAcquisitionError. Default 5_000. */
|
|
7
|
+
maxWaitMs?: number;
|
|
8
|
+
/** ms between retry attempts. Default 50. */
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Release function returned by `acquire()`. Idempotent. */
|
|
13
|
+
export type LockRelease = () => Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Acquire an advisory lock at `${path}.lock`. Returns a release function.
|
|
17
|
+
*
|
|
18
|
+
* @throws Error with `name === 'LockAcquisitionError'` when `maxWaitMs`
|
|
19
|
+
* elapses without acquiring.
|
|
20
|
+
*/
|
|
21
|
+
export function acquire(path: string, opts?: AcquireOptions): Promise<LockRelease>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// scripts/lib/logger/index.ts — Plan 21-04 (SDK-16).
|
|
2
|
+
//
|
|
3
|
+
// Public API for the Phase-21 structured logger. Consumers import ONLY
|
|
4
|
+
// from this file; `./types.ts` and `./sinks.ts` are implementation
|
|
5
|
+
// detail but their public types/classes are re-exported here for
|
|
6
|
+
// advanced callers (e.g., a test that wants to build a custom sink
|
|
7
|
+
// stack or a debug harness that wants to introspect levels).
|
|
8
|
+
//
|
|
9
|
+
// Surface:
|
|
10
|
+
// * createLogger(opts?) — build a Logger with auto mode detection
|
|
11
|
+
// * getLogger() — module-level singleton accessor
|
|
12
|
+
// * setLogger(l) — replace the singleton (tests)
|
|
13
|
+
// * resetLogger() — clear the singleton (tests)
|
|
14
|
+
// * Types + sinks re-exports — for advanced consumers / tests
|
|
15
|
+
//
|
|
16
|
+
// Mode detection (in priority order):
|
|
17
|
+
// 1. `opts.headless === true` → headless (JsonlSink)
|
|
18
|
+
// 2. `opts.headless === false` → interactive (ConsoleSink)
|
|
19
|
+
// 3. `process.env.GDD_HEADLESS === '1'` → headless
|
|
20
|
+
// 4. `process.env.GDD_HEADLESS === '0'` → interactive (explicit off wins)
|
|
21
|
+
// 5. `!process.stdout.isTTY` → headless
|
|
22
|
+
// 6. otherwise → interactive
|
|
23
|
+
//
|
|
24
|
+
// Event-stream integration (warn + error only):
|
|
25
|
+
// * `warn(msg, fields)` → `appendEvent({ type: 'error', payload: { level: 'warn', msg, fields } })`
|
|
26
|
+
// * `error(msg, fields)` → same, with level: 'error'.
|
|
27
|
+
// * `debug` and `info` never emit events.
|
|
28
|
+
// * Failures inside appendEvent are swallowed (logged once via
|
|
29
|
+
// process.stderr); the logger contract never propagates event-stream
|
|
30
|
+
// failures back to the caller.
|
|
31
|
+
|
|
32
|
+
import { appendEvent } from '../event-stream/index.ts';
|
|
33
|
+
import { ConsoleSink, JsonlSink, MultiSink } from './sinks.ts';
|
|
34
|
+
import {
|
|
35
|
+
LEVEL_ORDER,
|
|
36
|
+
type LogEntry,
|
|
37
|
+
type LogLevel,
|
|
38
|
+
type Logger,
|
|
39
|
+
type LoggerOptions,
|
|
40
|
+
type Sink,
|
|
41
|
+
} from './types.ts';
|
|
42
|
+
|
|
43
|
+
export type { LogLevel, LoggerOptions, LogEntry, Logger, Sink } from './types.ts';
|
|
44
|
+
export { LEVEL_ORDER } from './types.ts';
|
|
45
|
+
export { ConsoleSink, JsonlSink, MultiSink } from './sinks.ts';
|
|
46
|
+
export type {
|
|
47
|
+
ConsoleSinkOptions,
|
|
48
|
+
JsonlSinkOptions,
|
|
49
|
+
} from './sinks.ts';
|
|
50
|
+
export { DEFAULT_LOG_DIR, safeStringify } from './sinks.ts';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decide whether a logger with `opts` should run in headless mode.
|
|
54
|
+
* Explicit `opts.headless` wins; otherwise env; otherwise TTY check.
|
|
55
|
+
*/
|
|
56
|
+
function detectHeadless(opts: LoggerOptions): boolean {
|
|
57
|
+
if (opts.headless === true) return true;
|
|
58
|
+
if (opts.headless === false) return false;
|
|
59
|
+
const envFlag = process.env['GDD_HEADLESS'];
|
|
60
|
+
if (envFlag === '1') return true;
|
|
61
|
+
if (envFlag === '0') return false;
|
|
62
|
+
return !process.stdout.isTTY;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Flag so we only emit the `appendEvent failed` tripwire once per process. */
|
|
66
|
+
let eventStreamFailureLogged = false;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* LoggerImpl is not exported; callers build via `createLogger()` and
|
|
70
|
+
* hold a `Logger` interface reference. Keeping the implementation private
|
|
71
|
+
* lets us add fields (metrics, per-level counters) without breaking
|
|
72
|
+
* consumers.
|
|
73
|
+
*/
|
|
74
|
+
class LoggerImpl implements Logger {
|
|
75
|
+
private readonly sink: Sink;
|
|
76
|
+
private readonly minLevel: LogLevel;
|
|
77
|
+
private readonly scope: string | undefined;
|
|
78
|
+
private readonly baseFields: Record<string, unknown>;
|
|
79
|
+
private readonly now: () => string;
|
|
80
|
+
private readonly emitEvents: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
sink: Sink,
|
|
84
|
+
minLevel: LogLevel,
|
|
85
|
+
scope: string | undefined,
|
|
86
|
+
baseFields: Record<string, unknown>,
|
|
87
|
+
now: () => string,
|
|
88
|
+
emitEvents: boolean,
|
|
89
|
+
) {
|
|
90
|
+
this.sink = sink;
|
|
91
|
+
this.minLevel = minLevel;
|
|
92
|
+
this.scope = scope;
|
|
93
|
+
this.baseFields = baseFields;
|
|
94
|
+
this.now = now;
|
|
95
|
+
this.emitEvents = emitEvents;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private emit(level: LogLevel, msg: string, fields?: Record<string, unknown>): void {
|
|
99
|
+
// Level filter first — short-circuit below the minimum for perf.
|
|
100
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.minLevel]) return;
|
|
101
|
+
|
|
102
|
+
// Build the entry. Caller fields are merged shallow, then reserved
|
|
103
|
+
// keys are written last so they always override caller input.
|
|
104
|
+
const merged: Record<string, unknown> = { ...this.baseFields, ...(fields ?? {}) };
|
|
105
|
+
const entry: LogEntry = {
|
|
106
|
+
...merged,
|
|
107
|
+
ts: this.now(),
|
|
108
|
+
level,
|
|
109
|
+
msg,
|
|
110
|
+
pid: process.pid,
|
|
111
|
+
};
|
|
112
|
+
if (this.scope !== undefined) entry.scope = this.scope;
|
|
113
|
+
|
|
114
|
+
// Persist to the sink. Sinks are required not to throw, but we
|
|
115
|
+
// wrap defensively so a buggy custom sink cannot break callers.
|
|
116
|
+
try {
|
|
117
|
+
this.sink.write(entry);
|
|
118
|
+
} catch {
|
|
119
|
+
// Swallow.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Event-stream integration: warn + error emit an ErrorEvent.
|
|
123
|
+
if (this.emitEvents && (level === 'warn' || level === 'error')) {
|
|
124
|
+
try {
|
|
125
|
+
const payloadFields: Record<string, unknown> = { ...(fields ?? {}) };
|
|
126
|
+
appendEvent({
|
|
127
|
+
type: 'error',
|
|
128
|
+
timestamp: entry.ts,
|
|
129
|
+
sessionId: this.scope ?? 'anonymous',
|
|
130
|
+
payload: {
|
|
131
|
+
level,
|
|
132
|
+
msg,
|
|
133
|
+
fields: payloadFields,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
// Event-stream failures must not surface. Print one tripwire
|
|
138
|
+
// so operators notice, then stay silent.
|
|
139
|
+
if (!eventStreamFailureLogged) {
|
|
140
|
+
eventStreamFailureLogged = true;
|
|
141
|
+
try {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
'[logger] appendEvent failed; subsequent failures will be silent\n',
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
// If stderr is also dead, we've done all we can.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
debug(msg: string, fields?: Record<string, unknown>): void {
|
|
154
|
+
this.emit('debug', msg, fields);
|
|
155
|
+
}
|
|
156
|
+
info(msg: string, fields?: Record<string, unknown>): void {
|
|
157
|
+
this.emit('info', msg, fields);
|
|
158
|
+
}
|
|
159
|
+
warn(msg: string, fields?: Record<string, unknown>): void {
|
|
160
|
+
this.emit('warn', msg, fields);
|
|
161
|
+
}
|
|
162
|
+
error(msg: string, fields?: Record<string, unknown>): void {
|
|
163
|
+
this.emit('error', msg, fields);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
child(scope: string, fields?: Record<string, unknown>): Logger {
|
|
167
|
+
const childScope = this.scope !== undefined ? `${this.scope}.${scope}` : scope;
|
|
168
|
+
const mergedFields: Record<string, unknown> = {
|
|
169
|
+
...this.baseFields,
|
|
170
|
+
...(fields ?? {}),
|
|
171
|
+
};
|
|
172
|
+
return new LoggerImpl(
|
|
173
|
+
this.sink,
|
|
174
|
+
this.minLevel,
|
|
175
|
+
childScope,
|
|
176
|
+
mergedFields,
|
|
177
|
+
this.now,
|
|
178
|
+
this.emitEvents,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
flush(): void {
|
|
183
|
+
// v1 sinks are synchronous; reserved for future async implementations.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a Logger with auto mode detection. See the module-level
|
|
189
|
+
* mode-detection table.
|
|
190
|
+
*
|
|
191
|
+
* @throws RangeError when `opts.level` is not a member of LogLevel.
|
|
192
|
+
*/
|
|
193
|
+
export function createLogger(opts: LoggerOptions = {}): Logger {
|
|
194
|
+
const level = opts.level ?? 'info';
|
|
195
|
+
if (!(level in LEVEL_ORDER)) {
|
|
196
|
+
// Defensive: narrow with a runtime check — TypeScript cannot catch
|
|
197
|
+
// a caller passing `'verbose' as any`. Fail loud at construction
|
|
198
|
+
// rather than silently mis-filter downstream.
|
|
199
|
+
throw new RangeError(
|
|
200
|
+
`Invalid LogLevel: ${String(level)}. Expected one of: debug, info, warn, error.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const now = opts.nowOverride ?? ((): string => new Date().toISOString());
|
|
205
|
+
const emitEvents = opts.emitEventsOverride === false ? false : true;
|
|
206
|
+
const scope = opts.scope;
|
|
207
|
+
|
|
208
|
+
const headless = detectHeadless(opts);
|
|
209
|
+
const sink: Sink = headless
|
|
210
|
+
? new JsonlSink({
|
|
211
|
+
...(opts.logDir !== undefined ? { dir: opts.logDir } : {}),
|
|
212
|
+
...(opts.nowOverride !== undefined ? { nowOverride: opts.nowOverride } : {}),
|
|
213
|
+
})
|
|
214
|
+
: new ConsoleSink();
|
|
215
|
+
|
|
216
|
+
return new LoggerImpl(sink, level, scope, {}, now, emitEvents);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Module-level singleton. Built lazily on first `getLogger()` call. */
|
|
220
|
+
let defaultLogger: Logger | null = null;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Return the module-level default logger, constructing it on first
|
|
224
|
+
* call with default options. Used by modules that don't own a logger
|
|
225
|
+
* explicitly (session-runner, pipeline-runner, parallel-runners in
|
|
226
|
+
* later Phase-21 waves).
|
|
227
|
+
*/
|
|
228
|
+
export function getLogger(): Logger {
|
|
229
|
+
if (defaultLogger === null) {
|
|
230
|
+
defaultLogger = createLogger();
|
|
231
|
+
}
|
|
232
|
+
return defaultLogger;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Replace the module-level default logger. Intended for tests that
|
|
237
|
+
* want to inject a logger with a captured sink; also used by the
|
|
238
|
+
* session-runner boot path to install a logger with its pinned
|
|
239
|
+
* `logDir`/`scope` before any child import calls `getLogger()`.
|
|
240
|
+
*/
|
|
241
|
+
export function setLogger(l: Logger): void {
|
|
242
|
+
defaultLogger = l;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Clear the module-level default logger. Next `getLogger()` rebuilds
|
|
247
|
+
* with current env/TTY state. Primarily a test hook.
|
|
248
|
+
*/
|
|
249
|
+
export function resetLogger(): void {
|
|
250
|
+
defaultLogger = null;
|
|
251
|
+
}
|