@event4u/agent-config 2.15.0 → 2.17.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/ghostwriter/delete.md +118 -0
- package/.agent-src/commands/ghostwriter/fetch.md +185 -0
- package/.agent-src/commands/ghostwriter/list.md +102 -0
- package/.agent-src/commands/ghostwriter/show.md +113 -0
- package/.agent-src/commands/ghostwriter/write.md +160 -0
- package/.agent-src/commands/ghostwriter.md +96 -0
- package/.agent-src/commands/post-as/ghostwriter.md +66 -0
- package/.agent-src/commands/post-as/me.md +124 -0
- package/.agent-src/commands/post-as.md +58 -0
- package/.agent-src/ghostwriter/README.md +61 -0
- package/.agent-src/ghostwriter/fictional-fixture-v1.md +94 -0
- package/.agent-src/personas/README.md +8 -0
- package/.agent-src/rules/domain-safety-disclaimer-consulting.md +52 -0
- package/.agent-src/rules/domain-safety-disclaimer-financial.md +54 -0
- package/.agent-src/rules/domain-safety-disclaimer-legal.md +49 -0
- package/.agent-src/rules/domain-safety-disclaimer-medical.md +56 -0
- package/.agent-src/rules/domain-safety-export-redact.md +65 -0
- package/.agent-src/rules/domain-safety-logging-pii-floor.md +55 -0
- package/.agent-src/rules/domain-safety-pii-finance.md +57 -0
- package/.agent-src/rules/domain-safety-pii-marketing.md +60 -0
- package/.agent-src/rules/domain-safety-pii-recruiting.md +56 -0
- package/.agent-src/rules/domain-safety-pii-support.md +57 -0
- package/.agent-src/rules/domain-safety-retention-finance.md +48 -0
- package/.agent-src/rules/domain-safety-retention-support.md +55 -0
- package/.agent-src/skills/api-design/SKILL.md +3 -0
- package/.agent-src/skills/authz-review/SKILL.md +3 -0
- package/.agent-src/skills/competitive-moat-analysis/SKILL.md +3 -0
- package/.agent-src/skills/competitive-positioning/SKILL.md +3 -0
- package/.agent-src/skills/content-funnel-design/SKILL.md +3 -0
- package/.agent-src/skills/contracts-cognition/SKILL.md +3 -0
- package/.agent-src/skills/dashboard-design/SKILL.md +3 -0
- package/.agent-src/skills/data-handling-judgment/SKILL.md +3 -0
- package/.agent-src/skills/dcf-modeling/SKILL.md +3 -0
- package/.agent-src/skills/deal-qualification-meddic/SKILL.md +3 -0
- package/.agent-src/skills/discovery-interview/SKILL.md +3 -0
- package/.agent-src/skills/editorial-calendar/SKILL.md +3 -0
- package/.agent-src/skills/forecast-accuracy/SKILL.md +3 -0
- package/.agent-src/skills/forecasting/SKILL.md +3 -0
- package/.agent-src/skills/fundraising-narrative/SKILL.md +3 -0
- package/.agent-src/skills/gtm-launch/SKILL.md +3 -0
- package/.agent-src/skills/incident-commander/SKILL.md +3 -0
- package/.agent-src/skills/launch-readiness/SKILL.md +3 -0
- package/.agent-src/skills/messaging-architecture/SKILL.md +3 -0
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +3 -0
- package/.agent-src/skills/pipeline-strategy/SKILL.md +3 -0
- package/.agent-src/skills/playwright-architect/SKILL.md +3 -0
- package/.agent-src/skills/privacy-review/SKILL.md +4 -1
- package/.agent-src/skills/quality-tools/SKILL.md +3 -0
- package/.agent-src/skills/release-comms/SKILL.md +3 -0
- package/.agent-src/skills/runway-cognition/SKILL.md +3 -0
- package/.agent-src/skills/scenario-modeling/SKILL.md +3 -0
- package/.agent-src/skills/secrets-management/SKILL.md +3 -0
- package/.agent-src/skills/tech-debt-tracker/SKILL.md +3 -0
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +3 -0
- package/.agent-src/skills/voc-extract/SKILL.md +3 -0
- package/.agent-src/skills/voice-and-tone-design/SKILL.md +3 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +16 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +299 -20
- package/.claude-plugin/marketplace.json +10 -1
- package/CHANGELOG.md +200 -211
- package/README.md +55 -23
- package/config/gitignore-block.txt +8 -0
- package/docs/announcements/2026-05-non-dev-launch.md +79 -0
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-pre-2.15.0.md +244 -0
- package/docs/case-studies/_template.md +60 -0
- package/docs/catalog.md +24 -3
- package/docs/contracts/agent-user-schema.md +1 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +490 -0
- package/docs/contracts/ghostwriter-schema.md +337 -0
- package/docs/contracts/init-telemetry.md +133 -0
- package/docs/contracts/router-blending.md +71 -0
- package/docs/contracts/universal-skills.md +92 -0
- package/docs/contracts/write-engine.md +142 -0
- package/docs/getting-started-by-role.md +89 -0
- package/docs/getting-started-laravel.md +72 -0
- package/docs/getting-started.md +2 -2
- package/docs/installation.md +221 -2
- package/docs/safety.md +30 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +238 -8
- package/scripts/_cli/cmd_migrate.py +6 -1
- package/scripts/_cli/cmd_prune.py +8 -3
- package/scripts/_cli/cmd_sync.py +7 -3
- package/scripts/_cli/cmd_uninstall.py +4 -3
- package/scripts/_cli/cmd_update.py +5 -1
- package/scripts/_cli/cmd_validate.py +6 -3
- package/scripts/_cli/cmd_versions.py +15 -2
- package/scripts/_lib/agent_settings.py +299 -20
- package/scripts/agent-config +64 -0
- package/scripts/bench_runner.py +158 -0
- package/scripts/check_role_doc_links.py +110 -0
- package/scripts/compress.py +11 -0
- package/scripts/ghostwriter_fixture_allowlist.txt +16 -0
- package/scripts/install +39 -2
- package/scripts/install.py +304 -1
- package/scripts/install.sh +20 -0
- package/scripts/lint_ghostwriter_source.py +240 -0
- package/scripts/measure_skill_reduction.py +102 -0
- package/scripts/schemas/rule.schema.json +5 -0
- package/scripts/schemas/skill.schema.json +6 -0
- package/scripts/update-github-metadata.sh +84 -0
- package/templates/agent-config-wrapper.sh +7 -0
- package/templates/minimal/.agent-settings.yml +23 -0
- package/templates/minimal/agents-gitkeep +2 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: privacy-review
|
|
3
|
-
description: "Use when reviewing data flows for GDPR
|
|
3
|
+
description: "Use when reviewing data flows, support macros, refund templates for GDPR/CCPA/HIPAA fit — regime, consent, PII redaction (email, order-id), breach triage. Triggers 'is this GDPR-safe', 'PII redact'."
|
|
4
4
|
status: active
|
|
5
5
|
tier: senior
|
|
6
6
|
source: package
|
|
7
7
|
domain: process
|
|
8
8
|
context_spine: [regulatory-regime, customer-segment, product]
|
|
9
|
+
recommended_for_user_types: [ops, finance, creator]
|
|
10
|
+
|
|
11
|
+
|
|
9
12
|
---
|
|
10
13
|
|
|
11
14
|
# privacy-review
|
|
@@ -6,6 +6,9 @@ domain: devops
|
|
|
6
6
|
status: active
|
|
7
7
|
refresh_trigger: "A cited provider deprecates an auth method, OR External Secrets Operator ships a major version with breaking CRD changes, OR ≥30% of cited scanner tools change their gate semantics."
|
|
8
8
|
sunset_criterion: "When provider docs (Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) all converge on a single rotation + scanning standard AND consumer projects no longer cite this skill in PR reviews for two consecutive review cycles."
|
|
9
|
+
recommended_for_user_types: [ops, developer]
|
|
10
|
+
|
|
11
|
+
|
|
9
12
|
---
|
|
10
13
|
|
|
11
14
|
# secrets-management
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.py (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "2.
|
|
42
|
+
agent_config_version: "2.16.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -124,6 +124,21 @@ personas:
|
|
|
124
124
|
# Leave empty to require explicit --personas=+<id> opt-in.
|
|
125
125
|
auto_include: [qa]
|
|
126
126
|
|
|
127
|
+
# --- Ghostwriter (optional) ---
|
|
128
|
+
#
|
|
129
|
+
# Toggles for the /ghostwriter command cluster. Consumer-only —
|
|
130
|
+
# the package itself ignores this block (fixtures never carry aliases).
|
|
131
|
+
# See docs/contracts/ghostwriter-schema.md § Aliases.
|
|
132
|
+
ghostwriter:
|
|
133
|
+
# When true, `/ghostwriter:write --as=<value>` resolves <value> against
|
|
134
|
+
# both slug filenames AND every profile's `aliases:` list
|
|
135
|
+
# (case-insensitive). Conflicts are rejected at lint time
|
|
136
|
+
# (task lint-ghostwriter-source), never at runtime.
|
|
137
|
+
# Set to false to disable alias resolution (slug-only matching);
|
|
138
|
+
# any `aliases:` already in profiles are then ignored at resolve time
|
|
139
|
+
# but still validated by the lint.
|
|
140
|
+
aliases: true
|
|
141
|
+
|
|
127
142
|
# --- Quality pipeline (optional) ---
|
|
128
143
|
#
|
|
129
144
|
# Per-language gate tools the `quality-fix` command invokes. Empty
|
|
@@ -17,11 +17,21 @@ when shipped into consumer projects) with a read-fallback to the legacy
|
|
|
17
17
|
``~/.config/agent-config/agent-settings.yml`` so pre-2.4 installs keep
|
|
18
18
|
working during the namespace migration.
|
|
19
19
|
|
|
20
|
-
``<repo-root>`` is the nearest ancestor that
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
``<repo-root>`` is the nearest ancestor that anchors the project. As of
|
|
21
|
+
Step 7 the anchor set is (closest-leaf wins; tiebreaker
|
|
22
|
+
``.agent-settings.yml`` > ``agents/`` > ``.git``):
|
|
23
|
+
|
|
24
|
+
* ``.agent-settings.yml`` file,
|
|
25
|
+
* ``agents/`` directory containing ``roadmaps/``, ``.ai-council.yml``,
|
|
26
|
+
or ``roadmaps-progress.md`` (bare ``agents/`` does **not** anchor),
|
|
27
|
+
* ``.git`` file or directory (submodule support).
|
|
28
|
+
|
|
29
|
+
Set ``AGENT_CONFIG_LEGACY_ANCHOR=1`` to revert to the pre-Step-7
|
|
30
|
+
``.git``-only walk for one minor-version soak. The walk stops at the
|
|
31
|
+
first anchor — it never drifts into a parent repo or ``$HOME``. When
|
|
32
|
+
``cwd`` is ``None`` (default), the loader behaves identically to the
|
|
33
|
+
pre-cascade contract: project file + user-global only, no ancestor
|
|
34
|
+
walk. Back-compat is hard.
|
|
25
35
|
|
|
26
36
|
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
27
37
|
non-whitelisted key in the user-global file is silently ignored — the
|
|
@@ -41,6 +51,7 @@ Contract — pure, read-only, tolerant:
|
|
|
41
51
|
from __future__ import annotations
|
|
42
52
|
|
|
43
53
|
import logging
|
|
54
|
+
import os
|
|
44
55
|
from pathlib import Path
|
|
45
56
|
from typing import Any, Iterator
|
|
46
57
|
|
|
@@ -81,27 +92,295 @@ MERGEABLE_KEYS: tuple[str, ...] = (
|
|
|
81
92
|
_DEFAULTS: dict[str, Any] = {}
|
|
82
93
|
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
#: Anchor identifier returned by :func:`find_project_root_with_anchor`.
|
|
96
|
+
ANCHOR_AGENT_SETTINGS = "agent-settings"
|
|
97
|
+
ANCHOR_AGENTS_DIR = "agents-dir"
|
|
98
|
+
ANCHOR_GIT = "git"
|
|
99
|
+
|
|
100
|
+
#: Marker subpaths that qualify a bare ``agents/`` directory as a project
|
|
101
|
+
#: anchor (D1). Any one is sufficient. Bare ``agents/`` without a marker
|
|
102
|
+
#: is **not** an anchor.
|
|
103
|
+
_AGENTS_DIR_MARKERS: tuple[str, ...] = (
|
|
104
|
+
"roadmaps",
|
|
105
|
+
".ai-council.yml",
|
|
106
|
+
"roadmaps-progress.md",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
#: Kill-switch (D5). When set to ``"1"``, :func:`find_project_root` and
|
|
110
|
+
#: :func:`find_project_root_with_anchor` revert to the pre-Step-7
|
|
111
|
+
#: ``.git``-only walk for one minor-version soak.
|
|
112
|
+
_LEGACY_ANCHOR_ENV = "AGENT_CONFIG_LEGACY_ANCHOR"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _boundary_anchor_at(path: Path) -> str | None:
|
|
116
|
+
"""Return the boundary-anchor name at ``path`` or ``None``.
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
pointer) or directory (regular checkout), or ``None`` if the walk
|
|
89
|
-
reaches the filesystem root without finding one. The walk stops at
|
|
90
|
-
the project boundary — it never drifts into a parent repo or
|
|
91
|
-
``$HOME``.
|
|
118
|
+
Boundary anchors stop the walk and define the project root:
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
* ``agents/`` containing a D1 marker → ``"agents-dir"``
|
|
121
|
+
* ``.git`` (file or directory) → ``"git"``
|
|
122
|
+
|
|
123
|
+
``.agent-settings.yml`` is a **layer marker**, not a boundary
|
|
124
|
+
anchor (decision: ``step-7-d3-cascade-conflict-decision``). It
|
|
125
|
+
only anchors when no boundary is found in any ancestor — handled
|
|
126
|
+
by :func:`find_project_root_with_anchor` as a second pass.
|
|
127
|
+
|
|
128
|
+
Pure read-only — at most ``1 + len(_AGENTS_DIR_MARKERS)``
|
|
129
|
+
``exists()`` probes per call (D6 perf budget).
|
|
130
|
+
"""
|
|
131
|
+
agents_dir = path / "agents"
|
|
132
|
+
if agents_dir.is_dir():
|
|
133
|
+
for marker in _AGENTS_DIR_MARKERS:
|
|
134
|
+
if (agents_dir / marker).exists():
|
|
135
|
+
return ANCHOR_AGENTS_DIR
|
|
136
|
+
if (path / ".git").exists():
|
|
137
|
+
return ANCHOR_GIT
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def find_project_root_with_anchor(start: Path) -> tuple[Path, str] | None:
|
|
142
|
+
"""Walk up from ``start`` and return ``(root, anchor_name)`` or ``None``.
|
|
143
|
+
|
|
144
|
+
Two-tier lookup (boundary vs layer split — see council decision
|
|
145
|
+
``step-7-d3-cascade-conflict-decision``):
|
|
146
|
+
|
|
147
|
+
1. **Boundary pass.** Walk up from ``start``. First ancestor with
|
|
148
|
+
a boundary anchor wins:
|
|
149
|
+
|
|
150
|
+
* ``agents/`` containing **any** of ``roadmaps/``,
|
|
151
|
+
``.ai-council.yml``, or ``roadmaps-progress.md`` (D1) →
|
|
152
|
+
``"agents-dir"``
|
|
153
|
+
* ``.git`` (file or directory; submodule support) → ``"git"``
|
|
154
|
+
|
|
155
|
+
When both coexist at the same ancestor, ``agents/`` wins
|
|
156
|
+
(D3 ordering minus the layer marker).
|
|
157
|
+
|
|
158
|
+
2. **Layer fallback.** No boundary found in the chain. Walk again
|
|
159
|
+
and return the **outermost** ancestor containing
|
|
160
|
+
``.agent-settings.yml`` → ``"agent-settings"``. This delivers
|
|
161
|
+
Step-7's minimal-init goal without breaking the cascade.
|
|
162
|
+
|
|
163
|
+
When ``AGENT_CONFIG_LEGACY_ANCHOR=1`` is set (D5 kill-switch), only
|
|
164
|
+
the ``.git`` anchor is considered.
|
|
165
|
+
|
|
166
|
+
Pure read-only; never writes, never raises on missing paths.
|
|
95
167
|
"""
|
|
96
168
|
current = start.resolve() if start.exists() else start
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
legacy = os.environ.get(_LEGACY_ANCHOR_ENV) == "1"
|
|
170
|
+
chain = [current, *current.parents]
|
|
171
|
+
if legacy:
|
|
172
|
+
for candidate in chain:
|
|
173
|
+
if (candidate / ".git").exists():
|
|
174
|
+
return candidate, ANCHOR_GIT
|
|
175
|
+
return None
|
|
176
|
+
# Boundary pass.
|
|
177
|
+
for candidate in chain:
|
|
178
|
+
anchor = _boundary_anchor_at(candidate)
|
|
179
|
+
if anchor is not None:
|
|
180
|
+
return candidate, anchor
|
|
181
|
+
# Layer fallback — outermost .agent-settings.yml wins so the
|
|
182
|
+
# cascade can layer deeper files below it.
|
|
183
|
+
outermost: Path | None = None
|
|
184
|
+
for candidate in chain:
|
|
185
|
+
if (candidate / DEFAULT_PROJECT_FILE).exists():
|
|
186
|
+
outermost = candidate
|
|
187
|
+
if outermost is not None:
|
|
188
|
+
return outermost, ANCHOR_AGENT_SETTINGS
|
|
102
189
|
return None
|
|
103
190
|
|
|
104
191
|
|
|
192
|
+
def find_project_root(start: Path) -> Path | None:
|
|
193
|
+
"""Walk up from ``start`` and return the project root or ``None``.
|
|
194
|
+
|
|
195
|
+
Thin wrapper over :func:`find_project_root_with_anchor` that drops
|
|
196
|
+
the anchor-name component. Kept for back-compat — every pre-Step-7
|
|
197
|
+
caller already takes a ``Path | None`` here.
|
|
198
|
+
"""
|
|
199
|
+
result = find_project_root_with_anchor(start)
|
|
200
|
+
return result[0] if result is not None else None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def find_project_root_with_trace(
|
|
204
|
+
start: Path,
|
|
205
|
+
) -> tuple[Path | None, str | None, list[dict[str, Any]]]:
|
|
206
|
+
"""Walk up from ``start`` and return ``(root, anchor, trace)``.
|
|
207
|
+
|
|
208
|
+
Step 8 A1 — diagnostic variant of :func:`find_project_root_with_anchor`.
|
|
209
|
+
Returns the same ``(root, anchor)`` pair (or ``(None, None)`` when no
|
|
210
|
+
anchor is found) plus an ordered list of trace records describing
|
|
211
|
+
every ancestor probed.
|
|
212
|
+
|
|
213
|
+
Each trace record is a dict:
|
|
214
|
+
|
|
215
|
+
* ``ancestor`` — absolute path probed (string).
|
|
216
|
+
* ``pass`` — ``"boundary"`` or ``"layer"``.
|
|
217
|
+
* ``hit`` — anchor name on hit, ``None`` on miss.
|
|
218
|
+
* ``reason`` — one-line explanation (``agents/ has roadmaps/``,
|
|
219
|
+
``no .git``, ``layer marker``, ``legacy: only .git considered``,
|
|
220
|
+
etc.).
|
|
221
|
+
|
|
222
|
+
Pure read-only. No additional ``exists()`` cost beyond
|
|
223
|
+
:func:`find_project_root_with_anchor` — the trace records reuse the
|
|
224
|
+
same probes.
|
|
225
|
+
"""
|
|
226
|
+
trace: list[dict[str, Any]] = []
|
|
227
|
+
current = start.resolve() if start.exists() else start
|
|
228
|
+
legacy = os.environ.get(_LEGACY_ANCHOR_ENV) == "1"
|
|
229
|
+
chain = [current, *current.parents]
|
|
230
|
+
|
|
231
|
+
if legacy:
|
|
232
|
+
for candidate in chain:
|
|
233
|
+
hit = (candidate / ".git").exists()
|
|
234
|
+
trace.append({
|
|
235
|
+
"ancestor": str(candidate),
|
|
236
|
+
"pass": "boundary",
|
|
237
|
+
"hit": ANCHOR_GIT if hit else None,
|
|
238
|
+
"reason": (
|
|
239
|
+
"legacy: .git found" if hit
|
|
240
|
+
else "legacy: no .git"
|
|
241
|
+
),
|
|
242
|
+
})
|
|
243
|
+
if hit:
|
|
244
|
+
return candidate, ANCHOR_GIT, trace
|
|
245
|
+
return None, None, trace
|
|
246
|
+
|
|
247
|
+
# Boundary pass — same probes as find_project_root_with_anchor.
|
|
248
|
+
for candidate in chain:
|
|
249
|
+
agents_dir = candidate / "agents"
|
|
250
|
+
if agents_dir.is_dir():
|
|
251
|
+
for marker in _AGENTS_DIR_MARKERS:
|
|
252
|
+
if (agents_dir / marker).exists():
|
|
253
|
+
trace.append({
|
|
254
|
+
"ancestor": str(candidate),
|
|
255
|
+
"pass": "boundary",
|
|
256
|
+
"hit": ANCHOR_AGENTS_DIR,
|
|
257
|
+
"reason": f"agents/ has {marker}",
|
|
258
|
+
})
|
|
259
|
+
return candidate, ANCHOR_AGENTS_DIR, trace
|
|
260
|
+
if (candidate / ".git").exists():
|
|
261
|
+
trace.append({
|
|
262
|
+
"ancestor": str(candidate),
|
|
263
|
+
"pass": "boundary",
|
|
264
|
+
"hit": ANCHOR_GIT,
|
|
265
|
+
"reason": ".git present",
|
|
266
|
+
})
|
|
267
|
+
return candidate, ANCHOR_GIT, trace
|
|
268
|
+
trace.append({
|
|
269
|
+
"ancestor": str(candidate),
|
|
270
|
+
"pass": "boundary",
|
|
271
|
+
"hit": None,
|
|
272
|
+
"reason": "no agents/ marker, no .git",
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
# Layer fallback — outermost .agent-settings.yml wins.
|
|
276
|
+
outermost: Path | None = None
|
|
277
|
+
for candidate in chain:
|
|
278
|
+
present = (candidate / DEFAULT_PROJECT_FILE).exists()
|
|
279
|
+
trace.append({
|
|
280
|
+
"ancestor": str(candidate),
|
|
281
|
+
"pass": "layer",
|
|
282
|
+
"hit": ANCHOR_AGENT_SETTINGS if present else None,
|
|
283
|
+
"reason": (
|
|
284
|
+
f"{DEFAULT_PROJECT_FILE} present" if present
|
|
285
|
+
else f"no {DEFAULT_PROJECT_FILE}"
|
|
286
|
+
),
|
|
287
|
+
})
|
|
288
|
+
if present:
|
|
289
|
+
outermost = candidate
|
|
290
|
+
if outermost is not None:
|
|
291
|
+
return outermost, ANCHOR_AGENT_SETTINGS, trace
|
|
292
|
+
return None, None, trace
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
#: Origin tag returned by :func:`resolve_project_root` alongside the
|
|
296
|
+
#: anchor names defined above. Distinct values let callers (doctor,
|
|
297
|
+
#: tests, future telemetry) surface *how* the root was chosen.
|
|
298
|
+
ORIGIN_ROOT_FLAG = "root-flag" # --root global flag (Step 8 A3)
|
|
299
|
+
ORIGIN_EXPLICIT = "explicit" # --project arg on a subcommand
|
|
300
|
+
ORIGIN_ENV = "env" # AGENT_CONFIG_PROJECT_ROOT (wrapper-pinned)
|
|
301
|
+
ORIGIN_CWD_FALLBACK = "cwd-fallback" # no anchor found
|
|
302
|
+
|
|
303
|
+
PROJECT_ROOT_ENV = "AGENT_CONFIG_PROJECT_ROOT"
|
|
304
|
+
ROOT_OVERRIDE_ENV = "AGENT_CONFIG_ROOT_OVERRIDE"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ProjectRootError(Exception):
|
|
308
|
+
"""Raised when an explicit project-root override points to an invalid path.
|
|
309
|
+
|
|
310
|
+
Step 8 A3: ``--root <path>`` and ``AGENT_CONFIG_PROJECT_ROOT`` must
|
|
311
|
+
fail loudly when the target does not exist or is not a directory.
|
|
312
|
+
Callers translate this into exit code 2 (no silent CWD fallback).
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _validate_root_path(path: Path, origin_label: str) -> Path:
|
|
317
|
+
"""Resolve ``path``; raise :class:`ProjectRootError` when invalid.
|
|
318
|
+
|
|
319
|
+
``origin_label`` is one of ``--root``, ``AGENT_CONFIG_PROJECT_ROOT``,
|
|
320
|
+
or ``--project``; surfaced verbatim in the error message so the
|
|
321
|
+
operator can see which channel injected the bad value.
|
|
322
|
+
"""
|
|
323
|
+
resolved = Path(path).expanduser()
|
|
324
|
+
if not resolved.exists():
|
|
325
|
+
raise ProjectRootError(
|
|
326
|
+
f"{origin_label} points to a path that does not exist: {resolved}",
|
|
327
|
+
)
|
|
328
|
+
if not resolved.is_dir():
|
|
329
|
+
raise ProjectRootError(
|
|
330
|
+
f"{origin_label} points to a non-directory: {resolved}",
|
|
331
|
+
)
|
|
332
|
+
return resolved.resolve()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def resolve_project_root(
|
|
336
|
+
arg: str | Path | None,
|
|
337
|
+
*,
|
|
338
|
+
cwd: Path | None = None,
|
|
339
|
+
) -> tuple[Path, str]:
|
|
340
|
+
"""Return ``(root, origin)`` for any ``cmd_*`` entry point.
|
|
341
|
+
|
|
342
|
+
Resolution order (Step 8 A3 — explicit override hardening):
|
|
343
|
+
|
|
344
|
+
1. ``AGENT_CONFIG_PROJECT_ROOT`` env var with
|
|
345
|
+
``AGENT_CONFIG_ROOT_OVERRIDE=1`` set by the master CLI's ``--root``
|
|
346
|
+
flag → ``ORIGIN_ROOT_FLAG``. Fail-loud on invalid path.
|
|
347
|
+
2. Explicit ``--project`` / ``--target`` argument → ``ORIGIN_EXPLICIT``.
|
|
348
|
+
Fail-loud on invalid path.
|
|
349
|
+
3. ``AGENT_CONFIG_PROJECT_ROOT`` environment variable, set by the
|
|
350
|
+
project-local ``./agent-config`` wrapper → ``ORIGIN_ENV``.
|
|
351
|
+
Fail-loud on invalid path.
|
|
352
|
+
4. Anchor walk from ``cwd`` via
|
|
353
|
+
:func:`find_project_root_with_anchor` → anchor name
|
|
354
|
+
(``agents-dir`` / ``git`` / ``agent-settings``).
|
|
355
|
+
5. Fall back to ``cwd`` itself → ``ORIGIN_CWD_FALLBACK``.
|
|
356
|
+
|
|
357
|
+
The ``--root`` channel wins over a subcommand-level ``--project``
|
|
358
|
+
because it is a deliberate global override (Step 8 council decision).
|
|
359
|
+
Wrapper-set env (3) still wins over the anchor walk so subdir
|
|
360
|
+
invocations stay pinned.
|
|
361
|
+
|
|
362
|
+
Raises :class:`ProjectRootError` when any explicit override points
|
|
363
|
+
to a missing path or non-directory — callers map this to exit 2.
|
|
364
|
+
"""
|
|
365
|
+
if os.environ.get(ROOT_OVERRIDE_ENV) == "1":
|
|
366
|
+
env_value = os.environ.get(PROJECT_ROOT_ENV)
|
|
367
|
+
if env_value:
|
|
368
|
+
return _validate_root_path(Path(env_value), "--root"), ORIGIN_ROOT_FLAG
|
|
369
|
+
if arg is not None and str(arg) != "":
|
|
370
|
+
return _validate_root_path(Path(arg), "--project"), ORIGIN_EXPLICIT
|
|
371
|
+
env_value = os.environ.get(PROJECT_ROOT_ENV)
|
|
372
|
+
if env_value:
|
|
373
|
+
return (
|
|
374
|
+
_validate_root_path(Path(env_value), PROJECT_ROOT_ENV),
|
|
375
|
+
ORIGIN_ENV,
|
|
376
|
+
)
|
|
377
|
+
start = (cwd or Path.cwd()).resolve()
|
|
378
|
+
walked = find_project_root_with_anchor(start)
|
|
379
|
+
if walked is not None:
|
|
380
|
+
return walked
|
|
381
|
+
return start, ORIGIN_CWD_FALLBACK
|
|
382
|
+
|
|
383
|
+
|
|
105
384
|
def _resolve_cascade_paths(
|
|
106
385
|
cwd: Path | None,
|
|
107
386
|
project_path: Path | str | None,
|
|
@@ -111,7 +390,7 @@ def _resolve_cascade_paths(
|
|
|
111
390
|
When ``cwd`` is provided and ``find_project_root(cwd)`` succeeds, the
|
|
112
391
|
list contains every ``<dir>/.agent-settings.yml`` from the repo root
|
|
113
392
|
down to ``cwd`` (inclusive on both ends), shallowest first. When
|
|
114
|
-
``cwd`` is ``None`` or no
|
|
393
|
+
``cwd`` is ``None`` or no anchor is reached, falls back to the
|
|
115
394
|
single legacy project path — back-compat with the pre-cascade
|
|
116
395
|
loader.
|
|
117
396
|
"""
|
|
@@ -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": "2.
|
|
9
|
+
"version": "2.17.0",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"agent-config",
|
|
12
12
|
"skills",
|
|
@@ -170,6 +170,12 @@
|
|
|
170
170
|
"./.claude/skills/form-handler",
|
|
171
171
|
"./.claude/skills/fundraising-narrative",
|
|
172
172
|
"./.claude/skills/funnel-analysis",
|
|
173
|
+
"./.claude/skills/ghostwriter",
|
|
174
|
+
"./.claude/skills/ghostwriter-delete",
|
|
175
|
+
"./.claude/skills/ghostwriter-fetch",
|
|
176
|
+
"./.claude/skills/ghostwriter-list",
|
|
177
|
+
"./.claude/skills/ghostwriter-show",
|
|
178
|
+
"./.claude/skills/ghostwriter-write",
|
|
173
179
|
"./.claude/skills/git-workflow",
|
|
174
180
|
"./.claude/skills/github-ci",
|
|
175
181
|
"./.claude/skills/grafana",
|
|
@@ -264,6 +270,9 @@
|
|
|
264
270
|
"./.claude/skills/playwright-testing",
|
|
265
271
|
"./.claude/skills/po-discovery",
|
|
266
272
|
"./.claude/skills/positioning-strategy",
|
|
273
|
+
"./.claude/skills/post-as",
|
|
274
|
+
"./.claude/skills/post-as-ghostwriter",
|
|
275
|
+
"./.claude/skills/post-as-me",
|
|
267
276
|
"./.claude/skills/prepare-for-review",
|
|
268
277
|
"./.claude/skills/privacy-review",
|
|
269
278
|
"./.claude/skills/project-analysis-core",
|