@event4u/agent-config 1.18.0 โ†’ 1.20.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 (181) hide show
  1. package/.agent-src/commands/agent-handoff.md +14 -10
  2. package/.agent-src/commands/chat-history/import.md +170 -0
  3. package/.agent-src/commands/chat-history/learn.md +178 -0
  4. package/.agent-src/commands/chat-history/show.md +17 -18
  5. package/.agent-src/commands/chat-history.md +26 -25
  6. package/.agent-src/commands/council/default.md +77 -82
  7. package/.agent-src/commands/create-pr.md +28 -8
  8. package/.agent-src/commands/feature/roadmap.md +22 -0
  9. package/.agent-src/commands/roadmap/create.md +38 -6
  10. package/.agent-src/commands/roadmap/execute.md +36 -9
  11. package/.agent-src/commands/sync-gitignore.md +1 -1
  12. package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
  13. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
  14. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
  15. package/.agent-src/rules/agent-authority.md +1 -0
  16. package/.agent-src/rules/agent-docs.md +1 -0
  17. package/.agent-src/rules/analysis-skill-routing.md +1 -0
  18. package/.agent-src/rules/architecture.md +1 -0
  19. package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
  20. package/.agent-src/rules/artifact-engagement-recording.md +1 -0
  21. package/.agent-src/rules/ask-when-uncertain.md +1 -0
  22. package/.agent-src/rules/augment-portability.md +1 -0
  23. package/.agent-src/rules/augment-source-of-truth.md +1 -0
  24. package/.agent-src/rules/autonomous-execution.md +1 -0
  25. package/.agent-src/rules/capture-learnings.md +1 -0
  26. package/.agent-src/rules/cli-output-handling.md +2 -2
  27. package/.agent-src/rules/command-suggestion-policy.md +1 -0
  28. package/.agent-src/rules/commit-conventions.md +1 -0
  29. package/.agent-src/rules/commit-policy.md +1 -0
  30. package/.agent-src/rules/context-hygiene.md +22 -0
  31. package/.agent-src/rules/direct-answers.md +11 -2
  32. package/.agent-src/rules/docker-commands.md +1 -0
  33. package/.agent-src/rules/docs-sync.md +1 -0
  34. package/.agent-src/rules/downstream-changes.md +1 -0
  35. package/.agent-src/rules/e2e-testing.md +1 -0
  36. package/.agent-src/rules/guidelines.md +1 -0
  37. package/.agent-src/rules/improve-before-implement.md +1 -0
  38. package/.agent-src/rules/language-and-tone.md +38 -6
  39. package/.agent-src/rules/laravel-translations.md +1 -0
  40. package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
  41. package/.agent-src/rules/minimal-safe-diff.md +1 -0
  42. package/.agent-src/rules/missing-tool-handling.md +1 -0
  43. package/.agent-src/rules/model-recommendation.md +1 -0
  44. package/.agent-src/rules/no-attribution-footers.md +48 -0
  45. package/.agent-src/rules/no-cheap-questions.md +1 -0
  46. package/.agent-src/rules/no-roadmap-references.md +2 -1
  47. package/.agent-src/rules/non-destructive-by-default.md +1 -0
  48. package/.agent-src/rules/onboarding-gate.md +26 -0
  49. package/.agent-src/rules/package-ci-checks.md +1 -0
  50. package/.agent-src/rules/php-coding.md +1 -0
  51. package/.agent-src/rules/preservation-guard.md +1 -0
  52. package/.agent-src/rules/review-routing-awareness.md +1 -0
  53. package/.agent-src/rules/reviewer-awareness.md +1 -0
  54. package/.agent-src/rules/roadmap-progress-sync.md +22 -0
  55. package/.agent-src/rules/role-mode-adherence.md +2 -2
  56. package/.agent-src/rules/rule-type-governance.md +1 -0
  57. package/.agent-src/rules/runtime-safety.md +1 -0
  58. package/.agent-src/rules/scope-control.md +1 -0
  59. package/.agent-src/rules/security-sensitive-stop.md +1 -0
  60. package/.agent-src/rules/size-enforcement.md +1 -0
  61. package/.agent-src/rules/skill-improvement-trigger.md +1 -0
  62. package/.agent-src/rules/skill-quality.md +50 -0
  63. package/.agent-src/rules/slash-command-routing-policy.md +39 -0
  64. package/.agent-src/rules/think-before-action.md +1 -0
  65. package/.agent-src/rules/token-efficiency.md +1 -0
  66. package/.agent-src/rules/tool-safety.md +1 -0
  67. package/.agent-src/rules/ui-audit-gate.md +1 -0
  68. package/.agent-src/rules/upstream-proposal.md +1 -0
  69. package/.agent-src/rules/user-interaction.md +22 -5
  70. package/.agent-src/rules/verify-before-complete.md +1 -0
  71. package/.agent-src/skills/ai-council/SKILL.md +4 -5
  72. package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
  73. package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
  74. package/.agent-src/skills/md-language-check/SKILL.md +1 -1
  75. package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
  76. package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
  77. package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
  78. package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
  79. package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
  80. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
  81. package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
  82. package/.agent-src/templates/agent-settings.md +21 -26
  83. package/.agent-src/templates/roadmaps.md +8 -3
  84. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
  85. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
  86. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
  87. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
  88. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
  89. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
  90. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
  91. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +110 -0
  92. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
  93. package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
  94. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
  95. package/.agent-src/templates/skill.md +30 -1
  96. package/.claude-plugin/marketplace.json +8 -4
  97. package/AGENTS.md +44 -3
  98. package/CHANGELOG.md +173 -0
  99. package/README.md +22 -22
  100. package/config/agent-settings.template.yml +42 -13
  101. package/config/gitignore-block.txt +4 -4
  102. package/docs/architecture.md +3 -3
  103. package/docs/catalog.md +18 -13
  104. package/docs/contracts/adr-chat-history-split.md +10 -1
  105. package/docs/contracts/adr-settings-sync-engine.md +127 -0
  106. package/docs/contracts/command-clusters.md +1 -1
  107. package/docs/contracts/cross-wing-handoff.md +133 -0
  108. package/docs/contracts/decision-trace-v1.md +146 -0
  109. package/docs/contracts/file-ownership-matrix.json +348 -126
  110. package/docs/contracts/hook-architecture-v1.md +220 -0
  111. package/docs/contracts/memory-visibility-v1.md +122 -0
  112. package/docs/contracts/one-off-script-lifecycle.md +109 -0
  113. package/docs/contracts/rule-interactions.yml +22 -0
  114. package/docs/customization.md +2 -1
  115. package/docs/development.md +4 -1
  116. package/docs/getting-started.md +21 -29
  117. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
  118. package/docs/guidelines/agent-infra/layered-settings.md +32 -13
  119. package/docs/hook-payload-capture.md +221 -0
  120. package/docs/migrations/commands-1.15.0.md +17 -12
  121. package/docs/skills-catalog.md +5 -4
  122. package/llms.txt +4 -3
  123. package/package.json +1 -1
  124. package/scripts/agent-config +45 -1
  125. package/scripts/ai_council/_default_prices.py +4 -4
  126. package/scripts/ai_council/bundler.py +3 -3
  127. package/scripts/ai_council/clients.py +25 -9
  128. package/scripts/ai_council/modes.py +3 -4
  129. package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
  130. package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
  131. package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
  132. package/scripts/ai_council/pricing.py +10 -9
  133. package/scripts/ai_council/session.py +92 -0
  134. package/scripts/build_rule_trigger_matrix.py +1 -9
  135. package/scripts/capture_showcase_session.py +361 -0
  136. package/scripts/chat_history.py +963 -597
  137. package/scripts/check_always_budget.py +7 -2
  138. package/scripts/check_references.py +12 -2
  139. package/scripts/context_hygiene_hook.py +14 -6
  140. package/scripts/council_cli.py +407 -0
  141. package/scripts/hook_manifest.yaml +217 -0
  142. package/scripts/hooks/__init__.py +1 -0
  143. package/scripts/hooks/augment-chat-history.sh +10 -0
  144. package/scripts/hooks/augment-dispatcher.sh +72 -0
  145. package/scripts/hooks/cline-dispatcher.sh +86 -0
  146. package/scripts/hooks/cowork-dispatcher.sh +98 -0
  147. package/scripts/hooks/cursor-dispatcher.sh +76 -0
  148. package/scripts/hooks/dispatch_hook.py +383 -0
  149. package/scripts/hooks/envelope.py +98 -0
  150. package/scripts/hooks/gemini-dispatcher.sh +117 -0
  151. package/scripts/hooks/state_io.py +122 -0
  152. package/scripts/hooks/windsurf-dispatcher.sh +123 -0
  153. package/scripts/hooks_status.py +157 -0
  154. package/scripts/install-hooks.sh +2 -2
  155. package/scripts/install.py +725 -87
  156. package/scripts/install.sh +38 -1
  157. package/scripts/lint_handoffs.py +214 -0
  158. package/scripts/lint_hook_manifest.py +217 -0
  159. package/scripts/lint_one_off_age.py +184 -0
  160. package/scripts/lint_rule_tiers.py +78 -0
  161. package/scripts/lint_showcase_sessions.py +148 -0
  162. package/scripts/minimal_safe_diff_hook.py +245 -0
  163. package/scripts/onboarding_gate_hook.py +13 -8
  164. package/scripts/readme_linter.py +12 -3
  165. package/scripts/redact_hook_capture.py +148 -0
  166. package/scripts/roadmap_progress_hook.py +5 -0
  167. package/scripts/schemas/skill.schema.json +5 -0
  168. package/scripts/skill_linter.py +163 -1
  169. package/scripts/sync_agent_settings.py +32 -129
  170. package/scripts/sync_yaml_rt.py +734 -0
  171. package/scripts/update_prices.py +3 -3
  172. package/scripts/verify_before_complete_hook.py +216 -0
  173. package/.agent-src/commands/chat-history/checkpoint.md +0 -126
  174. package/.agent-src/commands/chat-history/clear.md +0 -103
  175. package/.agent-src/commands/chat-history/resume.md +0 -183
  176. package/.agent-src/rules/chat-history-cadence.md +0 -109
  177. package/.agent-src/rules/chat-history-ownership.md +0 -123
  178. package/.agent-src/rules/chat-history-visibility.md +0 -96
  179. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
  180. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
  181. package/scripts/check_phase_coupling.py +0 -148
