@event4u/agent-config 9.33.0 → 9.35.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.
Files changed (105) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +154 -145
  3. package/CONTRIBUTING.md +23 -14
  4. package/dist/agent-src/commands/agent-handoff.md +15 -3
  5. package/dist/agent-src/commands/roadmap/next.md +37 -5
  6. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  7. package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
  8. package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
  9. package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
  10. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
  11. package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
  12. package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
  13. package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
  14. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
  15. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
  18. package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
  19. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  20. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  21. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  22. package/dist/cli/registry.js +2 -2
  23. package/dist/cli/registry.js.map +1 -1
  24. package/dist/cli-delegate/{chunk-PMQ6Z3T3.js → chunk-2TGMGLBL.js} +11 -4
  25. package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
  26. package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-H6F2CQFI.js} +1 -1
  27. package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
  28. package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
  29. package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-ZAXH3ZWU.js} +3 -0
  30. package/dist/cli-delegate/cmd_conformance.js +2 -2
  31. package/dist/cli-delegate/cmd_doctor.js +2 -2
  32. package/dist/cli-delegate/cmd_export.js +2 -2
  33. package/dist/cli-delegate/cmd_handoff.js +3 -3
  34. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  35. package/dist/cli-delegate/cmd_prune.js +2 -2
  36. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  37. package/dist/cli-delegate/cmd_session_recycle.js +7 -2
  38. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  39. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  40. package/dist/cli-delegate/cmd_sync.js +2 -2
  41. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  42. package/dist/cli-delegate/cmd_validate.js +2 -2
  43. package/dist/discovery/deprecation-report.md +1 -1
  44. package/dist/discovery/discovery-manifest.json +13 -13
  45. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  46. package/dist/discovery/discovery-manifest.summary.md +2 -2
  47. package/dist/discovery/dormancy-report.md +17 -0
  48. package/dist/discovery/orphan-report.md +1 -1
  49. package/dist/discovery/packs.json +3 -3
  50. package/dist/discovery/trust-report.md +1 -1
  51. package/dist/discovery/workspaces.json +3 -3
  52. package/dist/hooks/dispatch.js +355 -85
  53. package/dist/install/install.mjs +3 -0
  54. package/dist/mcp/registry-manifest.json +3 -3
  55. package/dist/mcp/server.json +2 -2
  56. package/dist/mcp/server.mjs +192 -12
  57. package/dist/server/schemas/settings.js +1 -0
  58. package/dist/server/schemas/settings.js.map +1 -1
  59. package/docs/contracts/settings-classes.md +3 -2
  60. package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
  61. package/package.json +2 -2
  62. package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  63. package/src/agent-src/scripts/roadmap_gates.ts +119 -4
  64. package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
  65. package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
  66. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  67. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  68. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  69. package/src/config/agent-settings.template.yml +11 -2
  70. package/src/config/quorum-attendance-budget.json +11 -3
  71. package/src/scripts/_cli/cmd_session_recycle.ts +6 -1
  72. package/src/scripts/_dispatch.bash +6 -2
  73. package/src/scripts/_lib/env_kill_switch.ts +37 -0
  74. package/src/scripts/_lib/security_lint.ts +18 -4
  75. package/src/scripts/_lib/self_repair.ts +60 -0
  76. package/src/scripts/_lib/self_repair_store.ts +65 -5
  77. package/src/scripts/_lib/session_register.ts +67 -1
  78. package/src/scripts/_lib/subagent_capsule.ts +78 -1
  79. package/src/scripts/ai_council/config.ts +60 -0
  80. package/src/scripts/ai_council/events_log.ts +105 -6
  81. package/src/scripts/ai_council/orchestrator.ts +38 -0
  82. package/src/scripts/ai_council/prompts.ts +165 -3
  83. package/src/scripts/ai_council/quorum.ts +87 -0
  84. package/src/scripts/ai_team/review_gate.ts +2 -2
  85. package/src/scripts/build_discovery_manifest.ts +165 -0
  86. package/src/scripts/check_claims.ts +51 -2
  87. package/src/scripts/ci_time_ratio.ts +3 -1
  88. package/src/scripts/council_cli.ts +36 -1
  89. package/src/scripts/discovery_graph.ts +201 -27
  90. package/src/scripts/dispatch_r2_reviewer.ts +102 -1
  91. package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
  92. package/src/scripts/lint_framework_leakage.ts +195 -28
  93. package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
  94. package/src/scripts/lint_handoffs.ts +92 -2
  95. package/src/scripts/lint_hedge_words.ts +317 -0
  96. package/src/scripts/lint_mcp_config_security.ts +1 -1
  97. package/src/scripts/lint_skill_frontmatter_safety.ts +1 -1
  98. package/src/scripts/memory_lookup.ts +1 -2
  99. package/src/scripts/schemas/command.schema.json +4 -0
  100. package/src/scripts/schemas/skill.schema.json +4 -0
  101. package/src/scripts/self_repair_cli.ts +26 -1
  102. package/src/scripts/session_register_hook.ts +238 -18
  103. package/src/scripts/sessions_cli.ts +377 -12
  104. package/src/scripts/skill_linter.ts +58 -2
  105. package/src/server/schemas/settings.ts +3 -0
