@event4u/agent-config 9.34.0 → 9.36.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 +1 -1
- package/CHANGELOG.md +64 -77
- package/CONTRIBUTING.md +23 -14
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/commands/roadmap/next.md +37 -5
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
- package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-JP53LNVT.js} +12 -0
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-NR7HSN3P.js} +2 -2
- package/dist/cli-delegate/{chunk-REMEZSVP.js → chunk-SFGARMAB.js} +13 -3
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-U7CMAIOC.js} +7 -2
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +3 -3
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +8 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +3 -3
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +3 -3
- package/dist/cli-delegate/cmd_uninstall.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +789 -401
- package/dist/install/install.mjs +21 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +99 -7
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +5 -2
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/scripts/roadmap_gates.ts +119 -4
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/src/config/agent-settings.template.yml +43 -2
- package/src/config/gate-coverage.yml +12 -0
- package/src/config/quorum-attendance-budget.json +10 -2
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_dispatch.bash +6 -2
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/session_register.ts +67 -1
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +60 -0
- package/src/scripts/ai_council/events_log.ts +103 -4
- package/src/scripts/ai_council/quorum.ts +83 -6
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/council_cli.ts +36 -1
- package/src/scripts/hook_manifest.yaml +18 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_framework_leakage.ts +195 -28
- package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/memory_lookup.ts +1 -2
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/session_register_hook.ts +238 -18
- package/src/scripts/sessions_cli.ts +377 -12
- package/src/server/schemas/settings.ts +12 -0
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
|
@@ -299,14 +299,21 @@ interface AllowlistEntry {
|
|
|
299
299
|
file?: string;
|
|
300
300
|
lines?: number[] | '*';
|
|
301
301
|
/**
|
|
302
|
-
* Content anchor — a substring of the exempted line.
|
|
302
|
+
* Content anchor — a substring of the exempted line. The only per-line key.
|
|
303
303
|
*
|
|
304
|
-
* Position
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
304
|
+
* Position keying (`lines: [100]`) is retired, not merely discouraged.
|
|
305
|
+
* Inserting a paragraph above line 100 re-fires the ratchet on an entry
|
|
306
|
+
* nobody touched — recorded twice in one pull request — and, worse, it rots
|
|
307
|
+
* in silence: the migration that introduced this comment found three shipped
|
|
308
|
+
* entries that had stopped exempting anything, one naming line 100 of a
|
|
309
|
+
* 68-line file and two naming blank lines. Nothing reported it, because a
|
|
310
|
+
* drifted position key still parses.
|
|
311
|
+
*
|
|
312
|
+
* An anchor keys the exemption to the content being exempted, so it survives
|
|
313
|
+
* the edit that moves it and fails loudly when that content is gone.
|
|
314
|
+
* `validate_allowlist` additionally requires it to match exactly one line:
|
|
315
|
+
* `includes` is file-scoped, so a too-short anchor would silently exempt a
|
|
316
|
+
* second line nobody reviewed.
|
|
310
317
|
*/
|
|
311
318
|
anchor?: string;
|
|
312
319
|
reason?: string;
|
|
@@ -328,35 +335,124 @@ function _load_allowlist(): Allowlist {
|
|
|
328
335
|
}
|
|
329
336
|
}
|
|
330
337
|
|
|
331
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Every allowlist entry must still exempt something, and exactly one thing.
|
|
340
|
+
*
|
|
341
|
+
* A position-keyed entry rots in silence: the line it names drifts, the entry
|
|
342
|
+
* keeps parsing, and nothing reports that an exemption stopped exempting. This
|
|
343
|
+
* repository shipped three such entries — one naming line 100 of a 68-line file,
|
|
344
|
+
* two naming blank lines — and the rot was invisible until the whole set was read
|
|
345
|
+
* at once. An anchor cannot drift, but it can over-reach: `includes` is
|
|
346
|
+
* file-scoped, so an anchor occurring twice silently exempts a second line nobody
|
|
347
|
+
* reviewed.
|
|
348
|
+
*
|
|
349
|
+
* Both failures are decidable from the file, so they are checked rather than
|
|
350
|
+
* trusted. `lines: '*'` stays legal — a deliberate whole-file exemption is a
|
|
351
|
+
* different thing from a position key, and it cannot drift.
|
|
352
|
+
*/
|
|
353
|
+
/**
|
|
354
|
+
* The exact text `_allowlisted` compares an anchor against.
|
|
355
|
+
*
|
|
356
|
+
* The matcher sees `Hit.snippet`, not the raw line — trimmed and cut at 160
|
|
357
|
+
* chars. A validator reading the raw line would clear an anchor copied with its
|
|
358
|
+
* indentation, or drawn from past column 160, and the gate would then fail to
|
|
359
|
+
* suppress a hit the maintainer was told is exempt. One function, both callers.
|
|
360
|
+
*/
|
|
361
|
+
export function anchor_haystack(line: string): string {
|
|
362
|
+
return line.trim().slice(0, 160);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function validate_allowlist(allowlist: Allowlist, repo_root?: string): string[] {
|
|
366
|
+
// Default to the MUTABLE root, not the real one: the scan resolves entry
|
|
367
|
+
// paths against `REPO_ROOT`, and a validator resolving them anywhere else
|
|
368
|
+
// would report every entry missing the moment a caller redirects the tree.
|
|
369
|
+
const root = repo_root ?? REPO_ROOT;
|
|
370
|
+
const problems: string[] = [];
|
|
371
|
+
for (const entry of allowlist.entries ?? []) {
|
|
372
|
+
const file = entry.file;
|
|
373
|
+
if (typeof file !== 'string' || file === '') {
|
|
374
|
+
problems.push('entry without a `file`');
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(entry.lines)) {
|
|
378
|
+
problems.push(
|
|
379
|
+
`${file}: position-keyed (\`lines: [${entry.lines.join(', ')}]\`) — ` +
|
|
380
|
+
'use `anchor` with a substring of the exempted line; a line number ' +
|
|
381
|
+
'drifts out from under its own exemption on any edit above it',
|
|
382
|
+
);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (entry.lines === '*') {
|
|
386
|
+
// A whole-file exemption never consults the anchor, so validating one
|
|
387
|
+
// here would stop every scan for a field the matcher does not read.
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const anchor = entry.anchor;
|
|
391
|
+
if (typeof anchor !== 'string' || anchor === '') {
|
|
392
|
+
problems.push(`${file}: neither an \`anchor\` nor \`lines: "*"\``);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const abs = path.join(root, file);
|
|
396
|
+
if (!_isFile(abs)) {
|
|
397
|
+
problems.push(`${file}: anchored entry names a file that does not exist`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const matches = fs
|
|
401
|
+
.readFileSync(abs, 'utf-8')
|
|
402
|
+
.split('\n')
|
|
403
|
+
.filter((l) => anchor_haystack(l).includes(anchor)).length;
|
|
404
|
+
if (matches === 0) {
|
|
405
|
+
problems.push(
|
|
406
|
+
`${file}: anchor matches no line — the exempted content is gone, so the ` +
|
|
407
|
+
`entry exempts nothing: ${JSON.stringify(anchor.slice(0, 50))}`,
|
|
408
|
+
);
|
|
409
|
+
} else if (matches > 1) {
|
|
410
|
+
problems.push(
|
|
411
|
+
`${file}: anchor matches ${matches} lines — it would exempt more than the ` +
|
|
412
|
+
`reviewed one; lengthen it: ${JSON.stringify(anchor.slice(0, 50))}`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return problems;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Index of the entry that exempts this hit, or `-1`.
|
|
421
|
+
*
|
|
422
|
+
* Returns the index rather than a boolean so the run can report which
|
|
423
|
+
* exemptions were never used. An entry that suppresses nothing is the failure
|
|
424
|
+
* this gate shipped: three had rotted under position keys, and re-keying them to
|
|
425
|
+
* anchors preserved the rot, because an anchor resolving to a line says nothing
|
|
426
|
+
* about that line producing a hit.
|
|
427
|
+
*/
|
|
428
|
+
function _allowlist_index(
|
|
332
429
|
rel_path: string,
|
|
333
430
|
line_no: number,
|
|
334
431
|
allowlist: Allowlist,
|
|
335
432
|
line_text = '',
|
|
336
|
-
):
|
|
433
|
+
): number {
|
|
337
434
|
const logical = strip_source_prefix(rel_path);
|
|
338
|
-
|
|
435
|
+
const entries = allowlist.entries ?? [];
|
|
436
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
437
|
+
const entry = entries[i] as AllowlistEntry;
|
|
339
438
|
const entry_file = entry.file;
|
|
340
439
|
const entry_logical =
|
|
341
440
|
typeof entry_file === 'string' ? strip_source_prefix(entry_file) : null;
|
|
342
441
|
if (entry_file !== rel_path && (logical === null || entry_logical !== logical)) {
|
|
343
442
|
continue;
|
|
344
443
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return true;
|
|
444
|
+
if (entry.lines === '*') {
|
|
445
|
+
return i;
|
|
348
446
|
}
|
|
349
|
-
// Content anchor
|
|
350
|
-
//
|
|
447
|
+
// Content anchor, and nothing else. A numeric `lines` entry never reaches
|
|
448
|
+
// this point — `validate_allowlist` stops the run on one — so matching it
|
|
449
|
+
// here would be a branch that only fires for input the gate rejects.
|
|
351
450
|
const anchor = entry.anchor;
|
|
352
451
|
if (typeof anchor === 'string' && anchor !== '' && line_text.includes(anchor)) {
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
if (Array.isArray(lines) && lines.includes(line_no)) {
|
|
356
|
-
return true;
|
|
452
|
+
return i;
|
|
357
453
|
}
|
|
358
454
|
}
|
|
359
|
-
return
|
|
455
|
+
return -1;
|
|
360
456
|
}
|
|
361
457
|
|
|
362
458
|
function _families_in_window(lines: string[], idx: number, radius = 10): Set<string> {
|
|
@@ -502,7 +598,12 @@ function* iter_md_files(paths: Iterable<string>): Generator<string> {
|
|
|
502
598
|
}
|
|
503
599
|
}
|
|
504
600
|
|
|
505
|
-
function parse_args(argv: readonly string[]): {
|
|
601
|
+
function parse_args(argv: readonly string[]): {
|
|
602
|
+
json: boolean;
|
|
603
|
+
quiet: boolean;
|
|
604
|
+
narrowed: boolean;
|
|
605
|
+
paths: string[];
|
|
606
|
+
} {
|
|
506
607
|
let json = false;
|
|
507
608
|
let quiet = false;
|
|
508
609
|
const paths: string[] = [];
|
|
@@ -535,16 +636,26 @@ function parse_args(argv: readonly string[]): { json: boolean; quiet: boolean; p
|
|
|
535
636
|
process.exit(2);
|
|
536
637
|
}
|
|
537
638
|
}
|
|
538
|
-
|
|
639
|
+
// `narrowed` distinguishes an explicit `--paths` from the default full set.
|
|
640
|
+
// `paths.length === 0` cannot: the default is three subdirectories, so a full
|
|
641
|
+
// run also arrives here with a non-empty list — which silently disabled the
|
|
642
|
+
// unused-exemption check the first time it was written against that test.
|
|
643
|
+
return {
|
|
644
|
+
json,
|
|
645
|
+
quiet,
|
|
646
|
+
narrowed: paths.length > 0,
|
|
647
|
+
paths: paths.length > 0 ? paths : _default_paths(),
|
|
648
|
+
};
|
|
539
649
|
}
|
|
540
650
|
|
|
541
651
|
/**
|
|
542
652
|
* The allowlist entry that would silence one finding, ready to paste.
|
|
543
653
|
*
|
|
544
|
-
* Anchored on the matched token rather than on the line number: a
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
654
|
+
* Anchored on the matched token rather than on the line number: a position key
|
|
655
|
+
* re-fires the moment an insertion above moves the line, on an entry nobody
|
|
656
|
+
* touched. The form is no longer merely discouraged — `validate_allowlist`
|
|
657
|
+
* refuses it, and the last 18 position-keyed entries were migrated to anchors
|
|
658
|
+
* in the change that introduced the refusal.
|
|
548
659
|
*/
|
|
549
660
|
export function suppressionKey(rel: string, hit: Hit): string {
|
|
550
661
|
const anchor = hit.snippet.trim().slice(0, 60);
|
|
@@ -613,7 +724,19 @@ export function main(argv?: readonly string[]): number {
|
|
|
613
724
|
const args = parse_args(raw);
|
|
614
725
|
|
|
615
726
|
const allowlist = _load_allowlist();
|
|
727
|
+
// Judge the exemptions before judging any file. A rotted entry produces a
|
|
728
|
+
// green run that means nothing, which is the one failure mode a gate must
|
|
729
|
+
// not have. Structural problems are collected here and reported at the end,
|
|
730
|
+
// NOT returned on early: an early return skipped the `scanned:` line the
|
|
731
|
+
// gate-coverage collector consumes and left `--json` emitting nothing, so a
|
|
732
|
+
// consumer parsing the envelope threw on exactly the runs where the gate
|
|
733
|
+
// reports itself unusable.
|
|
734
|
+
const allowlist_problems = validate_allowlist(allowlist);
|
|
616
735
|
const file_hits: Array<[string, Hit[]]> = [];
|
|
736
|
+
// Which exemptions actually suppressed something. Only meaningful over a full
|
|
737
|
+
// scan: with `--paths` narrowing the tree, an entry for a file outside the
|
|
738
|
+
// scope is unused for a reason that says nothing about the entry.
|
|
739
|
+
const used_entries = new Set<number>();
|
|
617
740
|
let total_hits = 0;
|
|
618
741
|
let allowlisted_total = 0;
|
|
619
742
|
// Every enumerated file is planned up front, so the five exemption branches
|
|
@@ -661,7 +784,9 @@ export function main(argv?: readonly string[]): number {
|
|
|
661
784
|
if (h.cross_stack) {
|
|
662
785
|
continue;
|
|
663
786
|
}
|
|
664
|
-
|
|
787
|
+
const idx = _allowlist_index(rel, h.line, allowlist, h.snippet);
|
|
788
|
+
if (idx >= 0) {
|
|
789
|
+
used_entries.add(idx);
|
|
665
790
|
h.allowlisted = true;
|
|
666
791
|
allowlisted_total += 1;
|
|
667
792
|
continue;
|
|
@@ -679,10 +804,31 @@ export function main(argv?: readonly string[]): number {
|
|
|
679
804
|
}
|
|
680
805
|
}
|
|
681
806
|
|
|
807
|
+
// An exemption that suppressed nothing over a FULL scan exempts nothing at
|
|
808
|
+
// all — the rot the anchor migration was supposed to end, and which anchoring
|
|
809
|
+
// alone does not end: an anchor resolving to a line says nothing about that
|
|
810
|
+
// line producing a hit. Only checked on a full scan, because `--paths` makes
|
|
811
|
+
// "unused" mean "out of scope" instead.
|
|
812
|
+
if (!args.narrowed) {
|
|
813
|
+
const entries = allowlist.entries ?? [];
|
|
814
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
815
|
+
if (used_entries.has(i)) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const e = entries[i] as AllowlistEntry;
|
|
819
|
+
allowlist_problems.push(
|
|
820
|
+
`${String(e.file)}: exemption suppressed nothing this run — the line it names ` +
|
|
821
|
+
'produces no leakage hit, so it exempts nothing: ' +
|
|
822
|
+
JSON.stringify(String(e.anchor ?? e.lines).slice(0, 50)),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
682
827
|
const summary = {
|
|
683
828
|
total_hits,
|
|
684
829
|
files: file_hits.length,
|
|
685
830
|
allowlisted: allowlisted_total,
|
|
831
|
+
allowlist_problems: allowlist_problems.length,
|
|
686
832
|
};
|
|
687
833
|
|
|
688
834
|
// Finalize before either output branch: an unaccounted target must throw
|
|
@@ -696,10 +842,11 @@ export function main(argv?: readonly string[]): number {
|
|
|
696
842
|
hits.map((h) => ({ file: relToRepo(p), ...h })),
|
|
697
843
|
),
|
|
698
844
|
summary,
|
|
845
|
+
allowlist_problems,
|
|
699
846
|
ledger: tally,
|
|
700
847
|
};
|
|
701
848
|
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
702
|
-
return total_hits ? 1 : 0;
|
|
849
|
+
return total_hits || allowlist_problems.length ? 1 : 0;
|
|
703
850
|
}
|
|
704
851
|
|
|
705
852
|
if (!args.quiet) {
|
|
@@ -731,6 +878,26 @@ export function main(argv?: readonly string[]): number {
|
|
|
731
878
|
// judged) and is the same "reading M as coverage" trap the human summary
|
|
732
879
|
// carries. The ledger makes the honest number the cheap one to publish.
|
|
733
880
|
process.stderr.write(`scanned: ${String(tally.completed + tally.failed)}\n`);
|
|
881
|
+
|
|
882
|
+
// Reported AFTER the ledger and the `scanned:` line, so a broken allowlist
|
|
883
|
+
// still produces both output contracts. An exemption problem fails the run
|
|
884
|
+
// on its own: a gate that scans cleanly through unusable exemptions is
|
|
885
|
+
// reporting on a question nobody asked.
|
|
886
|
+
if (allowlist_problems.length > 0) {
|
|
887
|
+
process.stderr.write(
|
|
888
|
+
`\n❌ ${allowlist_problems.length} unusable allowlist entr` +
|
|
889
|
+
`${allowlist_problems.length === 1 ? 'y' : 'ies'}:\n`,
|
|
890
|
+
);
|
|
891
|
+
for (const p of allowlist_problems) {
|
|
892
|
+
process.stderr.write(` · ${p}\n`);
|
|
893
|
+
}
|
|
894
|
+
process.stderr.write(
|
|
895
|
+
' An exemption that names nothing, suppresses nothing, or reaches further\n' +
|
|
896
|
+
' than the line it was reviewed for is not an exemption. Remove it or\n' +
|
|
897
|
+
' re-anchor it to the content it is meant to cover.\n',
|
|
898
|
+
);
|
|
899
|
+
return 1;
|
|
900
|
+
}
|
|
734
901
|
return total_hits ? 1 : 0;
|
|
735
902
|
}
|
|
736
903
|
|
|
@@ -1,152 +1,160 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"_doc": "Each entry: { file: relative path from repo root,
|
|
3
|
+
"_doc": "Each entry: { file: relative path from repo root, anchor: a substring of the exempted line (the ONLY per-line key — position keys are refused by validate_allowlist), or lines: \"*\" for the whole file, reason: short justification }. Use sparingly — first ask whether the file should be neutralized or carved out. Policy cap: > 20 exempted FILES means the linter (or the content) is wrong — fix the tool or the text, never grow the list. The cap counts files because that is the unit it was written in, when one entry meant one file; entries are now per-line, so the two numbers differ on purpose (15 files / 31 entries as of the anchor migration) and the entry count is not the cap. Rebuilt 2026-07-12 (road-to-opt-hygiene-and-debt Phase 1): 104 -> minimal set after heuristic upgrades + 25 content neutralizations + 2 carve-outs. Re-keyed to content anchors 2026-08-12: 8 of the 39 exempted lines were found to exempt nothing — three named blank or out-of-range lines, five named lines that produce no hit — and were dropped rather than carried forward; the gate now refuses both shapes.",
|
|
4
4
|
"entries": [
|
|
5
|
-
{
|
|
6
|
-
"file": "src/rules/code-comment-discipline.md",
|
|
7
|
-
"lines": [
|
|
8
|
-
100
|
|
9
|
-
],
|
|
10
|
-
"reason": "Worked PHP docblock demo inside the rule's own wrong/right example block — quoted teaching content."
|
|
11
|
-
},
|
|
12
5
|
{
|
|
13
6
|
"file": "src/skills/api-testing/SKILL.md",
|
|
14
|
-
"
|
|
15
|
-
218
|
|
16
|
-
],
|
|
7
|
+
"anchor": "- Don't test framework internals (e.g., \"does Laravel return",
|
|
17
8
|
"reason": "Quoted example of a framework-internal behavior explicitly marked as what NOT to test — meta content, not a mandate."
|
|
18
9
|
},
|
|
19
10
|
{
|
|
20
11
|
"file": "src/skills/learning-to-rule-or-skill/SKILL.md",
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
"anchor": "Decision: Create focused skill for Laravel route inspection ",
|
|
13
|
+
"reason": "Example learnings/decisions quoted verbatim in the promotion-flow walkthrough — meta content."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"file": "src/skills/learning-to-rule-or-skill/SKILL.md",
|
|
17
|
+
"anchor": "Learning: \"I forgot to run PHPStan once.\"",
|
|
25
18
|
"reason": "Example learnings/decisions quoted verbatim in the promotion-flow walkthrough — meta content."
|
|
26
19
|
},
|
|
27
20
|
{
|
|
28
21
|
"file": "src/skills/markitdown/SKILL.md",
|
|
29
|
-
"
|
|
30
|
-
74,
|
|
31
|
-
111
|
|
32
|
-
],
|
|
22
|
+
"anchor": "uv pip install 'markitdown-mcp==0.0.1a4'",
|
|
33
23
|
"reason": "Tool-intrinsic install lines — markitdown IS a pip package; the install command is the tool's identity, not stack leakage."
|
|
34
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"file": "src/skills/markitdown/SKILL.md",
|
|
27
|
+
"anchor": "Plugins enable arbitrary code paths inside the conversion pi",
|
|
28
|
+
"reason": "Tool-intrinsic install lines — markitdown IS a pip package; the install command is the tool's identity, not stack leakage."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
32
|
+
"anchor": "### Laravel HMVC carve-out",
|
|
33
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
37
|
+
"anchor": "│ ├── api.php web.php console.php",
|
|
38
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
42
|
+
"anchor": "**Auto-loading:** `app/Providers/ModuleServiceProvider.php` ",
|
|
43
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
47
|
+
"anchor": "`app/Modules/` and registers routes (`Routes/api.php` → `/ap",
|
|
48
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
52
|
+
"anchor": "`api` middleware; `Routes/web.php` → `web` middleware) plus ",
|
|
53
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
57
|
+
"anchor": "// Routes/api.php — auto-prefixed /api",
|
|
58
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
59
|
+
},
|
|
35
60
|
{
|
|
36
61
|
"file": "src/skills/module-management/SKILL.md",
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
115,
|
|
44
|
-
124,
|
|
45
|
-
156,
|
|
46
|
-
172
|
|
47
|
-
],
|
|
62
|
+
"anchor": "### Symfony DDD-lite carve-out",
|
|
63
|
+
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"file": "src/skills/module-management/SKILL.md",
|
|
67
|
+
"anchor": "metadata in `pyproject.toml`.",
|
|
48
68
|
"reason": "Canonical multi-stack module catalogue (Laravel HMVC / Symfony DDD / Node / Python sections) — keep-in-place is a recorded council decision; per-stack sections sit farther apart than the ±10-line window."
|
|
49
69
|
},
|
|
50
70
|
{
|
|
51
71
|
"file": "src/skills/po-discovery/SKILL.md",
|
|
52
|
-
"
|
|
53
|
-
45
|
|
54
|
-
],
|
|
72
|
+
"anchor": "(\"upgrade Laravel to 11\") — discovery adds noise; skip.",
|
|
55
73
|
"reason": "Quoted example ticket title ('upgrade Laravel to 11') illustrating when discovery adds noise — meta content."
|
|
56
74
|
},
|
|
57
75
|
{
|
|
58
76
|
"file": "src/skills/repomix-packer/SKILL.md",
|
|
59
|
-
"
|
|
60
|
-
44
|
|
61
|
-
],
|
|
77
|
+
"anchor": "npm install -g repomix",
|
|
62
78
|
"reason": "Tool-intrinsic install line — repomix IS an npm package."
|
|
63
79
|
},
|
|
64
80
|
{
|
|
65
81
|
"file": "src/skills/rule-writing/SKILL.md",
|
|
66
|
-
"
|
|
67
|
-
78
|
|
68
|
-
],
|
|
82
|
+
"anchor": "description: \"Writing or reviewing PHP code — strict types, ",
|
|
69
83
|
"reason": "Quoted rule-description example (the php-coding rule's own description string) — meta content. Re-anchored 76→78 after the description-cap edit above it."
|
|
70
84
|
},
|
|
71
|
-
{
|
|
72
|
-
"file": "src/skills/security-audit/SKILL.md",
|
|
73
|
-
"lines": [
|
|
74
|
-
122
|
|
75
|
-
],
|
|
76
|
-
"reason": "Multi-ecosystem XSS row (Blade/JSX/Jinja peers on one line) + a quoted false-positive example ('Laravel's CSRF is already handled') — documentation, not a mandate."
|
|
77
|
-
},
|
|
78
85
|
{
|
|
79
86
|
"file": "src/skills/skill-reviewer/SKILL.md",
|
|
80
|
-
"
|
|
81
|
-
70
|
|
82
|
-
],
|
|
87
|
+
"anchor": "- ❌ Describing how Laravel middleware works (in a middlewar",
|
|
83
88
|
"reason": "Quoted anti-example ('❌ Describing how Laravel middleware works') — the line teaches what NOT to write."
|
|
84
89
|
},
|
|
85
90
|
{
|
|
86
91
|
"file": "src/skills/skill-writing/SKILL.md",
|
|
87
|
-
"
|
|
88
|
-
148,
|
|
89
|
-
456
|
|
90
|
-
],
|
|
92
|
+
"anchor": "Use when creating Laravel middleware for request filtering",
|
|
91
93
|
"reason": "Quoted description examples + the 'too-broad skill (Laravel skill)' anti-pattern — meta content."
|
|
92
94
|
},
|
|
93
95
|
{
|
|
94
96
|
"file": "src/skills/supply-chain-intake/SKILL.md",
|
|
95
|
-
"
|
|
96
|
-
80,
|
|
97
|
-
82
|
|
98
|
-
],
|
|
97
|
+
"anchor": "rg -n '\"[^\"]+\":\\s*\"(\\^|~|\\*|latest)' package.json",
|
|
99
98
|
"reason": "Per-ecosystem check commands in fenced blocks; the pip/composer siblings sit farther apart than the ±10-line window — genuine multi-ecosystem documentation."
|
|
100
99
|
},
|
|
101
100
|
{
|
|
102
|
-
"file": "src/skills/
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"reason": "
|
|
101
|
+
"file": "src/skills/supply-chain-intake/SKILL.md",
|
|
102
|
+
"anchor": "[ -f package.json ] && [ ! -f package-lock.json ] && echo \"n",
|
|
103
|
+
"reason": "Per-ecosystem check commands in fenced blocks; the pip/composer siblings sit farther apart than the ±10-line window — genuine multi-ecosystem documentation."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"file": "src/skills/testing-anti-patterns/SKILL.md",
|
|
107
|
+
"anchor": "Replacement: a `tests/Support/cleanupSession.php` helper or ",
|
|
108
|
+
"reason": "Multi-ecosystem capture-tools line and skip-marker grep spanning pytest/PHPUnit/JS in one pattern — documentation across stacks."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"file": "src/skills/testing-anti-patterns/SKILL.md",
|
|
112
|
+
"anchor": "**Concrete capture tools** for recording the real shape: `cu",
|
|
113
|
+
"reason": "Multi-ecosystem capture-tools line and skip-marker grep spanning pytest/PHPUnit/JS in one pattern — documentation across stacks."
|
|
110
114
|
},
|
|
111
115
|
{
|
|
112
116
|
"file": "src/skills/testing-anti-patterns/SKILL.md",
|
|
113
|
-
"
|
|
114
|
-
87,
|
|
115
|
-
124,
|
|
116
|
-
185
|
|
117
|
-
],
|
|
117
|
+
"anchor": "rg -n '\\b(it|test|describe)\\.skip\\b|\\.only\\b|xit\\(|xdescribe",
|
|
118
118
|
"reason": "Multi-ecosystem capture-tools line and skip-marker grep spanning pytest/PHPUnit/JS in one pattern — documentation across stacks."
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"file": "src/skills/test-driven-development/SKILL.md",
|
|
122
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
"anchor": "### Example A — PHP / Pest",
|
|
123
|
+
"reason": "Example A of a deliberate A/B pair: '### Example A — PHP / Pest' (L228) sits opposite '### Example B — TypeScript / Vitest' (L259). That is framework-neutrality's textbook 'multi-stack peers' shape, but the two headings are 31 lines apart and the cross-stack window is +/-10, so the heuristic cannot see the sibling. Allowlisted rather than widening the window for a single document — if a second per-stack A/B skill appears, build the section-aware window instead of adding a second entry here."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"file": "src/skills/test-driven-development/SKILL.md",
|
|
127
|
+
"anchor": "// tests/Unit/EmailValidatorTest.php — RED",
|
|
128
|
+
"reason": "Example A of a deliberate A/B pair: '### Example A — PHP / Pest' (L228) sits opposite '### Example B — TypeScript / Vitest' (L259). That is framework-neutrality's textbook 'multi-stack peers' shape, but the two headings are 31 lines apart and the cross-stack window is +/-10, so the heuristic cannot see the sibling. Allowlisted rather than widening the window for a single document — if a second per-stack A/B skill appears, build the section-aware window instead of adding a second entry here."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"file": "src/skills/test-driven-development/SKILL.md",
|
|
132
|
+
"anchor": "Run: `./vendor/bin/pest --filter='rejects empty email'` → fa",
|
|
133
|
+
"reason": "Example A of a deliberate A/B pair: '### Example A — PHP / Pest' (L228) sits opposite '### Example B — TypeScript / Vitest' (L259). That is framework-neutrality's textbook 'multi-stack peers' shape, but the two headings are 31 lines apart and the cross-stack window is +/-10, so the heuristic cannot see the sibling. Allowlisted rather than widening the window for a single document — if a second per-stack A/B skill appears, build the section-aware window instead of adding a second entry here."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"file": "src/skills/test-driven-development/SKILL.md",
|
|
137
|
+
"anchor": "// app/Validators/EmailValidator.php — GREEN (minimum)",
|
|
128
138
|
"reason": "Example A of a deliberate A/B pair: '### Example A — PHP / Pest' (L228) sits opposite '### Example B — TypeScript / Vitest' (L259). That is framework-neutrality's textbook 'multi-stack peers' shape, but the two headings are 31 lines apart and the cross-stack window is +/-10, so the heuristic cannot see the sibling. Allowlisted rather than widening the window for a single document — if a second per-stack A/B skill appears, build the section-aware window instead of adding a second entry here."
|
|
129
139
|
},
|
|
130
140
|
{
|
|
131
141
|
"file": "src/skills/image-editing/SKILL.md",
|
|
132
|
-
"
|
|
133
|
-
48
|
|
134
|
-
],
|
|
142
|
+
"anchor": "`node_modules/@event4u/agent-config/src/scripts/ai-image/ada",
|
|
135
143
|
"reason": "npm install-location path of THIS package's own image-adapter scripts (node_modules/@event4u/agent-config/...) — the factual consumer path after the consumer-internal-ref decoupling (road-to-credible-install Phase 4), not a JS-stack mandate."
|
|
136
144
|
},
|
|
137
145
|
{
|
|
138
146
|
"file": "src/skills/image-generation/SKILL.md",
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
"anchor": "`node_modules/@event4u/agent-config/src/scripts/ai-image/ada",
|
|
148
|
+
"reason": "npm install-location path of THIS package's own image-adapter scripts (node_modules/@event4u/agent-config/...) — the factual consumer path after the consumer-internal-ref decoupling (road-to-credible-install Phase 4), not a JS-stack mandate."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"file": "src/skills/prompt-engineering-image/SKILL.md",
|
|
152
|
+
"anchor": "`node_modules/@event4u/agent-config/src/scripts/ai-image/adapters/<provider>.sh`",
|
|
142
153
|
"reason": "npm install-location path of THIS package's own image-adapter scripts (node_modules/@event4u/agent-config/...) — the factual consumer path after the consumer-internal-ref decoupling (road-to-credible-install Phase 4), not a JS-stack mandate."
|
|
143
154
|
},
|
|
144
155
|
{
|
|
145
156
|
"file": "src/skills/prompt-engineering-image/SKILL.md",
|
|
146
|
-
"
|
|
147
|
-
74,
|
|
148
|
-
115
|
|
149
|
-
],
|
|
157
|
+
"anchor": "- `node_modules/@event4u/agent-config/src/scripts/ai-image/a",
|
|
150
158
|
"reason": "npm install-location path of THIS package's own image-adapter scripts (node_modules/@event4u/agent-config/...) — the factual consumer path after the consumer-internal-ref decoupling (road-to-credible-install Phase 4), not a JS-stack mandate."
|
|
151
159
|
}
|
|
152
160
|
]
|
|
@@ -216,6 +216,7 @@ function lint_roadmap(
|
|
|
216
216
|
_check_human_gate_steps(text, warnings);
|
|
217
217
|
_check_human_gate_phase_headings(text, warnings);
|
|
218
218
|
_check_human_gate_exit_criteria(text, warnings);
|
|
219
|
+
_check_external_population_gates(text, warnings);
|
|
219
220
|
}
|
|
220
221
|
return problems;
|
|
221
222
|
}
|
|
@@ -383,6 +384,65 @@ function _check_human_gate_exit_criteria(text: string, warnings: string[]): void
|
|
|
383
384
|
}
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
// A gate whose opening condition depends on a quantity the project cannot
|
|
388
|
+
// produce is not a gate — it is a permanent no in the shape of an answer. The
|
|
389
|
+
// measured case: a single-user private tool carried a roadmap gate on "external
|
|
390
|
+
// installations with write activity", which held 33 steps closed and would have
|
|
391
|
+
// held them closed forever. Matched only inside gate-shaped context (exit
|
|
392
|
+
// criteria, acceptance criteria, a blocker's `Resolved when:`), because these
|
|
393
|
+
// nouns are ordinary vocabulary in a roadmap for an actual product.
|
|
394
|
+
const EXTERNAL_POPULATION_PATS: Array<[RegExp, string]> = [
|
|
395
|
+
[/\bexternal (users?|installs?|installations?|adopters?|consumers?)\b/i, 'an external user population'],
|
|
396
|
+
// Bare `adopters` is deliberately absent: measured against the real roadmap
|
|
397
|
+
// corpus it matched four internal counts ("4 adopters" of a gate helper),
|
|
398
|
+
// and an external adopter count is already covered by the pattern above.
|
|
399
|
+
[/\b(installations?|sign-?ups?|subscribers?|active users?)\b/i, 'an external adoption count'],
|
|
400
|
+
[/\bmarket (demand|validation|traction|fit|signal)\b/i, 'a market signal'],
|
|
401
|
+
[/\bchurn(ing|ed)?\b/i, 'a metric only a real user population produces'],
|
|
402
|
+
[/\buser (population|base|segment)\b/i, 'a user population'],
|
|
403
|
+
[/\b(?:at least |≥\s?)\d+ (people|users|customers|installs|adopters)\b/i, 'a headcount threshold of external people'],
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
const RESOLVED_WHEN_PAT = /^\s*(?:[-*]\s+)?\*{0,2}resolved when\*{0,2}\s*:/i;
|
|
407
|
+
|
|
408
|
+
const AUDIENCE_HINT =
|
|
409
|
+
'is this project meant to have that population at all? A gate whose ' +
|
|
410
|
+
'condition depends on a quantity the project cannot produce is a permanent ' +
|
|
411
|
+
'no, not a gate — see project.audience and § 8-pre of ' +
|
|
412
|
+
'docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md';
|
|
413
|
+
|
|
414
|
+
function _check_external_population_gates(text: string, warnings: string[]): void {
|
|
415
|
+
let inHeadingBlock = false;
|
|
416
|
+
let inInlineBlock = false;
|
|
417
|
+
for (const rawLine of text.split('\n')) {
|
|
418
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(rawLine);
|
|
419
|
+
if (heading) {
|
|
420
|
+
inHeadingBlock = /\b(acceptance|exit criteria)\b/i.test(heading[1]!);
|
|
421
|
+
inInlineBlock = false;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (rawLine.trim() === '') {
|
|
425
|
+
inInlineBlock = false;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (EXIT_CRITERIA_OPENER_PAT.test(rawLine)) {
|
|
429
|
+
inInlineBlock = true;
|
|
430
|
+
}
|
|
431
|
+
const isResolvedWhen = RESOLVED_WHEN_PAT.test(rawLine);
|
|
432
|
+
if (!inHeadingBlock && !inInlineBlock && !isResolvedWhen) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
for (const [re, label] of EXTERNAL_POPULATION_PATS) {
|
|
436
|
+
if (re.test(rawLine)) {
|
|
437
|
+
warnings.push(
|
|
438
|
+
`gate rests on ${label}: "${rawLine.trim().slice(0, 80)}" — ` + AUDIENCE_HINT,
|
|
439
|
+
);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
386
446
|
function _check_execution_mode(fm: string, problems: string[]): void {
|
|
387
447
|
if (!fm || !/^execution:/m.test(fm)) {
|
|
388
448
|
return; // absent field = interactive default — valid
|