@@ -0,0 +1,163 @@
1
+ """``DecisionTraceHook`` โ€” emit a decision-trace JSON per phase.
2
+
3
+ Implements the v1 envelope from ``docs/contracts/decision-trace-v1.md``.
4
+ Default-off; opt-in via ``.agent-settings.yml``
5
+ ``decision_engine.surface_traces: true`` (mirrored into
6
+ ``hooks.decision_trace.enabled`` by :mod:`work_engine.hooks.settings`).
7
+
8
+ The hook is purely observational โ€” it never mutates ``DeliveryState``,
9
+ never raises terminal errors. Stream / disk failures surface as
10
+ :class:`HookError` (non-fatal per the three-tier contract).
11
+
12
+ Trace layout (matches the contract):
13
+
14
+ * ``schema_version: 1``
15
+ * ``work_id`` โ€” derived from the state-file directory name when the
16
+ caller follows the ``agents/state/work/<id>/state.json`` convention,
17
+ else from the state-file stem.
18
+ * ``phase`` โ€” engine ``step_name`` (refine/memory/.../report).
19
+ * ``started_at`` / ``ended_at`` โ€” ISO-8601 UTC timestamps captured on
20
+ ``BEFORE_STEP`` and ``AFTER_STEP``.
21
+ * ``confidence_band`` / ``risk_class`` โ€” heuristics defined in
22
+ :mod:`work_engine.scoring.decision_trace`.
23
+ * ``rules`` โ€” empty by default; the engine layer populates rule
24
+ applications when concerns wire into the trace bus (later phase).
25
+ * ``memory`` โ€” counts and ids snapshotted from ``state.memory``.
26
+ * ``verify`` โ€” claims/first-try-passes derived from ``state.verify``.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import time
32
+ from datetime import datetime, timezone
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from ...scoring.decision_trace import (
37
+ derive_confidence_band,
38
+ derive_risk_class,
39
+ summarise_memory,
40
+ summarise_verify,
41
+ )
42
+ from ..context import HookContext
43
+ from ..events import HookEvent
44
+ from ..exceptions import HookError
45
+ from ..registry import HookRegistry
46
+
47
+ SCHEMA_VERSION = 1
48
+ _MAX_MEMORY_IDS = 32
49
+
50
+
51
+ class DecisionTraceHook:
52
+ """Emit one decision-trace JSON file per dispatcher step.
53
+
54
+ Parameters
55
+ ----------
56
+ output_dir:
57
+ Optional override for the trace destination. When ``None`` the
58
+ hook writes alongside the WorkState file: if the state file
59
+ sits under ``agents/state/work/<id>/state.json`` the trace
60
+ lands at ``agents/state/work/<id>/decision-trace-<phase>.json``;
61
+ otherwise the trace lands next to the state file as
62
+ ``<stem>.decision-trace-<phase>.json``.
63
+ """
64
+
65
+ def __init__(self, output_dir: Path | None = None) -> None:
66
+ self._output_dir = output_dir
67
+ self._state_file: Path | None = None
68
+ self._step_started: dict[str, float] = {}
69
+
70
+ def register(self, registry: HookRegistry) -> None:
71
+ """Register the trace callbacks on the lifecycle events used."""
72
+ registry.register(HookEvent.BEFORE_LOAD, self._capture_state_file)
73
+ registry.register(HookEvent.AFTER_LOAD, self._capture_state_file)
74
+ registry.register(HookEvent.BEFORE_STEP, self._mark_step_start)
75
+ registry.register(HookEvent.AFTER_STEP, self._emit_trace)
76
+
77
+ # -- lifecycle callbacks ------------------------------------------
78
+
79
+ def _capture_state_file(self, ctx: HookContext) -> None:
80
+ if ctx.state_file is not None:
81
+ self._state_file = Path(ctx.state_file)
82
+
83
+ def _mark_step_start(self, ctx: HookContext) -> None:
84
+ if ctx.step_name:
85
+ self._step_started[ctx.step_name] = time.time()
86
+
87
+ def _emit_trace(self, ctx: HookContext) -> None:
88
+ if not ctx.step_name:
89
+ return
90
+ started = self._step_started.pop(ctx.step_name, time.time())
91
+ envelope = self._build_envelope(ctx, started)
92
+ target = self._target_path(ctx.step_name)
93
+ try:
94
+ target.parent.mkdir(parents=True, exist_ok=True)
95
+ target.write_text(
96
+ json.dumps(envelope, indent=2, sort_keys=False) + "\n",
97
+ encoding="utf-8",
98
+ )
99
+ except OSError as exc:
100
+ raise HookError(f"decision-trace write failed: {exc}") from exc
101
+
102
+ # -- envelope construction ----------------------------------------
103
+
104
+ def _build_envelope(
105
+ self, ctx: HookContext, started: float,
106
+ ) -> dict[str, Any]:
107
+ delivery = ctx.delivery
108
+ memory = summarise_memory(
109
+ getattr(delivery, "memory", None),
110
+ limit=_MAX_MEMORY_IDS,
111
+ )
112
+ verify = summarise_verify(getattr(delivery, "verify", None))
113
+ ambiguity = bool(getattr(delivery, "questions", None))
114
+ return {
115
+ "schema_version": SCHEMA_VERSION,
116
+ "work_id": self._work_id(),
117
+ "phase": ctx.step_name,
118
+ "started_at": _iso_utc(started),
119
+ "ended_at": _iso_utc(time.time()),
120
+ "confidence_band": derive_confidence_band(
121
+ memory_hits=memory["hits"],
122
+ verify_claims=verify["claims"],
123
+ verify_first_try_passes=verify["first_try_passes"],
124
+ ambiguity_flag=ambiguity,
125
+ ),
126
+ "risk_class": derive_risk_class(
127
+ getattr(delivery, "changes", None),
128
+ ),
129
+ "rules": [],
130
+ "memory": memory,
131
+ "verify": verify,
132
+ }
133
+
134
+ # -- path helpers --------------------------------------------------
135
+
136
+ def _work_id(self) -> str:
137
+ if self._state_file is None:
138
+ return "unknown"
139
+ parent = self._state_file.parent
140
+ if parent.name and parent.parent.name == "work":
141
+ return parent.name
142
+ return self._state_file.stem
143
+
144
+ def _target_path(self, phase: str) -> Path:
145
+ filename = f"decision-trace-{phase}.json"
146
+ if self._output_dir is not None:
147
+ return self._output_dir / filename
148
+ if self._state_file is None:
149
+ return Path(filename)
150
+ parent = self._state_file.parent
151
+ if parent.name and parent.parent.name == "work":
152
+ return parent / filename
153
+ return parent / f"{self._state_file.stem}.{filename}"
154
+
155
+
156
+ def _iso_utc(epoch: float) -> str:
157
+ return (
158
+ datetime.fromtimestamp(epoch, tz=timezone.utc)
159
+ .strftime("%Y-%m-%dT%H:%M:%SZ")
160
+ )
161
+
162
+
163
+ __all__ = ["DecisionTraceHook", "SCHEMA_VERSION"]
@@ -0,0 +1,110 @@
1
+ """``MemoryVisibilityHook`` โ€” emit the visibility line on save.
2
+
3
+ Implements the producer side of
4
+ ``docs/contracts/memory-visibility-v1.md``: derive ``asks/hits/ids``
5
+ from ``state.memory`` and thread the rendered line into
6
+ ``state.report`` so the agent's reply naturally carries the memory
7
+ visibility marker.
8
+
9
+ Fires on ``before_save``: ``cli._sync_back`` runs between
10
+ ``after_dispatch`` and ``before_save`` and reassigns
11
+ ``work.report = delivery.report``. A line written on
12
+ ``after_dispatch`` would be overwritten before ``_save``; firing on
13
+ ``before_save`` lands after the sync.
14
+
15
+ Default-off; opt-in via ``.agent-settings.yml``
16
+ ``hooks.memory_visibility.enabled: true`` (or implicitly when
17
+ ``memory.visibility`` is not ``off`` and the master switch is on).
18
+ The hook is purely observational: failures surface as
19
+ :class:`HookError` (non-fatal per the three-tier contract); the
20
+ engine never crashes on a visibility-line write.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Iterable
25
+
26
+ from ...scoring.memory_visibility import (
27
+ DEFAULT_ASKED_TYPES,
28
+ format_line,
29
+ should_emit,
30
+ summarise_visibility,
31
+ )
32
+ from ..context import HookContext
33
+ from ..events import HookEvent
34
+ from ..exceptions import HookError
35
+ from ..registry import HookRegistry
36
+
37
+
38
+ class MemoryVisibilityHook:
39
+ """Thread the ``๐Ÿง  Memory: <hits>/<asks> ยท ids=[โ€ฆ]`` line into the report.
40
+
41
+ Parameters
42
+ ----------
43
+ cost_profile:
44
+ Cadence profile from ``.agent-settings.yml`` (``lean`` /
45
+ ``standard`` / ``verbose``). ``lean`` suppresses the line
46
+ unless ``asks โ‰ฅ 3`` per the contract's cadence table.
47
+ visibility_off:
48
+ When ``True``, the hook stays silent โ€” used to mirror
49
+ ``memory.visibility: off`` in the consumer settings.
50
+ asked_types:
51
+ Optional override for the list of memory types treated as
52
+ ``asks`` in the visibility line. Defaults to the four types
53
+ the engine's memory step retrieves over.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ cost_profile: str = "standard",
60
+ visibility_off: bool = False,
61
+ asked_types: Iterable[str] | None = None,
62
+ ) -> None:
63
+ self._cost_profile = cost_profile
64
+ self._visibility_off = visibility_off
65
+ self._asked_types = (
66
+ tuple(asked_types) if asked_types is not None else DEFAULT_ASKED_TYPES
67
+ )
68
+
69
+ def register(self, registry: HookRegistry) -> None:
70
+ """Register the visibility-line emitter on ``before_save``."""
71
+ registry.register(HookEvent.BEFORE_SAVE, self._on_before_save)
72
+
73
+ def _on_before_save(self, ctx: HookContext) -> None:
74
+ work = ctx.work
75
+ if work is None:
76
+ return
77
+ memory = getattr(work, "memory", None)
78
+ summary = summarise_visibility(memory, asked_types=self._asked_types)
79
+ if not should_emit(
80
+ summary,
81
+ cost_profile=self._cost_profile,
82
+ visibility_off=self._visibility_off,
83
+ ):
84
+ return
85
+ line = format_line(summary)
86
+ if not line:
87
+ return
88
+ existing = getattr(work, "report", "") or ""
89
+ if line in existing:
90
+ return
91
+ sep = "\n\n" if existing else ""
92
+ try:
93
+ work.report = f"{existing}{sep}{line}"
94
+ except AttributeError as exc:
95
+ raise HookError(
96
+ "memory-visibility: state.report not writable",
97
+ ) from exc
98
+
99
+
100
+ def derive_visibility(memory: Any) -> str | None:
101
+ """Convenience helper: render the line directly from a memory list.
102
+
103
+ Used by external callers (CLI ad-hoc smoke tests, the audit-as-
104
+ memory consumer) that have a ``memory`` list but no ``HookContext``.
105
+ Returns ``None`` when ``asks == 0``.
106
+ """
107
+ return format_line(summarise_visibility(memory))
108
+
109
+
110
+ __all__ = ["MemoryVisibilityHook", "derive_visibility"]
@@ -39,6 +39,10 @@ class HookSettings:
39
39
  halt_surface_audit: bool = False
40
40
  state_shape_validation: bool = False
41
41
  directive_set_guard: bool = False
42
+ decision_trace: bool = False
43
+ memory_visibility: bool = False
44
+ memory_visibility_off: bool = False
45
+ cost_profile: str = "standard"
42
46
  chat_history_enabled: bool = False
43
47
  chat_history_script: str = DEFAULT_CHAT_HISTORY_SCRIPT
44
48
 
@@ -102,6 +106,34 @@ def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
102
106
  and _coerce_bool(global_chat.get("enabled"), False)
103
107
  )
104
108
 
109
+ decision_engine = data.get("decision_engine")
110
+ decision_trace_on = (
111
+ isinstance(decision_engine, dict)
112
+ and _coerce_bool(decision_engine.get("surface_traces"), False)
113
+ )
114
+
115
+ memory_section = data.get("memory")
116
+ visibility_off = False
117
+ if isinstance(memory_section, dict):
118
+ raw = memory_section.get("visibility")
119
+ if isinstance(raw, str) and raw.strip().lower() == "off":
120
+ visibility_off = True
121
+ elif isinstance(raw, bool) and raw is False:
122
+ visibility_off = True
123
+
124
+ memory_hooks = hooks.get("memory_visibility")
125
+ if isinstance(memory_hooks, dict):
126
+ memory_visibility_on = _coerce_bool(
127
+ memory_hooks.get("enabled"), True,
128
+ )
129
+ else:
130
+ memory_visibility_on = True
131
+
132
+ cost_profile_raw = data.get("cost_profile") or "standard"
133
+ cost_profile = (
134
+ str(cost_profile_raw).strip().lower() or "standard"
135
+ )
136
+
105
137
  return HookSettings(
106
138
  enabled=True,
107
139
  trace=_coerce_bool(hooks.get("trace"), False),
@@ -114,6 +146,10 @@ def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
114
146
  directive_set_guard=_coerce_bool(
115
147
  hooks.get("directive_set_guard"), True
116
148
  ),
149
+ decision_trace=decision_trace_on,
150
+ memory_visibility=memory_visibility_on,
151
+ memory_visibility_off=visibility_off,
152
+ cost_profile=cost_profile,
117
153
  chat_history_enabled=chat_block_enabled and global_chat_on,
118
154
  chat_history_script=chat_script,
119
155
  )
@@ -0,0 +1,141 @@
1
+ """Confidence-band + risk-class heuristics for decision-trace v1.
2
+
3
+ These heuristics back the JSON envelope emitted by
4
+ :class:`work_engine.hooks.builtin.DecisionTraceHook`. They live here
5
+ (under ``scoring/``) so the rules and the hook share a single source
6
+ of truth, and so unit tests can exercise the heuristics without
7
+ spinning up a dispatcher.
8
+
9
+ Confidence-band heuristic (per
10
+ ``docs/contracts/decision-trace-v1.md``):
11
+
12
+ * ``high`` โ€” ``memory.hits โ‰ฅ 2`` AND
13
+ ``verify.first_try_passes == verify.claims`` AND no ambiguity flag.
14
+ * ``medium`` โ€” ``memory.hits โ‰ฅ 1`` OR ``verify.first_try_passes โ‰ฅ 1``.
15
+ * ``low`` โ€” otherwise.
16
+
17
+ Edge case: ``verify.claims == 0`` is **not** ``high`` by default; it
18
+ folds into ``medium`` if at least one memory hit landed, ``low``
19
+ otherwise.
20
+
21
+ Risk-class heuristic: maximum risk across the files the phase
22
+ touched. With no file-ownership matrix wired in yet, the
23
+ implementation defaults to ``low`` and exposes a ``files`` argument
24
+ so a future hook can pass concrete paths. If the phase touched any
25
+ files at all the heuristic returns ``medium`` so reviewers stay
26
+ nudged toward a closer look until the matrix lands.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ from typing import Any, Iterable
31
+
32
+ BAND_HIGH = "high"
33
+ BAND_MEDIUM = "medium"
34
+ BAND_LOW = "low"
35
+
36
+ RISK_HIGH = "high"
37
+ RISK_MEDIUM = "medium"
38
+ RISK_LOW = "low"
39
+
40
+
41
+ def derive_confidence_band(
42
+ *,
43
+ memory_hits: int,
44
+ verify_claims: int,
45
+ verify_first_try_passes: int,
46
+ ambiguity_flag: bool,
47
+ ) -> str:
48
+ """Return ``high`` / ``medium`` / ``low`` per the v1 heuristic."""
49
+ if (
50
+ memory_hits >= 2
51
+ and verify_claims > 0
52
+ and verify_first_try_passes == verify_claims
53
+ and not ambiguity_flag
54
+ ):
55
+ return BAND_HIGH
56
+ if memory_hits >= 1 or verify_first_try_passes >= 1:
57
+ return BAND_MEDIUM
58
+ return BAND_LOW
59
+
60
+
61
+ def derive_risk_class(changes: Any) -> str:
62
+ """Return the trace-level risk class.
63
+
64
+ ``changes`` is the ``delivery.changes`` slice โ€” a list of dicts in
65
+ the canonical engine shape, or ``None`` for pure planning phases.
66
+ Until the file-ownership matrix is wired in, "any change touched"
67
+ maps to ``medium``; "no change" maps to ``low``. ``high`` is
68
+ reserved for the future ownership-matrix lookup.
69
+ """
70
+ if not changes:
71
+ return RISK_LOW
72
+ if isinstance(changes, Iterable):
73
+ try:
74
+ count = sum(1 for _ in changes)
75
+ except TypeError:
76
+ return RISK_LOW
77
+ return RISK_MEDIUM if count > 0 else RISK_LOW
78
+ return RISK_LOW
79
+
80
+
81
+ def summarise_memory(
82
+ memory: Any, *, limit: int = 32,
83
+ ) -> dict[str, Any]:
84
+ """Reduce ``state.memory`` into the trace-envelope ``memory`` slice.
85
+
86
+ The engine stores memory entries as dicts with at least an ``id``
87
+ or ``rule_id`` key plus arbitrary per-entry payload. The trace
88
+ only carries ids โ€” bodies stay behind the privacy floor.
89
+ """
90
+ if not memory:
91
+ return {"asks": 0, "hits": 0, "ids": []}
92
+ ids: list[str] = []
93
+ asks = 0
94
+ hits = 0
95
+ for entry in memory:
96
+ if not isinstance(entry, dict):
97
+ continue
98
+ asks += int(entry.get("asks", 1) or 0) or 1
99
+ if entry.get("hit", True):
100
+ hits += 1
101
+ entry_id = entry.get("id") or entry.get("rule_id")
102
+ if entry_id and len(ids) < limit:
103
+ ids.append(str(entry_id))
104
+ return {"asks": asks, "hits": hits, "ids": ids}
105
+
106
+
107
+ def summarise_verify(verify: Any) -> dict[str, int]:
108
+ """Reduce ``state.verify`` into the trace-envelope ``verify`` slice.
109
+
110
+ ``verify`` may be ``None`` (no verify run yet), a dict carrying
111
+ ``claims`` / ``first_try_passes``, or a list of attempt records.
112
+ Anything else collapses to zeros.
113
+ """
114
+ if verify is None:
115
+ return {"claims": 0, "first_try_passes": 0}
116
+ if isinstance(verify, dict):
117
+ claims = int(verify.get("claims", 0) or 0)
118
+ passes = int(verify.get("first_try_passes", 0) or 0)
119
+ return {"claims": claims, "first_try_passes": passes}
120
+ if isinstance(verify, list):
121
+ claims = len(verify)
122
+ passes = sum(
123
+ 1 for entry in verify
124
+ if isinstance(entry, dict) and entry.get("first_try_pass")
125
+ )
126
+ return {"claims": claims, "first_try_passes": passes}
127
+ return {"claims": 0, "first_try_passes": 0}
128
+
129
+
130
+ __all__ = [
131
+ "BAND_HIGH",
132
+ "BAND_MEDIUM",
133
+ "BAND_LOW",
134
+ "RISK_HIGH",
135
+ "RISK_MEDIUM",
136
+ "RISK_LOW",
137
+ "derive_confidence_band",
138
+ "derive_risk_class",
139
+ "summarise_memory",
140
+ "summarise_verify",
141
+ ]
@@ -0,0 +1,125 @@
1
+ """Producer-side helpers for the memory-visibility line.
2
+
3
+ Implements the v1 line shape from
4
+ ``docs/contracts/memory-visibility-v1.md``:
5
+
6
+ ๐Ÿง  Memory: <hits>/<asks> ยท ids=[<comma-separated-ids>]
7
+
8
+ The semantics matched to the work-engine model:
9
+
10
+ * The ``memory`` step retrieves across the four allowed memory types
11
+ (``MEMORY_TYPES`` in ``directives.backend.memory``). Each type is
12
+ one ``ask`` from the visibility-line perspective.
13
+ * ``hits`` counts distinct types that returned at least one entry.
14
+ * ``ids`` is the deduped list of returned entry ids preserving the
15
+ retrieval order encoded in ``state.memory``.
16
+
17
+ Privacy floor: this module never emits entry bodies, summaries,
18
+ ``path``/``source`` fields, or anything beyond ``id`` and ``type``.
19
+ The privacy regression test (``tests/contracts/test_memory_
20
+ visibility_redaction.py``) keeps this guarantee enforced.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Iterable
25
+
26
+ ICON = "\U0001F9E0" # ๐Ÿง 
27
+ DEFAULT_MAX_INLINE_IDS = 5
28
+ DEFAULT_ASKED_TYPES = (
29
+ "domain-invariants",
30
+ "architecture-decisions",
31
+ "incident-learnings",
32
+ "historical-patterns",
33
+ )
34
+
35
+
36
+ def summarise_visibility(
37
+ memory: Any,
38
+ *,
39
+ asked_types: Iterable[str] = DEFAULT_ASKED_TYPES,
40
+ ) -> dict[str, Any]:
41
+ """Reduce ``state.memory`` into the visibility-line slice.
42
+
43
+ ``memory`` is the list of hit dicts produced by
44
+ ``directives.backend.memory``. Returns ``{"asks", "hits", "ids"}``
45
+ with privacy-safe values only.
46
+ """
47
+ asked = tuple(asked_types)
48
+ if not memory or not isinstance(memory, list):
49
+ return {"asks": 0, "hits": 0, "ids": []}
50
+ asks = len(asked)
51
+ seen_types: set[str] = set()
52
+ ids: list[str] = []
53
+ seen_ids: set[str] = set()
54
+ for entry in memory:
55
+ if not isinstance(entry, dict):
56
+ continue
57
+ type_value = entry.get("type")
58
+ if isinstance(type_value, str):
59
+ seen_types.add(type_value)
60
+ entry_id = entry.get("id") or entry.get("rule_id")
61
+ if not isinstance(entry_id, (str, int)):
62
+ continue
63
+ sid = str(entry_id)
64
+ if sid in seen_ids:
65
+ continue
66
+ seen_ids.add(sid)
67
+ ids.append(sid)
68
+ hits = len(seen_types) if seen_types else (1 if ids else 0)
69
+ return {"asks": asks, "hits": hits, "ids": ids}
70
+
71
+
72
+ def format_line(
73
+ summary: dict[str, Any],
74
+ *,
75
+ max_inline_ids: int = DEFAULT_MAX_INLINE_IDS,
76
+ ) -> str | None:
77
+ """Render the visibility line; return ``None`` when ``asks == 0``.
78
+
79
+ Cap inline ids at ``max_inline_ids`` and append ``โ€ฆ+N`` when the
80
+ list is longer. Returning ``None`` enforces the contract clause
81
+ "If ``asks == 0``, the engine MUST suppress the line entirely".
82
+ """
83
+ asks = int(summary.get("asks", 0) or 0)
84
+ if asks <= 0:
85
+ return None
86
+ hits = int(summary.get("hits", 0) or 0)
87
+ raw_ids = summary.get("ids") or []
88
+ ids = [str(i) for i in raw_ids if isinstance(i, (str, int))]
89
+ if max_inline_ids < 0:
90
+ max_inline_ids = 0
91
+ inline = ids[:max_inline_ids]
92
+ overflow = len(ids) - len(inline)
93
+ rendered_ids = ", ".join(inline)
94
+ if overflow > 0:
95
+ suffix = ", " if rendered_ids else ""
96
+ rendered_ids = f"{rendered_ids}{suffix}\u2026+{overflow}"
97
+ return f"{ICON} Memory: {hits}/{asks} \u00b7 ids=[{rendered_ids}]"
98
+
99
+
100
+ def should_emit(
101
+ summary: dict[str, Any],
102
+ *,
103
+ cost_profile: str = "standard",
104
+ visibility_off: bool = False,
105
+ ) -> bool:
106
+ """Apply the cadence + opt-out gates from the contract."""
107
+ if visibility_off:
108
+ return False
109
+ asks = int(summary.get("asks", 0) or 0)
110
+ if asks <= 0:
111
+ return False
112
+ profile = (cost_profile or "standard").strip().lower()
113
+ if profile == "lean":
114
+ return asks >= 3
115
+ return True
116
+
117
+
118
+ __all__ = [
119
+ "DEFAULT_ASKED_TYPES",
120
+ "DEFAULT_MAX_INLINE_IDS",
121
+ "ICON",
122
+ "format_line",
123
+ "should_emit",
124
+ "summarise_visibility",
125
+ ]
@@ -118,6 +118,35 @@ Do NOT use when:
118
118
  - Do NOT {anti-pattern 1}.
119
119
  - Do NOT {anti-pattern 2}.
120
120
  - Do NOT {anti-pattern 3}.
121
+
122
+ <!-- SENIOR-TIER STUB BLOCKS (delete entire section if not `tier: senior`):
123
+ Senior-tier skills (frontmatter `tier: senior`) require four extra
124
+ blocks per `.agent-src.uncompressed/rules/skill-quality.md` ยง
125
+ Senior-Tier Required Structure. Mid-tier and untiered skills MUST
126
+ remove this section entirely. The four blocks are enforced by
127
+ `scripts/skill_linter.py` for `tier: senior` skills only.
128
+
129
+ ## Related Skills
130
+
131
+ **WHEN to use this**
132
+ - {situation A this skill resolves better than peer skill}
133
+ - {situation B}
134
+
135
+ **WHEN NOT to use this**
136
+ - {situation C} โ€” route to [`{peer-skill}`](../{peer-skill}/SKILL.md)
137
+ - {situation D} โ€” route to [`{peer-skill}`](../{peer-skill}/SKILL.md)
138
+
139
+ ## When the agent should load this
140
+
141
+ - "{user phrase 1 โ€” concrete paraphrase}"
142
+ - "{user phrase 2}"
143
+ - "{user phrase 3}"
144
+
145
+ ## Output
146
+
147
+ 1. **{artifact-name.md}** โ€” {shape: markdown table / tree / report}
148
+ 2. **{artifact-name-2.md}** โ€” {shape}
149
+ -->
121
150
  ````
122
151
 
123
152
  ## Quality Checklist (5 Skill Killers)
@@ -132,5 +161,5 @@ Before considering a skill complete, verify it passes all 5 checks:
132
161
  - [ ] **K6: Under 500 lines** โ€” if larger, extract reference tables or templates into separate files in the skill folder
133
162
  - [ ] **English only** โ€” all content in English
134
163
  - [ ] **No duplication** โ€” doesn't repeat rules or guidelines that are already enforced elsewhere
135
- - [ ] **No "Related skills" section** โ€” the agent discovers skills via `<available_skills>` descriptions; cross-links waste tokens and create maintenance burden
164
+ - [ ] **No "Related skills" section for mid-tier / untiered skills** โ€” the agent discovers them via `<available_skills>` descriptions; cross-links waste tokens. Senior-tier skills (`tier: senior`) MUST include the block per `skill-quality.md` ยง Senior-Tier Required Structure (linter-enforced).
136
165