@event4u/agent-config 14.18.0 → 14.19.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 (146) 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 +169 -130
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  17. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  21. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  22. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  23. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  24. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  25. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  26. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  27. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  28. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  29. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  30. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  31. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  32. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  33. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  34. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  35. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  36. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  37. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  38. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  39. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  40. package/dist/cli/commands/recordTriggerEval.js +2 -2
  41. package/dist/cli/main.js +1 -1
  42. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  43. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  44. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  45. package/dist/cli-delegate/cmd_conformance.js +420 -16
  46. package/dist/cli-delegate/cmd_doctor.js +1 -1
  47. package/dist/cli-delegate/cmd_export.js +1 -1
  48. package/dist/cli-delegate/cmd_handoff.js +1 -1
  49. package/dist/cli-delegate/cmd_prune.js +1 -1
  50. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  51. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  52. package/dist/cli-delegate/cmd_sync.js +1 -1
  53. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  54. package/dist/cli-delegate/cmd_validate.js +1 -1
  55. package/dist/discovery/deprecation-report.md +1 -1
  56. package/dist/discovery/discovery-manifest.json +28 -27
  57. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  58. package/dist/discovery/discovery-manifest.summary.md +1 -1
  59. package/dist/discovery/dormancy-report.md +1 -1
  60. package/dist/discovery/orphan-report.md +1 -1
  61. package/dist/discovery/packs.json +2 -2
  62. package/dist/discovery/trust-report.md +1 -1
  63. package/dist/discovery/workspaces.json +2 -2
  64. package/dist/hooks/dispatch.js +2132 -1462
  65. package/dist/install/install.mjs +588 -527
  66. package/dist/mcp/registry-manifest.json +1 -1
  67. package/dist/mcp/server.json +2 -2
  68. package/dist/mcp/server.mjs +1619 -1045
  69. package/dist/scripts/_lib/model_tier.js +1 -1
  70. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  71. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  72. package/docs/guidelines/design-antipatterns.md +87 -64
  73. package/package.json +1 -1
  74. package/src/agent-src/README.md +1 -1
  75. package/src/config/ci-local-parity.yml +6 -0
  76. package/src/config/gate-coverage.yml +170 -0
  77. package/src/config/gate-violation-baselines.json +3 -3
  78. package/src/config/release-gate-locality.yml +12 -0
  79. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  80. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  81. package/src/scripts/_cli/cmd_update.ts +1 -1
  82. package/src/scripts/_cli/cmd_versions.ts +1 -1
  83. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  84. package/src/scripts/_lib/consequence_objects.ts +152 -0
  85. package/src/scripts/_lib/exec_evidence.ts +7 -0
  86. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  87. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  88. package/src/scripts/_lib/model_tier.ts +1 -1
  89. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  90. package/src/scripts/_lib/release_highlights.ts +93 -0
  91. package/src/scripts/_lib/release_material.ts +64 -1
  92. package/src/scripts/_lib/runtime_journal.ts +26 -3
  93. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  94. package/src/scripts/_lib/session_register.ts +11 -6
  95. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  96. package/src/scripts/_lib/surface.ts +86 -0
  97. package/src/scripts/_lib/ui_authority.ts +116 -0
  98. package/src/scripts/adr/regenerate_index.ts +54 -1
  99. package/src/scripts/adr_cite_check.ts +71 -15
  100. package/src/scripts/ai_council/clients.ts +1 -1
  101. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  102. package/src/scripts/build_proof.ts +15 -2
  103. package/src/scripts/check_beta_review_markers.ts +141 -17
  104. package/src/scripts/check_enforcement_coverage.ts +1 -1
  105. package/src/scripts/check_enforcement_denominator.ts +121 -11
  106. package/src/scripts/check_finding_dispositions.ts +66 -9
  107. package/src/scripts/check_gate_coverage.ts +96 -0
  108. package/src/scripts/check_release_highlights.ts +74 -1
  109. package/src/scripts/check_skill_activation_claim.ts +179 -0
  110. package/src/scripts/check_trigger_evals.ts +1 -1
  111. package/src/scripts/conformance_scan.ts +44 -3
  112. package/src/scripts/explain_run.ts +1 -1
  113. package/src/scripts/generate_capability_matrix.ts +1 -1
  114. package/src/scripts/git_authorization_hook.ts +75 -1
  115. package/src/scripts/hook_manifest.json +1 -1
  116. package/src/scripts/hook_manifest.yaml +14 -11
  117. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  118. package/src/scripts/hooks/category_a.ts +291 -0
  119. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  120. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  121. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  122. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  123. package/src/scripts/hooks/host_lowering.ts +215 -0
  124. package/src/scripts/hooks/host_lowering.yaml +185 -0
  125. package/src/scripts/hooks/host_semantics.ts +105 -37
  126. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  127. package/src/scripts/hooks/permission_gate.ts +117 -0
  128. package/src/scripts/hooks_status.ts +12 -10
  129. package/src/scripts/install.ts +53 -82
  130. package/src/scripts/install_anthropic_key.sh +2 -2
  131. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  132. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  133. package/src/scripts/lint_hook_manifest.ts +87 -3
  134. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  135. package/src/scripts/lint_readme_size.ts +158 -12
  136. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  137. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  138. package/src/scripts/measure_sibling_naming.ts +150 -0
  139. package/src/scripts/redact_hook_capture.ts +1 -1
  140. package/src/scripts/report_skill_activation.ts +319 -7
  141. package/src/scripts/schemas/review-findings.schema.json +5 -0
  142. package/src/scripts/schemas/skill.schema.json +2 -2
  143. package/src/scripts/session_canary_hook.ts +1 -1
  144. package/src/scripts/session_register_hook.ts +4 -3
  145. package/src/scripts/trigger_eval_grandfather.json +5 -1
  146. package/src/scripts/validate_agent_settings.ts +1 -1
