@event4u/agent-config 7.0.2 → 7.1.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 (101) hide show
  1. package/.claude-plugin/marketplace.json +8 -1
  2. package/CHANGELOG.md +78 -0
  3. package/README.md +4 -2
  4. package/dist/agent-src/commands/council/default.md +28 -20
  5. package/dist/agent-src/commands/evals/fix.json +5 -1
  6. package/dist/agent-src/commands/fix/comments.md +101 -0
  7. package/dist/agent-src/commands/fix.md +5 -2
  8. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +65 -0
  9. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +68 -0
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +68 -0
  11. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +62 -0
  12. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +59 -0
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +8 -3
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +56 -0
  15. package/dist/agent-src/contexts/execution/subagent-routing.md +57 -0
  16. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-steering.md +71 -0
  18. package/dist/agent-src/contexts/execution/verify-budget.md +50 -0
  19. package/dist/agent-src/rules/delegation-policy.md +84 -0
  20. package/dist/agent-src/rules/design-fidelity.md +98 -0
  21. package/dist/agent-src/rules/domain-safety-pii.md +22 -0
  22. package/dist/agent-src/rules/legal-safety-floor.md +144 -0
  23. package/dist/agent-src/rules/lethal-trifecta-guard.md +25 -22
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +102 -15
  25. package/dist/agent-src/scripts/update_roadmap_progress.ts +30 -3
  26. package/dist/agent-src/skills/ai-council/SKILL.md +28 -1
  27. package/dist/agent-src/skills/contract-review/SKILL.md +124 -0
  28. package/dist/agent-src/skills/contract-review/evals/triggers.json +17 -0
  29. package/dist/agent-src/skills/dpa-review/SKILL.md +129 -0
  30. package/dist/agent-src/skills/dpa-review/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/laravel/SKILL.md +1 -0
  32. package/dist/agent-src/skills/legal-intake-triage/SKILL.md +127 -0
  33. package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +17 -0
  34. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +100 -0
  35. package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +17 -0
  36. package/dist/agent-src/skills/nda-triage/SKILL.md +113 -0
  37. package/dist/agent-src/skills/nda-triage/evals/triggers.json +17 -0
  38. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +3 -1
  39. package/dist/agent-src/skills/recursive-verification/SKILL.md +186 -0
  40. package/dist/agent-src/skills/roadmap-management/SKILL.md +17 -0
  41. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -0
  42. package/dist/agent-src/templates/agent-settings.md +64 -19
  43. package/dist/discovery/deprecation-report.md +1 -1
  44. package/dist/discovery/discovery-manifest.json +278 -23
  45. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  46. package/dist/discovery/discovery-manifest.summary.md +12 -4
  47. package/dist/discovery/orphan-report.md +1 -1
  48. package/dist/discovery/packs.json +50 -8
  49. package/dist/discovery/trust-report.md +7 -5
  50. package/dist/discovery/workspaces.json +34 -6
  51. package/dist/mcp/registry-manifest.json +3 -3
  52. package/dist/router.json +1 -1
  53. package/dist/server/schemas/settings.js +14 -0
  54. package/dist/server/schemas/settings.js.map +1 -1
  55. package/docs/architecture.md +4 -4
  56. package/docs/benchmark.md +46 -0
  57. package/docs/catalog.md +18 -6
  58. package/docs/command-flows.md +7 -1
  59. package/docs/contracts/ai-council-config.md +17 -10
  60. package/docs/contracts/audit-log-v1.md +7 -0
  61. package/docs/contracts/auto-orchestration-v1.md +70 -0
  62. package/docs/contracts/command-clusters.md +1 -1
  63. package/docs/contracts/discovery-manifest.schema.json +2 -2
  64. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +19 -0
  65. package/docs/decisions/ADR-020-global-only-consumer-scope.md +13 -6
  66. package/docs/decisions/ADR-049-configuration-trust-boundary.md +11 -1
  67. package/docs/decisions/ADR-093-ai-council-config-user-global.md +10 -4
  68. package/docs/decisions/ADR-104-ai-council-config-global-only.md +119 -0
  69. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +102 -0
  70. package/docs/decisions/ADR-106-recursive-verification-benchmark-gate.md +126 -0
  71. package/docs/decisions/ADR-107-legal-domain-pack-adoption.md +51 -0
  72. package/docs/decisions/ADR-108-open-source-forever-no-commercial-tier.md +46 -0
  73. package/docs/decisions/INDEX.md +5 -0
  74. package/docs/getting-started.md +2 -2
  75. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
  76. package/docs/guidelines/agent-infra/domain-pack-architecture.md +55 -0
  77. package/package.json +1 -1
  78. package/src/config/agent-settings.template.yml +72 -3
  79. package/src/config/discovery/packs.yml +13 -0
  80. package/src/config/discovery/workspaces.yml +7 -0
  81. package/src/scripts/_lib/auto_dispatch.ts +95 -0
  82. package/src/scripts/_lib/host_capability.ts +59 -0
  83. package/src/scripts/_lib/orchestration_gate.ts +98 -0
  84. package/src/scripts/_lib/subagent_bundle.ts +154 -0
  85. package/src/scripts/_lib/subagent_response.ts +82 -0
  86. package/src/scripts/_lib/subagent_routing.ts +81 -0
  87. package/src/scripts/_lib/subagent_spawn.ts +97 -0
  88. package/src/scripts/_lib/subagent_steering.ts +64 -0
  89. package/src/scripts/_lib/verify_budget.ts +54 -0
  90. package/src/scripts/ai_council/advisors.ts +4 -2
  91. package/src/scripts/ai_council/clients.ts +7 -1
  92. package/src/scripts/ai_council/config.ts +22 -17
  93. package/src/scripts/bench_ab_v2_run.ts +127 -0
  94. package/src/scripts/bench_ab_v2_stats.ts +39 -0
  95. package/src/scripts/check_council_config_location.ts +6 -5
  96. package/src/scripts/lint_discovery_vocabulary.ts +6 -0
  97. package/src/scripts/lint_framework_leakage_allowlist.json +21 -0
  98. package/src/scripts/lint_legal_pack.ts +139 -0
  99. package/src/scripts/probe_projection_fidelity.ts +7 -1
  100. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  101. package/src/scripts/skill_linter.ts +3 -0
