@event4u/agent-config 11.0.0 → 12.0.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 (249) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +71 -249
  3. package/dist/agent-src/commands/agent-handoff.md +0 -1
  4. package/dist/agent-src/commands/agent-status.md +0 -1
  5. package/dist/agent-src/commands/agents/audit.md +0 -1
  6. package/dist/agent-src/commands/agents/init.md +0 -1
  7. package/dist/agent-src/commands/agents/optimize.md +0 -1
  8. package/dist/agent-src/commands/agents/user/accept.md +0 -1
  9. package/dist/agent-src/commands/agents/user/delete.md +0 -1
  10. package/dist/agent-src/commands/agents/user/init.md +0 -1
  11. package/dist/agent-src/commands/agents/user/review.md +0 -1
  12. package/dist/agent-src/commands/agents/user/show.md +0 -1
  13. package/dist/agent-src/commands/agents/user/update.md +0 -1
  14. package/dist/agent-src/commands/agents/user.md +0 -1
  15. package/dist/agent-src/commands/agents.md +0 -1
  16. package/dist/agent-src/commands/analytics/prune.md +0 -1
  17. package/dist/agent-src/commands/analytics/show.md +0 -1
  18. package/dist/agent-src/commands/analytics.md +0 -1
  19. package/dist/agent-src/commands/analyze/conformance.md +0 -1
  20. package/dist/agent-src/commands/analyze/decision.md +0 -1
  21. package/dist/agent-src/commands/analyze/inbox.md +0 -1
  22. package/dist/agent-src/commands/analyze/incident.md +0 -1
  23. package/dist/agent-src/commands/analyze/near-miss.md +0 -1
  24. package/dist/agent-src/commands/analyze/postmortem.md +0 -1
  25. package/dist/agent-src/commands/analyze/premortem.md +0 -1
  26. package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
  27. package/dist/agent-src/commands/analyze.md +0 -1
  28. package/dist/agent-src/commands/brand/identity.md +0 -1
  29. package/dist/agent-src/commands/brand/review.md +0 -1
  30. package/dist/agent-src/commands/brand/strategy.md +0 -1
  31. package/dist/agent-src/commands/brand/tokens.md +0 -1
  32. package/dist/agent-src/commands/brand/voice.md +0 -1
  33. package/dist/agent-src/commands/brand.md +0 -1
  34. package/dist/agent-src/commands/bug/fix.md +0 -1
  35. package/dist/agent-src/commands/bug/investigate.md +0 -1
  36. package/dist/agent-src/commands/bug.md +0 -1
  37. package/dist/agent-src/commands/challenge-me/vision.md +0 -1
  38. package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
  39. package/dist/agent-src/commands/challenge-me.md +0 -1
  40. package/dist/agent-src/commands/chat-history/import.md +0 -1
  41. package/dist/agent-src/commands/chat-history.md +0 -1
  42. package/dist/agent-src/commands/check-current-md.md +0 -1
  43. package/dist/agent-src/commands/commit/in-chunks.md +0 -1
  44. package/dist/agent-src/commands/commit.md +0 -1
  45. package/dist/agent-src/commands/condense.md +0 -1
  46. package/dist/agent-src/commands/context/create.md +0 -1
  47. package/dist/agent-src/commands/context/refactor.md +0 -1
  48. package/dist/agent-src/commands/context.md +0 -1
  49. package/dist/agent-src/commands/contribution-precheck.md +0 -1
  50. package/dist/agent-src/commands/cost/profile.md +0 -1
  51. package/dist/agent-src/commands/cost/report.md +0 -1
  52. package/dist/agent-src/commands/cost.md +0 -1
  53. package/dist/agent-src/commands/council/analysis.md +0 -1
  54. package/dist/agent-src/commands/council/debate.md +0 -1
  55. package/dist/agent-src/commands/council/default.md +0 -1
  56. package/dist/agent-src/commands/council/design.md +0 -1
  57. package/dist/agent-src/commands/council/optimize.md +0 -1
  58. package/dist/agent-src/commands/council/pr.md +0 -1
  59. package/dist/agent-src/commands/council.md +0 -1
  60. package/dist/agent-src/commands/design-system/capture.md +0 -1
  61. package/dist/agent-src/commands/design-system/generate.md +0 -1
  62. package/dist/agent-src/commands/design-system/import.md +0 -1
  63. package/dist/agent-src/commands/design-system.md +0 -1
  64. package/dist/agent-src/commands/estimate-ticket.md +0 -1
  65. package/dist/agent-src/commands/explain-run.md +0 -1
  66. package/dist/agent-src/commands/feature/dev.md +0 -1
  67. package/dist/agent-src/commands/feature/explore.md +0 -1
  68. package/dist/agent-src/commands/feature/plan.md +0 -1
  69. package/dist/agent-src/commands/feature/refactor.md +0 -1
  70. package/dist/agent-src/commands/feature/roadmap.md +0 -1
  71. package/dist/agent-src/commands/feature.md +0 -1
  72. package/dist/agent-src/commands/fix/ci.md +0 -1
  73. package/dist/agent-src/commands/fix/comments.md +0 -1
  74. package/dist/agent-src/commands/fix/portability.md +0 -1
  75. package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
  76. package/dist/agent-src/commands/fix/pr-comments.md +0 -1
  77. package/dist/agent-src/commands/fix/quality.md +0 -1
  78. package/dist/agent-src/commands/fix/refs.md +0 -1
  79. package/dist/agent-src/commands/fix/route.md +0 -1
  80. package/dist/agent-src/commands/fix/seeder.md +0 -1
  81. package/dist/agent-src/commands/fix.md +0 -1
  82. package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
  83. package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
  84. package/dist/agent-src/commands/ghostwriter/list.md +0 -1
  85. package/dist/agent-src/commands/ghostwriter/show.md +0 -1
  86. package/dist/agent-src/commands/ghostwriter/write.md +0 -1
  87. package/dist/agent-src/commands/ghostwriter.md +0 -1
  88. package/dist/agent-src/commands/grill-me.md +0 -1
  89. package/dist/agent-src/commands/humanize.md +0 -1
  90. package/dist/agent-src/commands/image/analyse.md +0 -1
  91. package/dist/agent-src/commands/image/create.md +0 -1
  92. package/dist/agent-src/commands/image/verify.md +0 -1
  93. package/dist/agent-src/commands/image.md +0 -1
  94. package/dist/agent-src/commands/implement-ticket.md +0 -1
  95. package/dist/agent-src/commands/jira-ticket.md +0 -1
  96. package/dist/agent-src/commands/judge/on-diff.md +0 -1
  97. package/dist/agent-src/commands/judge/solo.md +0 -1
  98. package/dist/agent-src/commands/judge/steps.md +0 -1
  99. package/dist/agent-src/commands/judge.md +0 -1
  100. package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
  101. package/dist/agent-src/commands/knowledge/forget.md +0 -1
  102. package/dist/agent-src/commands/knowledge/ingest.md +0 -1
  103. package/dist/agent-src/commands/knowledge/list.md +0 -1
  104. package/dist/agent-src/commands/knowledge.md +0 -1
  105. package/dist/agent-src/commands/memory/add.md +0 -1
  106. package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
  107. package/dist/agent-src/commands/memory/load.md +0 -1
  108. package/dist/agent-src/commands/memory/mine-session.md +0 -1
  109. package/dist/agent-src/commands/memory/promote.md +0 -1
  110. package/dist/agent-src/commands/memory/propose.md +0 -1
  111. package/dist/agent-src/commands/memory.md +0 -1
  112. package/dist/agent-src/commands/mission/upgrade.md +0 -1
  113. package/dist/agent-src/commands/mode.md +0 -1
  114. package/dist/agent-src/commands/module/create.md +0 -1
  115. package/dist/agent-src/commands/module/explore.md +0 -1
  116. package/dist/agent-src/commands/module.md +0 -1
  117. package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
  118. package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
  119. package/dist/agent-src/commands/optimize/deep.md +0 -1
  120. package/dist/agent-src/commands/optimize/project.md +0 -1
  121. package/dist/agent-src/commands/optimize/prompt.md +0 -1
  122. package/dist/agent-src/commands/optimize/rtk.md +0 -1
  123. package/dist/agent-src/commands/optimize/skills.md +0 -1
  124. package/dist/agent-src/commands/optimize.md +0 -1
  125. package/dist/agent-src/commands/orchestrate.md +0 -1
  126. package/dist/agent-src/commands/override/create.md +0 -1
  127. package/dist/agent-src/commands/override/manage.md +0 -1
  128. package/dist/agent-src/commands/override.md +0 -1
  129. package/dist/agent-src/commands/package/reset.md +0 -1
  130. package/dist/agent-src/commands/package/test.md +0 -1
  131. package/dist/agent-src/commands/package.md +0 -1
  132. package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
  133. package/dist/agent-src/commands/post-as/me.md +0 -1
  134. package/dist/agent-src/commands/post-as.md +0 -1
  135. package/dist/agent-src/commands/pr/create/description-only.md +0 -1
  136. package/dist/agent-src/commands/pr/create.md +23 -9
  137. package/dist/agent-src/commands/prediction-pool.md +0 -1
  138. package/dist/agent-src/commands/prepare-for-review.md +0 -1
  139. package/dist/agent-src/commands/profile/activate.md +0 -1
  140. package/dist/agent-src/commands/profile/deactivate.md +0 -1
  141. package/dist/agent-src/commands/profile/show.md +0 -1
  142. package/dist/agent-src/commands/profile.md +0 -1
  143. package/dist/agent-src/commands/project/analyze.md +0 -1
  144. package/dist/agent-src/commands/project/health.md +0 -1
  145. package/dist/agent-src/commands/project.md +0 -1
  146. package/dist/agent-src/commands/refine-ticket.md +0 -1
  147. package/dist/agent-src/commands/research/deep.md +0 -1
  148. package/dist/agent-src/commands/research/report.md +0 -1
  149. package/dist/agent-src/commands/research.md +0 -1
  150. package/dist/agent-src/commands/review/changes.md +0 -1
  151. package/dist/agent-src/commands/review/routing.md +0 -1
  152. package/dist/agent-src/commands/review.md +0 -1
  153. package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
  154. package/dist/agent-src/commands/roadmap/create.md +0 -1
  155. package/dist/agent-src/commands/roadmap/materialize.md +0 -1
  156. package/dist/agent-src/commands/roadmap/next.md +0 -1
  157. package/dist/agent-src/commands/roadmap/process-full.md +0 -1
  158. package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
  159. package/dist/agent-src/commands/roadmap/process-step.md +0 -1
  160. package/dist/agent-src/commands/roadmap.md +0 -1
  161. package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
  162. package/dist/agent-src/commands/security-audit-config.md +0 -1
  163. package/dist/agent-src/commands/skill/preview.md +0 -1
  164. package/dist/agent-src/commands/skill.md +0 -1
  165. package/dist/agent-src/commands/skills/discover.md +0 -1
  166. package/dist/agent-src/commands/skills.md +0 -1
  167. package/dist/agent-src/commands/sync/agent-settings.md +0 -1
  168. package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
  169. package/dist/agent-src/commands/sync/gitignore.md +0 -1
  170. package/dist/agent-src/commands/sync.md +0 -1
  171. package/dist/agent-src/commands/tdd/green.md +0 -1
  172. package/dist/agent-src/commands/tdd/red.md +0 -1
  173. package/dist/agent-src/commands/tdd/refactor.md +0 -1
  174. package/dist/agent-src/commands/tdd.md +0 -1
  175. package/dist/agent-src/commands/team/adversarial.md +0 -1
  176. package/dist/agent-src/commands/team/delegate.md +0 -1
  177. package/dist/agent-src/commands/team/review.md +0 -1
  178. package/dist/agent-src/commands/team/status.md +0 -1
  179. package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
  180. package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
  181. package/dist/agent-src/commands/team-knowledge.md +0 -1
  182. package/dist/agent-src/commands/team.md +0 -1
  183. package/dist/agent-src/commands/tests/create.md +0 -1
  184. package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
  185. package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
  186. package/dist/agent-src/commands/tests/execute.md +0 -1
  187. package/dist/agent-src/commands/tests.md +0 -1
  188. package/dist/agent-src/commands/threat-model.md +0 -1
  189. package/dist/agent-src/commands/update-form-request-messages.md +0 -1
  190. package/dist/agent-src/commands/upstream-contribute.md +0 -1
  191. package/dist/agent-src/commands/video/from-script.md +0 -1
  192. package/dist/agent-src/commands/video/from-song.md +0 -1
  193. package/dist/agent-src/commands/video/scene.md +0 -1
  194. package/dist/agent-src/commands/video/stitch.md +0 -1
  195. package/dist/agent-src/commands/video/storyboard.md +0 -1
  196. package/dist/agent-src/commands/video.md +0 -1
  197. package/dist/agent-src/commands/work.md +0 -1
  198. package/dist/agent-src/commands/worktree/cleanup.md +0 -1
  199. package/dist/agent-src/commands/worktree/create.md +0 -1
  200. package/dist/agent-src/commands/worktree/status.md +0 -1
  201. package/dist/agent-src/commands/worktree/verify.md +0 -1
  202. package/dist/agent-src/commands/worktree.md +0 -1
  203. package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
  204. package/dist/agent-src/skills/git-workflow/SKILL.md +9 -1
  205. package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
  206. package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
  207. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  208. package/dist/cli/commands/commands.candidates.test.js +1 -1
  209. package/dist/cli/commands/commands.candidates.test.js.map +1 -1
  210. package/dist/cli/commands/commands.js +9 -15
  211. package/dist/cli/commands/commands.js.map +1 -1
  212. package/dist/cli/commands/profiles.test.js +7 -7
  213. package/dist/cli/commands/profiles.test.js.map +1 -1
  214. package/dist/cli/discovery/loadManifest.js.map +1 -1
  215. package/dist/cli/main.js +2 -2
  216. package/dist/cli/main.js.map +1 -1
  217. package/dist/cli-delegate/cmd_conformance.js +14 -2
  218. package/dist/discovery/deprecation-report.md +1 -1
  219. package/dist/discovery/discovery-manifest.json +211 -410
  220. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  221. package/dist/discovery/discovery-manifest.summary.md +2 -2
  222. package/dist/discovery/dormancy-report.md +1 -1
  223. package/dist/discovery/orphan-report.md +1 -1
  224. package/dist/discovery/packs.json +3 -3
  225. package/dist/discovery/trust-report.md +1 -1
  226. package/dist/discovery/workspaces.json +3 -3
  227. package/dist/hooks/dispatch.js +14 -2
  228. package/dist/mcp/registry-manifest.json +3 -3
  229. package/dist/mcp/server.json +2 -2
  230. package/dist/mcp/server.mjs +46 -5
  231. package/docs/MIGRATION.md +1 -0
  232. package/docs/contracts/settings-classes.md +41 -0
  233. package/package.json +1 -1
  234. package/src/config/agent-settings.template.yml +34 -0
  235. package/src/config/gate-violation-baselines.json +3 -3
  236. package/src/scripts/_dispatch.bash +42 -5
  237. package/src/scripts/audit_command_surface.ts +21 -15
  238. package/src/scripts/build_catalog_index.ts +1 -1
  239. package/src/scripts/build_discovery_manifest.ts +30 -34
  240. package/src/scripts/check_branch_freshness.ts +210 -61
  241. package/src/scripts/gen_discovery_baseline.ts +3 -3
  242. package/src/scripts/hook_manifest.yaml +20 -0
  243. package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
  244. package/src/scripts/lint_command_tiers.ts +29 -75
  245. package/src/scripts/lint_originality.ts +101 -14
  246. package/src/scripts/schemas/command.schema.json +4 -9
  247. package/src/scripts/schemas/skill.schema.json +10 -1
  248. package/src/scripts/skill_linter.ts +82 -2
  249. package/src/scripts/worktree_cleanup_check.ts +111 -7
