@hegemonart/get-design-done 1.28.8 → 1.30.5
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +116 -0
- package/README.de.md +25 -0
- package/README.fr.md +25 -0
- package/README.it.md +25 -0
- package/README.ja.md +25 -0
- package/README.ko.md +25 -0
- package/README.md +30 -0
- package/README.zh-CN.md +25 -0
- package/SKILL.md +2 -0
- package/agents/design-authority-watcher.md +42 -1
- package/agents/design-reflector.md +50 -0
- package/package.json +1 -1
- package/reference/capability-gap-stage-gate.md +261 -0
- package/reference/known-failure-modes.md +521 -0
- package/reference/pseudonymization-rules.md +189 -0
- package/reference/registry.json +22 -1
- package/reference/schemas/events.schema.json +158 -3
- package/reference/schemas/generated.d.ts +319 -4
- package/scripts/cli/gdd-events.mjs +35 -2
- package/scripts/gsd-cleanup-incubator.cjs +367 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +455 -0
- package/scripts/lib/authority-watcher/index.cjs +201 -0
- package/scripts/lib/bandit-router.cjs +92 -9
- package/scripts/lib/failure-mode-matcher.cjs +460 -0
- package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
- package/scripts/lib/incubator-author.cjs +845 -0
- package/scripts/lib/install/interactive.cjs +27 -2
- package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
- package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
- package/scripts/lib/issue-reporter/dedup.cjs +458 -0
- package/scripts/lib/issue-reporter/destination.cjs +37 -0
- package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
- package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
- package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
- package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
- package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
- package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
- package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
- package/scripts/lib/pseudonymize.cjs +444 -0
- package/scripts/lib/reflections-cycle-writer.cjs +172 -0
- package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
- package/scripts/lib/reflector-capability-gap-aggregator.cjs +352 -0
- package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
- package/scripts/release-smoke-test.cjs +33 -2
- package/scripts/validate-incubator-scope.cjs +133 -0
- package/skills/apply-reflections/SKILL.md +20 -1
- package/skills/apply-reflections/apply-reflections-procedure.md +106 -4
- package/skills/fast/SKILL.md +46 -0
- package/skills/reflect/SKILL.md +9 -0
- package/skills/reflect/procedures/capability-gap-scan.md +120 -0
- package/skills/report-issue/SKILL.md +53 -0
- package/skills/report-issue/report-issue-procedure.md +120 -0
- package/skills/router/SKILL.md +5 -0
- package/skills/router/capability-gap-emitter.md +65 -0
- package/skills/update/SKILL.md +3 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payload-assembly.cjs — Phase 30 Plan 30-02 issue payload assembler.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for what a reported issue payload looks like
|
|
5
|
+
* BEFORE it ever hits disk (D-04) or a clipboard. Pure module: no I/O,
|
|
6
|
+
* no globals consumed, no env reads, no clock reads. Deterministic for
|
|
7
|
+
* fixed inputs (this is what enables the golden snapshot test).
|
|
8
|
+
*
|
|
9
|
+
* Two-layer scrub pipeline (order is non-negotiable):
|
|
10
|
+
* Step 1: Phase 22 redact.cjs → strips secrets to `[REDACTED:type]`
|
|
11
|
+
* Step 2: Phase 30 pseudonymize → rewrites identity (user/path/host)
|
|
12
|
+
*
|
|
13
|
+
* The order matters: if pseudonymize ran first, the username PORTION of
|
|
14
|
+
* a token like `sk-ant-aliceUser-…` would be rewritten before the
|
|
15
|
+
* redact pattern got a chance to match the whole token, leaving a
|
|
16
|
+
* half-mangled secret hint in the payload. Case 9 of the test suite
|
|
17
|
+
* locks this order with a negative test (see threat T-30-02-01).
|
|
18
|
+
*
|
|
19
|
+
* Pseudonymize (Plan 30-01) is late-bound INSIDE assemble() — NOT at
|
|
20
|
+
* module-scope. This makes 30-02 parallel-safe with 30-01 at planning
|
|
21
|
+
* time. If 30-01 hasn't shipped yet when assemble() is first called,
|
|
22
|
+
* a clear remediation error is thrown.
|
|
23
|
+
*
|
|
24
|
+
* D-01: Disclaimer text is hardcoded as module constants. No template
|
|
25
|
+
* files, no i18n indirection, no env override.
|
|
26
|
+
* D-04: Returns a STRING. Persistence is a separate concern (Plan 30-04).
|
|
27
|
+
* D-14: capability_gap inclusion iterates EXACTLY the 7 Phase 29 D-02
|
|
28
|
+
* fields by name; extra keys on the input object are silently
|
|
29
|
+
* dropped. This is the enforcement mechanism for D-14.
|
|
30
|
+
*
|
|
31
|
+
* hostOsClass contract: the caller (30-03 collector) is responsible for
|
|
32
|
+
* narrowing the OS string to one of "linux" | "darwin" | "windows".
|
|
33
|
+
* Full uname / kernel version is OUT OF SCOPE and must not be passed in
|
|
34
|
+
* (threat T-30-02-03).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const crypto = require('node:crypto');
|
|
40
|
+
const { redact } = require('../redact.cjs');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* D-01 disclaimer constants. Hardcoded, verbatim, bilingual.
|
|
44
|
+
* Tests Cases 2 + 3 assert these exact substrings appear in the output;
|
|
45
|
+
* any change to the prose must update the tests in lockstep.
|
|
46
|
+
*/
|
|
47
|
+
const DISCLAIMER_RU =
|
|
48
|
+
'Это псевдонимизация, не анонимизация. Содержимое промптов и кода может косвенно идентифицировать. Финальный ревью — на тебе.';
|
|
49
|
+
const DISCLAIMER_EN =
|
|
50
|
+
'This is pseudonymization, not anonymization. Prompt and code contents can still indirectly identify. Final review is on you.';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The seven Phase 29 D-02 capability_gap fields, in fixed render order.
|
|
54
|
+
* D-14: iteration is BY THIS LIST. Extra keys on the input event object
|
|
55
|
+
* are intentionally dropped — never rendered. Adding fields here would
|
|
56
|
+
* leak fields that don't exist in the Phase 29 source contract.
|
|
57
|
+
*/
|
|
58
|
+
const CAPABILITY_GAP_FIELDS = [
|
|
59
|
+
'event_type',
|
|
60
|
+
'command_name',
|
|
61
|
+
'capability_id',
|
|
62
|
+
'expected_outcome',
|
|
63
|
+
'observed_outcome',
|
|
64
|
+
'runtime',
|
|
65
|
+
'timestamp',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalize a stack-trace string for stable fingerprinting.
|
|
70
|
+
*
|
|
71
|
+
* Strips:
|
|
72
|
+
* - line:col offsets (`:42:18` → '')
|
|
73
|
+
* - absolute path prefixes (POSIX `/.../` and Windows `\...\\` → '')
|
|
74
|
+
*
|
|
75
|
+
* Keeps:
|
|
76
|
+
* - frame leading text (`at Object.<anonymous> (`)
|
|
77
|
+
* - basename of the file
|
|
78
|
+
* - trailing characters after the location (e.g., closing paren)
|
|
79
|
+
*
|
|
80
|
+
* This is what makes fingerprints stable across machines and across
|
|
81
|
+
* runs from different working directories. Two users hitting the same
|
|
82
|
+
* bug from different cwd's get the same fingerprint → dedup works.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} stack
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function normalizeStack(stack) {
|
|
88
|
+
if (typeof stack !== 'string' || stack.length === 0) return '';
|
|
89
|
+
const lines = stack.split('\n');
|
|
90
|
+
const normalized = lines.map((rawLine) => {
|
|
91
|
+
let line = rawLine;
|
|
92
|
+
// Strip :line:col offsets (handle one or both; line:col is the common
|
|
93
|
+
// Node format; line-only also appears for some runtimes).
|
|
94
|
+
line = line.replace(/:\d+:\d+/g, '');
|
|
95
|
+
line = line.replace(/:\d+(?=\)|\s|$)/g, '');
|
|
96
|
+
// Strip absolute path prefixes — keep basename only. Match both
|
|
97
|
+
// POSIX (`/`) and Windows (`\\`) separators. The regex is greedy:
|
|
98
|
+
// remove everything up through the last path separator.
|
|
99
|
+
line = line.replace(/[A-Za-z]?:?[/\\][^()\s]*[/\\]/g, '');
|
|
100
|
+
return line.trim();
|
|
101
|
+
});
|
|
102
|
+
return normalized.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compute a deterministic fingerprint for dedup grouping.
|
|
107
|
+
*
|
|
108
|
+
* Formula: sha256(normalize(stack) + '|' + command_name + '|' + runtime + '|' + plugin_version)
|
|
109
|
+
*
|
|
110
|
+
* Locked by Cases 5 (determinism), 6 (cross-cwd stability), 7+8 (changes
|
|
111
|
+
* when the inputs change). See threat T-30-02-05.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} args
|
|
114
|
+
* @param {string} [args.stack] — error stack trace string
|
|
115
|
+
* @param {string} [args.commandName] — e.g., "gsd:plan-phase"
|
|
116
|
+
* @param {string} [args.runtime] — e.g., "claude-code"
|
|
117
|
+
* @param {string} [args.pluginVersion] — e.g., "1.30.0"
|
|
118
|
+
* @returns {string} — 64-char hex digest
|
|
119
|
+
*/
|
|
120
|
+
function computeFingerprint(args) {
|
|
121
|
+
const stack = args && args.stack != null ? args.stack : '';
|
|
122
|
+
const commandName = args && args.commandName != null ? args.commandName : '';
|
|
123
|
+
const runtime = args && args.runtime != null ? args.runtime : '';
|
|
124
|
+
const pluginVersion = args && args.pluginVersion != null ? args.pluginVersion : '';
|
|
125
|
+
const material =
|
|
126
|
+
normalizeStack(String(stack)) +
|
|
127
|
+
'|' +
|
|
128
|
+
String(commandName) +
|
|
129
|
+
'|' +
|
|
130
|
+
String(runtime) +
|
|
131
|
+
'|' +
|
|
132
|
+
String(pluginVersion);
|
|
133
|
+
return crypto.createHash('sha256').update(material).digest('hex');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Late-bound require of Plan 30-01's pseudonymize. Called INSIDE
|
|
138
|
+
* assemble() rather than at module scope so that 30-02 can be planned
|
|
139
|
+
* and reviewed before 30-01 has landed. If 30-01 hasn't shipped, the
|
|
140
|
+
* call throws a clear remediation error instead of crashing at require.
|
|
141
|
+
*
|
|
142
|
+
* The real 30-01 API is `pseudonymize(payload, opts) -> { payload, replacements }`
|
|
143
|
+
* (see scripts/lib/pseudonymize.cjs). Caller supplies identity/hostname/
|
|
144
|
+
* repoOrigin via opts; this module unwraps `.payload` from the return.
|
|
145
|
+
*
|
|
146
|
+
* @returns {(payload: unknown, opts?: object) => { payload: unknown, replacements: Array<object> }}
|
|
147
|
+
*/
|
|
148
|
+
function loadPseudonymize() {
|
|
149
|
+
try {
|
|
150
|
+
// eslint-disable-next-line global-require
|
|
151
|
+
const mod = require('../pseudonymize.cjs');
|
|
152
|
+
if (!mod || typeof mod.pseudonymize !== 'function') {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'pseudonymize.cjs loaded but does not export a `pseudonymize` function.'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return mod.pseudonymize;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Phase 30 payload assembly requires scripts/lib/pseudonymize.cjs ' +
|
|
161
|
+
'(Plan 30-01). Run Plan 30-01 first. Underlying error: ' +
|
|
162
|
+
(err && err.message ? err.message : String(err))
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the `opts` object passed to Plan 30-01's pseudonymize() from the
|
|
169
|
+
* fields the caller stuffed onto errorContext. All fields are optional —
|
|
170
|
+
* pseudonymize handles missing inputs gracefully (rule helpers no-op on
|
|
171
|
+
* empty identity/hostname).
|
|
172
|
+
*
|
|
173
|
+
* @param {object} errorContext
|
|
174
|
+
* @returns {object} opts compatible with scripts/lib/pseudonymize.cjs
|
|
175
|
+
*/
|
|
176
|
+
function buildPseudonymizeOpts(errorContext) {
|
|
177
|
+
const ctx = errorContext || {};
|
|
178
|
+
return {
|
|
179
|
+
identity: ctx.identity && typeof ctx.identity === 'object' ? ctx.identity : {},
|
|
180
|
+
hostname: typeof ctx.hostname === 'string' ? ctx.hostname : '',
|
|
181
|
+
repoOrigin: typeof ctx.repoOrigin === 'string' ? ctx.repoOrigin : '',
|
|
182
|
+
repoVisibility: ctx.repoVisibility,
|
|
183
|
+
envSnapshot:
|
|
184
|
+
ctx.envSnapshot && typeof ctx.envSnapshot === 'object' ? ctx.envSnapshot : {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Render the bilingual disclaimer block. RU above EN, both inside a
|
|
190
|
+
* single GitHub-flavored markdown blockquote with an [!IMPORTANT] alert.
|
|
191
|
+
* D-01 mandates this block be the FIRST thing in the payload, before any
|
|
192
|
+
* technical content. Locked by Cases 2, 3, 4.
|
|
193
|
+
*
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function renderDisclaimer() {
|
|
197
|
+
return (
|
|
198
|
+
'> [!IMPORTANT] Disclaimer / Дисклеймер\n' +
|
|
199
|
+
'> ' +
|
|
200
|
+
DISCLAIMER_RU +
|
|
201
|
+
'\n' +
|
|
202
|
+
'>\n' +
|
|
203
|
+
'> ' +
|
|
204
|
+
DISCLAIMER_EN
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the optional capability_gap section. D-14: iterate the 7 D-02
|
|
210
|
+
* fields explicitly by name. Extra keys on `event` are dropped — this
|
|
211
|
+
* is the leak prevention. Returns null when event is null/undefined so
|
|
212
|
+
* the caller can omit the section header entirely.
|
|
213
|
+
*
|
|
214
|
+
* @param {object|null|undefined} event
|
|
215
|
+
* @returns {string|null}
|
|
216
|
+
*/
|
|
217
|
+
function renderCapabilityGap(event) {
|
|
218
|
+
if (event == null) return null;
|
|
219
|
+
const lines = ['## Capability Gap'];
|
|
220
|
+
// D-14: only the 7 D-02 fields are rendered; extra keys on the event
|
|
221
|
+
// are intentionally dropped.
|
|
222
|
+
for (const field of CAPABILITY_GAP_FIELDS) {
|
|
223
|
+
const raw = event[field];
|
|
224
|
+
const value = raw == null ? '' : String(raw);
|
|
225
|
+
lines.push('- ' + field + ': ' + value);
|
|
226
|
+
}
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render trajectory reference. When provided, prints verbatim (the path
|
|
232
|
+
* is NOT dereferenced — that's the caller's concern). When omitted,
|
|
233
|
+
* renders the italic placeholder `_not provided_`.
|
|
234
|
+
*
|
|
235
|
+
* @param {string|null|undefined} ref
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
function renderTrajectoryRef(ref) {
|
|
239
|
+
if (ref == null || ref === '') return '_not provided_';
|
|
240
|
+
return String(ref);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Assemble a deterministic, scrubbed, bilingual-disclaimer issue payload.
|
|
245
|
+
*
|
|
246
|
+
* Pure: no I/O, no globals consumed, deterministic for fixed inputs.
|
|
247
|
+
*
|
|
248
|
+
* errorContext.identity / .hostname / .repoOrigin / .repoVisibility /
|
|
249
|
+
* .envSnapshot are passed to 30-01 pseudonymize() via opts. Any of those
|
|
250
|
+
* may be omitted — pseudonymize no-ops on empty inputs.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} commandName e.g., "gsd:plan-phase"
|
|
253
|
+
* @param {object} errorContext { message, stack, runtime, pluginVersion, nodeVersion, hostOsClass, identity?, hostname?, repoOrigin?, repoVisibility?, envSnapshot? }
|
|
254
|
+
* @param {string} [trajectoryRef] relative path or ID; printed verbatim
|
|
255
|
+
* @param {object} [capabilityGapEvent] full D-02 event; only its 7 fields rendered
|
|
256
|
+
* @returns {string} markdown payload
|
|
257
|
+
*/
|
|
258
|
+
function assemble(commandName, errorContext, trajectoryRef, capabilityGapEvent) {
|
|
259
|
+
// Late-bind 30-01's pseudonymize at call-time. Keeps 30-02 parallel-
|
|
260
|
+
// safe with 30-01 at planning time. If 30-01 hasn't shipped, this
|
|
261
|
+
// throws an informative error instead of crashing at module load.
|
|
262
|
+
const pseudonymize = loadPseudonymize();
|
|
263
|
+
|
|
264
|
+
// Step 1: redact secrets (Phase 22). MUST run BEFORE pseudonymize.
|
|
265
|
+
// See header comment + threat T-30-02-01 + Case 9 negative test.
|
|
266
|
+
const ctxRedacted = redact(errorContext == null ? {} : errorContext);
|
|
267
|
+
const gapRedacted =
|
|
268
|
+
capabilityGapEvent == null ? null : redact(capabilityGapEvent);
|
|
269
|
+
|
|
270
|
+
// Step 2: pseudonymize identity (Phase 30 Plan 30-01).
|
|
271
|
+
// 30-01 API: pseudonymize(payload, opts) -> { payload, replacements }
|
|
272
|
+
const pseudoOpts = buildPseudonymizeOpts(ctxRedacted);
|
|
273
|
+
const ctxResult = pseudonymize(ctxRedacted, pseudoOpts);
|
|
274
|
+
const ctxScrubbed = ctxResult && ctxResult.payload != null ? ctxResult.payload : ctxRedacted;
|
|
275
|
+
|
|
276
|
+
let gapScrubbed = null;
|
|
277
|
+
if (gapRedacted != null) {
|
|
278
|
+
const gapResult = pseudonymize(gapRedacted, pseudoOpts);
|
|
279
|
+
gapScrubbed = gapResult && gapResult.payload != null ? gapResult.payload : gapRedacted;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Pull scrubbed fields out for rendering. Default to '' so the markdown
|
|
283
|
+
// shape remains stable even when the caller passes a sparse object.
|
|
284
|
+
const scrubbedMessage =
|
|
285
|
+
ctxScrubbed && ctxScrubbed.message != null ? String(ctxScrubbed.message) : '';
|
|
286
|
+
const scrubbedStack =
|
|
287
|
+
ctxScrubbed && ctxScrubbed.stack != null ? String(ctxScrubbed.stack) : '';
|
|
288
|
+
const runtime =
|
|
289
|
+
ctxScrubbed && ctxScrubbed.runtime != null ? String(ctxScrubbed.runtime) : '';
|
|
290
|
+
const pluginVersion =
|
|
291
|
+
ctxScrubbed && ctxScrubbed.pluginVersion != null
|
|
292
|
+
? String(ctxScrubbed.pluginVersion)
|
|
293
|
+
: '';
|
|
294
|
+
const nodeVersion =
|
|
295
|
+
ctxScrubbed && ctxScrubbed.nodeVersion != null
|
|
296
|
+
? String(ctxScrubbed.nodeVersion)
|
|
297
|
+
: '';
|
|
298
|
+
const hostOsClass =
|
|
299
|
+
ctxScrubbed && ctxScrubbed.hostOsClass != null
|
|
300
|
+
? String(ctxScrubbed.hostOsClass)
|
|
301
|
+
: '';
|
|
302
|
+
|
|
303
|
+
// Step 3: fingerprint. Use SCRUBBED stack so pseudonymized identifiers
|
|
304
|
+
// are baked into the fingerprint — same bug from two different users
|
|
305
|
+
// still hashes the same after Plan 30-01's identity rewrite.
|
|
306
|
+
const fingerprint = computeFingerprint({
|
|
307
|
+
stack: scrubbedStack,
|
|
308
|
+
commandName: String(commandName == null ? '' : commandName),
|
|
309
|
+
runtime,
|
|
310
|
+
pluginVersion,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Step 4: render markdown. Disclaimer FIRST (D-01), then command,
|
|
314
|
+
// then fingerprint, then runtime metadata, then error + stack, then
|
|
315
|
+
// trajectory, then optional capability_gap.
|
|
316
|
+
const sections = [];
|
|
317
|
+
|
|
318
|
+
sections.push(renderDisclaimer());
|
|
319
|
+
|
|
320
|
+
sections.push('## Command\n`' + String(commandName == null ? '' : commandName) + '`');
|
|
321
|
+
|
|
322
|
+
sections.push(
|
|
323
|
+
'## Fingerprint\n`' +
|
|
324
|
+
fingerprint +
|
|
325
|
+
'` — derived from normalized stack + command + runtime + version'
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
sections.push(
|
|
329
|
+
'## Runtime\n' +
|
|
330
|
+
'- Node: ' +
|
|
331
|
+
nodeVersion +
|
|
332
|
+
'\n' +
|
|
333
|
+
'- Plugin: ' +
|
|
334
|
+
pluginVersion +
|
|
335
|
+
'\n' +
|
|
336
|
+
'- OS class: ' +
|
|
337
|
+
hostOsClass
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
sections.push('## Error\n```\n' + scrubbedMessage + '\n```');
|
|
341
|
+
|
|
342
|
+
sections.push('### Stack (normalized)\n```\n' + scrubbedStack + '\n```');
|
|
343
|
+
|
|
344
|
+
sections.push('## Trajectory\n' + renderTrajectoryRef(trajectoryRef));
|
|
345
|
+
|
|
346
|
+
const capGapSection = renderCapabilityGap(gapScrubbed);
|
|
347
|
+
if (capGapSection !== null) {
|
|
348
|
+
sections.push(capGapSection);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Join with blank lines between sections. Trailing newline keeps the
|
|
352
|
+
// file POSIX-friendly and makes `cat`/`git diff` happy.
|
|
353
|
+
return sections.join('\n\n') + '\n';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
assemble,
|
|
358
|
+
computeFingerprint,
|
|
359
|
+
DISCLAIMER_RU,
|
|
360
|
+
DISCLAIMER_EN,
|
|
361
|
+
// Internal helpers — exported only because tests want stable hooks.
|
|
362
|
+
// Treat as private API; downstream plans must not import these.
|
|
363
|
+
_internal: {
|
|
364
|
+
normalizeStack,
|
|
365
|
+
CAPABILITY_GAP_FIELDS,
|
|
366
|
+
},
|
|
367
|
+
};
|