@@ -67,14 +67,20 @@ type Dict = Record<string, unknown>;
67
67
  interface ArmSpec {
68
68
  setting_sources: string | null;
69
69
  inject: string | null; // None | "rdp" | "placebo"
70
+ recursive?: boolean; // ADR-106 D₂ arm: depth-bounded attempt→critic→re-attempt loop
70
71
  }
71
72
 
72
73
  // Arm -> (setting_sources, inject) where inject ∈ {None, "rdp", "placebo"}.
74
+ // `package-recursive` (ADR-106's D₂) reuses the real-plugin `package` config but
75
+ // runs the recursion loop instead of a single --print. It only executes when
76
+ // explicitly selected (`--arms package-recursive`); the four default arms are
77
+ // untouched, so existing runs and their golden-parity outputs are unchanged.
73
78
  const ARMS: Record<string, ArmSpec> = {
74
79
  vanilla: { setting_sources: 'project,local', inject: null },
75
80
  package: { setting_sources: null, inject: null },
76
81
  'package-rdp': { setting_sources: null, inject: 'rdp' },
77
82
  placebo: { setting_sources: 'project,local', inject: 'placebo' },
83
+ 'package-recursive': { setting_sources: null, inject: null, recursive: true },
78
84
  };
79
85
 
80
86
  /**
@@ -165,10 +171,14 @@ interface RunOneOpts {
165
171
  timeout: number;
166
172
  placebo_chars: number;
167
173
  sp_dir: string;
174
+ max_depth?: number | null; // recursion arm only — hard cap on correction rounds (default 1)
168
175
  }
169
176
 
170
177
  export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
171
178
  const spec = ARMS[arm] as ArmSpec;
179
+ if (spec.recursive) {
180
+ return run_one_recursive(task, opts);
181
+ }
172
182
  const [clone, fixture] = reset_fixture(task);
173
183
  const sp_text = injected_text(spec.inject, opts.placebo_chars);
174
184
  let sp_file: string | null = null;
@@ -198,6 +208,123 @@ export function run_one(task: Dict, arm: string, opts: RunOneOpts): Dict {
198
208
  };
199
209
  }
200
210
 
211
+ export interface RecursiveAttempt {
212
+ run: Dict;
213
+ score: scoring.ScoreResultV2;
214
+ output?: string; // pair-capture: changed-file contents at this depth (for human-preference judging)
215
+ }
216
+
217
+ /**
218
+ * ADR-106 D₂ arm — depth-bounded recursive self-verification.
219
+ *
220
+ * Loop: attempt → critic verdict → conditional re-attempt, depth = hard compute
221
+ * cap. The critic is the DETERMINISTIC v2 scorer (accept iff the attempt is both
222
+ * capability- AND discipline-passing) — cheapest critic, no extra model call,
223
+ * fully testable. A model-based / cross-vendor critic is a later option (see
224
+ * road-to-recursive-verification.md Phase 4).
225
+ *
226
+ * `attemptFn(depth, priorVerdict)` is an injectable seam: the default runs the
227
+ * real `package`-config agent (prior verdict threaded as the system prompt on
228
+ * re-attempts) and scores it; tests inject a scripted attemptFn to exercise the
229
+ * loop control flow with NO live model call.
230
+ *
231
+ * Verifies the loop's CONTROL FLOW (stop conditions, depth cap, verdict
232
+ * threading). Whether recursion actually lifts capability/discipline is the
233
+ * empirical question the live `bench:ab` run answers — not asserted here.
234
+ */
235
+ export function run_one_recursive(
236
+ task: Dict,
237
+ opts: RunOneOpts,
238
+ attemptFn?: (depth: number, priorVerdict: string | null) => RecursiveAttempt,
239
+ onAttempt?: (depth: number, attempt: RecursiveAttempt) => void,
240
+ ): Dict {
241
+ const maxDepth = Math.max(0, opts.max_depth ?? 1);
242
+ const makeAttempt =
243
+ attemptFn ??
244
+ ((depth: number, priorVerdict: string | null): RecursiveAttempt => {
245
+ const [clone, fixture] = reset_fixture(task);
246
+ let sp_file: string | null = null;
247
+ if (priorVerdict) {
248
+ sp_file = path.join(opts.sp_dir, `.sp-recursive-${depth}.txt`);
249
+ fs.writeFileSync(sp_file, priorVerdict, { encoding: 'utf-8' });
250
+ }
251
+ const run = v1.run_live(task, clone, opts.timeout, {
252
+ sysprompt_file: sp_file,
253
+ setting_sources: (ARMS['package'] as ArmSpec).setting_sources,
254
+ max_budget: opts.max_budget,
255
+ model: opts.model,
256
+ }) as unknown as Dict;
257
+ const score = scoring.score_task_v2(task, {
258
+ fixture_root: fixture,
259
+ clone_root: clone,
260
+ transcript: String(run['transcript'] ?? ''),
261
+ });
262
+ // Capture the attempt's output (changed-file contents) for the
263
+ // human-preference pair, before the next depth resets the clone.
264
+ const output = score.files_changed
265
+ .map((f: string) => {
266
+ try {
267
+ return `--- ${f} ---\n${fs.readFileSync(path.join(clone, f), 'utf-8')}`;
268
+ } catch {
269
+ return `--- ${f} --- (unreadable)`;
270
+ }
271
+ })
272
+ .join('\n\n')
273
+ .slice(0, 4000);
274
+ return { run, score, output };
275
+ });
276
+
277
+ const accepts = (s: scoring.ScoreResultV2): boolean => Boolean(s.capability_pass) && Boolean(s.discipline_pass);
278
+
279
+ let depth = 0;
280
+ let prior: RecursiveAttempt | null = null;
281
+ let attempt = makeAttempt(0, null);
282
+ onAttempt?.(0, attempt);
283
+ let stop_reason: string;
284
+ let last_verdict_len = 0;
285
+ for (;;) {
286
+ const s = attempt.score;
287
+ if (accepts(s)) {
288
+ stop_reason = 'accept';
289
+ break;
290
+ }
291
+ if (depth >= maxDepth) {
292
+ stop_reason = 'max_depth';
293
+ break;
294
+ }
295
+ if (
296
+ prior !== null &&
297
+ s.discipline_score === prior.score.discipline_score &&
298
+ Boolean(s.capability_pass) === Boolean(prior.score.capability_pass)
299
+ ) {
300
+ stop_reason = 'no_progress';
301
+ break;
302
+ }
303
+ depth += 1;
304
+ const verdict =
305
+ `Prior attempt (depth ${depth - 1}) did not pass: ` +
306
+ `capability=${s.capability_pass}, discipline=${s.discipline_score}. ` +
307
+ `Revise to satisfy the unmet acceptance criteria; change only what the task requires.`;
308
+ last_verdict_len = verdict.length;
309
+ prior = attempt;
310
+ attempt = makeAttempt(depth, verdict);
311
+ onAttempt?.(depth, attempt);
312
+ }
313
+
314
+ const { run, score } = attempt;
315
+ return {
316
+ errored: _pyBool(run['errored']),
317
+ reason: run['reason'] ?? null,
318
+ capability_pass: score.capability_pass,
319
+ discipline_score: new PyFloat(score.discipline_score),
320
+ discipline_pass: score.discipline_pass,
321
+ metrics: trajectory_metrics(run, score),
322
+ injected_chars: last_verdict_len,
323
+ depth_reached: depth,
324
+ stop_reason,
325
+ };
326
+ }
327
+
201
328
  export function main(argv: string[] | null = null): number {
202
329
  const args = parse_args(argv ?? process.argv.slice(2));
203
330
 
@@ -41,6 +41,11 @@ const COMPARISONS: Array<[string, string, string]> = [
41
41
  ['package', 'vanilla', 'package lift'],
42
42
  ['package-rdp', 'package', 'RDP lift'],
43
43
  ['package', 'placebo', 'attribution (content vs length)'],
44
+ // ADR-106: recursion's NOVEL lift over rules-only (D₂ − D₁). The analyse()
45
+ // loop is arm-guarded (`arms.includes(t) && arms.includes(b)`), so this row
46
+ // renders ONLY when a `package-recursive` arm is present — existing runs
47
+ // (no such arm) are byte-identical, golden parity preserved.
48
+ ['package-recursive', 'package', 'recursion novel lift (D₂ − D₁)'],
44
49
  ];
45
50
 
46
51
  // ── CPython math.erf / math.comb ports ────────────────────────────────────
@@ -333,6 +338,40 @@ export function compare(records: Dict[], arm_t: string, arm_b: string): Dict {
333
338
  };
334
339
  }
