@event4u/agent-config 14.19.0 → 14.21.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 (161) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -23,10 +23,48 @@
23
23
  * sitting in the active tree silently counts as backlog the dashboard and
24
24
  * ``/roadmap:process-*`` would try to execute.
25
25
  *
26
- * B. Every roadmap under ``agents/roadmaps/later/`` MUST record a **resume
27
- * condition** so it never rots without a "when does it come back": either
28
- * ``status: later`` frontmatter, or a body line matching
29
- * ``Blocked until`` / ``Resume when`` / ``Trigger`` (case-insensitive).
26
+ * B. Every parked roadmap MUST record a **wake
27
+ * condition** so it never rots without a "when does it come back".
28
+ *
29
+ * Until 2026-09-07 this rule short-circuited on the frontmatter STATUS
30
+ * word: ``if (status !== 'later' && !RESUME_RE.test(body))``, so a file
31
+ * whose frontmatter said ``status: later`` passed with no resume text
32
+ * present at all. Measured over the tree at that date, 63 of 82 parked
33
+ * roadmaps passed on the status word alone, 8 on a real ``Blocked until``
34
+ * / ``Resume when``, and 11 on the bare word ``trigger`` — so 74 of 82
35
+ * carried no machine-readable wake condition. The word ``trigger`` was
36
+ * the second hole: a body that merely mentions it in passing ("the
37
+ * trigger fires on push") satisfied a governance gate.
38
+ *
39
+ * The corrected contract, in force regardless of ``status``:
40
+ *
41
+ * 1. **Structured** — an ``entry_condition:`` MAPPING in the
42
+ * frontmatter with three non-empty parts:
43
+ *
44
+ * entry_condition:
45
+ * what: what would change the decision
46
+ * when: when that could arrive
47
+ * who: who would have to act (``none`` is a legal answer)
48
+ *
49
+ * A condition that cannot be observed is a deferral wearing a
50
+ * condition's clothes, which is why all three are required and why
51
+ * blankness is not the same answer as ``none``.
52
+ *
53
+ * 2. **Legacy** — a body line matching ``Blocked until`` /
54
+ * ``Resume when`` / ``blocked-until`` / ``resume-when``. Kept so the
55
+ * ratchet below has a floor to walk DOWN toward rather than a tree
56
+ * to fail on the day it lands.
57
+ *
58
+ * A malformed ``entry_condition`` (scalar, or missing a part) is a HARD
59
+ * finding and is never baselined: it is a defect the author introduced in
60
+ * the change that wrote it, not inherited debt.
61
+ *
62
+ * C. Every parked roadmap MUST carry ``review_by``
63
+ * in its frontmatter. Ratcheted from the measured count, same reasoning.
64
+ *
65
+ * Both ratchets live in ``src/config/gate-violation-baselines.json`` under
66
+ * ``lint_roadmap_later_disposition:wake-condition`` and
67
+ * ``…:review-by``: the count may fall and never rise.
30
68
  *
31
69
  * Rationale: "roadmaps with open tasks deferred for later are always moved to
32
70
  * ``later/``" (user directive 2026-06-16). The active tree holds only roadmaps
@@ -44,6 +82,7 @@ import * as fs from 'node:fs';
44
82
  import * as path from 'node:path';
45
83
  import { fileURLToPath, pathToFileURL } from 'node:url';
46
84
 
85
+ import { checkRatchet } from './_lib/gate_baseline.js';
47
86
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
48
87
 
49
88
  const _HERE = fileURLToPath(import.meta.url);
@@ -64,8 +103,17 @@ function _setRoadmapRootForTest(p: string): void {
64
103
  const FRONTMATTER_RE = /^---[ \t\f\v]*\n([\s\S]*?)\n---[ \t\f\v]*\n/;
65
104
  // STATUS_RE = re.compile(r"^status:\s*([A-Za-z0-9_-]+)\s*$", re.MULTILINE)
66
105
  const STATUS_RE = /^status:[ \t\f\v\r\n]*([A-Za-z0-9_-]+)[ \t\f\v\r]*$/m;
67
- // RESUME_RE = re.compile(r"\b(blocked until|resume when|trigger|blocked-until|resume-when)\b", re.IGNORECASE)
68
- const RESUME_RE = /\b(blocked until|resume when|trigger|blocked-until|resume-when)\b/i;
106
+ // The four UNAMBIGUOUS phrases. `trigger` was dropped 2026-09-07: it matched
107
+ // any prose mention of the word, so 11 of 82 parked roadmaps satisfied a
108
+ // governance gate on a sentence about something else entirely.
109
+ const RESUME_RE = /\b(blocked until|resume when|blocked-until|resume-when)\b/i;
110
+
111
+ /** Ratchet keys in `src/config/gate-violation-baselines.json`. */
112
+ export const WAKE_GATE = 'lint_roadmap_later_disposition:wake-condition';
113
+ export const REVIEW_BY_GATE = 'lint_roadmap_later_disposition:review-by';
114
+
115
+ /** The three parts a structured `entry_condition` must name. */
116
+ export const ENTRY_CONDITION_PARTS = ['what', 'when', 'who'] as const;
69
117
 
70
118
  // Non-roadmap files that live in the tree but are not roadmaps.
71
119
  const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
@@ -79,6 +127,67 @@ const EXCLUDE_PREFIXES: readonly string[] = ['open-questions'];
79
127
  interface Violation {
80
128
  file: string;
81
129
  reason: string;
130
+ /**
131
+ * `hard` fails immediately; `wake` and `review-by` are ratcheted against a
132
+ * committed baseline. A hard finding is one the AUTHOR introduced (a
133
+ * malformed structured field, a misplaced `status: later`); a ratcheted one
134
+ * is debt the corrected gate revealed.
135
+ */
136
+ cls: 'hard' | 'wake' | 'review-by';
137
+ }
138
+
139
+ /**
140
+ * The three-part reading of a structured `entry_condition`.
141
+ *
142
+ * Deliberately parsed from the raw frontmatter text rather than through a YAML
143
+ * loader: this gate's contract is byte-pinned against a retired Python
144
+ * implementation and must not acquire a parser dependency whose error
145
+ * behavior differs. The shape is fixed and shallow, so a line reader is
146
+ * sufficient and its failure mode is legible.
147
+ */
148
+ export function entryConditionParts(frontmatter: string): {
149
+ present: boolean;
150
+ scalar: boolean;
151
+ parts: Record<string, string>;
152
+ } {
153
+ const lines = frontmatter.split('\n');
154
+ const idx = lines.findIndex((l) => /^entry_condition:/.test(l));
155
+ if (idx === -1) {
156
+ return { present: false, scalar: false, parts: {} };
157
+ }
158
+ const head = (lines[idx] as string).slice('entry_condition:'.length).trim();
159
+ if (head !== '') {
160
+ // `entry_condition: "…"` — the pre-contract scalar form.
161
+ return { present: true, scalar: true, parts: {} };
162
+ }
163
+ const parts: Record<string, string> = {};
164
+ for (let i = idx + 1; i < lines.length; i++) {
165
+ const line = lines[i] as string;
166
+ if (!/^\s+\S/.test(line)) break;
167
+ const m = /^\s+([a-z_]+):\s*(.*)$/.exec(line);
168
+ if (m === null) continue;
169
+ parts[m[1] as string] = _unquote((m[2] as string).trim());
170
+ }
171
+ return { present: true, scalar: false, parts };
172
+ }
173
+
174
+ function _unquote(v: string): string {
175
+ if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
176
+ return v.slice(1, -1).trim();
177
+ }
178
+ return v.trim();
179
+ }
180
+
181
+ /** Missing / empty part names, `[]` when the field satisfies the contract. */
182
+ export function entryConditionProblems(frontmatter: string): string[] {
183
+ const read = entryConditionParts(frontmatter);
184
+ if (!read.present) return ['absent'];
185
+ if (read.scalar) return ['scalar'];
186
+ const missing: string[] = [];
187
+ for (const part of ENTRY_CONDITION_PARTS) {
188
+ if ((read.parts[part] ?? '').trim() === '') missing.push(part);
189
+ }
190
+ return missing;
82
191
  }
83
192
 
84
193
  function _is_roadmap(p: string): boolean {
@@ -167,6 +276,7 @@ function check(root: string): Violation[] {
167
276
  if (status === 'later' && !in_later) {
168
277
  out.push({
169
278
  file: rel,
279
+ cls: 'hard',
170
280
  reason:
171
281
  'frontmatter `status: later` but file is not under ' +
172
282
  '`agents/roadmaps/later/` — a blocked-for-later roadmap ' +
@@ -175,19 +285,57 @@ function check(root: string): Violation[] {
175
285
  });
176
286
  }
177
287
 
178
- // Rule B — every later/ roadmap records a resume condition.
179
- if (in_later) {
180
- const body = text.slice(_frontmatter(text).length);
181
- if (status !== 'later' && !RESUME_RE.test(body)) {
182
- out.push({
183
- file: rel,
184
- reason:
185
- 'roadmap under `agents/roadmaps/later/` has no resume ' +
186
- 'condition — add `status: later` to the frontmatter or a ' +
187
- '`Blocked until` / `Resume when` / `Trigger` line so it ' +
188
- 'records when the work comes back.',
189
- });
190
- }
288
+ if (!in_later) {
289
+ continue;
290
+ }
291
+
292
+ const fm = _frontmatter(text);
293
+ const body = text.slice(fm.length);
294
+ const problems = entryConditionProblems(fm);
295
+ const structured = problems.length === 0;
296
+
297
+ // Rule B(1) — a PRESENT entry_condition must satisfy the contract.
298
+ // Hard, never ratcheted: a malformed field is authored, not inherited.
299
+ if (!structured && problems[0] !== 'absent') {
300
+ out.push({
301
+ file: rel,
302
+ cls: 'hard',
303
+ reason:
304
+ problems[0] === 'scalar'
305
+ ? '`entry_condition` is a scalar string — it must be a mapping naming ' +
306
+ '`what` (what would change the decision), `when` (when it could ' +
307
+ 'arrive) and `who` (who would have to act; `none` is a legal answer).'
308
+ : '`entry_condition` is missing or blank in: ' +
309
+ problems.join(', ') +
310
+ '. A condition that cannot be observed is a deferral wearing a ' +
311
+ "condition's clothes; `none` is a legal answer for `who` and " +
312
+ 'blankness is not.',
313
+ });
314
+ }
315
+
316
+ // Rule B(2) — a wake condition is required REGARDLESS of `status`.
317
+ if (!structured && !RESUME_RE.test(body)) {
318
+ out.push({
319
+ file: rel,
320
+ cls: 'wake',
321
+ reason:
322
+ 'roadmap under `agents/roadmaps/later/` has no wake condition — the ' +
323
+ 'frontmatter `status` word no longer satisfies this gate, and neither ' +
324
+ 'does the bare word `trigger`. Add a structured `entry_condition` ' +
325
+ '(`what` / `when` / `who`) or a `Blocked until` / `Resume when` line.',
326
+ });
327
+ }
328
+
329
+ // Rule C — review_by in the frontmatter of every parked roadmap.
330
+ if (!/^review_by:\s*\S/m.test(fm)) {
331
+ out.push({
332
+ file: rel,
333
+ cls: 'review-by',
334
+ reason:
335
+ 'roadmap under `agents/roadmaps/later/` has no `review_by:` in its ' +
336
+ 'frontmatter — a park with no review date is indistinguishable from ' +
337
+ 'an abandonment.',
338
+ });
191
339
  }
192
340
  }
193
341
  return out;
@@ -337,24 +485,65 @@ function main(argv?: readonly string[]): number {
337
485
  }
338
486
 
339
487
  const violations = check(ROADMAP_ROOT);
488
+ const hard = violations.filter((v) => v.cls === 'hard');
489
+ const wake = violations.filter((v) => v.cls === 'wake');
490
+ const reviewBy = violations.filter((v) => v.cls === 'review-by');
340
491
 
341
492
  if (args.json) {
342
- // json.dumps([asdict(v) for v in violations], indent=2)
343
- const payload = violations.map((v) => ({ file: v.file, reason: v.reason }));
493
+ // The `file` / `reason` pair is the pinned shape; `cls` is additive, so
494
+ // a consumer reading the two original keys is unaffected.
495
+ const payload = violations.map((v) => ({ file: v.file, reason: v.reason, cls: v.cls }));
344
496
  process.stdout.write(_jsonDumpsIndent2(payload) + '\n');
345
- } else if (violations.length) {
497
+ // A JSON consumer wants the findings, not the ratchet verdict; the exit
498
+ // code still carries it.
499
+ return _verdict(hard, wake, reviewBy, true);
500
+ }
501
+
502
+ if (hard.length) {
346
503
  process.stdout.write('❌ later/ disposition violations:\n');
347
- for (const v of violations) {
504
+ for (const v of hard) {
348
505
  process.stdout.write(` • ${v.file}\n ${v.reason}\n`);
349
506
  }
350
- } else {
507
+ }
508
+ return _verdict(hard, wake, reviewBy, false);
509
+ }
510
+
511
+ /**
512
+ * Ratcheted verdict. The two revealed-debt classes print their findings either
513
+ * way — at baseline they are the backlog this gate is walking down, and
514
+ * printing them is what makes the next lowering possible.
515
+ */
516
+ function _verdict(
517
+ hard: readonly Violation[],
518
+ wake: readonly Violation[],
519
+ reviewBy: readonly Violation[],
520
+ quiet: boolean,
521
+ ): number {
522
+ let failed = hard.length > 0;
523
+ for (const [gate, found] of [
524
+ [WAKE_GATE, wake],
525
+ [REVIEW_BY_GATE, reviewBy],
526
+ ] as const) {
527
+ const verdict = checkRatchet({ gate, actual: found.length, repoRoot: REPO_ROOT });
528
+ if (!verdict.ok) {
529
+ failed = true;
530
+ if (!quiet) {
531
+ for (const v of found) {
532
+ process.stderr.write(` • ${v.file}\n ${v.reason}\n`);
533
+ }
534
+ process.stderr.write(`❌ ${verdict.message}\n`);
535
+ }
536
+ } else if (!quiet) {
537
+ process.stdout.write(`✅ ${verdict.message}\n`);
538
+ }
539
+ }
540
+ if (!failed && !quiet) {
351
541
  process.stdout.write(
352
542
  '✅ later/ disposition: every blocked-for-later roadmap is parked ' +
353
- 'correctly with a resume condition.\n',
543
+ 'correctly, and both wake-condition ratchets hold.\n',
354
544
  );
355
545
  }
356
-
357
- return violations.length ? 1 : 0;
546
+ return failed ? 1 : 0;
358
547
  }
359
548
 
360
549
  function _isCliEntry(): boolean {
@@ -386,6 +575,7 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
386
575
  export {
387
576
  REPO_ROOT,
388
577
  ROADMAP_ROOT,
578
+ type Violation,
389
579
  LATER_DIR,
390
580
  FRONTMATTER_RE,
391
581
  STATUS_RE,
@@ -138,9 +138,19 @@ export function grantSurface(rel: string): GrantSurface {
138
138
  }
139
139
 
140
140
  export function _scan(sf: sl.ScannedFile): sl.Finding[] {
141
- if (sf.pragma_allows(CHECK)) {
142
- return [];
141
+ // A BOUND pragma suppresses only the matches it fingerprints, so the scan
142
+ // runs and its output is filtered at the end. An UNBOUND one keeps the old
143
+ // whole-file bail and reports itself.
144
+ const pragmaForm = sf.pragma_form(CHECK);
145
+ if (pragmaForm === 'legacy') {
146
+ return [sl.legacy_pragma_finding(sf, CHECK)];
143
147
  }
148
+ // EVERY exit from this function goes through `finish`. Wrapping only the
149
+ // terminal `return` is the defect that shipped for one commit here: this
150
+ // scan has an early return for an artifact with no `execution:` block, and a
151
+ // bound pragma silently did not apply to exactly those artifacts.
152
+ const finish = (hits: sl.Finding[]): sl.Finding[] =>
153
+ pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
144
154
  const fm = _frontmatter(sf);
145
155
  if (!fm) {
146
156
  return [];
@@ -261,7 +271,7 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
261
271
  }
262
272
 
263
273
  if (Object.keys(ex).length === 0) {
264
- return out;
274
+ return finish(out);
265
275
  }
266
276
  const etype = _stripQuotes((ex['type'] ?? [0, ''])[1]);
267
277
  const safety = _stripQuotes((ex['safety_mode'] ?? [0, ''])[1]);
@@ -331,15 +341,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
331
341
  ),
332
342
  );
333
343
  }
334
- return out;
344
+ return finish(out);
335
345
  }
336
346
 
337
347
  interface Args {
348
+ /** Scan base for the bounded root mode; absent = the package root. */
349
+ root?: string;
338
350
  json: boolean;
339
351
  }
340
352
 
341
353
  function _argError(msg: string): never {
342
- process.stderr.write('usage: lint_skill_frontmatter_safety [-h] [--json]\n');
354
+ process.stderr.write('usage: lint_skill_frontmatter_safety [-h] [--json] [--root DIR]\n');
343
355
  process.stderr.write(`lint_skill_frontmatter_safety: error: ${msg}\n`);
344
356
  process.exit(2);
345
357
  }
@@ -347,12 +359,28 @@ function _argError(msg: string): never {
347
359
  function parse_args(argv: readonly string[]): Args {
348
360
  const out: Args = { json: false };
349
361
  const extra: string[] = [];
362
+ let rootPending = false;
350
363
  for (const a of argv) {
364
+ if (rootPending) {
365
+ out.root = a;
366
+ rootPending = false;
367
+ continue;
368
+ }
351
369
  if (a === '-h' || a === '--help') {
352
- process.stdout.write('usage: lint_skill_frontmatter_safety [-h] [--json]\n');
370
+ process.stdout.write('usage: lint_skill_frontmatter_safety [-h] [--json] [--root DIR]\n');
353
371
  process.exit(0);
354
372
  } else if (a === '--json') {
355
373
  out.json = true;
374
+ } else if (a === '--root' || a.startsWith('--root=')) {
375
+ // Bounded root mode. Absent, this linter's own default roots stand;
376
+ // present, the same relative roots resolve against DIR instead. Used by
377
+ // the scout to scan a quarantined candidate, and by --self-test fixtures.
378
+ const eq = a.indexOf('=');
379
+ if (eq !== -1) {
380
+ out.root = a.slice(eq + 1);
381
+ } else {
382
+ rootPending = true;
383
+ }
356
384
  } else {
357
385
  extra.push(a);
358
386
  }
@@ -365,6 +393,7 @@ function parse_args(argv: readonly string[]): Args {
365
393
 
366
394
  export function main(argv: readonly string[] | null = null): number {
367
395
  const args = parse_args(argv ?? process.argv.slice(2));
396
+ const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
368
397
 
369
398
  const findings: sl.Finding[] = [];
370
399
  // `src/subagents` is the fourth root, added because its omission is what made
@@ -372,7 +401,7 @@ export function main(argv: readonly string[] | null = null): number {
372
401
  // bare shell lives there, and the gate had never read it.
373
402
  const roots = ['src/skills', 'src/agent-src', 'src/domains', 'src/subagents'];
374
403
  let scanned = 0;
375
- for (const sf of sl.iter_corpus(roots, ['.md'])) {
404
+ for (const sf of sl.iter_corpus(roots, ['.md'], scanBase)) {
376
405
  scanned += 1;
377
406
  for (const h of _scan(sf)) {
378
407
  findings.push(h);
@@ -400,6 +429,13 @@ export function main(argv: readonly string[] | null = null): number {
400
429
  throw exc;
401
430
  }
402
431
 
432
+ // Publish what this child inspected so the umbrella can aggregate a real
433
+ // corpus size rather than a count of children. Same number the assertion
434
+ // above just accepted, so the published figure cannot drift from it.
435
+ if (args.json) {
436
+ sl.report_child_scanned(scanned);
437
+ }
438
+
403
439
  if (args.json) {
404
440
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
405
441
  process.stdout.write(payload + '\n');
@@ -41,6 +41,10 @@ export interface Resource {
41
41
  readonly source: string;
42
42
  readonly mime_type: string;
43
43
  readonly kind: ResourceKind;
44
+ /** Frontmatter `type:` (`always` / `auto` / `manual`) where the asset has one. */
45
+ readonly rule_type?: string;
46
+ /** File mtime, ISO-8601, for the protocol's `lastModified` annotation. */
47
+ readonly last_modified?: string;
44
48
  }
45
49
 
46
50
  // Python: re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
@@ -160,9 +164,20 @@ function _load(
160
164
  source: meta.source ?? 'package',
161
165
  mime_type: MIME_MARKDOWN,
162
166
  kind,
167
+ ...(meta.type === undefined ? {} : { rule_type: meta.type.trim() }),
168
+ ...(_mtimeIso(p) === null ? {} : { last_modified: _mtimeIso(p) as string }),
163
169
  };
164
170
  }
165
171
 
172
+ /** File mtime as ISO-8601, or `null` when it cannot be read. */
173
+ function _mtimeIso(p: string): string | null {
174
+ try {
175
+ return fs.statSync(p).mtime.toISOString();
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
166
181
  export function scan_rules(root?: string | null): [Resource[], string[]] {
167
182
  const base = root ?? _project_root();
168
183
  const rules_root = path.join(base, 'dist/agent-src', 'rules');
@@ -256,6 +271,25 @@ function _strCmp(a: string, b: string): number {
256
271
  return 0;
257
272
  }
258
273
 
274
+ /**
275
+ * Priority in the protocol's 0..1 range.
276
+ *
277
+ * The roadmap that asked for this said to derive it "from the existing `tier`".
278
+ * There is no `tier` on a Resource — measured, not assumed — so it derives from
279
+ * what does exist: the frontmatter `type:` an always-loaded rule carries, then
280
+ * the asset kind. A rule the host loads on every turn outranks one it loads on
281
+ * a trigger, which outranks reference material.
282
+ *
283
+ * PROTOCOL CONFORMANCE ONLY. No shipped host is known to consume `priority` for
284
+ * selection, and this function must not be cited as evidence that one does.
285
+ */
286
+ export function resourcePriority(resource: Resource): number {
287
+ if (resource.rule_type === 'always') return 1;
288
+ if (resource.rule_type === 'auto') return 0.7;
289
+ if (resource.rule_type === 'manual') return 0.3;
290
+ return resource.kind === 'rule' ? 0.7 : resource.kind === 'context' ? 0.5 : 0.3;
291
+ }
292
+
259
293
  /** Project a Resource into MCP `Resource` constructor kwargs. */
260
294
  export function to_mcp_resource_meta(resource: Resource): Record<string, unknown> {
261
295
  return {
@@ -263,6 +297,13 @@ export function to_mcp_resource_meta(resource: Resource): Record<string, unknown
263
297
  name: resource.name,
264
298
  description: resource.description,
265
299
  mimeType: resource.mime_type,
300
+ // `audience` is the model, not the end user: these are governance
301
+ // assets a host injects, never something a person reads in a client.
302
+ audience: ['assistant'],
303
+ priority: resourcePriority(resource),
304
+ ...(resource.last_modified === undefined
305
+ ? {}
306
+ : { lastModified: resource.last_modified }),
266
307
  _meta: { source: resource.source, kind: resource.kind },
267
308
  };
268
309
  }
@@ -3,7 +3,9 @@
3
3
  // Per `agents/roadmaps/archive/road-to-mcp-full-coverage.md` §Phase 1 J4 +
4
4
  // `docs/contracts/mcp-tool-stub-envelope.md`, both transports log every
5
5
  // `tools/call` with `{tool_name, client_id_hash, ts, transport,
6
- // outcome}`. Payload bodies are never logged; the client identifier is
6
+ // outcome}`. The stdio-lite surface (`src/cli/mcp/`) appends a sixth,
7
+ // optional `host` field; callers that do not know their client omit it and
8
+ // their records stay byte-identical to the frozen five-field envelope. Payload bodies are never logged; the client identifier is
7
9
  // hashed at the server boundary so the queryable store never sees raw
8
10
  // identity.
9
11
  //
@@ -107,15 +109,26 @@ export function build_record(options: {
107
109
  transport: string;
108
110
  client_id_hash_value?: string | null | undefined;
109
111
  ts?: string | null | undefined;
112
+ host?: string | null | undefined;
110
113
  }): Record<string, unknown> {
111
- const { tool_name, outcome, transport, client_id_hash_value, ts } = options;
112
- return {
114
+ const { tool_name, outcome, transport, client_id_hash_value, ts, host } = options;
115
+ const record: Record<string, unknown> = {
113
116
  tool_name,
114
117
  client_id_hash: client_id_hash_value || hash_client_id(),
115
118
  ts: ts || _now_iso(),
116
119
  transport,
117
120
  outcome,
118
121
  };
122
+ // `host` is APPENDED, and only when a caller supplies one, so a record
123
+ // written by a caller that does not know its client stays byte-identical
124
+ // to the five-field envelope the stub contract froze. It is a closed
125
+ // vocabulary resolved at the boundary (see `src/cli/mcp/telemetry.ts`),
126
+ // never a free-form string: a record type that cannot hold arbitrary text
127
+ // has no scrubber to fail.
128
+ if (typeof host === 'string' && host !== '') {
129
+ record.host = host;
130
+ }
131
+ return record;
119
132
  }
120
133
 
121
134
  /**
@@ -132,13 +145,15 @@ export function record_call(options: {
132
145
  transport: string;
133
146
  consumer_root?: string | null | undefined;
134
147
  client_id_hash_value?: string | null | undefined;
148
+ host?: string | null | undefined;
135
149
  }): Record<string, unknown> | null {
136
- const { tool_name, outcome, transport, consumer_root, client_id_hash_value } = options;
150
+ const { tool_name, outcome, transport, consumer_root, client_id_hash_value, host } = options;
137
151
  const record = build_record({
138
152
  tool_name,
139
153
  outcome,
140
154
  transport,
141
155
  client_id_hash_value,
156
+ host,
142
157
  });
143
158
  const target = _resolve_log_path(consumer_root ?? undefined);
144
159
  try {
@@ -73,7 +73,7 @@ export const ELIGIBLE: Record<string, [string, string]> = {
73
73
  'audit my agent docs, check the state of the agents/ directory',
74
74
  'stale files under agents/ or recent edits to .augment/ without doc updates',
75
75
  ],
76
- 'analyze-reference-repo': [
76
+ 'analyze-repo': [
77
77
  "look at how X does this, compare with that other repo, study this competitor's approach",
78
78
  'external repo URL or path mentioned in the prompt',
79
79
  ],
@@ -10,7 +10,7 @@
10
10
  * ./scripts-run src/scripts/orchestration_record \
11
11
  * --spawn-count 1 --token-delta -72000 --provenance measured \
12
12
  * --tier-chosen lite --tier-source inferred --task-class read-only-fanout \
13
- * [--tiers sonnet,opus] [--agent-combo implementer,judge] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
13
+ * [--rules-applied a,b] [--tiers sonnet,opus] [--agent-combo implementer,judge] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
14
14
  * [--first-pass-success true|false] [--escalated true|false] \
15
15
  * [--init-tokens 1200] [--payload-hash <hex8-64>] [--lookup-class definition|references|string-existence|report-run] \
16
16
  * [--route-taken primitive|subagent|ask] [--budget-hit true|false] [--correctness-match true|false] \
@@ -141,6 +141,17 @@ export function main(argv: string[] = process.argv.slice(2)): number {
141
141
  if (raw === undefined) return undefined;
142
142
  return raw.split(',').map((v) => v.trim()).filter(Boolean);
143
143
  })(),
144
+ // `--rules-applied=a,b` records the rules the run actually carried.
145
+ // Unlike `--skills-applied`, an OMITTED flag and an empty value mean
146
+ // the same thing here: audit-log-v1 makes `rules_applied` mandatory on
147
+ // every line, so `[]` is how "nothing observed" is spelled, and there
148
+ // is no absent-key slot to distinguish it from. What this flag replaced
149
+ // is the fabricated `['delegation-policy']` this producer wrote on
150
+ // every line until 2026-09-07.
151
+ rules_applied: str(flags, 'rules-applied')
152
+ ?.split(',')
153
+ .map((v) => v.trim())
154
+ .filter(Boolean),
144
155
  task_size_estimate: int(flags, 'task-size-estimate'),
145
156
  wall_clock_ms: int(flags, 'wall-clock-ms'),
146
157
  dispatch_tokens: int(flags, 'dispatch-tokens'),