@event4u/agent-config 1.38.0 → 1.40.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/onboard.md +131 -50
- package/.agent-src/commands/orchestrate.md +123 -0
- package/.agent-src/commands/sync-gitignore/fix.md +135 -0
- package/.agent-src/commands/sync-gitignore.md +31 -5
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
- package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/.agent-src/templates/agent-settings.md +9 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
- package/.claude-plugin/marketplace.json +3 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +76 -0
- package/README.md +17 -6
- package/bin/install.php +13 -6
- package/config/agent-settings.template.yml +21 -0
- package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +3 -2
- package/docs/contracts/audit-log-v1.md +142 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +20 -0
- package/docs/contracts/orchestration-dsl-v1.md +152 -0
- package/docs/customization.md +45 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/installation.md +132 -0
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +16 -0
- package/docs/setup/per-ide/aider.md +48 -0
- package/docs/setup/per-ide/claude-code.md +108 -0
- package/docs/setup/per-ide/claude-desktop.md +148 -0
- package/docs/setup/per-ide/cline.md +43 -0
- package/docs/setup/per-ide/codex.md +46 -0
- package/docs/setup/per-ide/copilot.md +80 -0
- package/docs/setup/per-ide/cursor.md +125 -0
- package/docs/setup/per-ide/gemini-cli.md +45 -0
- package/docs/setup/per-ide/windsurf.md +120 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
- package/scripts/compress.py +153 -1
- package/scripts/extract_audit_patterns.py +202 -0
- package/scripts/install +156 -1
- package/scripts/install.py +270 -10
- package/scripts/install.sh +52 -7
- package/scripts/lint_orchestration_dsl.py +214 -0
- package/scripts/skill_linter.py +9 -0
- package/scripts/sync_gitignore.py +56 -1
- package/templates/claude_desktop_config.json.template +21 -0
- package/templates/cursor-rule.mdc.j2 +7 -0
- package/templates/global-install-manifest.yml +91 -0
- package/templates/marketing-copy.yml +64 -0
- package/templates/windsurf-rule.md.j2 +7 -0
|
@@ -240,6 +240,15 @@ prefer the cheaper one (`do-and-judge` < `do-and-judge-two-stage` <
|
|
|
240
240
|
`do-in-steps` < `do-in-parallel` < `do-competitively` <
|
|
241
241
|
`judge-with-debate` < `do-in-worktrees`).
|
|
242
242
|
|
|
243
|
+
**Mode 6 (`do-in-worktrees`) is gated by `worktrees.mode`** from
|
|
244
|
+
`.agent-settings.yml` (default: `ask`). Resolve before picking:
|
|
245
|
+
|
|
246
|
+
| `worktrees.mode` | Mode 6 |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `ask` | Eligible. `using-git-worktrees` will run the per-creation permission ask. |
|
|
249
|
+
| `on` | Eligible. Per-creation ask suppressed. |
|
|
250
|
+
| `off` | **Not eligible.** Fall back to mode 3 (`do-in-steps`) — same step-by-step chain, in-place on the current branch. Unless the user **explicitly asked this turn** for a worktree chain, in which case proceed with mode 6 and acknowledge the override per [`using-git-worktrees § Pre-flight`](../using-git-worktrees/SKILL.md). |
|
|
251
|
+
|
|
243
252
|
### 4. Dispatch
|
|
244
253
|
|
|
245
254
|
Hand off to the matching command:
|
|
@@ -44,6 +44,31 @@ work and makes it impossible to tell what you broke.
|
|
|
44
44
|
|
|
45
45
|
## Procedure
|
|
46
46
|
|
|
47
|
+
### 0. Pre-flight — read `worktrees.mode`
|
|
48
|
+
|
|
49
|
+
Before anything else, read `worktrees.mode` from `.agent-settings.yml`
|
|
50
|
+
(default: `ask`). The setting is a **mechanical layer on top of**
|
|
51
|
+
`scope-control`'s permission gate — it narrows, never widens.
|
|
52
|
+
|
|
53
|
+
| `worktrees.mode` | Behaviour |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `ask` | Status quo. Continue to step 1; `scope-control` permission gate applies for every worktree creation. |
|
|
56
|
+
| `on` | Standing permission. Skip the per-creation permission ask; continue to step 1. Iron-Law gates (ignore-check, clean baseline) still apply. |
|
|
57
|
+
| `off` | No autonomous worktree creation. **Refuse** unless the user explicitly asked **this turn** for a worktree ("do this in a worktree", "use mode 6", "spawn a worktree for X"). |
|
|
58
|
+
|
|
59
|
+
**Off, no explicit request** → stop. Tell the user the setting is `off`,
|
|
60
|
+
suggest the in-place alternative (`subagent-orchestration` mode 3
|
|
61
|
+
`do-in-steps`, or just stay on the current branch). Do not re-ask on
|
|
62
|
+
the same task.
|
|
63
|
+
|
|
64
|
+
**Off, with explicit request this turn** → acknowledge once
|
|
65
|
+
("`worktrees.mode` is `off`; running this on your explicit request
|
|
66
|
+
for this task") and continue to step 1. The override is for this one
|
|
67
|
+
task — it does not flip the setting.
|
|
68
|
+
|
|
69
|
+
The setting only suppresses **unprompted** usage. The tool stays
|
|
70
|
+
available when the user wants it.
|
|
71
|
+
|
|
47
72
|
### 1. Inspect current state
|
|
48
73
|
|
|
49
74
|
Before creating anything, check existing conventions — do not assume:
|
|
@@ -230,6 +230,14 @@ subagents:
|
|
|
230
230
|
# Set to 1 to serialize. Hard cap enforced by runtime.
|
|
231
231
|
max_parallel: 3
|
|
232
232
|
|
|
233
|
+
# --- Git worktrees ---
|
|
234
|
+
worktrees:
|
|
235
|
+
# off | on | ask (default: ask)
|
|
236
|
+
# off = no autonomous worktree creation (explicit user request overrides)
|
|
237
|
+
# on = standing permission (skill skips the per-creation ask)
|
|
238
|
+
# ask = status quo — skill asks before creating
|
|
239
|
+
mode: ask
|
|
240
|
+
|
|
233
241
|
# --- Role modes (see guidelines/agent-infra/role-contracts.md) ---
|
|
234
242
|
roles:
|
|
235
243
|
# Role the agent defaults to at the start of a session.
|
|
@@ -442,6 +450,7 @@ the canonical narrative lives in
|
|
|
442
450
|
| `subagents.implementer_model` | model alias or empty | _(empty)_ | Model for implementer subagents. Empty = same tier as session model. See [subagent-configuration](../contexts/subagent-configuration.md). |
|
|
443
451
|
| `subagents.judge_model` | model alias or empty | _(empty)_ | Model for judge subagents. Empty = one tier above implementer (opus if sonnet, sonnet if haiku). |
|
|
444
452
|
| `subagents.max_parallel` | integer | `3` | Maximum parallel subagent invocations. `1` serializes. |
|
|
453
|
+
| `worktrees.mode` | `off`, `on`, `ask` | `ask` | Controls autonomous `git worktree` usage. `off` = skill refuses unless the user explicitly asks for a worktree that turn (then it runs); `subagent-orchestration` mode 6 falls back to mode 3. `on` = standing permission (skill skips the per-creation ask; ignore-check and clean-baseline gates still apply). `ask` = status quo — `scope-control` permission gate runs every time. |
|
|
445
454
|
| `roles.default_role` | `""`, `developer`, `reviewer`, `tester`, `po`, `incident`, `planner` | _(empty)_ | Role the agent defaults to at the start of a session. See [`role-contracts`](../docs/guidelines/agent-infra/role-contracts.md). |
|
|
446
455
|
| `roles.active_role` | same as `default_role` | _(empty)_ | Role currently active; set by `/mode <name>`, cleared by `/mode none`. Enables the `role-mode-adherence` rule. |
|
|
447
456
|
| `personas.override` | list of persona ids | `[]` | Developer-local override of the team default lens cast. Empty = inherit `personas.default` from `.agent-project-settings.yml`. See [`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md). |
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Precedence (lowest → highest):
|
|
8
8
|
# 1. Package defaults (shipped by event4u/agent-config)
|
|
9
|
-
# 2.
|
|
10
|
-
#
|
|
9
|
+
# 2. ~/.config/agent-config/agent-settings.yml — user-global DX-comfort
|
|
10
|
+
# defaults (whitelist: name, ide, cost_profile, personal.bot_icon,
|
|
11
|
+
# personal.autonomy, caveman.speak_scope). Created on opt-in via
|
|
12
|
+
# /onboard; project-local files always win.
|
|
13
|
+
# 3. This file (.agent-project-settings.yml) — team defaults
|
|
14
|
+
# 4. .agent-settings.yml — developer overrides (gitignored)
|
|
11
15
|
#
|
|
12
16
|
# Any key marked `locked: true` in this file CANNOT be overridden by
|
|
13
17
|
# .agent-settings.yml. Use sparingly — locked keys reduce developer
|
|
14
18
|
# autonomy. Reserve for compliance or correctness concerns (e.g.
|
|
15
19
|
# forcing a test framework, pinning a coding style).
|
|
16
20
|
#
|
|
21
|
+
# Full precedence model + user-global whitelist contract:
|
|
22
|
+
# docs/guidelines/agent-infra/layered-settings.md
|
|
23
|
+
#
|
|
17
24
|
# Copy this file to `.agent-project-settings.yml` (drop the `.example`)
|
|
18
25
|
# and commit it.
|
|
19
26
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Internal helpers shared across work_engine submodules.
|
|
2
|
+
|
|
3
|
+
Currently houses :mod:`agent_settings` — the byte-identical mirror of
|
|
4
|
+
``scripts/_lib/agent_settings.py`` from the agent-config package. The
|
|
5
|
+
parity test in ``tests/test_template_agent_settings_parity.py`` guards
|
|
6
|
+
the two files against drift.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Centralized loader for ``.agent-settings.yml`` with user-global fallback.
|
|
2
|
+
|
|
3
|
+
Phase 1 of road-to-portable-dev-preferences. Single source of truth for
|
|
4
|
+
how scripts read agent settings — replaces ~15 ad-hoc loaders in P3.
|
|
5
|
+
|
|
6
|
+
Resolution order (project wins, user-global fills gaps for whitelisted
|
|
7
|
+
keys only):
|
|
8
|
+
|
|
9
|
+
1. Project ``./.agent-settings.yml`` (full file, all keys)
|
|
10
|
+
2. ``~/.config/agent-config/agent-settings.yml`` (whitelist only)
|
|
11
|
+
3. Built-in defaults (currently empty)
|
|
12
|
+
|
|
13
|
+
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
14
|
+
non-whitelisted key in the user-global file is silently ignored — the
|
|
15
|
+
``verbose=True`` flag surfaces ignored paths via ``logging.info`` for
|
|
16
|
+
debugging.
|
|
17
|
+
|
|
18
|
+
Contract — pure, read-only, tolerant:
|
|
19
|
+
|
|
20
|
+
* Lazy PyYAML import; no yaml installed → defaults returned.
|
|
21
|
+
* Missing project file → user-global + defaults.
|
|
22
|
+
* Missing user-global file → project + defaults.
|
|
23
|
+
* Both missing → defaults.
|
|
24
|
+
* Malformed YAML / unreadable file → defaults, logged at WARNING.
|
|
25
|
+
* No file is ever created or written by this module.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_PROJECT_FILE = ".agent-settings.yml"
|
|
36
|
+
DEFAULT_USER_GLOBAL_FILE = (
|
|
37
|
+
Path.home() / ".config" / "agent-config" / "agent-settings.yml"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#: Exact dotted paths allowed to cascade from user-global into the merged
|
|
41
|
+
#: settings. Anything not listed here is silently ignored when present in
|
|
42
|
+
#: the user-global file. Adding a key requires an ADR — see
|
|
43
|
+
#: ``agents/roadmaps/road-to-portable-dev-preferences.md``.
|
|
44
|
+
MERGEABLE_KEYS: tuple[str, ...] = (
|
|
45
|
+
"name",
|
|
46
|
+
"ide",
|
|
47
|
+
"cost_profile",
|
|
48
|
+
"personal.bot_icon",
|
|
49
|
+
"personal.autonomy",
|
|
50
|
+
"caveman.speak_scope",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_DEFAULTS: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_agent_settings(
|
|
57
|
+
project_path: Path | str | None = None,
|
|
58
|
+
user_global_path: Path | str | None = None,
|
|
59
|
+
verbose: bool = False,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Return the merged settings dict.
|
|
62
|
+
|
|
63
|
+
``project_path`` defaults to ``./.agent-settings.yml`` (CWD-relative).
|
|
64
|
+
``user_global_path`` defaults to
|
|
65
|
+
``~/.config/agent-config/agent-settings.yml``. Both arguments accept
|
|
66
|
+
``Path`` or ``str``. Pass ``verbose=True`` to log keys present in
|
|
67
|
+
user-global that are not on the whitelist.
|
|
68
|
+
"""
|
|
69
|
+
project = _read_yaml(
|
|
70
|
+
Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE),
|
|
71
|
+
) or {}
|
|
72
|
+
user_global_raw = _read_yaml(
|
|
73
|
+
Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE,
|
|
74
|
+
) or {}
|
|
75
|
+
|
|
76
|
+
user_global_filtered, ignored = _filter_whitelist(
|
|
77
|
+
user_global_raw, MERGEABLE_KEYS,
|
|
78
|
+
)
|
|
79
|
+
if verbose and ignored:
|
|
80
|
+
logger.info(
|
|
81
|
+
"agent_settings: ignored non-whitelisted user-global keys: %s",
|
|
82
|
+
sorted(ignored),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
merged: dict[str, Any] = _deep_copy_defaults(_DEFAULTS)
|
|
86
|
+
_deep_merge(merged, user_global_filtered)
|
|
87
|
+
_deep_merge(merged, project)
|
|
88
|
+
return merged
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
92
|
+
"""Best-effort YAML read; never raises. Returns ``None`` on any failure."""
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
import yaml # type: ignore[import-untyped]
|
|
97
|
+
except ImportError:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
with path.open(encoding="utf-8") as fh:
|
|
101
|
+
data = yaml.safe_load(fh) or {}
|
|
102
|
+
except (OSError, yaml.YAMLError):
|
|
103
|
+
logger.warning("agent_settings: unreadable or malformed YAML at %s", path)
|
|
104
|
+
return None
|
|
105
|
+
return data if isinstance(data, dict) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _filter_whitelist(
|
|
109
|
+
raw: dict[str, Any], allowed: tuple[str, ...],
|
|
110
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
111
|
+
"""Return ``(filtered_dict, ignored_paths)`` from a user-global blob."""
|
|
112
|
+
filtered: dict[str, Any] = {}
|
|
113
|
+
for dotted in allowed:
|
|
114
|
+
value = _get_dotted(raw, dotted)
|
|
115
|
+
if value is not None:
|
|
116
|
+
_set_dotted(filtered, dotted, value)
|
|
117
|
+
ignored = [p for p in _leaf_paths(raw) if p not in allowed]
|
|
118
|
+
return filtered, ignored
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
|
|
122
|
+
cursor: Any = data
|
|
123
|
+
for part in dotted.split("."):
|
|
124
|
+
if not isinstance(cursor, dict) or part not in cursor:
|
|
125
|
+
return None
|
|
126
|
+
cursor = cursor[part]
|
|
127
|
+
return cursor
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _set_dotted(target: dict[str, Any], dotted: str, value: Any) -> None:
|
|
131
|
+
parts = dotted.split(".")
|
|
132
|
+
cursor = target
|
|
133
|
+
for part in parts[:-1]:
|
|
134
|
+
nxt = cursor.setdefault(part, {})
|
|
135
|
+
if not isinstance(nxt, dict):
|
|
136
|
+
nxt = {}
|
|
137
|
+
cursor[part] = nxt
|
|
138
|
+
cursor = nxt
|
|
139
|
+
cursor[parts[-1]] = value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _leaf_paths(data: dict[str, Any], prefix: str = "") -> list[str]:
|
|
143
|
+
paths: list[str] = []
|
|
144
|
+
for key, value in data.items():
|
|
145
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
146
|
+
if isinstance(value, dict) and value:
|
|
147
|
+
paths.extend(_leaf_paths(value, path))
|
|
148
|
+
else:
|
|
149
|
+
paths.append(path)
|
|
150
|
+
return paths
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
|
154
|
+
"""Merge ``src`` into ``dst`` in-place; nested dicts are merged recursively."""
|
|
155
|
+
for key, value in src.items():
|
|
156
|
+
if (
|
|
157
|
+
isinstance(value, dict)
|
|
158
|
+
and isinstance(dst.get(key), dict)
|
|
159
|
+
):
|
|
160
|
+
_deep_merge(dst[key], value)
|
|
161
|
+
else:
|
|
162
|
+
dst[key] = value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _deep_copy_defaults(src: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
out: dict[str, Any] = {}
|
|
167
|
+
_deep_merge(out, src)
|
|
168
|
+
return out
|
|
@@ -13,6 +13,12 @@ settings.py``):
|
|
|
13
13
|
* Chat-history hooks gate on **two** switches: ``hooks.chat_history.
|
|
14
14
|
enabled`` AND the global ``chat_history.enabled``. Either off → no
|
|
15
15
|
chat-history hook registers.
|
|
16
|
+
|
|
17
|
+
Per road-to-portable-dev-preferences P3, the YAML read goes through
|
|
18
|
+
:func:`work_engine._lib.agent_settings.load_agent_settings`, which
|
|
19
|
+
cascades the whitelisted ``cost_profile`` (and other DX-comfort keys)
|
|
20
|
+
from ``~/.config/agent-config/agent-settings.yml`` when the project
|
|
21
|
+
file omits them. Project values always win.
|
|
16
22
|
"""
|
|
17
23
|
from __future__ import annotations
|
|
18
24
|
|
|
@@ -20,6 +26,8 @@ from dataclasses import dataclass
|
|
|
20
26
|
from pathlib import Path
|
|
21
27
|
from typing import Any
|
|
22
28
|
|
|
29
|
+
from work_engine._lib.agent_settings import load_agent_settings
|
|
30
|
+
|
|
23
31
|
DEFAULT_SETTINGS_FILE = ".agent-settings.yml"
|
|
24
32
|
DEFAULT_CHAT_HISTORY_SCRIPT = "scripts/chat_history.py"
|
|
25
33
|
|
|
@@ -52,36 +60,27 @@ _DEFAULT = HookSettings()
|
|
|
52
60
|
|
|
53
61
|
def load_hook_settings(
|
|
54
62
|
settings_path: Path | str | None = None,
|
|
63
|
+
user_global_path: Path | str | None = None,
|
|
55
64
|
) -> HookSettings:
|
|
56
65
|
"""Return :class:`HookSettings` hydrated from ``.agent-settings.yml``.
|
|
57
66
|
|
|
58
67
|
``settings_path`` defaults to ``./.agent-settings.yml`` relative to
|
|
59
68
|
the current working directory — same convention as chat-history.
|
|
69
|
+
``user_global_path`` defaults to
|
|
70
|
+
``~/.config/agent-config/agent-settings.yml`` and only cascades the
|
|
71
|
+
whitelisted DX-comfort keys (currently ``cost_profile``) when the
|
|
72
|
+
project file omits them. See road-to-portable-dev-preferences P3.
|
|
60
73
|
"""
|
|
61
74
|
path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
|
|
62
|
-
raw =
|
|
63
|
-
|
|
75
|
+
raw = load_agent_settings(
|
|
76
|
+
project_path=path,
|
|
77
|
+
user_global_path=user_global_path,
|
|
78
|
+
)
|
|
79
|
+
if not raw:
|
|
64
80
|
return _DEFAULT
|
|
65
81
|
return _settings_from_raw(raw)
|
|
66
82
|
|
|
67
83
|
|
|
68
|
-
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
69
|
-
if not path.is_file():
|
|
70
|
-
return None
|
|
71
|
-
try:
|
|
72
|
-
import yaml # type: ignore[import-untyped]
|
|
73
|
-
except ImportError:
|
|
74
|
-
return None
|
|
75
|
-
try:
|
|
76
|
-
with path.open(encoding="utf-8") as fh:
|
|
77
|
-
data = yaml.safe_load(fh) or {}
|
|
78
|
-
except (OSError, yaml.YAMLError):
|
|
79
|
-
return None
|
|
80
|
-
if not isinstance(data, dict):
|
|
81
|
-
return None
|
|
82
|
-
return data
|
|
83
|
-
|
|
84
|
-
|
|
85
84
|
def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
|
|
86
85
|
hooks = data.get("hooks")
|
|
87
86
|
if not isinstance(hooks, dict):
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""State machine stub for the ``/orchestrate`` command.
|
|
2
|
+
|
|
3
|
+
Reads a pipeline file conforming to
|
|
4
|
+
``docs/contracts/orchestration-dsl-v1.md`` and produces an ordered
|
|
5
|
+
sequence of step descriptors the agent dispatches one at a time.
|
|
6
|
+
The runtime itself is **not** in Python — each step is executed by the
|
|
7
|
+
agent via skill / command / persona / subagent dispatch. This module
|
|
8
|
+
holds the deterministic bookkeeping:
|
|
9
|
+
|
|
10
|
+
- load + interpolate
|
|
11
|
+
- step iteration with success / failure / when-guard tracking
|
|
12
|
+
- output-map resolution at the end
|
|
13
|
+
|
|
14
|
+
Design constraints (R1 carve-outs from
|
|
15
|
+
``road-to-distribution-and-adoption.md``):
|
|
16
|
+
|
|
17
|
+
- No external dependencies. YAML loading reuses the dispatcher's
|
|
18
|
+
loader so the runtime sees what the linter sees.
|
|
19
|
+
- No side effects. The state machine never edits files, runs commands,
|
|
20
|
+
or emits hooks of its own. Audit emission is the caller's job.
|
|
21
|
+
- Forward-ref free. ``steps[].with`` references can only reach
|
|
22
|
+
earlier steps; this is enforced both by the linter and at runtime.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Iterator
|
|
30
|
+
|
|
31
|
+
_INTERP_RE = re.compile(
|
|
32
|
+
r"\$\{\{\s*(inputs|steps)\.([a-z0-9_-]+)(?:\.output)?\s*\}\}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class StepResult:
|
|
38
|
+
"""One step's record after dispatch."""
|
|
39
|
+
step_id: str
|
|
40
|
+
kind: str
|
|
41
|
+
ref: str
|
|
42
|
+
success: bool = False
|
|
43
|
+
output: str = ""
|
|
44
|
+
error: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class PipelineState:
|
|
49
|
+
"""Bookkeeping for a single ``/orchestrate`` run."""
|
|
50
|
+
name: str
|
|
51
|
+
inputs: dict[str, str]
|
|
52
|
+
results: dict[str, StepResult] = field(default_factory=dict)
|
|
53
|
+
halted: bool = False
|
|
54
|
+
halt_reason: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_pipeline(path: Path) -> dict[str, Any]:
|
|
58
|
+
"""Reuse the linter's loader so the runtime accepts the same shape.
|
|
59
|
+
|
|
60
|
+
Walks parents to find a directory containing ``scripts/hooks/``
|
|
61
|
+
so the loader is reachable both when this module runs from the
|
|
62
|
+
consumer projection (``.agent-src/templates/scripts/work_engine/``)
|
|
63
|
+
and from the source-of-truth tree
|
|
64
|
+
(``.agent-src.uncompressed/templates/scripts/work_engine/``).
|
|
65
|
+
"""
|
|
66
|
+
import sys
|
|
67
|
+
here = Path(__file__).resolve()
|
|
68
|
+
for parent in here.parents:
|
|
69
|
+
candidate = parent / "scripts" / "hooks" / "dispatch_hook.py"
|
|
70
|
+
if candidate.is_file():
|
|
71
|
+
sys.path.insert(0, str(parent / "scripts"))
|
|
72
|
+
break
|
|
73
|
+
from hooks.dispatch_hook import _load_yaml # noqa: E402
|
|
74
|
+
doc = _load_yaml(path)
|
|
75
|
+
if not isinstance(doc, dict):
|
|
76
|
+
raise ValueError(f"{path}: top-level must be a mapping")
|
|
77
|
+
return doc
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _interpolate(value: Any, state: PipelineState) -> Any:
|
|
81
|
+
"""Substitute ``${{ inputs.X }}`` / ``${{ steps.Y.output }}`` in a
|
|
82
|
+
nested value. Unknown references raise — the linter should have
|
|
83
|
+
caught them, but the runtime double-checks."""
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
def replace(match: re.Match[str]) -> str:
|
|
86
|
+
ns, ident = match.group(1), match.group(2)
|
|
87
|
+
if ns == "inputs":
|
|
88
|
+
if ident not in state.inputs:
|
|
89
|
+
raise KeyError(f"unknown input '{ident}'")
|
|
90
|
+
return state.inputs[ident]
|
|
91
|
+
if ident not in state.results:
|
|
92
|
+
raise KeyError(f"unknown step '{ident}'")
|
|
93
|
+
return state.results[ident].output
|
|
94
|
+
return _INTERP_RE.sub(replace, value)
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
return {k: _interpolate(v, state) for k, v in value.items()}
|
|
97
|
+
if isinstance(value, list):
|
|
98
|
+
return [_interpolate(v, state) for v in value]
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _when_passes(when: str | None, state: PipelineState) -> bool:
|
|
103
|
+
"""Evaluate the limited ``when`` mini-language. Supports
|
|
104
|
+
``steps.X.success`` / ``steps.X.failure`` and equality on a
|
|
105
|
+
single ``${{ steps.X.output }}`` template against a literal."""
|
|
106
|
+
if not when:
|
|
107
|
+
return True
|
|
108
|
+
when = when.strip()
|
|
109
|
+
m = re.fullmatch(r"steps\.([a-z0-9_-]+)\.(success|failure)", when)
|
|
110
|
+
if m:
|
|
111
|
+
sid, kind = m.group(1), m.group(2)
|
|
112
|
+
if sid not in state.results:
|
|
113
|
+
return False
|
|
114
|
+
return state.results[sid].success if kind == "success" else not state.results[sid].success
|
|
115
|
+
m = re.fullmatch(r'\$\{\{\s*steps\.([a-z0-9_-]+)\.output\s*\}\}\s*==\s*"([^"]*)"', when)
|
|
116
|
+
if m:
|
|
117
|
+
sid, literal = m.group(1), m.group(2)
|
|
118
|
+
return state.results.get(sid, StepResult(sid, "", "")).output == literal
|
|
119
|
+
raise ValueError(f"unsupported when expression: {when!r}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def iter_steps(path: Path, inputs: dict[str, str]) -> Iterator[dict[str, Any]]:
|
|
123
|
+
"""Yield interpolated step descriptors in order.
|
|
124
|
+
|
|
125
|
+
Caller dispatches each descriptor via skill / command / persona /
|
|
126
|
+
subagent and feeds the result back via :func:`record_result`.
|
|
127
|
+
"""
|
|
128
|
+
doc = _load_pipeline(path)
|
|
129
|
+
merged_inputs = {
|
|
130
|
+
inp["id"]: inputs.get(inp["id"], inp.get("default", ""))
|
|
131
|
+
for inp in (doc.get("inputs") or [])
|
|
132
|
+
if isinstance(inp, dict) and isinstance(inp.get("id"), str)
|
|
133
|
+
}
|
|
134
|
+
state = PipelineState(name=doc.get("name", ""), inputs=merged_inputs)
|
|
135
|
+
for step in doc.get("steps") or []:
|
|
136
|
+
if state.halted:
|
|
137
|
+
break
|
|
138
|
+
if not _when_passes(step.get("when"), state):
|
|
139
|
+
continue
|
|
140
|
+
yield {
|
|
141
|
+
"id": step["id"],
|
|
142
|
+
"kind": step["kind"],
|
|
143
|
+
"ref": step["ref"],
|
|
144
|
+
"with": _interpolate(step.get("with") or {}, state),
|
|
145
|
+
"_state": state,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def record_result(descriptor: dict[str, Any], *, success: bool, output: str = "", error: str | None = None) -> None:
|
|
150
|
+
"""Caller hands the descriptor + outcome back so subsequent steps
|
|
151
|
+
can see ``${{ steps.<id>.output }}``."""
|
|
152
|
+
state: PipelineState = descriptor["_state"]
|
|
153
|
+
state.results[descriptor["id"]] = StepResult(
|
|
154
|
+
step_id=descriptor["id"], kind=descriptor["kind"], ref=descriptor["ref"],
|
|
155
|
+
success=success, output=output, error=error,
|
|
156
|
+
)
|
|
157
|
+
if not success:
|
|
158
|
+
state.halted = True
|
|
159
|
+
state.halt_reason = f"step {descriptor['id']} failed"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve_outputs(path: Path, state: PipelineState) -> dict[str, str]:
|
|
163
|
+
"""Resolve the pipeline's ``outputs:`` map against the captured
|
|
164
|
+
step outputs. Returns an empty map if the pipeline declares no
|
|
165
|
+
outputs."""
|
|
166
|
+
doc = _load_pipeline(path)
|
|
167
|
+
raw = doc.get("outputs") or {}
|
|
168
|
+
return {k: _interpolate(v, state) for k, v in raw.items()}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Shared agent configuration \u2014 skills for AI coding tools (Claude Code, Augment, Cursor, Cline, Windsurf, Gemini CLI).",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.40.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
"./.claude/skills/optimize-prompt",
|
|
185
185
|
"./.claude/skills/optimize-rtk",
|
|
186
186
|
"./.claude/skills/optimize-skills",
|
|
187
|
+
"./.claude/skills/orchestrate",
|
|
187
188
|
"./.claude/skills/override",
|
|
188
189
|
"./.claude/skills/override-create",
|
|
189
190
|
"./.claude/skills/override-manage",
|
|
@@ -262,6 +263,7 @@
|
|
|
262
263
|
"./.claude/skills/subagent-orchestration",
|
|
263
264
|
"./.claude/skills/sync-agent-settings",
|
|
264
265
|
"./.claude/skills/sync-gitignore",
|
|
266
|
+
"./.claude/skills/sync-gitignore-fix",
|
|
265
267
|
"./.claude/skills/systematic-debugging",
|
|
266
268
|
"./.claude/skills/tailwind-engineer",
|
|
267
269
|
"./.claude/skills/tech-debt-tracker",
|
package/AGENTS.md
CHANGED
|
@@ -16,12 +16,12 @@ task ci # full pipeline — green before PR
|
|
|
16
16
|
|
|
17
17
|
## Pointers
|
|
18
18
|
|
|
19
|
-
- **Package self-orientation** — identity, four-wing cognition map, repo layout, tech stack, key-rules table, telemetry, command-suggester: [`docs/contracts/package-self-orientation.md`](docs/contracts/package-self-orientation.md).
|
|
20
|
-
- **Kernel + Router** — 9 always-loaded Iron-Law rules, tier-1 / tier-2 routing, cost profiles, per-rule char caps enforced by `task lint-rule-budget`: [`kernel-membership`](docs/contracts/kernel-membership.md) + [`rule-router`](docs/contracts/rule-router.md).
|
|
19
|
+
- **Package self-orientation** (beta) — identity, four-wing cognition map, repo layout, tech stack, key-rules table, telemetry, command-suggester: [`docs/contracts/package-self-orientation.md`](docs/contracts/package-self-orientation.md).
|
|
20
|
+
- **Kernel + Router** (beta) — 9 always-loaded Iron-Law rules, tier-1 / tier-2 routing, cost profiles, per-rule char caps enforced by `task lint-rule-budget`: [`kernel-membership`](docs/contracts/kernel-membership.md) + [`rule-router`](docs/contracts/rule-router.md).
|
|
21
21
|
- **Multi-tool projection** — Augment, Claude Code, Cursor, Cline, Windsurf, Gemini CLI, Claude.ai bundle pipeline that ships from `.agent-src/` to consumer surfaces: [`docs/architecture.md`](docs/architecture.md#cloud-bundle-pipeline).
|
|
22
22
|
- **Editing this repo** — Iron-Law rules (portability, source-of-truth, skill-quality) and the Thin-Root contract (caps · pointer-ratio · triage block) governing AGENTS.md: [`augment-portability`](.agent-src/rules/augment-portability.md), [`augment-source-of-truth`](.agent-src/rules/augment-source-of-truth.md), [`skill-quality`](.agent-src/rules/skill-quality.md), [`agents-md-thin-root`](.agent-src/skills/agents-md-thin-root/SKILL.md).
|
|
23
|
-
- **Consumer story + architecture deep-dive** —
|
|
24
|
-
- **Personas** — 11 review-lens cast (6 core · 5 specialist), `personas:` vs `/mode` axes, citation map, override pattern: [`docs/personas.md`](docs/personas.md), schema [`docs/contracts/persona-schema.md`](docs/contracts/persona-schema.md).
|
|
23
|
+
- **Consumer story + architecture deep-dive** — install story + ship pipeline: [`README.md`](README.md), [`docs/architecture.md`](docs/architecture.md).
|
|
24
|
+
- **Personas** — 11 review-lens cast (6 core · 5 specialist), `personas:` vs `/mode` axes, citation map, override pattern: [`docs/personas.md`](docs/personas.md), schema [`docs/contracts/persona-schema.md`](docs/contracts/persona-schema.md) (beta).
|
|
25
25
|
|
|
26
26
|
## Emergency triage — read this when nothing else is reachable
|
|
27
27
|
|
package/CHANGELOG.md
CHANGED
|
@@ -318,6 +318,82 @@ our recommendation order, not its support status.
|
|
|
318
318
|
users" tension without removing any path that an existing user
|
|
319
319
|
might rely on.
|
|
320
320
|
|
|
321
|
+
## [1.40.0](https://github.com/event4u-app/agent-config/compare/1.39.0...1.40.0) (2026-05-11)
|
|
322
|
+
|
|
323
|
+
### Features
|
|
324
|
+
|
|
325
|
+
* **settings:** add worktrees.mode (off/on/ask) to gate autonomous worktree usage ([a07080f](https://github.com/event4u-app/agent-config/commit/a07080f28113dfa2452830b660deb4cb467256ec))
|
|
326
|
+
* **commands:** add /sync-gitignore:fix subcommand for legacy cleanup ([5b058b0](https://github.com/event4u-app/agent-config/commit/5b058b0a8a5b451eac89c4905076e4cc9773e299))
|
|
327
|
+
* **audit:** add pattern-extraction script for audit-log-v1 (roadmap Q2) ([966e472](https://github.com/event4u-app/agent-config/commit/966e47215eb80cfff6e2229b90c6e28b806c7dff))
|
|
328
|
+
* **learning-skill:** wire audit-log-v1 into learning-to-rule-or-skill (roadmap Q3) ([f53d129](https://github.com/event4u-app/agent-config/commit/f53d12947b41334c58952b3a6017962507abbe45))
|
|
329
|
+
* **orchestrate:** add /orchestrate command + state machine (roadmap G2) ([4b39691](https://github.com/event4u-app/agent-config/commit/4b396910c0c7078430aa11958f121d49ddda766c))
|
|
330
|
+
* **contracts:** add orchestration-dsl-v1 + linter (roadmap G1) ([5cb2801](https://github.com/event4u-app/agent-config/commit/5cb2801f14a06d2dabe9f2dc57b7ca424d7e4d4d))
|
|
331
|
+
* **contracts:** add audit-log-v1 schema (roadmap Q1) ([e0849b2](https://github.com/event4u-app/agent-config/commit/e0849b2305949ad5409ff9981970922ec702c68e))
|
|
332
|
+
* **install:** one-liner setup.sh + npm wrapper refinements ([aee688f](https://github.com/event4u-app/agent-config/commit/aee688fcbf9bb18b0253bc7ce96192cc7e5afe2e))
|
|
333
|
+
* **compress:** project Cursor .mdc + Windsurf .md rules + flatten commands ([486bafd](https://github.com/event4u-app/agent-config/commit/486bafd83c1f5224ee61b29b46cc827b21440623))
|
|
334
|
+
* **install:** multi-tool installer + global user-level install ([6113872](https://github.com/event4u-app/agent-config/commit/6113872b146321b784b01a428c5c2abcbad9e08d))
|
|
335
|
+
|
|
336
|
+
### Bug Fixes
|
|
337
|
+
|
|
338
|
+
* **ci:** bump README command count and exclude projection dirs from skill-lint ([c5a1493](https://github.com/event4u-app/agent-config/commit/c5a1493d4ef31b2e9af16e0945330cccaa02576c))
|
|
339
|
+
* **clusters:** register /orchestrate as new cluster head ([fb73e58](https://github.com/event4u-app/agent-config/commit/fb73e58c7eb72d53e6ee111a18fe8f5a1f1e053b))
|
|
340
|
+
* **roadmap:** annotate council reference with allowed-marker ([e2997d0](https://github.com/event4u-app/agent-config/commit/e2997d00b5e7dc2625367f87dab3bc611b9cf8e5))
|
|
341
|
+
* **readme:** unlink experimental mcp-cloud-scope contract ([c7ac883](https://github.com/event4u-app/agent-config/commit/c7ac883be116e6f92f2125d5334f38f17b97da27))
|
|
342
|
+
|
|
343
|
+
### Documentation
|
|
344
|
+
|
|
345
|
+
* bump command count to 105 for /orchestrate ([df49c6c](https://github.com/event4u-app/agent-config/commit/df49c6c268e08df7567128c97cc267fed9e38694))
|
|
346
|
+
|
|
347
|
+
### Refactoring
|
|
348
|
+
|
|
349
|
+
* **roadmap:** apply council shape-fix to distribution roadmap ([4c7f770](https://github.com/event4u-app/agent-config/commit/4c7f770d66b07107164c2eafcc886ddf6bf29aa6))
|
|
350
|
+
* **distribution:** extract marketplace listings to docs/DISTRIBUTION_CHECKLIST.md ([5fc6075](https://github.com/event4u-app/agent-config/commit/5fc607567a96bc5e3d9a38d276ce85f88dae7015))
|
|
351
|
+
* **roadmap:** move S33 (screencasts) to distribution roadmap H5 ([95d27f6](https://github.com/event4u-app/agent-config/commit/95d27f65b577824d9c330478af5c7c7acddfe9f3))
|
|
352
|
+
|
|
353
|
+
### Chores
|
|
354
|
+
|
|
355
|
+
* **ownership:** regenerate ownership matrix for new files ([f728343](https://github.com/event4u-app/agent-config/commit/f728343cd4bc9a7340bb19a218d1ccdc6b5bfb9e))
|
|
356
|
+
* **index:** tag orchestrate command with cluster in catalogs ([c4ed9ee](https://github.com/event4u-app/agent-config/commit/c4ed9ee9eaca972bc35549330131c98dd6b05966))
|
|
357
|
+
* **index:** regenerate catalog for /orchestrate command ([c6fe13a](https://github.com/event4u-app/agent-config/commit/c6fe13ae9d83e142154226235bf65d1d2f72cacd))
|
|
358
|
+
* **roadmaps:** regen dashboard after distribution-roadmap shape-fix ([e0ad5b3](https://github.com/event4u-app/agent-config/commit/e0ad5b3554d34a4eaaf747b0955cc16e32f5c8e2))
|
|
359
|
+
* **roadmaps:** archive road-to-simplicity-and-everywhere + regen dashboard ([d2d5c31](https://github.com/event4u-app/agent-config/commit/d2d5c310db1ec8fd3cd44111400b098c4d99c856))
|
|
360
|
+
|
|
361
|
+
Tests: 3141 (+442 since 1.39.0)
|
|
362
|
+
|
|
363
|
+
## [1.39.0](https://github.com/event4u-app/agent-config/compare/1.38.0...1.39.0) (2026-05-11)
|
|
364
|
+
|
|
365
|
+
### Features
|
|
366
|
+
|
|
367
|
+
* **onboard,docs:** wire user-global DX defaults into onboarding + docs ([761a969](https://github.com/event4u-app/agent-config/commit/761a96979c42b880d83f72ccd2ac2d752ae47f0b))
|
|
368
|
+
* **settings:** add centralized agent-settings loader ([d5699be](https://github.com/event4u-app/agent-config/commit/d5699bee9ebb47dd2f1d78716e1ff84a7139b5da))
|
|
369
|
+
|
|
370
|
+
### Bug Fixes
|
|
371
|
+
|
|
372
|
+
* **ci:** grant contents:write to deploy-mcp-worker for release comment ([fb5c895](https://github.com/event4u-app/agent-config/commit/fb5c89537daef55e5dde12bfe85cc703bfa181ed))
|
|
373
|
+
|
|
374
|
+
### Documentation
|
|
375
|
+
|
|
376
|
+
* **roadmap:** add road-to-simplicity-and-everywhere (highest prio) ([417a8fa](https://github.com/event4u-app/agent-config/commit/417a8fa9b7059dac783d3bc9423c64a994a9421a))
|
|
377
|
+
* **roadmap:** add road-to-mcp-full-coverage (Discovery-First) ([fabf897](https://github.com/event4u-app/agent-config/commit/fabf89761322a437b39178366e45f68efbdd7924))
|
|
378
|
+
* **mcp-cloud:** clarify Lite-vs-Full scope at endpoint surface ([97c4684](https://github.com/event4u-app/agent-config/commit/97c4684d07be4fe49c248a1ee0a60cd36b82e071))
|
|
379
|
+
* **stability:** align public surface with stability markers ([d9afe4a](https://github.com/event4u-app/agent-config/commit/d9afe4ad67aad80e2bb12b1c707189f3ee8f47c2))
|
|
380
|
+
* **mcp:** clarify .agent-settings.yml vs. MCP client config ([27a77b2](https://github.com/event4u-app/agent-config/commit/27a77b2459cad1faff12cd8a237e59091ec687de))
|
|
381
|
+
* **mcp:** add per-client setup guide for hosted Remote MCP ([179da38](https://github.com/event4u-app/agent-config/commit/179da38631ea9693a94f259fb1edd2746b89425d))
|
|
382
|
+
|
|
383
|
+
### Refactoring
|
|
384
|
+
|
|
385
|
+
* **work-engine:** centralize agent-settings loading in shared _lib ([3c548bb](https://github.com/event4u-app/agent-config/commit/3c548bb72d3c2022362050a4c88ebe412bf7a897))
|
|
386
|
+
|
|
387
|
+
### Chores
|
|
388
|
+
|
|
389
|
+
* **compress:** regenerate commands/onboard.md after onboard,docs source edit ([e676915](https://github.com/event4u-app/agent-config/commit/e67691564405f05fba033f29b0df696e25788dd0))
|
|
390
|
+
|
|
391
|
+
### Other
|
|
392
|
+
|
|
393
|
+
* add portable-dev-preferences (3 phases, refined via AI council) ([7468a4c](https://github.com/event4u-app/agent-config/commit/7468a4cd2d352d6bfcd7797a8dd8b6ed8ec8f27a))
|
|
394
|
+
|
|
395
|
+
Tests: 2699 (+20 since 1.38.0)
|
|
396
|
+
|
|
321
397
|
## [1.38.0](https://github.com/event4u-app/agent-config/compare/1.37.0...1.38.0) (2026-05-11)
|
|
322
398
|
|
|
323
399
|
### Features
|