335
340
 
341
+ /**
342
+ * Recursion's NOVEL discipline lift over rules-only (ADR-106).
343
+ *
344
+ * Compares the `package-recursive` arm (D₂ = rules + recursion) against
345
+ * `package` (D₁ = rules only) via the existing pure `compare()`, so recursion's
346
+ * marginal lift `D₂ − D₁` is a measured fact, never an assumed redundancy.
347
+ *
348
+ * ADDITIVE: does NOT touch the rendered COMPARISONS path — existing CLI output
349
+ * (and its golden-parity tests) are unaffected. Returns `null` when no
350
+ * `package-recursive` arm is present in the records, so a future renderer can
351
+ * skip the block cleanly for non-recursion runs.
352
+ *
353
+ * NOTE (Phase 2b live-integration, NOT done here): wiring this into the rendered
354
+ * stats output (a guarded COMPARISONS entry + a golden-parity fixture refresh)
355
+ * and producing the `package-recursive` arm itself (a multi-call recursion loop
356
+ * the single-`--print` runner does not yet support) require the live bench and
357
+ * are deferred to the supervised Phase 3 session.
358
+ */
359
+ export const RECURSIVE_ARM = 'package-recursive';
360
+ export const RECURSIVE_BASELINE_ARM = 'package';
361
+
362
+ export function recursiveNovelLift(records: Dict[]): Dict | null {
363
+ const hasRecursion = records.some((rec) => {
364
+ const runs = _dictOr(rec['arms'])[RECURSIVE_ARM];
365
+ return Array.isArray(runs) && runs.length > 0;
366
+ });
367
+ if (!hasRecursion) return null;
368
+ return {
369
+ label: 'recursion novel lift (D₂ − D₁)',
370
+ arms: [RECURSIVE_ARM, RECURSIVE_BASELINE_ARM],
371
+ ...compare(records, RECURSIVE_ARM, RECURSIVE_BASELINE_ARM),
372
+ };
373
+ }
374
+
336
375
  export function mean_tokens_by_arm(records: Dict[], arms: string[]): Dict {
337
376
  // Mean total tokens per arm over non-errored runs (the cost axis, L10).
338
377
  // Truncated/errored runs are excluded — their token count is capped by the
@@ -9,11 +9,12 @@
9
9
  * and the same `<!-- council-config-allowed -->` escape pragma. No behaviour
10
10
  * changes — latent bugs replicated.
11
11
  *
12
- * Per ADR-093 the council reads a dedicated `.ai-council.yml` — resolved
13
- * project-local (`agents/settings/.ai-council.yml`) first, else user-global
14
- * (`~/.event4u/agent-config/settings/.ai-council.yml`). Keys are top-level in
15
- * that file; the legacy `ai_council.*` block under `.agent-settings.yml` was
16
- * removed in Phase 0.
12
+ * Per ADR-104 (superseding ADR-093) the council reads a dedicated
13
+ * `.ai-council.yml` resolved ALWAYS from the user-global location
14
+ * (`~/.event4u/agent-config/settings/.ai-council.yml`) — the project tree
15
+ * is never searched (the only escape is the explicit `$AI_COUNCIL_CONFIG`
16
+ * path). Keys are top-level in that file; the legacy `ai_council.*` block
17
+ * under `.agent-settings.yml` was removed in Phase 0.
17
18
  *
18
19
  * What it flags, in the council command/skill surfaces + the config contract:
19
20
  *
@@ -29,6 +29,8 @@ const PACKS_YML = path.join(REPO_ROOT, 'src', 'config', 'discovery', 'packs.yml'
29
29
  const ADR_WORKSPACES: ReadonlySet<string> = new Set<string>([
30
30
  'engineering', 'product', 'finance', 'founder', 'gtm', 'ops',
31
31
  'small-business', 'construction', 'agent-config-maintainer',
32
+ // road-to-legal-pack Phase 0 (ADR-013 amendment, same PR).
33
+ 'legal',
32
34
  ]);
33
35
  const ADR_PACKS: ReadonlySet<string> = new Set<string>([
34
36
  'engineering-base', 'php', 'laravel', 'symfony', 'javascript',
@@ -40,6 +42,10 @@ const ADR_PACKS: ReadonlySet<string> = new Set<string>([
40
42
  'memory', 'analytics', 'product-reasoning',
41
43
  // road-to-image-brand-typography Phase B (ADR-013 amendment, same PR).
42
44
  'brand',
45
+ // Pre-existing drift restore: present in packs.yml but missing from this set.
46
+ 'ai-image', 'analysis-workbench',
47
+ // road-to-legal-pack Phase 0 (ADR-013 amendment, same PR).
48
+ 'legal',
43
49
  ]);
44
50
 
45
51
  // ADR-010 non-overlap reservations.
@@ -577,6 +577,27 @@
577
577
  24
578
578
  ],
579
579
  "reason": "Single Laravel-shape example for per-module agents directory (road-to-configurable-modules Phase D)"
580
+ },
581
+ {
582
+ "file": "src/skills/code-review/SKILL.md",
583
+ "lines": [
584
+ 214
585
+ ],
586
+ "reason": "Cross-stack formatter enumeration (ECS, Prettier, Ruff, gofmt) — multi-ecosystem documentation"
587
+ },
588
+ {
589
+ "file": "src/skills/context-document/SKILL.md",
590
+ "lines": [
591
+ 82
592
+ ],
593
+ "reason": "Laravel example path inside a stack-agnostic module-context table"
594
+ },
595
+ {
596
+ "file": "src/skills/iconography/SKILL.md",
597
+ "lines": [
598
+ 29
599
+ ],
600
+ "reason": "Cross-stack UI-stack enumeration (React, Blade, Tailwind)"
580
601
  }
581
602
  ]
582
603
  }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Lint the legal pack's deterministic safety-floor backstops.
