@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.
- package/.agent-src/commands/agent-handoff.md +14 -10
- package/.agent-src/commands/chat-history/import.md +170 -0
- package/.agent-src/commands/chat-history/learn.md +178 -0
- package/.agent-src/commands/chat-history/show.md +17 -18
- package/.agent-src/commands/chat-history.md +26 -25
- package/.agent-src/commands/council/default.md +77 -82
- package/.agent-src/commands/create-pr.md +28 -8
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +22 -0
- package/.agent-src/rules/direct-answers.md +11 -2
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +38 -6
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-attribution-footers.md +48 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +2 -1
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +50 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +22 -5
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/ai-council/SKILL.md +4 -5
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +21 -26
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +110 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +8 -4
- package/AGENTS.md +44 -3
- package/CHANGELOG.md +173 -0
- package/README.md +22 -22
- package/config/agent-settings.template.yml +42 -13
- package/config/gitignore-block.txt +4 -4
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +18 -13
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +348 -126
- package/docs/contracts/hook-architecture-v1.md +220 -0
- package/docs/contracts/memory-visibility-v1.md +122 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +2 -1
- package/docs/development.md +4 -1
- package/docs/getting-started.md +21 -29
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/hook-payload-capture.md +221 -0
- package/docs/migrations/commands-1.15.0.md +17 -12
- package/docs/skills-catalog.md +5 -4
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/scripts/agent-config +45 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +25 -9
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/pricing.py +10 -9
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +963 -597
- package/scripts/check_always_budget.py +7 -2
- package/scripts/check_references.py +12 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +407 -0
- package/scripts/hook_manifest.yaml +217 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +383 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +157 -0
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.py +725 -87
- package/scripts/install.sh +38 -1
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +217 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +163 -1
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/verify_before_complete_hook.py +216 -0
- package/.agent-src/commands/chat-history/checkpoint.md +0 -126
- package/.agent-src/commands/chat-history/clear.md +0 -103
- package/.agent-src/commands/chat-history/resume.md +0 -183
- package/.agent-src/rules/chat-history-cadence.md +0 -109
- package/.agent-src/rules/chat-history-ownership.md +0 -123
- package/.agent-src/rules/chat-history-visibility.md +0 -96
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
- 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
|
|
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
|
|