neo-agent-skills 0.1.70 → 0.1.71

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.
@@ -1,7 +1,9 @@
1
- # Design authority — a ticket that calls an existing behavior wrong
1
+ # Design authority — non-epic agent tickets
2
2
 
3
- A receipt — geometry, timing, a rendered frame — shows what happens, never whether it is wanted. When a ticket calls an existing behavior wrong and there is no crash, no red and no contradiction with the surface's own JSDoc, its Architectural Reality carries `Design authority: <the record sentence, quoted>` — the ADR section, guide or owning JSDoc that made the behavior intended — before The Fix is written. Search the decision-record directory, the guides and `ask_knowledge_base`. Nothing found → `Design authority: none found (searched: …)` and a fork on the owning record (its epic, or a Discussion), not a Fix.
3
+ A receipt — geometry, timing, a rendered frame — shows what happens, never whether it is wanted. Every non-epic agent ticket without a defect excuse (fenced error, spec path or failing test in Problem/Context, or quoted `@`-JSDoc) carries in The Architectural Reality before The Fix:
4
4
 
5
- Reviewers read the line as the premise: `pr-review` §9.0.
5
+ - `Design authority: <the record sentence, quoted>` — ADR, guide or owning JSDoc making the behavior intended.
6
+ - `Design authority: none found (searched: …)` — searched decisions, guides and `ask_knowledge_base`; fork on the owning record, not a Fix.
7
+ - `Design authority: n/a — <reason>` — calls no existing behavior wrong.
6
8
 