@@ -483,3 +483,47 @@ export function readAdrAxisCells(text: string): AdrAxisCells {
483
483
  : `${strength} (${discovery})`,
484
484
  };
485
485
  }
486
+
487
+ /**
488
+ * The one shape this tool decides: a trigger whose condition IS a leading date.
489
+ *
490
+ * Deliberately anchored at the start of the string and deliberately narrow. The
491
+ * boundary is the whole safety property — the moment a trigger is matched on a
492
+ * date found anywhere in its prose, "reopens when N exceeds M" is one edit away
493
+ * and the machine-readable grammar two council seats rejected on 2026-08-19 has
494
+ * been built by increments.
495
+ */
496
+ export const DATED_TRIGGER_RE = /^\s*(?:expir(?:y|es)\b\s*[:—-]?\s*)?(\d{4})-(\d{2})-(\d{2})(?!\d)/iu;
497
+
498
+ /**
499
+ * A trigger that ANNOUNCES itself as dated, whether or not the date parses.
500
+ *
501
+ * The gap between this and `DATED_TRIGGER_RE` is the `dated-unparsed` state.
502
+ * Exactly one ADR in the corpus carries a dated expiry, so a parse tuned to its
503
+ * phrasing can fail on the second one — and the failure mode is silence,
504
+ * because the old answer for every trigger was `indeterminate`. This probe is
505
+ * what turns that silence into a reported defect.
506
+ */
507
+ export const DATED_TRIGGER_PROBE = /^\s*(?:expir(?:y|es)\b|\d{4}-\d{1,2}-\d{1,2}\b)/iu;
508
+
509
+ /** The UTC calendar day of an instant, as `YYYY-MM-DD`. */
510
+ export function iso_day(d: Date): string {
511
+ return d.toISOString().slice(0, 10);
512
+ }
513
+
514
+ /**
515
+ * The calendar day a dated trigger names, or null when there is none to read.
516
+ *
517
+ * Round-tripping through `Date.UTC` rejects `2026-13-45`, which matches the
518
+ * shape and is not a date.
519
+ */
520
+ export function dated_trigger_day(trigger: string): string | null {
521
+ const m = DATED_TRIGGER_RE.exec(trigger);
522
+ if (m === null) return null;
523
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
524
+ const at = new Date(Date.UTC(y, mo - 1, d));
525
+ if (at.getUTCFullYear() !== y || at.getUTCMonth() !== mo - 1 || at.getUTCDate() !== d) {
526
+ return null;
527
+ }
528
+ return iso_day(at);
529
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * consequence_objects — the consequence vocabulary's prose half.
3
+ *
4
+ * Two things live here: the phrase rows that recognise a consequence
5
+ * authorization, and the extraction of the OBJECT such an authorization names.
6
+ * An authorization for one table, one environment or one recipient class is not
7
+ * an authorization for another, and until this existed the ledger could express
8
+ * the difference for nothing but pull-request numbers.
9
+ *
10
+ * IT IS RECOGNITION AND EXTRACTION, NOT MATCHING. Nothing here decides
11
+ * anything; the one consumer that acts on the vocabulary is `conformance_scan`,
12
+ * which counts after the fact. Making the object a matching condition is the
13
+ * ledger item ADR-254 left to the owner.
14
+ *
15
+ * It sits beside `git_authorization_hook` rather than inside it because that
16
+ * file is against the per-file source-size budget, which charges every line
17
+ * above 1500. The split is along a real seam: the hook owns the machinery —
18
+ * sentence bounds, negation, fenced-paste handling — and this module owns the
19
+ * consequence vocabulary that machinery is applied to.
20
+ */
21
+ import type { GitOp } from "../git_authorization_hook.js";
22
+
23
+ /**
24
+ * Prose phrases, German and English, that authorize a consequence operation.
25
+ *
26
+ * Spliced into `PHRASES` at its definition, so every row inherits the
27
+ * interrogative filter, the clause-scoped negation check and the fenced-paste
28
+ * split rather than reimplementing them.
29
+ *
30
+ * Every row REQUIRES ITS OBJECT NOUN, and that is why the patterns are long. A
31
+ * bare verb cannot tell an authorization from a mention, and for this class the
32
+ * mention is the common case: "deploy" appears in a hundred sentences that
33
+ * authorize nothing, while "deploy nach production" appears only when someone
34
+ * means it.
35
+ *
36
+ * German verb endings are `(e|en)?` because the imperative drops its `-e`, and
37
+ * both word orders are covered — the house style stated above `PHRASES`. The
38
+ * leading boundary is an explicit character class rather than `\b`: JavaScript's
39
+ * `\b` is ASCII-only, so a word STARTING with an umlaut ("Ändere") has no
40
+ * boundary before it and the row silently never fires.
41
+ */
42
+ export const CONSEQUENCE_PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
43
+ {
44
+ op: "prod-deploy",
45
+ re: /(?<![A-Za-z0-9_À-ɏ])(deploy(e|en|t|st)?|ausroll(e|en)?|ship(pe|pen)?|live\s*schalt(e|en)?|rollout)\b[^.\n]{0,30}\b(produktion|production|prod|live|prod-umgebung)\b|\b(produktion|production|prod|live)\b[^.\n]{0,30}\b(deploy(e|en)?|ausrollen|ausroll(e|en)?|live\s*schalten|rollout)\b/i,
46
+ },
47
+ {
48
+ op: "prod-data-destroy",
49
+ re: /(?<![A-Za-z0-9_À-ɏ])(l(ö|oe)sch(e|en)?|entfern(e|en)?|leer(e|en)?|verwerf(e|en)?|drop|truncate|delete|wipe|purge|zerst(ö|oe)r(e|en)?)\b[^.\n]{0,40}\b(tabelle|tabellen|datenbank|daten|bucket|collection|zeilen|rows|table|database|dataset)\b|\b(tabelle|tabellen|datenbank|daten|bucket|collection|table|database)\b[^.\n]{0,40}\b(l(ö|oe)schen|leeren|verwerfen|droppen|truncaten|entfernen|drop|truncate|delete|wipe|purge)\b/i,
50
+ },
51
+ {
52
+ op: "prod-infra-change",
53
+ re: /(?<![A-Za-z0-9_À-ɏ])((ver)?(ä|ae)nder(e|n)?|anpass(e|en)?|umbau(e|en)?|apply|destroy|(neu\s*)?provisionier(e|en)?)\b[^.\n]{0,40}\b(infrastruktur|infrastructure|cluster|terraform|iam|dns|netzwerk|network|firewall|policy|policies|security[-\s]group)\b|\b(infrastruktur|infrastructure|cluster|terraform|iam|dns|netzwerk|firewall)\b[^.\n]{0,40}\b((ver)?(ä|ae)ndern|anpassen|umbauen|zerst(ö|oe)ren|apply|destroy)\b/i,
54
+ },
55
+ {
56
+ op: "external-send",
57
+ re: /(?<![A-Za-z0-9_À-ɏ])(schick(e|en)?|verschick(e|en)?|send(e|en|et)?|mail(e|en)?|raussend(e|en)?|versend(e|en)?|post|publish)\b[^.\n]{0,40}\b(mail|mails|email|e-mail|nachricht|nachrichten|newsletter|webhook|kunden|kundin|kunde|empf(ä|ae)nger|customers?|recipients?|message)\b|\b(mail|email|e-mail|nachricht|newsletter|kunden|customers?)\b[^.\n]{0,40}\b(schicken|verschicken|senden|versenden|rausschicken|send|deliver)\b/i,
58
+ },
59
+ {
60
+ op: "money-movement",
61
+ re: /(?<![A-Za-z0-9_À-ɏ])(erstatt(e|en)?|r(ü|ue)ckerstatt(e|en)?|zahl(e|en)?|(ü|ue)berweis(e|en)?|abbuch(e|en)?|belast(e|en)?|refund|charge|payout|transfer|bill)\b[^.\n]{0,40}\b(betrag|geld|rechnung|zahlung|euro|dollar|kunde|kunden|karte|konto|invoice|payment|customer|card|account)\b|\b(betrag|geld|rechnung|zahlung|invoice|payment)\b[^.\n]{0,40}\b(erstatten|zur(ü|ue)ckzahlen|(ü|ue)berweisen|abbuchen|belasten|refund|charge|transfer)\b/i,
62
+ },
63
+ {
64
+ op: "trunk-force-push",
65
+ re: /(?<![A-Za-z0-9_À-ɏ])(force[-\s]?push(e|en|t)?|force[-\s]?pushen|(ü|ue)berschreib(e|en)?)\b[^.\n]{0,40}\b(main|master|trunk|produktions?[-\s]?branch|production|release[-\s]?branch)\b|\b(main|master|trunk|production)\b[^.\n]{0,40}\b(force[-\s]?pushen|force[-\s]?push|(ü|ue)berschreiben)\b/i,
66
+ },
67
+ {
68
+ op: "out-of-scope-destruction",
69
+ re: /(?<![A-Za-z0-9_À-ɏ])(l(ö|oe)sch(e|en)?|entfern(e|en)?|r(ä|ae)um(e|en)?\s*auf|aufr(ä|ae)um(e|en)?|delete|remove|wipe)\b[^.\n]{0,40}\b(au(ß|ss)erhalb|ausserhalb|unabh(ä|ae)ngig|nicht\s+zur\s+aufgabe|outside\s+(the\s+)?(task|scope)|unrelated|everything\s+else|alles\s+andere)\b|\b(au(ß|ss)erhalb|outside\s+(the\s+)?(task|scope)|unrelated)\b[^.\n]{0,40}\b(l(ö|oe)schen|entfernen|aufr(ä|ae)umen|delete|remove|wipe)\b/i,
70
+ },
71
+ ];
72
+
73
+ /**
74
+ * Operations whose authorization is meaningless without its object.
75
+ *
76
+ * "Delete the table" is not an authorization to delete a table; it is an
77
+ * authorization to delete THAT table. The same holds for a recipient class and
78
+ * for a payment. The other four consequence operations are environment-scoped
79
+ * rather than object-scoped, so an object is recorded for them when the prose
80
+ * names one and never required.
81
+ */
82
+ export const OBJECT_REQUIRED_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
83
+ "prod-data-destroy",
84
+ "external-send",
85
+ "money-movement",
86
+ ]);
87
+
88
+ /**
89
+ * Words that can follow an object noun and are never the object's NAME.
90
+ *
91
+ * "Lösch die Tabelle bitte" names no table; the extractor's next-token rule
92
+ * captures `bitte` unless something says otherwise. A stop list is the honest
93
+ * shape for that — the same reviewable-keyword-list discipline the negation and
94
+ * pasted-output vocabularies above use, and for the same reason: a parser that
95
+ * guessed here would record a fabricated object, and a fabricated object is
96
+ * worse than a missing one because the conformance count treats a recorded
97
+ * object as an authorization the turn actually gave.
98
+ */
99
+ const OBJECT_STOP_WORDS: ReadonlySet<string> = new Set([
100
+ "bitte", "doch", "mal", "nicht", "jetzt", "gleich", "auch", "noch", "wieder",
101
+ "in", "im", "aus", "von", "vom", ": ", "der", "die", "das", "den", "dem",
102
+ "und", "oder", "mit", "auf", "zu", "komplett", "ganz", "alle", "allen",
103
+ "please", "now", "again", "too", "also", "from", "and", "or", "the", "a",
104
+ "an", "there", "here", "completely", "all", "with", "on", "to", "in",
105
+ ]);
106
+
107
+ /** Lower-case, strip surrounding quotes and trailing punctuation. */
108
+ function _normalizeObject(raw: string): string {
109
+ return raw.trim().replace(/^["'`]+|["'`]+$/g, "").replace(/[.,;:]+$/, "").toLowerCase();
110
+ }
111
+
112
+ /** Per-operation object patterns, each with the capture group that holds it. */
113
+ const OBJECT_PATTERNS: Readonly<Record<string, readonly RegExp[]>> = {
114
+ "prod-data-destroy": [
115
+ /\b(?:tabelle|table|datenbank|database|schema|collection|index)\s+["'`]?([A-Za-z_][\w.$-]*)/gi,
116
+ /\bs3:\/\/([\w.-]+)/gi,
117
+ /\b(?:bucket)\s+["'`]?([A-Za-z_][\w.-]*)/gi,
118
+ ],
119
+ "external-send": [
120
+ /([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g,
121
+ /\bhttps?:\/\/([^\s/"'`]+)/gi,
122
+ /\b(?:an\s+(?:die|den|alle)|to\s+(?:the|all))\s+([A-Za-z][\w-]*)/gi,
123
+ ],
124
+ "money-movement": [
125
+ /(\d[\d.,]*)\s*(?:€|eur\b|euro\b|\$|usd\b)/gi,
126
+ /(?:€|\$)\s*(\d[\d.,]*)/g,
127
+ /\b(?:kunde|kunden|customer|account|konto)\s+["'`]?([A-Za-z0-9][\w.-]*)/gi,
128
+ ],
129
+ };
130
+
131
+ /**
132
+ * The objects a sentence names for one operation.
133
+ *
134
+ * Returns `[]` when the prose names none — which is a real answer and the one
135
+ * `conformance_scan` reads as "the record does not name this object". An empty
136
+ * list is never widened into a wildcard.
137
+ */
138
+ export function extractObjects(op: GitOp, text: string): string[] {
139
+ const patterns = OBJECT_PATTERNS[op];
140
+ if (!patterns) return [];
141
+ const out: string[] = [];
142
+ for (const re of patterns) {
143
+ const local = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
144
+ let m: RegExpExecArray | null;
145
+ while ((m = local.exec(text)) !== null) {
146
+ const value = _normalizeObject(m[1] ?? "");
147
+ if (value && !OBJECT_STOP_WORDS.has(value) && !out.includes(value)) out.push(value);
148
+ if (m.index === local.lastIndex) local.lastIndex++;
149
+ }
150
+ }
151
+ return out;
152
+ }
@@ -68,6 +68,13 @@ export const ALLOWLIST_PREFIXES: readonly (readonly string[])[] = [
68
68
  // a name. Read-only, deterministic, model-free, and already run in CI for its
69
69
  // own reasons — the same class as its neighbours here.
70
70
  ['check_no_external_sources'],
71
+ // Compares two COMMITTED files — the skill-activation census record against
72
+ // the sentence `docs/CLAIMS.md` publishes — so its exit code IS the verdict
73
+ // for that claim rather than evidence that some report exists. The census
74
+ // itself is deliberately NOT here: it reads a transcript store under `$HOME`
75
+ // that is absent on a runner, which would make the verdict an artifact of
76
+ // where it ran.
77
+ ['check_skill_activation_claim'],
71
78
  ['check_token_regression'],
72
79
  ['lint_agent_security'],
73
80
  ['measure_lexical_ranking'],
@@ -356,7 +356,7 @@ function as_posix(p: string): string {
356
356
  *
357
357
  * `dry_run=true` computes and returns the would-delete set (only paths that
358
358
  * currently exist on disk) WITHOUT unlinking anything or pruning empty
359
- * directories — the preview surface for `install.py --dry-run`. The selection
359
+ * directories — the preview surface for `agent-config install --dry-run`. The selection
360
360
  * logic (orphan diff, containment proof, directory guard) is identical to the
361
361
  * live path, so the preview is exact.
362
362
  */
@@ -456,7 +456,7 @@ export function reap_stale(
456
456
  *
457
457
  * `dry_run=true` returns the would-delete set (tagged orphans actually present
458
458
  * on disk) WITHOUT unlinking or pruning — the preview surface for
459
- * `install.py --dry-run`. Selection logic is identical to the live path.
459
+ * `agent-config install --dry-run`. Selection logic is identical to the live path.
460
460
  */
461
461
  export function reap_tagged_orphans(
462
462
  anchor: string,
@@ -0,0 +1,251 @@
1
+ /**
2
+ * host_permission_checks — report the host permission settings that produce
3
+ * confirmation prompts, and write none of them.
4
+ *
5
+ * WHY REPORT AND NOT WRITE. Three settings decide how often a normal coding
6
+ * task is interrupted, and all three belong to the consumer:
7
+ * `permissions.deny` rules that cover reads, the working tree's absence from
8
+ * `permissions.additionalDirectories`, and `permissions.defaultMode`. Writing
9
+ * any of them would be this package deciding a consumer's permission posture
10
+ * from inside their repository, which is the one thing a permission surface
11
+ * must never do quietly. `src/templates/consumer-settings/claude-settings.json`
12
+ * carries `enabledPlugins` and `hooks` and zero `permissions` keys; this check
13
+ * exists so that stays true and the consumer still gets the answer.
14
+ *
15
+ * WHY IT NAMES ALL THREE EVEN WHEN ALL THREE ARE ABSENT. Absent is a real
16
+ * answer and the most common one. A check that goes silent on the default
17
+ * project reports "nothing found" and "I did not look" identically, which is
18
+ * the measured-zero-versus-not-measured failure this tree's own report
19
+ * contracts argue against. So each setting is named on every run, with its
20
+ * resolved value or the word absent.
21
+ *
22
+ * READ ONLY, AND FAILURE-TOLERANT. An unreadable or malformed settings file is
23
+ * reported, never repaired: a consumer's JSON is theirs, and a doctor that
24
+ * rewrites what it came to inspect is not a doctor.
25
+ */
26
+ import * as fs from "node:fs";
27
+ import * as os from "node:os";
28
+ import * as path from "node:path";
29
+
30
+ import type { WiringCheck } from "./runtime_wiring_checks.js";
31
+
32
+ /** Injected so a test never reads the developer's own home directory. */
33
+ export interface HostPermissionDeps {
34
+ projectRoot: string;
35
+ homeDir: string;
36
+ readFile: (p: string) => string | null;
37
+ isDirectory: (p: string) => boolean;
38
+ }
39
+
40
+ /** The permission block as the host writes it; every field optional. */
41
+ interface PermissionBlock {
42
+ allow?: unknown;
43
+ deny?: unknown;
44
+ ask?: unknown;
45
+ additionalDirectories?: unknown;
46
+ defaultMode?: unknown;
47
+ }
48
+
49
+ /** One settings layer, in the host's own precedence order. */
50
+ export interface SettingsLayer {
51
+ label: string;
52
+ file: string;
53
+ present: boolean;
54
+ unreadable: boolean;
55
+ permissions: PermissionBlock;
56
+ }
57
+
58
+ /**
59
+ * Tool names whose every documented effect is a read, plus the read-shaped
60
+ * `Bash(<cmd>:*)` heads. A deny covering one of these is the class that turns a
61
+ * category-A call back into a prompt, which is why it is worth naming
62
+ * separately from a deny on a write.
63
+ */
64
+ const READ_SHAPED = [
65
+ "Read", "Glob", "Grep", "LS", "NotebookRead",
66
+ "Bash(cat", "Bash(ls", "Bash(head", "Bash(tail", "Bash(grep",
67
+ "Bash(rg", "Bash(find", "Bash(wc", "Bash(git status", "Bash(git log",
68
+ "Bash(git diff", "Bash(git show",
69
+ ];
70
+
71
+ /** The four `defaultMode` values probed from Claude Code 2.1.263. */
72
+ const KNOWN_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
73
+
74
+ function _strings(value: unknown): string[] {
75
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
76
+ }
77
+
78
+ /** Default deps — the only place the real filesystem is touched. */
79
+ export function defaultHostPermissionDeps(projectRoot: string): HostPermissionDeps {
80
+ return {
81
+ projectRoot,
82
+ homeDir: os.homedir(),
83
+ readFile: (p) => {
84
+ try {
85
+ return fs.readFileSync(p, "utf8");
86
+ } catch {
87
+ return null;
88
+ }
89
+ },
90
+ isDirectory: (p) => {
91
+ try {
92
+ return fs.statSync(p).isDirectory();
93
+ } catch {
94
+ return false;
95
+ }
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Resolve the three settings layers the host reads, in its precedence order.
102
+ *
103
+ * There was no resolver for this before — `security_audit_config.ts` is the
104
+ * only place all three paths appear together, and only as glob strings — so
105
+ * this is the first code in the tree that reads `permissions` at all.
106
+ */
107
+ export function readSettingsLayers(deps: HostPermissionDeps): SettingsLayer[] {
108
+ const candidates: Array<[string, string]> = [
109
+ ["user-global", path.join(deps.homeDir, ".claude", "settings.json")],
110
+ ["project", path.join(deps.projectRoot, ".claude", "settings.json")],
111
+ ["project-local", path.join(deps.projectRoot, ".claude", "settings.local.json")],
112
+ ];
113
+ return candidates.map(([label, file]) => {
114
+ const raw = deps.readFile(file);
115
+ if (raw === null) {
116
+ return { label, file, present: false, unreadable: false, permissions: {} };
117
+ }
118
+ try {
119
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
120
+ const perms = parsed["permissions"];
121
+ return {
122
+ label,
123
+ file,
124
+ present: true,
125
+ unreadable: false,
126
+ permissions:
127
+ perms !== null && typeof perms === "object" ? (perms as PermissionBlock) : {},
128
+ };
129
+ } catch {
130
+ return { label, file, present: true, unreadable: true, permissions: {} };
131
+ }
132
+ });
133
+ }
134
+
135
+ /** Deny rules that cover a read, with the layer each came from. */
136
+ export function readDenyRules(layers: readonly SettingsLayer[]): Array<[string, string]> {
137
+ const hits: Array<[string, string]> = [];
138
+ for (const layer of layers) {
139
+ for (const rule of _strings(layer.permissions.deny)) {
140
+ if (READ_SHAPED.some((prefix) => rule.startsWith(prefix))) {
141
+ hits.push([layer.label, rule]);
142
+ }
143
+ }
144
+ }
145
+ return hits;
146
+ }
147
+
148
+ /** Every `additionalDirectories` entry across the layers; the host unions them. */
149
+ export function additionalDirectories(layers: readonly SettingsLayer[]): string[] {
150
+ const seen: string[] = [];
151
+ for (const layer of layers) {
152
+ for (const dir of _strings(layer.permissions.additionalDirectories)) {
153
+ if (!seen.includes(dir)) seen.push(dir);
154
+ }
155
+ }
156
+ return seen;
157
+ }
158
+
159
+ /** The winning `defaultMode`, or `null` when no layer sets one. */
160
+ export function resolvedDefaultMode(layers: readonly SettingsLayer[]): string | null {
161
+ let winner: string | null = null;
162
+ for (const layer of layers) {
163
+ const mode = layer.permissions.defaultMode;
164
+ if (typeof mode === "string" && mode.trim()) winner = mode.trim();
165
+ }
166
+ return winner;
167
+ }
168
+
169
+ /**
170
+ * Is the project root a LINKED worktree?
171
+ *
172
+ * A linked worktree's `.git` is a file holding a `gitdir:` pointer, never a
173
+ * directory. It matters here because a worktree root is a working directory the
174
+ * host was not started in, which is exactly the case
175
+ * `additionalDirectories` exists to cover — and the case a consumer is least
176
+ * likely to have thought about.
177
+ */
178
+ export function isLinkedWorktree(deps: HostPermissionDeps): boolean {
179
+ const dotGit = path.join(deps.projectRoot, ".git");
180
+ const raw = deps.readFile(dotGit);
181
+ if (raw === null) return false;
182
+ return !deps.isDirectory(dotGit) && raw.trimStart().startsWith("gitdir:");
183
+ }
184
+
185
+ /** The copyable snippet — the consumer pastes it, this package never writes it. */
186
+ export function permissionSnippet(projectRoot: string): string {
187
+ return JSON.stringify(
188
+ { permissions: { additionalDirectories: [projectRoot], defaultMode: "default" } },
189
+ null,
190
+ 2,
191
+ );
192
+ }
193
+
194
+ /**
195
+ * The doctor row.
196
+ *
197
+ * `warn` when something reportable was found, `ok` otherwise. Never `fail`:
198
+ * these are the consumer's own choices, and a doctor that fails a build over a
199
+ * setting it is forbidden to write would be asking for a change it will not
200
+ * make.
201
+ */
202
+ export function checkHostPermissionSettings(deps: HostPermissionDeps): WiringCheck {
203
+ const layers = readSettingsLayers(deps);
204
+ const unreadable = layers.filter((l) => l.unreadable);
205
+ const denies = readDenyRules(layers);
206
+ const dirs = additionalDirectories(layers);
207
+ const mode = resolvedDefaultMode(layers);
208
+ const worktree = isLinkedWorktree(deps);
209
+ const rootListed = dirs.some(
210
+ (d) => path.resolve(d) === path.resolve(deps.projectRoot),
211
+ );
212
+
213
+ const lines: string[] = [];
214
+ lines.push(
215
+ denies.length === 0
216
+ ? "read-deny rules: absent — no permissions.deny rule covers a read"
217
+ : `read-deny rules: ${denies.length} — ` +
218
+ denies.map(([layer, rule]) => `${rule} (${layer})`).join(", "),
219
+ );
220
+ lines.push(
221
+ rootListed
222
+ ? `working tree in additionalDirectories: present (${deps.projectRoot})`
223
+ : `working tree in additionalDirectories: absent (${deps.projectRoot}` +
224
+ `${worktree ? ", a linked worktree" : ""})`,
225
+ );
226
+ lines.push(
227
+ mode === null
228
+ ? "permissions.defaultMode: absent — the host's own built-in default applies"
229
+ : `permissions.defaultMode: ${mode}` +
230
+ (KNOWN_MODES.includes(mode) ? "" : " — not a value this tree has probed"),
231
+ );
232
+ for (const layer of unreadable) {
233
+ lines.push(`unreadable: ${layer.file} (${layer.label}) is not valid JSON`);
234
+ }
235
+
236
+ // A linked worktree unlisted in additionalDirectories is the one finding
237
+ // worth surfacing on its own: the host was started somewhere else, so every
238
+ // call into this tree is a call into a directory it was not granted.
239
+ const reportable =
240
+ denies.length > 0 || unreadable.length > 0 || (worktree && !rootListed);
241
+
242
+ return {
243
+ id: "host-permission-settings",
244
+ status: reportable ? "warn" : "ok",
245
+ message: lines.join("; "),
246
+ remedy:
247
+ "agent-config never writes permissions — paste this into " +
248
+ `${path.join(deps.projectRoot, ".claude", "settings.local.json")} yourself if you want it: ` +
249
+ permissionSnippet(deps.projectRoot).replace(/\s+/g, " "),
250
+ };
251
+ }
@@ -11,7 +11,7 @@
11
11
  * - the repo generator (`condense.py generate_claude_skills`), which
12
12
  * builds the package's own `.claude/skills/` tree, and
13
13
  * - the consumer install finalizer
14
- * (`install.py finalize_claude_model_tiers`), which rewrites the
14
+ * (the installer's `finalize_claude_model_tiers`), which rewrites the
15
15
  * installed `.claude/skills/` tree so Claude Code performs the
16
16
  * per-turn model switch on a consumer with `model.auto_switch: auto`.
17
17
  *
@@ -275,7 +275,7 @@ export function slot_frequency(platform: string, slot: Slot): Frequency {
275
275
  }
276
276
 
277
277
  /**
278
- * Platforms with no hook surface at all.
278
+ * Platforms this package binds nothing on.
279
279
  *
280
280
  * Copilot declares `fallback_only: true` in the manifest — concerns route
281
281
  * through rule-only fallback and the dispatcher no-ops. Every hook-declared
@@ -294,7 +294,7 @@ export function is_hook_capable(platform_block: Record<string, unknown>): boolea
294
294
  export interface PlatformBinding {
295
295
  /** Platform id → slot → concern ids bound in that slot. */
296
296
  slots: Map<string, Map<Slot, string[]>>;
297
- /** Platform ids that carry no hook surface at all. */
297
+ /** Platform ids declared `fallback_only` — this package binds nothing there. */
298
298
  fallback_only: Set<string>;
299
299
  }
300
300
 
@@ -29,6 +29,10 @@ import {
29
29
  CURATED_HEAD_INSTRUCTION,
30
30
  MIX_RESPONSE_MARKER,
31
31
  MIX_RESPONSE_PLACEHOLDERS,
32
+ PROMISE_OUTCOMES,
33
+ PROMISE_PHRASE,
34
+ PROMISE_READBACK_MARKER,
35
+ extract_changelog_section,
32
36
  } from './release_material.js';
33
37
 
34
38
  /** The five curated labels, in the order an operator reads them. */
@@ -546,6 +550,95 @@ export function mix_response_blockers(
546
550
  return [];
547
551
  }
548
552
 
553
+ /**
554
+ * The promise the PREVIOUS release head made, or null when it made none.
555
+ *
556
+ * Scoped to that section's governance-response block, not to its whole body: a
557
+ * product line that happens to contain the phrase is not a promise the project
558
+ * made about the next cycle.
559
+ */
560
+ export function previous_promise(changelogText: string, previousVersion: string): string | null {
561
+ const section = extract_changelog_section(changelogText, previousVersion);
562
+ if (section === null) return null;
563
+ const idx = section.body.indexOf(MIX_RESPONSE_MARKER);
564
+ if (idx === -1) return null;
565
+ const block = mix_response_block(section.body, idx);
566
+ return block.includes(PROMISE_PHRASE) ? block : null;
567
+ }
568
+
569
+ /**
570
+ * The ONE predicate for "did this head answer the previous head's promise?".
571
+ *
572
+ * The defect: `_lib/release_material.ts` generates a `Next cycle ships …` line
573
+ * and nothing has ever read the previous one back, so an unmet promise can be
574
+ * restated indefinitely at zero cost — the debt is booked and never called. A
575
+ * promise nobody reads is not a commitment, it is a sentence.
576
+ *
577
+ * Three outcomes are accepted and one of them must be chosen, which is the
578
+ * whole mechanism: it does not require the promise to have been KEPT, and no
579
+ * ratio, deadline or count is enforced here. Refusing an unkept promise would
580
+ * make the honest answer the expensive one and teach an author to promise less
581
+ * rather than to answer truthfully.
582
+ *
583
+ * Length floor and placeholder scan reuse the governance-response discipline
584
+ * for the same reason it exists there: a bare marker, or the writer's own
585
+ * scaffolding, is not an answer a human wrote.
586
+ */
587
+ export function promise_readback_blockers(
588
+ sectionBody: string,
589
+ version: string,
590
+ previousVersion: string,
591
+ promise: string | null,
592
+ where = '`main`',
593
+ ): string[] {
594
+ if (promise === null) return [];
595
+ const idx = sectionBody.indexOf(PROMISE_READBACK_MARKER);
596
+ if (idx === -1) {
597
+ return [
598
+ `the ${version} section does not answer the ${previousVersion} head's ` +
599
+ `\`${PROMISE_PHRASE}\` promise. Add one \`> ${PROMISE_READBACK_MARKER}\` line ` +
600
+ `immediately under the curated head on ${where}, naming the promise and whether ` +
601
+ `it ${PROMISE_OUTCOMES.join(', ')} (the last one with its reason).`,
602
+ ];
603
+ }
604
+ const block = mix_response_block(sectionBody, idx);
605
+ const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
606
+ if (left.length > 0) {
607
+ return [
608
+ `the ${version} previous-cycle answer still carries the writer's placeholder(s) ` +
609
+ `(${left.map((t) => `\`${t}\``).join(', ')}). \`CHANGELOG.md\` is published to npm. ` +
610
+ `Write the outcome on ${where} and re-run.`,
611
+ ];
612
+ }
613
+ const lower = block.toLowerCase();
614
+ const named = PROMISE_OUTCOMES.filter((o) => lower.includes(o));
615
+ if (named.length === 0) {
616
+ return [
617
+ `the ${version} previous-cycle answer names no outcome. It has to say which of ` +
618
+ `${PROMISE_OUTCOMES.join(', ')} applies to the ${previousVersion} promise — an ` +
619
+ 'answer that restates the promise without resolving it is the failure this reads ' +
620
+ 'back for.',
621
+ ];
622
+ }
623
+ if (readback_answer(block).length < MIX_RESPONSE_MIN_CHARS) {
624
+ return [
625
+ `the ${version} previous-cycle answer is shorter than a written answer ` +
626
+ `(${String(MIX_RESPONSE_MIN_CHARS)} characters after the marker). Name what was ` +
627
+ `promised and what became of it on ${where}, not the outcome word alone.`,
628
+ ];
629
+ }
630
+ return [];
631
+ }
632
+
633
+ /** What the author wrote under the read-back marker, marker and quoting removed. */
634
+ export function readback_answer(block: string): string {
635
+ return block
636
+ .replace(PROMISE_READBACK_MARKER, '')
637
+ .replace(/^[>\s.]+|[>\s.]+$/gu, '')
638
+ .replace(/[>\s]+/gu, ' ')
639
+ .trim();
640
+ }
641
+
549
642
  /**
550
643
  * The whole response block: the marker's line plus every blockquote line under it.
551
644
  *