4
+ *
5
+ * The `legal-safety-floor` rule is prose; this linter makes three of its
6
+ * Iron-Law elements machine-checkable over every `packs: [legal]` skill —
7
+ * so they are governance, not just prompt instructions (road-to-legal-pack
8
+ * Phase 1, the "deterministic hardening = differentiator" thesis):
9
+ *
10
+ * 1.1 disclaimer-presence — the exact attorney-review line must appear in
11
+ * the skill body (a skill that drafts legal work must instruct the model
12
+ * to emit it).
13
+ * 1.2 jurisdiction-tag — the skill must reference the machine-checkable
14
+ * `Jurisdiction:` tag (jurisdiction-honesty made deterministic).
15
+ * 1.3 freshness — any legal-pack artefact that DECLARES `last_verified` /
16
+ * `freshness_window` frontmatter must use a valid shape and not be stale
17
+ * (no-op when absent; the mechanism for when positions carry dates).
18
+ *
19
+ * Scope is the legal pack only — it never touches the other 250+ skills.
20
+ * Exit codes: 0 all legal-pack skills compliant, 1 one or more violations.
21
+ */
22
+
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import { fileURLToPath, pathToFileURL } from 'node:url';
26
+
27
+ const QUIET = process.argv.includes('--quiet');
28
+
29
+ const _HERE = fileURLToPath(import.meta.url);
30
+ const REPO = path.resolve(path.dirname(_HERE), '..', '..');
31
+ const SKILLS_DIR = path.join(REPO, 'src', 'skills');
32
+
33
+ // The exact line the floor requires in every legal-pack deliverable.
34
+ const ATTORNEY_LINE = 'Attorney review required on material use';
35
+ const JURISDICTION_TAG = 'Jurisdiction:';
36
+ const FRESHNESS_WINDOW_RE = /^\d+\s+(day|days|month|months|year|years)$/;
37
+
38
+ // D4 — definitive-legal-language blocklist (the floor bans these; this is the
39
+ // deterministic backstop over skill bodies). Narrow on purpose; the floor's
40
+ // prompt layer is the primary enforcement.
41
+ const DEFINITIVE_PATTERNS: readonly (readonly [RegExp, string])[] = [
42
+ [/\bthis (contract|nda|agreement|dpa|clause) is (valid|enforceable|compliant|legal|illegal)\b/i, 'definitive validity/compliance verdict'],
43
+ [/\b(is|are) (fully |now )?gdpr[- ]compliant\b/i, 'definitive GDPR-compliance claim'],
44
+ [/\byou are (legally )?required to\b/i, 'definitive legal obligation'],
45
+ [/\byou will (win|lose|prevail)\b/i, 'outcome prediction'],
46
+ [/\byour chances (of success )?are\b/i, 'success prognosis'],
47
+ ];
48
+ // A line is exempt when it is clearly a negative example / guidance / quote —
49
+ // so "Do NOT say 'this contract is valid'" or "Forbidden: …" never false-positive.
50
+ const NEG_EXAMPLE_RE = /(never|avoid|forbidden|do not|don'?t|instead|rather than|not say|→|⚠️)/i;
51
+
52
+ function _print(msg: string): void {
53
+ if (!QUIET) process.stdout.write(`${msg}\n`);
54
+ }
55
+
56
+ function _readText(p: string): string {
57
+ return fs.readFileSync(p, 'utf-8');
58
+ }
59
+
60
+ /** Return [frontmatterText, body] — frontmatter null when no `---` fence. */
61
+ function _splitFrontmatter(content: string): [string | null, string] {
62
+ if (!content.startsWith('---\n')) return [null, content];
63
+ const end = content.indexOf('\n---', 4);
64
+ if (end === -1) return [null, content];
65
+ return [content.slice(4, end), content.slice(end + 4)];
66
+ }
67
+
68
+ /** True when the frontmatter `packs:` block lists `legal`. */
69
+ function _isLegalPack(fm: string): boolean {
70
+ // Match a `packs:` block then a `- legal` item before the next top-level key.
71
+ const m = /(^|\n)packs:\s*\n((?:\s*-\s*[^\n]+\n?)+)/.exec(fm);
72
+ if (!m) return false;
73
+ return /(^|\n)\s*-\s*legal\s*$/m.test(m[2] ?? '');
74
+ }
75
+
76
+ interface Violation {
77
+ file: string;
78
+ rule: string;
79
+ msg: string;
80
+ }
81
+
82
+ export function lintLegalPack(skillsDir: string = SKILLS_DIR): Violation[] {
83
+ const violations: Violation[] = [];
84
+ let dirs: string[] = [];
85
+ try {
86
+ dirs = fs.readdirSync(skillsDir).sort();
87
+ } catch {
88
+ return violations;
89
+ }
90
+ for (const d of dirs) {
91
+ const file = path.join(skillsDir, d, 'SKILL.md');
92
+ if (!fs.existsSync(file)) continue;
93
+ const content = _readText(file);
94
+ const [fm, body] = _splitFrontmatter(content);
95
+ if (fm === null || !_isLegalPack(fm)) continue;
96
+ const rel = path.relative(REPO, file);
97
+
98
+ // 1.1 disclaimer-presence
99
+ if (!body.includes(ATTORNEY_LINE)) {
100
+ violations.push({ file: rel, rule: 'disclaimer-presence', msg: `missing the attorney-review line ("${ATTORNEY_LINE}")` });
101
+ }
102
+ // 1.2 jurisdiction-tag
103
+ if (!body.includes(JURISDICTION_TAG)) {
104
+ violations.push({ file: rel, rule: 'jurisdiction-tag', msg: `missing the machine-checkable "${JURISDICTION_TAG}" tag` });
105
+ }
106
+ // 1.3 freshness — only when declared
107
+ const fw = /(^|\n)freshness_window:\s*"?([^"\n]+)"?/.exec(fm);
108
+ if (fw && !FRESHNESS_WINDOW_RE.test((fw[2] ?? '').trim())) {
109
+ violations.push({ file: rel, rule: 'freshness', msg: `freshness_window "${fw[2]}" is not a valid "<N> days|months|years" shape` });
110
+ }
111
+ // 4.1 — no definitive legal language (line-scoped, skips negative examples)
112
+ for (const line of body.split('\n')) {
113
+ if (NEG_EXAMPLE_RE.test(line)) continue;
114
+ for (const [re, label] of DEFINITIVE_PATTERNS) {
115
+ if (re.test(line)) {
116
+ violations.push({ file: rel, rule: 'definitive-language', msg: `${label}: ${line.trim().slice(0, 80)}` });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return violations;
122
+ }
123
+
124
+ export function main(): number {
125
+ const violations = lintLegalPack();
126
+ if (violations.length === 0) {
127
+ _print('✅ legal pack — all legal-pack skills carry the attorney-review line + Jurisdiction tag');
128
+ return 0;
129
+ }
130
+ _print(`❌ legal pack — ${violations.length} violation(s):`);
131
+ for (const v of violations) {
132
+ _print(` - [${v.rule}] ${v.file}: ${v.msg}`);
133
+ }
134
+ return 1;
135
+ }
136
+
137
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
138
+ process.exit(main());
139
+ }
@@ -154,7 +154,13 @@ function check_entry(entry: Record<string, unknown>): EntryResult {
154
154
  for (const [tool, spec] of Object.entries(checks)) {
155
155
  const result: CheckResult = { status: 'pass', details: [] };
156
156
  const expect_present = 'present' in spec ? Boolean(spec['present']) : true;
157
- const pathLocated = locate(tool, out.type, entry['source'] as string);
157
+ // Honor an explicit per-tool `location` from the fixture when present
158
+ // (e.g. a renamed command whose projection stem differs from its source
159
+ // stem); otherwise derive the path from the artefact source.
160
+ const explicitLoc = spec['location'] as string | undefined;
161
+ const pathLocated = typeof explicitLoc === 'string'
162
+ ? (fs.existsSync(path.join(ROOT, explicitLoc)) ? path.join(ROOT, explicitLoc) : null)
163
+ : locate(tool, out.type, entry['source'] as string);
158
164
 
159
165
  if (tool === 'windsurf' && spec['concatenated_in']) {
160
166
  const fp = path.join(ROOT, spec['concatenated_in'] as string);
@@ -72,6 +72,17 @@
72
72
  "enum": ["off", "on", "ask"]
73
73
  }
74
74
  }
75
+ },
76
+ "design": {
77
+ "type": "object",
78
+ "additionalProperties": true,
79
+ "properties": {
80
+ "fidelity_mode": {
81
+ "type": "string",
82
+ "enum": ["strict", "structural", "hard-floor"],
83
+ "description": "How strictly the agent must follow a user-provided prototype / mockup / design system. Consumed by the design-fidelity rule. strict = build 1:1, every visible deviation needs confirmation; structural = structure locked, silent gaps fillable with a stated assumption; hard-floor = any deviation is never autonomous."
84
+ }
85
+ }
75
86
  }
76
87
  }
77
88
  }
@@ -1407,6 +1407,7 @@ export function lint_skill(p: string, text: string): LintResult {
1407
1407
  const skillName = basename(p) === 'SKILL.md' ? parentName(p) : stem(p);
1408
1408
  if (skillName && !skillName.includes('-') && skillName.length >= 3) {
1409
1409
  const ALLOWED_BARE_NOUNS = new Set([
1410
+ 'brand',
1410
1411
  'database',
1411
1412
  'devcontainer',
1412
1413
  'docker',
@@ -1414,12 +1415,14 @@ export function lint_skill(p: string, text: string): LintResult {
1414
1415
  'flux',
1415
1416
  'forecasting',
1416
1417
  'grafana',
1418
+ 'iconography',
1417
1419
  'laravel',
1418
1420
  'livewire',
1419
1421
  'markitdown',
1420
1422
  'mcp',
1421
1423
  'openapi',
1422
1424
  'performance',
1425
+ 'premortem',
1423
1426
  'security',
1424
1427
  'terraform',
1425
1428
  'terragrunt',