@@ -1,20 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Lint slash-command frontmatter for the `tier:` key.
3
+ * Lint slash-command frontmatter for the `visibility:` key.
4
4
  *
5
5
  * Ported from the retired Python `src/scripts/lint_command_tiers.py` (ADR-200, Phase 4 /
6
- * Wave 4b). Mirrors the Python CLI contract exactly: same scan scope, file
7
- * ordering, finding messages, stdout/stderr split, and exit codes
8
- * (return-code bitwise-OR accumulation across the domain + per-root +
9
- * condensed-projection passes).
6
+ * Wave 4b). Keeps the Python CLI contract's shape: same scan scope, file
7
+ * ordering, stdout/stderr split, and exit codes (return-code bitwise-OR
8
+ * accumulation across the domain + per-root + condensed-projection passes).
10
9
  *
11
- * Hard-fails CI if any command lacks a `tier:` declaration or uses an unknown
12
- * tier value. The valid tier set is locked by
10
+ * The integer `tier:` alias this file was named for is GONE
11
+ * (road-to-tier-removal Phase 4): `visibility:` is now the sole command-surface
12
+ * classifier. The filename is retained because the gate id, its Taskfile entry,
13
+ * and the contract doc all key on it; only the checked key changed.
14
+ *
15
+ * Hard-fails CI if any command lacks a `visibility:` declaration or uses an
16
+ * unknown value. The valid set is locked by
13
17
  * docs/contracts/command-surface-tiers.md.