@@ -1,152 +1,160 @@
1
1
  {
2
2
  "version": 1,
3
- "_doc": "Each entry: { file: relative path from repo root, lines: [int,...] | \"*\" for whole file, reason: short justification }. Use sparingly — first ask whether the file should be neutralized or carved out. Policy cap: > 20 entries means the linter (or the content) is wrong — fix the tool or the text, never grow the list. Rebuilt 2026-07-12 (road-to-opt-hygiene-and-debt Phase 1): 104 → minimal set after heuristic upgrades + 25 content neutralizations + 2 carve-outs.",
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
- "lines": [
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
- "lines": [
22
- 320,
23
- 322
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
- "lines": [
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
- "lines": [
38
- 87,
39
- 99,
40
- 107,
41
- 108,
42
- 109,
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
- "lines": [
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
- "lines": [
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
- "lines": [
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
- "lines": [
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
- "lines": [
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
- "lines": [
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/test-driven-development/SKILL.md",
103
- "lines": [
104
- 197,
105
- 200,
106
- 208,
107
- 212
108
- ],
109
- "reason": "Worked Example A (PHP/Pest) paired with Example B (TypeScript/Jest) farther apart than the ±10-line window — canonical two-stack demo."
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
- "lines": [
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
- "lines": [
123
- 228,
124
- 231,
125
- 239,
126
- 243
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
- "lines": [
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
- "lines": [
140
- 45
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
- "lines": [
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
  ]
@@ -360,6 +360,91 @@ export function validate_handoff_artifact(text: string): string[] {
360
360
  return missing;
361
361
  }
362
362
 
363
+ /**
364
+ * The body of a `## <heading>` section, up to the next heading of the same or a
365
+ * higher level. Fenced blocks are stripped first so a `##` inside a code fence
366
+ * cannot end the section early — a resume artefact routinely quotes commands.
367
+ */
368
+ export function handoff_section_body(text: string, heading: string): string | null {
369
+ const withoutFences = text.replace(/^```[\s\S]*?^```\s*$/gm, '');
370
+ const start = new RegExp(`^##\\s+${heading}\\s*$`, 'mi').exec(withoutFences);
371
+ if (start === null) {
372
+ return null;
373
+ }
374
+ const rest = withoutFences.slice(start.index + start[0].length);
375
+ const next = /^#{1,2}\s+\S/m.exec(rest);
376
+ return next === null ? rest : rest.slice(0, next.index);
377
+ }
378
+
379
+ // An explicit "there are none" answer. Kept as a closed list rather than a
380
+ // heuristic so the escape cannot be widened by accident: a section saying
381
+ // nothing and a section saying "none" are different facts, and only the first is
382
+ // a defect.
383
+ const _NO_OPEN_QUESTIONS = new Set(['none', 'keine', 'n/a', 'na', 'no open questions', 'nothing']);
384
+
385
+ /**
386
+ * Strip list markers, checkbox glyphs, emphasis and trailing sentence
387
+ * punctuation. The trailing strip is skipped when it would empty the line: a
388
+ * line that is ONLY punctuation (`...`) is a placeholder to name, not an empty
389
+ * section to report as blank, and the two findings say different things.
390
+ */
391
+ function _bareLine(line: string): string {
392
+ const stripped = line
393
+ .trim()
394
+ .replace(/^[-*+]\s*/, '')
395
+ .replace(/^\[[ x~-]\]\s*/i, '')
396
+ .replace(/[*_`]/g, '')
397
+ .trim()
398
+ .toLowerCase();
399
+ const trimmed = stripped.replace(/[.!]+$/, '').trim();
400
+ return trimmed.length > 0 ? trimmed : stripped;
401
+ }
402
+
403
+ /**
404
+ * Shape check on the one required section whose emptiness is the defect rather
405
+ * than a style nit. `validate_handoff_artifact` only tests that the heading
406
+ * exists, so `## Open questions` followed by nothing passed while carrying
407
+ * nothing — and a resume artefact whose open questions are blank hands the next
408
+ * session a false all-clear.
409
+ *
410
+ * **The step this implements claimed the false-positive class was empty because
411
+ * one `?`-terminated line is required. It is not, and the counterexample ships
412
+ * in this repo:** `tests/scripts/lint_handoffs_artifact.test.ts` carries
413
+ * `## Open questions` / `- none`, which is an honest answer and would be forced
414
+ * to grow a fake question. So the check accepts EITHER a real question OR an
415
+ * explicit none-marker, and fires only on a section that answers neither —
416
+ * blank, or a bare placeholder. That keeps the defect the step names without
417
+ * turning a truthful "none" into a lint error.
418
+ *
419
+ * Returns a finding string, or null when the shape holds. A MISSING section is
420
+ * not this check's business — `validate_handoff_artifact` already reports it.
421
+ */
422
+ export function validate_handoff_open_questions(text: string): string | null {
423
+ const body = handoff_section_body(text, 'Open questions');
424
+ if (body === null) {
425
+ return null;
426
+ }
427
+ const lines = body
428
+ .split('\n')
429
+ .map((l) => l.trim())
430
+ .filter((l) => l.length > 0);
431
+ if (lines.some((l) => l.length > 1 && l.endsWith('?'))) {
432
+ return null;
433
+ }
434
+ const bare = lines.map(_bareLine).filter((l) => l.length > 0);
435
+ if (bare.some((l) => _NO_OPEN_QUESTIONS.has(l))) {
436
+ return null;
437
+ }
438
+ if (bare.length === 0) {
439
+ return 'section `## Open questions` is empty — carry a `?`-terminated question, or state `none`';
440
+ }
441
+ const placeholder = new Set(['tbd', 'todo', 'wip', '...', '…', '?']);
442
+ if (bare.every((l) => placeholder.has(l))) {
443
+ return 'section `## Open questions` carries only a placeholder — carry a `?`-terminated question, or state `none`';
444
+ }
445
+ return null;
446
+ }
447
+
363
448
  export function main(argv?: readonly string[]): number {
364
449
  let skills_dir = SKILLS_DIR;
365
450
  // Positional-only: flags are NOT paths. Before this filter the CI invocation
@@ -373,14 +458,19 @@ export function main(argv?: readonly string[]): number {
373
458
  // artifact mode — validate the workflow-resume file's required fields
374
459
  const text = fs.readFileSync(_resolve(args[0] as string), 'utf-8');
375
460
  const missing = validate_handoff_artifact(text);
376
- if (missing.length === 0) {
461
+ const shape = validate_handoff_open_questions(text);
462
+ if (missing.length === 0 && shape === null) {
377
463
  if (!QUIET) process.stdout.write('✅ HANDOFF artifact: all required fields present\n');
378
464
  return 0;
379
465
  }
380
466
  for (const m of missing) {
381
467
  process.stdout.write(`${args[0]}:1:handoff-artifact-missing-field: missing required section \`## ${m}\`\n`);
382
468
  }
383
- process.stderr.write(`❌ ${missing.length} missing HANDOFF field(s)\n`);
469
+ if (shape !== null) {
470
+ process.stdout.write(`${args[0]}:1:handoff-artifact-empty-section: ${shape}\n`);
471
+ }
472
+ const problems = missing.length + (shape === null ? 0 : 1);
473
+ process.stderr.write(`❌ ${problems} HANDOFF artifact problem(s)\n`);
384
474
  return 1;
385
475
  }
386
476
  if (args.length > 0) {