7
- *Retire this reference once a body lint enforces the line on behavior-change tickets.*
9
+ Reviewers read the line as the premise: `pr-review` §9.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo-agent-skills",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Install the workflow substrate a cross-model AI team actually runs on: ticket intake, PR review gates, lane coordination and guards, versioned as a package. The skills Neo.mjs maintainers load every session — materialized into your repo, not copied.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,7 @@
22
22
  "bin": {
23
23
  "neo-agent-skills-materialize": "./scripts/materialize-harness-skills.mjs",
24
24
  "neo-agent-skills-pr-body": "./scripts/check-pr-body.mjs",
25
+ "neo-agent-skills-ticket-body": "./scripts/check-ticket-body.mjs",
25
26
  "neo-agent-skills-substrate-size": "./scripts/check-substrate-size.mjs",
26
27
  "neo-agent-skills-ticket-archaeology": "./scripts/check-ticket-archaeology.mjs",
27
28
  "neo-agent-skills-workflow-concurrency": "./scripts/check-workflow-concurrency.mjs",
@@ -47,6 +48,7 @@
47
48
  "scripts/check-secrets.mjs",
48
49
  "scripts/check-substrate-size.mjs",
49
50
  "scripts/check-ticket-archaeology.mjs",
51
+ "scripts/check-ticket-body.mjs",
50
52
  "scripts/check-workflow-concurrency.mjs",
51
53
  "scripts/generate-agents-md.mjs",
52
54
  "scripts/materialize-harness-skills.mjs"
@@ -54,7 +56,7 @@
54
56
  "scripts": {
55
57
  "materialize": "node scripts/materialize-harness-skills.mjs",
56
58
  "lint": "node scripts/lint-skill-corpus.mjs",
57
- "test": "node scripts/test-lint-skill-corpus.mjs && node scripts/test-reusable-pr-baseline.mjs && node scripts/test-reusable-dependabot-automerge.mjs && node scripts/test-ticket-archaeology.mjs && node scripts/test-substrate-size.mjs && node scripts/test-check-pr-body.mjs && node scripts/test-workflow-concurrency.mjs && node scripts/test-generate-agents-md.mjs && node scripts/test-materialize-harness-skills.mjs && node scripts/test-npm-overrides.mjs && node scripts/test-secrets.mjs && node scripts/test-commit-authorship.mjs && node scripts/test-check-discussion-fidelity.mjs && node scripts/test-tag-release.mjs && node scripts/test-check-version-bump.mjs && node scripts/test-release-origin.mjs",
59
+ "test": "node scripts/test-lint-skill-corpus.mjs && node scripts/test-reusable-pr-baseline.mjs && node scripts/test-reusable-dependabot-automerge.mjs && node scripts/test-ticket-archaeology.mjs && node scripts/test-substrate-size.mjs && node scripts/test-check-pr-body.mjs && node scripts/test-check-ticket-body.mjs && node scripts/test-workflow-concurrency.mjs && node scripts/test-generate-agents-md.mjs && node scripts/test-materialize-harness-skills.mjs && node scripts/test-npm-overrides.mjs && node scripts/test-secrets.mjs && node scripts/test-commit-authorship.mjs && node scripts/test-check-discussion-fidelity.mjs && node scripts/test-tag-release.mjs && node scripts/test-check-version-bump.mjs && node scripts/test-release-origin.mjs",
58
60
  "generate-agents-md": "node scripts/generate-agents-md.mjs",
59
61
  "postpublish": "node scripts/tag-release.mjs"
60
62
  },
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @summary Validates a GitHub issue body against the Design-authority gate and its three defect excuses.
4
+ *
5
+ * Pure and transport-free: receives a ticket body and returns findings without reading the network.
6
+ *
7
+ * Two arms (enforced on every non-epic agent ticket):
8
+ * 1. The `Design authority:` line is present in content using one of the three grammars from
9
+ * `.agents/skills/ticket-create/references/design-authority.md`:
10
+ * - `Design authority: "<the record sentence, quoted>"`
11
+ * - `Design authority: none found (searched: …)`
12
+ * - `Design authority: n/a — <reason>`
13
+ * 2. The line is absent, and the body carries at least one of the three defect excuses:
14
+ * - `crash`: a fenced code block containing a crash, error, or stack trace;
15
+ * - `red`: a spec/test path or failing test title in the Problem/Context sections;
16
+ * - `jsdoc`: a quoted `@`-JSDoc line showing the contradicted contract sentence.
17
+ *
18
+ * When neither arm holds, the guard exits non-zero and points to `design-authority.md`.
19
+ */
20
+
21
+ import {readFileSync, realpathSync} from 'node:fs';
22
+ import process from 'node:process';
23
+ import {parseArgs} from 'node:util';
24
+ import {fileURLToPath} from 'node:url';
25
+
26
+ /**
27
+ * Canonical skill reference path named in failure diagnostics.
28
+ * @member {String} DESIGN_AUTHORITY_REF
29
+ */
30
+ export const DESIGN_AUTHORITY_REF = '.agents/skills/ticket-create/references/design-authority.md';
31
+
32
+ const
33
+ INDENT_CODE_RE = /^(?: {4,}|\t)/,
34
+ CRASH_TOKENS = Object.freeze([
35
+ 'Error:',
36
+ 'TypeError:',
37
+ 'ReferenceError:',
38
+ 'SyntaxError:',
39
+ 'RangeError:',
40
+ 'URIError:',
41
+ 'EvalError:',
42
+ 'AssertionError',
43
+ 'ERR_',
44
+ 'Uncaught ',
45
+ 'Exception',
46
+ 'Segmentation fault',
47
+ 'FATAL',
48
+ 'Command exited with code',
49
+ 'npm ERR!',
50
+ 'FAIL ',
51
+ 'FAILED'
52
+ ]),
53
+ JSDOC_TAGS = Object.freeze([
54
+ '@summary',
55
+ '@param',
56
+ '@returns',
57
+ '@return',
58
+ '@throws',
59
+ '@member',
60
+ '@class',
61
+ '@config',
62
+ '@function',
63
+ '@fires',
64
+ '@see',
65
+ '@default',
66
+ '@type',
67
+ '@example',
68
+ '@description'
69
+ ]),
70
+ STOP_SECTIONS = Object.freeze([
71
+ 'the architectural reality',
72
+ 'architectural reality',
73
+ 'the fix',
74
+ 'proposed fix',
75
+ 'fix',
76
+ 'acceptance criteria',
77
+ 'out of scope',
78
+ 'avoided traps',
79
+ 'related'
80
+ ]);
81
+
82
+ /**
83
+ * @summary Parses a Markdown fence line (` ``` ` or ` ~~~ ` with at most 3 leading spaces) in linear time.
84
+ * @param {String} line
85
+ * @returns {{fence: String, info: String}|null}
86
+ */
87
+ function parseFenceLine(line) {
88
+ let spaces = 0;
89
+ while (spaces < 4 && spaces < line.length && line[spaces] === ' ') {
90
+ spaces++
91
+ }
92
+ if (spaces > 3 || spaces >= line.length) return null;
93
+
94
+ const ch = line[spaces];
95
+ if (ch !== '`' && ch !== '~') return null;
96
+
97
+ let runLen = 0;
98
+ while (spaces + runLen < line.length && line[spaces + runLen] === ch) {
99
+ runLen++
100
+ }
101
+ if (runLen < 3) return null;
102
+
103
+ return {
104
+ fence: line.slice(spaces, spaces + runLen),
105
+ info : line.slice(spaces + runLen).trim()
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @summary Extracts a Markdown heading's normalized lowercase title in linear time.
111
+ * @param {String} line
112
+ * @returns {String|null}
113
+ */
114
+ function parseHeadingTitle(line) {
115
+ let spaces = 0;
116
+ while (spaces < 4 && spaces < line.length && line[spaces] === ' ') {
117
+ spaces++
118
+ }
119
+ if (spaces > 3 || spaces >= line.length) return null;
120
+
121
+ let hashes = 0;
122
+ while (hashes < 7 && spaces + hashes < line.length && line[spaces + hashes] === '#') {
123
+ hashes++
124
+ }
125
+ if (hashes < 1 || hashes > 6) return null;
126
+
127
+ const after = line.slice(spaces + hashes);
128
+ if (!after || (after[0] !== ' ' && after[0] !== '\t')) return null;
129
+
130
+ const title = after.replaceAll('`', '').trim().toLowerCase();
131
+ return title || null
132
+ }
133
+
134
+ /**
135
+ * @summary Extracts prose lines outside fenced and indented code blocks.
136
+ * @param {String} [body='']
137
+ * @returns {String[]}
138
+ */
139
+ export function markdownContentLines(body = '') {
140
+ const
141
+ lines = String(body).split('\n'),
142
+ out = [];
143
+
144
+ let fence = null;
145
+
146
+ for (const rawLine of lines) {
147
+ const
148
+ line = rawLine.replace(/\r$/, ''),
149
+ opener = parseFenceLine(line);
150
+
151
+ if (fence) {
152
+ if (opener && opener.fence[0] === fence[0] && opener.fence.length >= fence.length && !opener.info) {
153
+ fence = null
154
+ }
155
+ continue
156
+ }
157
+
158
+ if (opener) {
159
+ fence = opener.fence;
160
+ continue
161
+ }
162
+
163
+ if (INDENT_CODE_RE.test(line)) continue;
164
+
165
+ out.push(line)
166
+ }
167
+
168
+ return out
169
+ }
170
+
171
+ /**
172
+ * @summary Extracts fenced code blocks (`info` string and inner `text`) from a Markdown body.
173
+ * @param {String} [body='']
174
+ * @returns {Array<{info: String, text: String}>}
175
+ */
176
+ export function fencedBlocks(body = '') {
177
+ const
178
+ lines = String(body).split('\n'),
179
+ blocks = [];
180
+
181
+ let active = null;
182
+
183
+ for (const rawLine of lines) {
184
+ const
185
+ line = rawLine.replace(/\r$/, ''),
186
+ opener = parseFenceLine(line);
187
+
188
+ if (active) {
189
+ if (opener && opener.fence[0] === active.fence[0] && opener.fence.length >= active.fence.length && !opener.info) {
190
+ blocks.push({info: active.info, text: active.lines.join('\n')});
191
+ active = null;
192
+ continue
193
+ }
194
+ active.lines.push(line);
195
+ continue
196
+ }
197
+
198
+ if (opener) {
199
+ active = {fence: opener.fence, info: opener.info.toLowerCase(), lines: []}
200
+ }
201
+ }
202
+
203
+ if (active) {
204
+ blocks.push({info: active.info, text: active.lines.join('\n')})
205
+ }
206
+
207
+ return blocks
208
+ }
209
+
210
+ /**
211
+ * @summary Collects all quoted or backticked substrings within a single line.
212
+ * @param {String} line
213
+ * @returns {String[]}
214
+ */
215
+ function extractQuotedSpans(line) {
216
+ const
217
+ spans = [],
218
+ pairs = [
219
+ ['"', '"'],
220
+ ["'", "'"],
221
+ ['\u201c', '\u201d'],
222
+ ['`', '`']
223
+ ];
224
+
225
+ for (const [open, close] of pairs) {
226
+ let pos = 0;
227
+
228
+ while (pos < line.length) {
229
+ const start = line.indexOf(open, pos);
230
+ if (start === -1) break;
231
+
232
+ const end = line.indexOf(close, start + open.length);
233
+ if (end === -1) break;
234
+
235
+ const inner = line.slice(start + open.length, end).trim();
236
+ if (inner) spans.push(inner);
237
+
238
+ pos = end + close.length
239
+ }
240
+ }
241
+
242
+ return spans
243
+ }
244
+
245
+ /**
246
+ * @summary Checks whether a ticket body carries a valid `Design authority:` line in content.
247
+ * @param {String} [body='']
248
+ * @returns {Boolean}
249
+ */
250
+ export function hasDesignAuthorityLine(body = '') {
251
+ const lines = markdownContentLines(body);
252
+
253
+ for (const rawLine of lines) {
254
+ let text = rawLine.trim();
255
+ if (!text || text.startsWith('|')) continue;
256
+
257
+ if (text.startsWith('- ') || text.startsWith('* ') || text.startsWith('+ ')) {
258
+ text = text.slice(2).trimStart()
259
+ }
260
+
261
+ const normalized = text.replaceAll('**', '').trimStart();
262
+ if (!normalized.toLowerCase().startsWith('design authority:')) continue;
263
+
264
+ const
265
+ rest = normalized.slice('design authority:'.length).trim(),
266
+ lower = rest.toLowerCase();
267
+
268
+ if (!rest) continue;
269
+
270
+ if (lower.startsWith('none found')) {
271
+ const sIdx = lower.indexOf('(searched:');
272
+ if (sIdx !== -1) {
273
+ const closeIdx = rest.indexOf(')', sIdx + 10);
274
+ if (closeIdx !== -1) {
275
+ const searched = rest.slice(sIdx + 10, closeIdx).trim();
276
+ if (searched.length > 0 && searched !== '...' && searched !== '\u2026') {
277
+ return true
278
+ }
279
+ }
280
+ }
281
+ continue
282
+ }
283
+
284
+ if (lower.startsWith('n/a')) {
285
+ const tail = rest.slice(3).trimStart();
286
+ if (tail.startsWith('\u2014') || tail.startsWith('-') || tail.startsWith(':')) {
287
+ const reason = tail.slice(1).trim();
288
+ if (reason.length >= 3) return true
289
+ }
290
+ continue
291
+ }
292
+
293
+ if (rest.startsWith('>') && rest.slice(1).trim().length >= 3) {
294
+ return true
295
+ }
296
+
297
+ const quoted = extractQuotedSpans(rest);
298
+ if (quoted.some(span => span.length >= 3)) {
299
+ return true
300
+ }
301
+ }
302
+
303
+ return false
304
+ }
305
+
306
+ /**
307
+ * @summary Checks whether a line inside a fenced block looks like a stack trace frame.
308
+ * @param {String} line
309
+ * @returns {Boolean}
310
+ */
311
+ function isStackFrameLine(line) {
312
+ const trimmed = line.trim();
313
+ if (!trimmed.startsWith('at ')) return false;
314
+
315
+ const rest = trimmed.slice(3).trim();
316
+ return Boolean(rest) && (rest.includes('(') || rest.includes('/') || rest.includes('.mjs') || rest.includes('.js'))
317
+ }
318
+
319
+ /**
320
+ * @summary Extracts prose lines belonging to `## Context` and `## The Problem` (or pre-Fix prose).
321
+ * @param {String} [body='']
322
+ * @returns {String[]}
323
+ */
324
+ export function problemContextLines(body = '') {
325
+ const
326
+ lines = markdownContentLines(body),
327
+ out = [];
328
+
329
+ let inProblemOrContext = true;
330
+
331
+ for (const line of lines) {
332
+ const title = parseHeadingTitle(line);
333
+
334
+ if (title) {
335
+ if (title === 'context' || title === 'the context' || title === 'problem' || title === 'the problem') {
336
+ inProblemOrContext = true;
337
+ continue
338
+ }
339
+
340
+ if (STOP_SECTIONS.some(stop => title === stop || title.startsWith(`${stop} `))) {
341
+ inProblemOrContext = false;
342
+ continue
343
+ }
344
+ }
345
+
346
+ if (inProblemOrContext) {
347
+ out.push(line)
348
+ }
349
+ }
350
+
351
+ return out
352
+ }
353
+
354
+ /**
355
+ * @summary Tests whether a token names a test or spec file path.
356
+ * @param {String} rawToken
357
+ * @returns {Boolean}
358
+ */
359
+ function isSpecPathToken(rawToken) {
360
+ let token = rawToken.trim();
361
+
362
+ while (token.length > 0 && '`"([\'<'.includes(token[0])) {
363
+ token = token.slice(1)
364
+ }
365
+ while (token.length > 0 && '`")]\',.:;>'.includes(token[token.length - 1])) {
366
+ token = token.slice(0, -1)
367
+ }
368
+
369
+ const lineColIdx = token.indexOf(':');
370
+ if (lineColIdx > 0) {
371
+ token = token.slice(0, lineColIdx)
372
+ }
373
+
374
+ if (!token || token.includes(' ')) return false;
375
+
376
+ if (
377
+ token.endsWith('.spec.mjs') ||
378
+ token.endsWith('.spec.js') ||
379
+ token.endsWith('.test.mjs') ||
380
+ token.endsWith('.test.js')
381
+ ) {
382
+ return token.length > 9
383
+ }
384
+
385
+ if (
386
+ (token.startsWith('test/') || token.startsWith('tests/') || token.startsWith('scripts/test-')) &&
387
+ (token.endsWith('.mjs') || token.endsWith('.js'))
388
+ ) {
389
+ return true
390
+ }
391
+
392
+ return token.startsWith('test-') && (token.endsWith('.mjs') || token.endsWith('.js'))
393
+ }
394
+
395
+ /**
396
+ * @summary Checks whether a text segment carries a JSDoc `@tag` with descriptive contract text.
397
+ * @param {String} segment
398
+ * @returns {Boolean}
399
+ */
400
+ function containsJsdocClause(segment) {
401
+ const trimmed = segment.trim();
402
+ if (!trimmed) return false;
403
+
404
+ for (const tag of JSDOC_TAGS) {
405
+ let searchFrom = 0;
406
+
407
+ while (searchFrom < trimmed.length) {
408
+ const idx = trimmed.indexOf(tag, searchFrom);
409
+ if (idx === -1) break;
410
+
411
+ const
412
+ beforeOk = idx === 0 || ' \t*({["\'\u201c`'.includes(trimmed[idx - 1]),
413
+ afterIdx = idx + tag.length,
414
+ afterCh = afterIdx < trimmed.length ? trimmed[afterIdx] : '';
415
+
416
+ if (beforeOk && (afterCh === ' ' || afterCh === '\t')) {
417
+ const rest = trimmed.slice(afterIdx).trim();
418
+ if (rest.length >= 3) return true
419
+ }
420
+
421
+ searchFrom = idx + 1
422
+ }
423
+ }
424
+
425
+ return false
426
+ }
427
+
428
+ /**
429
+ * @summary Evaluates the three defect excuses (`crash`, `red`, `jsdoc`) on a ticket body.
430
+ * @param {String} [body='']
431
+ * @returns {{crash: Boolean, red: Boolean, jsdoc: Boolean, excused: Boolean}}
432
+ */
433
+ export function hasDefectExcuse(body = '') {
434
+ const
435
+ blocks = fencedBlocks(body),
436
+ content = markdownContentLines(body),
437
+ probCtx = problemContextLines(body);
438
+
439
+ const crash = blocks.some(block => {
440
+ if (['error', 'stderr', 'stack', 'trace'].includes(block.info) && block.text.trim().length > 0) {
441
+ return true
442
+ }
443
+ if (CRASH_TOKENS.some(token => block.text.includes(token))) {
444
+ return true
445
+ }
446
+ return block.text.split('\n').some(isStackFrameLine)
447
+ });
448
+
449
+ const red = probCtx.some(line => {
450
+ const tokens = line.split(/[\s|]+/).filter(Boolean);
451
+ if (tokens.some(isSpecPathToken)) return true;
452
+
453
+ const lower = line.toLowerCase();
454
+ if (
455
+ (lower.includes('failing test:') || lower.includes('red test:') || lower.includes('failing spec:')) &&
456
+ extractQuotedSpans(line).some(span => span.length >= 3)
457
+ ) {
458
+ return true
459
+ }
460
+
461
+ if (lower.includes('test-') && (lower.includes('exit 1') || lower.includes('failed'))) {
462
+ return true
463
+ }
464
+
465
+ return false
466
+ });
467
+
468
+ const jsdoc = content.some(line => {
469
+ const trimmed = line.trim();
470
+ if (!trimmed) return false;
471
+
472
+ if (trimmed.startsWith('>') && containsJsdocClause(trimmed.slice(1))) {
473
+ return true
474
+ }
475
+
476
+ return extractQuotedSpans(trimmed).some(containsJsdocClause)
477
+ });
478
+
479
+ return {
480
+ crash,
481
+ red,
482
+ jsdoc,
483
+ excused: crash || red || jsdoc
484
+ }
485
+ }
486
+
487
+ /**
488
+ * @summary Returns ticket-body violations when neither `Design authority:` nor any defect excuse is present.
489
+ * @param {Object} [options={}]
490
+ * @param {String} [options.body='']
491
+ * @returns {{
492
+ * ok: Boolean,
493
+ * satisfiedBy: 'design-authority'|'defect-excuse'|null,
494
+ * excuses: {crash: Boolean, red: Boolean, jsdoc: Boolean, excused: Boolean},
495
+ * violations: String[]
496
+ * }}
497
+ */
498
+ export function findTicketBodyViolations({body = ''} = {}) {
499
+ const
500
+ hasAuthority = hasDesignAuthorityLine(body),
501
+ excuses = hasDefectExcuse(body);
502
+
503
+ if (hasAuthority) {
504
+ return {
505
+ ok : true,
506
+ satisfiedBy: 'design-authority',
507
+ excuses,
508
+ violations : []
509
+ }
510
+ }
511
+
512
+ if (excuses.excused) {
513
+ return {
514
+ ok : true,
515
+ satisfiedBy: 'defect-excuse',
516
+ excuses,
517
+ violations : []
518
+ }
519
+ }
520
+
521
+ return {
522
+ ok : false,
523
+ satisfiedBy: null,
524
+ excuses,
525
+ violations : [
526
+ `Ticket body carries neither a valid \`Design authority:\` line nor a defect excuse. See ${DESIGN_AUTHORITY_REF}.`
527
+ ]
528
+ }
529
+ }
530
+
531
+ /**
532
+ * @summary CLI entry point. Reads a ticket body from `--body-file` or stdin and validates it.
533
+ * @param {String[]} [argv]
534
+ * @param {Object} [options={}]
535
+ * @param {Function} [options.out=console.log]
536
+ * @param {Function} [options.error=console.error]
537
+ * @param {String} [options.stdin='']
538
+ * @returns {Number} Exit code (`0` on pass, `1` on violation or read error).
539
+ */
540
+ export function run(argv = process.argv.slice(2), {out = console.log, error = console.error, stdin = ''} = {}) {
541
+ const parsed = parseArgs({
542
+ args : argv,
543
+ allowPositionals: false,
544
+ strict : true,
545
+ options : {'body-file': {type: 'string'}}
546
+ });
547
+
548
+ let body = stdin;
549
+
550
+ if (parsed.values['body-file']) {
551
+ try {
552
+ body = readFileSync(parsed.values['body-file'], 'utf8')
553
+ } catch (cause) {
554
+ error(`check-ticket-body: cannot read ${parsed.values['body-file']} — ${cause.message}`);
555
+ return 1
556
+ }
557
+ }
558
+
559
+ const result = findTicketBodyViolations({body});
560
+
561
+ if (result.ok) {
562
+ out(`✅ Ticket body passes design-authority check (${result.satisfiedBy}).`);
563
+ return 0
564
+ }
565
+
566
+ error('❌ Ticket body is missing `Design authority:` and carries no defect excuse.');
567
+ error(` ${result.violations[0]}`);
568
+ return 1
569
+ }
570
+
571
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
572
+ const args = process.argv.slice(2);
573
+
574
+ if (args.some(arg => arg === '--body-file' || arg.startsWith('--body-file='))) {
575
+ process.exit(run(args))
576
+ } else {
577
+ const chunks = [];
578
+
579
+ process.stdin.on('data', chunk => chunks.push(chunk))
580
+ .on('end', () => process.exit(run(args, {stdin: chunks.join('')})))
581
+ }
582
+ }