14
18
  *
15
19
  * Exit codes:
16
- * 0 every command declares a valid tier
17
- * 1 one or more commands missing or using an invalid tier
20
+ * 0 every command declares a valid visibility
21
+ * 1 one or more commands missing or using an invalid visibility
18
22
  */
19
23
  import fs from "node:fs";
20
24
  import path from "node:path";
@@ -79,20 +83,13 @@ function COMMANDS_DIRS(): string[] {
79
83
  }
80
84
 
81
85
  const DOMAINS_DIR = path.join(REPO, "src", "domains");
82
- // Consumer-facing projection — must also carry tier so .augment/commands/
83
- // (which symlinks to dist/agent-src/commands/) renders the tier filter.
86
+ // Consumer-facing projection — must also carry visibility so .augment/commands/
87
+ // (which symlinks to dist/agent-src/commands/) renders the surface filter.
84
88
  const COMMANDS_DIR_CONDENSED = path.join(REPO, "dist/agent-src", "commands");
85
89
 
86
- const VALID_TIERS: ReadonlySet<string> = new Set(["0", "1", "2"]);
87
- // ADR-092: `visibility:` is the named source of truth; `tier:` is the
88
- // back-compat integer alias. Both are validated; when both are present they
89
- // MUST agree per this mapping.
90
+ // ADR-092 named `visibility:` the source of truth; road-to-tier-removal Phase 4
91
+ // removed the integer `tier:` alias, so this set is the whole vocabulary.
90
92
  const VALID_VISIBILITIES: ReadonlySet<string> = new Set(["visible", "advanced", "internal"]);
91
- const TIER_TO_VISIBILITY: Readonly<Record<string, string>> = {
92
- "0": "visible",
93
- "1": "advanced",
94
- "2": "internal",
95
- };
96
93
 
97
94
  export function parse_field(text: string, key: string): string | null {
98
95
  if (!text.startsWith("---\n")) {
@@ -116,15 +113,13 @@ export function parse_field(text: string, key: string): string | null {
116
113
  return null;
117
114
  }
118
115
 
119
- export function parse_tier(text: string): string | null {
120
- return parse_field(text, "tier");
121
- }
122
-
123
116
  /**
124
117
  * Validate the visibility field (ADR-092). Returns an error string or null.
125
118
  *
126
- * Requires a present + valid visibility, and consistency with the tier alias
127
- * whenever both are declared.
119
+ * Requires a present + valid visibility. The tier↔visibility consistency clause
120
+ * is gone with the alias itself (road-to-tier-removal Phase 4) — a stray `tier:`
121
+ * key is now rejected by `command.schema.json` (`additionalProperties: false`),
122
+ * not reconciled here.
128
123
  */
129
124
  export function visibility_error(text: string): string | null {
130
125
  const vis = parse_field(text, "visibility");
@@ -134,10 +129,6 @@ export function visibility_error(text: string): string | null {
134
129
  if (!VALID_VISIBILITIES.has(vis)) {
135
130
  return `invalid visibility '${vis}'`;
136
131
  }
137
- const tier = parse_field(text, "tier");
138
- if (tier !== null && tier in TIER_TO_VISIBILITY && TIER_TO_VISIBILITY[tier] !== vis) {
139
- return `visibility '${vis}' disagrees with tier '${tier}' (expected '${TIER_TO_VISIBILITY[tier]}')`;
140
- }
141
132
  return null;
142
133
  }
143
134
 
@@ -153,12 +144,6 @@ function stripChar(s: string, ch: string): string {
153
144
  return s.slice(start, end);
154
145
  }
155
146
 
156
- // Mirror Python's `print(sorted(set))` list repr: ['0', '1', ...].
157
- function sortedTiersRepr(): string {
158
- const items = [...VALID_TIERS].sort();
159
- return `[${items.map((s) => `'${s}'`).join(", ")}]`;
160
- }
161
-
162
147
  // Mirror Python's `print(sorted(VALID_VISIBILITIES))` list repr.
163
148
  function sortedVisibilitiesRepr(): string {
164
149
  const items = [...VALID_VISIBILITIES].sort();
@@ -184,43 +169,27 @@ export function lint(commands_dir: string, quiet = false): number {
184
169
  return 1;
185
170
  }
186
171
 
187
- const missing: string[] = [];
188
- const invalid: Array<[string, string]> = [];
189
172
  const vis_errors: Array<[string, string]> = [];
190
173
 
191
174
  for (const cmd of commands) {
192
175
  const rel = relPosix(cmd, commands_dir);
193
176
  const text = fs.readFileSync(cmd, "utf-8");
194
- const tier = parse_tier(text);
195
- if (tier === null) {
196
- missing.push(rel);
197
- } else if (!VALID_TIERS.has(tier)) {
198
- invalid.push([rel, tier]);
199
- }
200
177
  const ve = visibility_error(text);
201
178
  if (ve !== null) {
202
179
  vis_errors.push([rel, ve]);
203
180
  }
204
181
  }
205
182
 
206
- if (missing.length > 0 || invalid.length > 0 || vis_errors.length > 0) {
183
+ if (vis_errors.length > 0) {
207
184
  process.stderr.write(
208
- `❌ lint_command_tiers: ${missing.length} missing tier, ` +
209
- `${invalid.length} invalid tier, ${vis_errors.length} visibility ` +
185
+ `❌ lint_command_tiers: ${vis_errors.length} visibility ` +
210
186
  `(of ${commands.length} commands)\n`,
211
187
  );
212
- for (const name of missing) {
213
- process.stderr.write(` missing tier: ${name}\n`);
214
- }
215
- for (const [name, tier] of invalid) {
216
- process.stderr.write(` invalid tier '${tier}': ${name}\n`);
217
- }
218
188
  for (const [name, err] of vis_errors) {
219
189
  process.stderr.write(` ${err}: ${name}\n`);
220
190
  }
221
191
  process.stderr.write(
222
- ` valid tiers: ${sortedTiersRepr()}; ` +
223
- `valid visibility: ${sortedVisibilitiesRepr()}\n`,
192
+ ` valid visibility: ${sortedVisibilitiesRepr()}\n`,
224
193
  );
225
194
  process.stderr.write(
226
195
  " contract: docs/contracts/command-surface-tiers.md (ADR-092)\n",
@@ -231,7 +200,7 @@ export function lint(commands_dir: string, quiet = false): number {
231
200
  if (!quiet) {
232
201
  process.stdout.write(
233
202
  `✅ lint_command_tiers: ${commands.length} commands, ` +
234
- "all tier + visibility values valid\n",
203
+ "all visibility values valid\n",
235
204
  );
236
205
  }
237
206
  return 0;
@@ -246,35 +215,20 @@ export function lint_domain_sources(quiet = false): number {
246
215
  );
247
216
  return 1;
248
217
  }
249
- const missing: string[] = [];
250
- const invalid: Array<[string, string]> = [];
251
218
  const vis_errors: Array<[string, string]> = [];
252
219
  for (const c of commands) {
253
220
  const rel = relPosix(c, REPO);
254
221
  const text = fs.readFileSync(c, "utf-8");
255
- const t = parse_tier(text);
256
- if (t === null) {
257
- missing.push(rel);
258
- } else if (!VALID_TIERS.has(t)) {
259
- invalid.push([rel, t]);
260
- }
261
222
  const ve = visibility_error(text);
262
223
  if (ve !== null) {
263
224
  vis_errors.push([rel, ve]);
264
225
  }
265
226
  }
266
- if (missing.length > 0 || invalid.length > 0 || vis_errors.length > 0) {
227
+ if (vis_errors.length > 0) {
267
228
  process.stderr.write(
268
- `❌ lint_command_tiers: ${missing.length} missing tier, ` +
269
- `${invalid.length} invalid tier, ${vis_errors.length} visibility ` +
229
+ `❌ lint_command_tiers: ${vis_errors.length} visibility ` +
270
230
  `(of ${commands.length} domain commands)\n`,
271
231
  );
272
- for (const name of missing) {
273
- process.stderr.write(` missing tier: ${name}\n`);
274
- }
275
- for (const [name, tier] of invalid) {
276
- process.stderr.write(` invalid tier '${tier}': ${name}\n`);
277
- }
278
232
  for (const [name, err] of vis_errors) {
279
233
  process.stderr.write(` ${err}: ${name}\n`);
280
234
  }
@@ -283,7 +237,7 @@ export function lint_domain_sources(quiet = false): number {
283
237
  if (!quiet) {
284
238
  process.stdout.write(
285
239
  `✅ lint_command_tiers: ${commands.length} domain commands, ` +
286
- "all tier + visibility values valid\n",
240
+ "all visibility values valid\n",
287
241
  );
288
242
  }
289
243
  return 0;
@@ -354,7 +308,7 @@ export function main(): number {
354
308
  }
355
309
  // The condensed projection is the consumer-facing tree (via the
356
310
  // .augment/commands → dist/agent-src/commands symlink). It must also
357
- // carry tier so the surface stays uniform.
311
+ // carry visibility so the surface stays uniform.
358
312
  if (_isDir(COMMANDS_DIR_CONDENSED)) {
359
313
  rc |= lint(COMMANDS_DIR_CONDENSED, QUIET);
360
314
  }
@@ -20,6 +20,8 @@
20
20
  *
21
21
  * Modes:
22
22
  * (no args) full pairwise audit → agents/reports/originality.{json,md}
23
+ * --base <ref> the revision the change set is measured against; defines
24
+ * what counts as ESTABLISHED scaffold (see below)
23
25
  * --changed <f...> CI path: each changed file vs the whole same-class corpus
24
26
  * AND vs the other changed files of its class
25
27
  *
@@ -41,7 +43,7 @@
41
43
  * it drifts as commands/skills are added or removed. Re-run the full audit
42
44
  * after any large corpus change; do not treat 40 % as invariant.
43
45
  * 2. Adversarial batch masking. In `--changed` mode the boilerplate set is
44
- * computed from the corpus MINUS the change set, so a batch of ≥ _dfFloor
46
+ * derived from the ESTABLISHED corpus, so a batch of ≥ _dfFloor
45
47
  * near-identical re-skins submitted together cannot classify its own shared
46
48
  * shingles as boilerplate (see `_changed`). The full-audit sweep (no
47
49
  * "changed" notion) does NOT have this guard — a batch of ≥ floor identical
@@ -51,8 +53,35 @@
51
53
  * computes the PR's changed corpus files and runs `--changed` on them
52
54
  * (empty changed set reports INCONCLUSIVE, never a pass).
53
55
  *
56
+ * HOW "established" is resolved, and why it changed (2026-08-14):
57
+ * With `--base <ref>` the established corpus is the class corpus AS IT
58
+ * EXISTS AT THAT REVISION — content read from git, not from the worktree.
59
+ * Without it, the older approximation applies: on-disk corpus MINUS the
60
+ * change set.
61
+ *
62
+ * That approximation has a degeneracy the base-ref form does not. When a
63
+ * change set covers (nearly) the whole class — a mechanical edit such as
64
+ * dropping one frontmatter key from every command — "corpus minus change
65
+ * set" is EMPTY, no shingle clears the DF floor, nothing is scaffold, and
66
+ * the class's shared skeleton is scored as authored overlap. Measured on
67
+ * road-to-tier-removal Phase 4: the full audit reported worst 40 % / 0
68
+ * failures, while `--changed` over the same 200 unmodified-in-substance
69
+ * files reported 67.9 % and 3 failures — byte-identical on the base
70
+ * revision's own content, so the delta was entirely the masking collapse.
71
+ *
72
+ * The base-ref form does NOT weaken the guard, and it is worth being
73
+ * precise about why rather than claiming more. Both forms close the
74
+ * re-skin-batch axis, for different reasons: the old one excludes the
75
+ * change set outright, the new one relies on those files' shared shingles
76
+ * being absent from the base content (a NEW file contributes nothing at
77
+ * all; a REWRITTEN file contributes only its OLD text). Either way a
78
+ * batch cannot promote its own new shingles to scaffold. What changes is
79
+ * only the degenerate case: an established corpus that is empty because
80
+ * the change set swallowed it.
81
+ *
54
82
  * Exit 0 clean/warn · 1 fail · 2 usage.
55
83
  */
84
+ import { spawnSync } from 'node:child_process';
56
85
  import * as fs from 'node:fs';
57
86
  import * as path from 'node:path';
58
87
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -176,7 +205,11 @@ function _templateShingles(spec: ClassSpec): Set<string> {
176
205
  }
177
206
 
178
207
  function _load(abs: string, cls: ArtifactClass, tmpl: Set<string>): Artifact {
179
- const text = fs.readFileSync(abs, 'utf-8');
208
+ return _loadText(fs.readFileSync(abs, 'utf-8'), abs, cls, tmpl);
209
+ }
210
+
211
+ /** `_load` over supplied text — used to load a file's content at a base ref. */
212
+ function _loadText(text: string, abs: string, cls: ArtifactClass, tmpl: Set<string>): Artifact {
180
213
  const raw = shingles(text, K);
181
214
  // Subtract shared scaffold — a shingle also present in the class template
182
215
  // is boilerplate, not authored content.
@@ -332,14 +365,41 @@ function _writeReport(byClass: Map<ArtifactClass, Artifact[]>, pairs: Pair[], di
332
365
 
333
366
  // --- changed mode ------------------------------------------------------------
334
367
 
335
- function _changed(files: string[]): { pairs: Pair[]; fails: number } {
336
- // The boilerplate DF set is derived from the ESTABLISHED corpus only — the
337
- // change set under review is excluded. Without this, a batch of ≥ _dfFloor
338
- // near-identical re-skins submitted together would lift its OWN shared
339
- // shingles over the floor, be reclassified as boilerplate, subtracted from
340
- // every copy, and score 0 against each other — the gate blinds itself on
341
- // exactly the attack it exists to catch. "What is scaffold" must be defined
342
- // by the corpus as it stands, never by the diff proposing to change it.
368
+ /**
369
+ * A corpus file's content at `ref`, or null when the file does not exist there.
370
+ *
371
+ * Null is the load-bearing case: a file absent at the base revision is NEW in
372
+ * this change set, so it contributes nothing to the scaffold definition. That is
373
+ * what keeps a batch of re-skinned newcomers from masking itself.
374
+ *
375
+ * A git failure is indistinguishable from "absent" at this layer, and both
376
+ * resolve the same conservative way — the file is excluded from scaffold, which
377
+ * can only make the gate stricter, never blinder.
378
+ */
379
+ function _textAtRef(ref: string, relpath: string): string | null {
380
+ const r = spawnSync('git', ['show', `${ref}:${relpath}`], {
381
+ cwd: ROOT,
382
+ encoding: 'utf-8',
383
+ maxBuffer: 32 * 1024 * 1024,
384
+ });
385
+ if (r.status !== 0 || typeof r.stdout !== 'string') return null;
386
+ return r.stdout;
387
+ }
388
+
389
+ function _changed(files: string[], base: string | null = null): { pairs: Pair[]; fails: number } {
390
+ // The boilerplate DF set is derived from the ESTABLISHED corpus only.
391
+ // Without this, a batch of ≥ _dfFloor near-identical re-skins submitted
392
+ // together would lift its OWN shared shingles over the floor, be
393
+ // reclassified as boilerplate, subtracted from every copy, and score 0
394
+ // against each other — the gate blinds itself on exactly the attack it
395
+ // exists to catch. "What is scaffold" must be defined by the corpus as it
396
+ // stands, never by the diff proposing to change it.
397
+ //
398
+ // `--base` makes "as it stands" literal: scaffold comes from the class
399
+ // corpus AT THAT REVISION. Without it we fall back to the older
400
+ // approximation (on-disk minus the change set), which is equivalent for a
401
+ // small change set but degenerates to an EMPTY established corpus when the
402
+ // change set covers the class — see the § 2 note in the module header.
343
403
  const changedRel = new Set(files.map((f) => _rel(path.resolve(ROOT, f))));
344
404
 
345
405
  // Build the corpus in RAW (template-subtracted, pre-DF) form per class so we
@@ -351,8 +411,18 @@ function _changed(files: string[]): { pairs: Pair[]; fails: number } {
351
411
  const tmpl = _templateShingles(spec);
352
412
  templateCache.set(spec.name, tmpl);
353
413
  const arts = spec.files().map((f) => _load(f, spec.name, tmpl));
354
- // Established corpus = on-disk arts minus the change set.
355
- const boiler = _boilerplateSet(arts.filter((a) => !changedRel.has(a.relpath)));
414
+ // Established corpus: at `base` when given (a file absent there is NEW
415
+ // and contributes no scaffold), else on-disk arts minus the change set.
416
+ const established =
417
+ base === null
418
+ ? arts.filter((a) => !changedRel.has(a.relpath))
419
+ : arts
420
+ .map((a) => {
421
+ const text = _textAtRef(base, a.relpath);
422
+ return text === null ? null : _loadText(text, path.resolve(ROOT, a.relpath), spec.name, tmpl);
423
+ })
424
+ .filter((a): a is Artifact => a !== null);
425
+ const boiler = _boilerplateSet(established);
356
426
  for (const a of arts) _subtract(a, boiler);
357
427
  byClass.set(spec.name, arts);
358
428
  boilerByClass.set(spec.name, boiler);
@@ -404,13 +474,30 @@ export function main(argv?: string[]): number {
404
474
  }
405
475
  let quiet = false;
406
476
  let changedFiles: string[] | null = null;
477
+ let base: string | null = null;
407
478
  for (let i = 0; i < args.length; i++) {
408
479
  const a = args[i] as string;
409
480
  if (a === '--quiet') { quiet = true; }
481
+ else if (a === '--base') {
482
+ // `--changed` swallows the rest of argv, so `--base` must precede it.
483
+ const v = args[i + 1];
484
+ if (v === undefined || v.startsWith('-')) {
485
+ process.stderr.write(`--base needs a git ref\n${_usage()}`);
486
+ return 2;
487
+ }
488
+ base = v;
489
+ i += 1;
490
+ }
410
491
  else if (a === '--changed') { changedFiles = args.slice(i + 1).filter((x) => !x.startsWith('-')); break; }
411
492
  else if (a.startsWith('-')) { process.stderr.write(`unrecognized argument: ${a}\n${_usage()}`); return 2; }
412
493
  else { process.stderr.write(`unexpected argument: ${a} (did you mean --changed ${a}?)\n${_usage()}`); return 2; }
413
494
  }
495
+ if (base !== null && changedFiles === null) {
496
+ // Silently ignoring it would let a workflow think the base-anchored
497
+ // scaffold was in effect when the full audit does not use one at all.
498
+ process.stderr.write(`--base is only meaningful with --changed\n${_usage()}`);
499
+ return 2;
500
+ }
414
501
 
415
502
  // Asserted for BOTH modes, and before either: `--changed` compares each
416
503
  // candidate against the same corpus, so a dead root there reports "no
@@ -434,7 +521,7 @@ export function main(argv?: string[]): number {
434
521
  }
435
522
 
436
523
  if (changedFiles !== null) {
437
- const { pairs, fails } = _changed(changedFiles);
524
+ const { pairs, fails } = _changed(changedFiles, base);
438
525
  if (pairs.length > 0) {
439
526
  const stream = fails > 0 ? process.stderr : process.stdout;
440
527
  stream.write(`lint_originality: ${pairs.length} overlap(s) ≥ WARN ${WARN}% (${fails} ≥ FAIL ${FAIL}%):\n`);
@@ -466,7 +553,7 @@ export function main(argv?: string[]): number {
466
553
  }
467
554
 
468
555
  function _usage(): string {
469
- return 'usage: lint_originality [--quiet] [--changed <file...>]\n';
556
+ return 'usage: lint_originality [--quiet] [--base <ref>] [--changed <file...>]\n';
470
557
  }
471
558
 
472
559
  function _isCliEntry(): boolean {
@@ -14,15 +14,10 @@
14
14
  "maxLength": 64,
15
15
  "$comment": "Always the path-derived hyphen slug (`commit`, `council-default`) — identical to the `.claude/skills/` directory slug generated by condense.py. Agent-Skills-spec-compliant (lowercase letters, digits, hyphens; no leading/trailing/double hyphen; <=64 chars) so strict consumers like Zed accept projected SKILL.md entries. Colon names (`council:default`) are FORBIDDEN since the 2026-06 Zed fix; Claude Code's `/cluster:sub` invocation is path-derived from `commands/<cluster>/<sub>.md` and unaffected by this field."
16
16
  },
17
- "tier": {
18
- "type": "integer",
19
- "enum": [0, 1, 2],
20
- "description": "Command-surface tier per docs/contracts/command-surface-tiers.md. 0 = daily-driver (rendered in default `./agent-config --help`), 1 = power-user (rendered with `--tier=1`), 2 = maintenance/internal (rendered only with `--tier=all`). Default for new commands is 2 — promotion is gated by ADR criteria, never by author preference. BACK-COMPAT ALIAS since ADR-092: `visibility:` is the named source of truth (visible↔0, advanced↔1, internal↔2); `tier:` is retained as a derived integer alias and dual-emitted in the discovery manifest during the deprecation window. When both are present they MUST agree (enforced by lint_command_tiers.py)."
21
- },
22
17
  "visibility": {
23
18
  "type": "string",
24
19
  "enum": ["visible", "advanced", "internal"],
25
- "description": "Command-surface visibility (ADR-092) — the NAMED source of truth that supersedes the integer `tier:` proxy. `visible` = daily-driver (default `./agent-config --help`, ↔ tier 0), `advanced` = power-user (`--tier=1`, ↔ tier 1), `internal` = maintenance/hidden (`--tier=all` only, ↔ tier 2). Read by the surface classifier (commands ls/explain), the per-pack visible-command budget audit, and the discovery-manifest builder, each preferring `visibility:` and falling back to `tier:`. Default for new commands is `internal`; promotion is gated by ADR criteria per docs/contracts/command-surface-tiers.md."
20
+ "description": "Command-surface visibility per docs/contracts/command-surface-tiers.md — the sole command-surface classifier since the integer `tier:` alias was removed. `visible` = daily-driver (default `./agent-config --help`), `advanced` = power-user (`--tier=1`), `internal` = maintenance/hidden (`--tier=all` only). Read by the surface classifier (commands ls/explain), the per-pack visible-command budget audit, and the discovery-manifest builder. Default for new commands is `internal`; promotion is gated by ADR criteria. Presence and validity are enforced by lint_command_tiers.ts."
26
21
  },
27
22
  "description": {
28
23
  "type": "string",
@@ -71,7 +66,7 @@
71
66
  "type": "string",
72
67
  "minLength": 1,
73
68
  "maxLength": 200,
74
- "description": "6.0.0-C Phase 2 routing metadata. One-line existence justification: the single user intent this command serves (e.g. 'Implement a ticket end-to-end from its tracker id'). REQUIRED on every visible command (tier 0/1), enforced by scripts/lint_command_routing.py; optional on internal (tier 2) commands. The human-readable answer to 'why does this command exist?'"
69
+ "description": "6.0.0-C Phase 2 routing metadata. One-line existence justification: the single user intent this command serves (e.g. 'Implement a ticket end-to-end from its tracker id'). REQUIRED on every visible command (visibility `visible`/`advanced`), enforced by scripts/lint_command_routing.py; optional on `internal` commands. The human-readable answer to 'why does this command exist?'"
75
70
  },
76
71
  "routes_to": {
77
72
  "type": "array",
@@ -81,7 +76,7 @@
81
76
  "type": "string",
82
77
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
83
78
  },
84
- "description": "6.0.0-C Phase 2 routing metadata. The data backbone the routing-eval lint checks against: the skill slug(s), cluster sub-command(s), or command(s) this command routes the intent to (e.g. ['refine-ticket', 'feature-planning'] or ['fix-ci']). Always hyphen slugs — colon identifiers forbidden since the 2026-06 Zed fix. REQUIRED on every visible command (tier 0/1), enforced by scripts/lint_command_routing.py; optional on internal commands."
79
+ "description": "6.0.0-C Phase 2 routing metadata. The data backbone the routing-eval lint checks against: the skill slug(s), cluster sub-command(s), or command(s) this command routes the intent to (e.g. ['refine-ticket', 'feature-planning'] or ['fix-ci']). Always hyphen slugs — colon identifiers forbidden since the 2026-06 Zed fix. REQUIRED on every visible command (visibility `visible`/`advanced`), enforced by scripts/lint_command_routing.py; optional on `internal` commands."
85
80
  },
86
81
  "category": {
87
82
  "type": "string",
@@ -96,7 +91,7 @@
96
91
  "pattern": "^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$",
97
92
  "$comment": "Colon form stays ALLOWED here on purpose: `replaces` lists PRIOR names/aliases, and pre-2026-06 colon names (`commit:in-chunks`) are legitimate historical aliases."
98
93
  },
99
- "description": "6.0.0-C Phase 2 routing metadata. The alias / deprecation link: prior command name(s) or alias(es) this command absorbs or supersedes. Empty array `[]` when the command replaces nothing (the common case). REQUIRED-as-a-key on every visible command (tier 0/1) so the routing surface is explicit, enforced by scripts/lint_command_routing.py; optional on internal commands. Distinct from `superseded_by` (set on the OLD shim pointing forward); `replaces` is set on the NEW canonical command pointing back."
94
+ "description": "6.0.0-C Phase 2 routing metadata. The alias / deprecation link: prior command name(s) or alias(es) this command absorbs or supersedes. Empty array `[]` when the command replaces nothing (the common case). REQUIRED-as-a-key on every visible command (visibility `visible`/`advanced`) so the routing surface is explicit, enforced by scripts/lint_command_routing.py; optional on `internal` commands. Distinct from `superseded_by` (set on the OLD shim pointing forward); `replaces` is set on the NEW canonical command pointing back."
100
95
  },
101
96
  "type": {
102
97
  "type": "string",
@@ -31,7 +31,16 @@
31
31
  "compatibility": {
32
32
  "type": "string",
33
33
  "maxLength": 500,
34
- "description": "Agent-Skills spec field (optional): machine-readable declaration of environment requirements — system packages, network access, product/host constraints. Mirrors the public spec so media/doc skills can state requirements outside prose."
34
+ "deprecated": true,
35
+ "description": "DEPRECATED (announced 2026-08-14, removal maintainer-scheduled — see docs/MIGRATION.md § Scheduled deprecations). Agent-Skills spec field (optional): free-text declaration of environment requirements — system packages, network access, product/host constraints. Superseded by `harness_compat` (the machine-readable class) plus `runtime_requires` (the probeable detail). Still accepted and still emitted so the public-spec mirror keeps working during the window; new skills declare `harness_compat` instead."
36
+ },
37
+ "harness_compat": {
38
+ "type": "string",
39
+ "enum": [
40
+ "consumer-installed-deps"
41
+ ],
42
+ "description": "Structured replacement for the free-text `compatibility` class claim: WHAT KIND of harness constraint the skill carries, in a form `doctor`/`preflight` can branch on. Distinct from `runtime_requires`, which names WHICH bins/env/network to probe — a Python module such as `python-docx` is not a PATH binary, so it has no `bins` entry and would otherwise be invisible to any machine reader. Deliberately single-valued: the enum is derived from the only two skills that ever declared `compatibility`, and both attest exactly one class. A second value is added when a skill attests one, never pre-emptively — a speculative vocabulary is the pro-forma-field risk this schema already refuses for `runtime_requires`. `consumer-installed-deps` = the skill runs only where the consumer has itself installed named third-party libraries or engines; this package ships no runtime for it.",
43
+ "$comment": "road-to-skill-ecosystem-executable-payloads Phase 1, under the maintainer's 2026-08-14 resolution of blocker `compatibility-deprecation-is-a-consumer-visible-decision`: ADDITIVE — `harness_compat` lands beside `compatibility`, which is NOT removed here; its removal is approved in principle and scheduled through docs/MIGRATION.md § Scheduled deprecations, never dropped silently. Enum evidence (verbatim, the complete population of `compatibility` users): docx-authoring — \"Requires a consumer-installed OOXML library (python-docx …) … Ships zero runtime in this package.\"; pdf-tools — \"Requires consumer-installed PDF libraries: pypdf …, reportlab …; an OCR engine (e.g. tesseract) … Ships zero runtime in this package.\" Both state one class; the label generalises \"library\"/\"engine\" to \"deps\" and adds no claim neither text makes."
35
44
  },
36
45
  "runtime_requires": {
37
46
  "type": "object",
@@ -254,6 +254,42 @@ const EXTERNAL_EXECUTION_HANDLERS = new Set(['shell', 'php', 'node']);
254
254
  * span lines and accept a bare key whose value sits somewhere below it.
255
255
  */
256
256
  const RUNTIME_REQUIRES_BLOCK_PATTERN = /^runtime_requires:[ \t]*(\{.*)?$/m;
257
+
258
+ /**
259
+ * The deprecated free-text `compatibility:` key and its structured successor
260
+ * `harness_compat:`, both top-level. Anchored at column 0 so a `compatibility:`
261
+ * nested inside another block (or the word inside a quoted description) is not
262
+ * read as the frontmatter key — the same reason the neighbouring patterns above
263
+ * use `^` rather than a bare substring test. A value is required after the colon
264
+ * so a bare key with the value on the next line is not counted as a declaration.
265
+ */
266
+ const COMPATIBILITY_KEY_PATTERN = /^compatibility:[ \t]*\S/m;
267
+ const HARNESS_COMPAT_KEY_PATTERN = /^harness_compat:[ \t]*\S/m;
268
+
269
+ /**
270
+ * `safety_mode: strict` is a claim made in the frontmatter about a skill whose
271
+ * execution path may mutate something. Nothing checked that the body honours it,
272
+ * so the claim could be pure metadata. These patterns recognise the two shapes
273
+ * the action-reference split allows (`skill-writing` § Action-reference split):
274
+ * an inline precondition that gates the write, or a deferral of the write-path
275
+ * steps into the skill's own `references/`.
276
+ *
277
+ * Deliberately a warning over a phrase list rather than an error over a required
278
+ * section: a skill that gates its write path some third way is doing the right
279
+ * thing, and a hard gate over a corpus of one strict skill would be a gate that
280
+ * mostly cannot fail — the defect class `road-to-skill-ecosystem-gate-integrity`
281
+ * exists to remove.
282
+ */
283
+ const STRICT_WRITE_GATE_PATTERNS = [
284
+ /--dry-run/i,
285
+ /\bnever silent[- ]run/i,
286
+ /\bpropose,? never\b/i,
287
+ /\bconfirms? before\b/i,
288
+ /\bexplicit (?:user )?confirmation\b/i,
289
+ /\basks? (?:the user )?(?:first|before)\b/i,
290
+ ];
291
+ const SKILL_REFERENCE_DEFERRAL_PATTERN = /\]\(\.{0,2}\/?references\//;
292
+
257
293
  const VALID_EXECUTION_FIELDS = new Set([
258
294
  'type',
259
295
  'handler',
@@ -1295,7 +1331,7 @@ function lint_wing4_boundaries(text: string): Issue[] {
1295
1331
  * for an optional parameter — a future caller that forgets it gets a confident
1296
1332
  * error about a declaration it never looked for.
1297
1333
  */
1298
- function lint_execution_metadata(execution: ExecutionBlock, frontmatter: string): Issue[] {
1334
+ function lint_execution_metadata(execution: ExecutionBlock, frontmatter: string, body = ''): Issue[] {
1299
1335
  const issues: Issue[] = [];
1300
1336
 
1301
1337
  const execType = execution.type;
@@ -1370,6 +1406,29 @@ function lint_execution_metadata(execution: ExecutionBlock, frontmatter: string)
1370
1406
  );
1371
1407
  }
1372
1408
 
1409
+ // road-to-skill-ecosystem-executable-payloads Phase 5 — the action-reference
1410
+ // split. A strict-mode skill either gates its write path inline, or says it
1411
+ // does not define the mutating workflow and defers those steps to its own
1412
+ // `references/` behind a precondition. Neither present → the strict claim is
1413
+ // frontmatter only, and the body reads like an ordinary skill.
1414
+ if (
1415
+ safetyMode === 'strict' &&
1416
+ body !== '' &&
1417
+ !STRICT_WRITE_GATE_PATTERNS.some((p) => p.test(body)) &&
1418
+ !SKILL_REFERENCE_DEFERRAL_PATTERN.test(body)
1419
+ ) {
1420
+ issues.push(
1421
+ new Issue(
1422
+ 'warning',
1423
+ 'strict_mode_missing_write_gate',
1424
+ "execution.safety_mode is 'strict' but the body names no precondition on the write " +
1425
+ 'path and defers no write-path steps to the skill\'s references/ — state the gate ' +
1426
+ 'inline (propose-then-confirm, --dry-run) or defer it, per skill-writing ' +
1427
+ '§ Action-reference split',
1428
+ ),
1429
+ );
1430
+ }
1431
+
1373
1432
  const timeout = execution.timeout_seconds;
1374
1433
  if (timeout !== undefined) {
1375
1434
  if (typeof timeout !== 'number' || !Number.isInteger(timeout) || timeout <= 0) {
@@ -1583,9 +1642,30 @@ export function lint_skill(p: string, text: string, absPath: string | null = nul
1583
1642
  }
1584
1643
  }
1585
1644
 
1645
+ // `compatibility:` is deprecated in favour of the structured
1646
+ // `harness_compat:` (schema, 2026-08-14). The field is NOT removed — the
1647
+ // schema still accepts it and both existing users still carry it — so this
1648
+ // is a warning, not an error: its job is to stop NEW adoption during the
1649
+ // window, which is the only thing a deprecation notice with no gate cannot
1650
+ // do. Regex, not a YAML parse, to match the file's existing convention.
1651
+ if (
1652
+ COMPATIBILITY_KEY_PATTERN.test(frontmatter) &&
1653
+ !HARNESS_COMPAT_KEY_PATTERN.test(frontmatter)
1654
+ ) {
1655
+ issues.push(
1656
+ new Issue(
1657
+ 'warning',
1658
+ 'deprecated_compatibility_field',
1659
+ "Frontmatter declares the deprecated free-text 'compatibility:' without the structured " +
1660
+ "'harness_compat:' that supersedes it — add harness_compat (see skill.schema.json and " +
1661
+ 'docs/MIGRATION.md § Scheduled deprecations); compatibility stays accepted until removal',
1662
+ ),
1663
+ );
1664
+ }
1665
+
1586
1666
  const execution = parseExecutionBlock(frontmatter);
1587
1667
  if (execution !== null) {
1588
- issues.push(...lint_execution_metadata(execution, frontmatter));
1668
+ issues.push(...lint_execution_metadata(execution, frontmatter, text.replace(FRONTMATTER_PATTERN, '')));
1589
1669
  }
1590
1670
 
1591
1671
  const tierMatch = TIER_PATTERN.exec(frontmatter);