@event4u/agent-config 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.agent-src/contexts/execution/roadmap-process-loop.md +30 -4
  2. package/.agent-src/rules/linked-projects-onboarding-gate.md +82 -0
  3. package/.agent-src/rules/roadmap-progress-sync.md +39 -5
  4. package/.agent-src/scripts/update_roadmap_progress.py +63 -7
  5. package/.agent-src/skills/roadmap-management/SKILL.md +121 -21
  6. package/.agent-src/skills/roadmap-writing/SKILL.md +63 -0
  7. package/.agent-src/templates/agent-settings.md +16 -0
  8. package/.agent-src/templates/roadmaps.md +22 -1
  9. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +20 -3
  10. package/.claude-plugin/marketplace.json +1 -1
  11. package/CHANGELOG.md +49 -0
  12. package/README.md +6 -1
  13. package/dist/discovery/deprecation-report.md +1 -1
  14. package/dist/discovery/discovery-manifest.json +33 -11
  15. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  16. package/dist/discovery/discovery-manifest.summary.md +3 -3
  17. package/dist/discovery/orphan-report.md +1 -1
  18. package/dist/discovery/packs.json +6 -5
  19. package/dist/discovery/trust-report.md +3 -3
  20. package/dist/discovery/workspaces.json +5 -4
  21. package/dist/mcp/registry-manifest.json +2 -2
  22. package/dist/router.json +1 -1
  23. package/docs/architecture.md +1 -1
  24. package/docs/catalog.md +3 -2
  25. package/docs/decisions/ADR-032-linked-projects-scope.md +118 -0
  26. package/docs/decisions/ADR-033-distribution-identity-npm-primary.md +81 -0
  27. package/docs/decisions/INDEX.md +2 -0
  28. package/docs/distribution/registries.md +29 -0
  29. package/docs/getting-started.md +1 -1
  30. package/docs/guides/cross-repo-linked-projects.md +86 -0
  31. package/package.json +1 -1
  32. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  33. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  34. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  35. package/scripts/_lib/agent_settings.py +20 -3
  36. package/scripts/_lib/linked_projects.py +238 -0
  37. package/scripts/check_no_local_settings_committed.py +51 -0
  38. package/scripts/lint_commit_subjects.py +139 -0
@@ -0,0 +1,118 @@
1
+ ---
2
+ adr: 032
3
+ status: accepted
4
+ date: 2026-05-29
5
+ decision: linked-projects-scope-go-option-a
6
+ supersedes: —
7
+ superseded_by: —
8
+ phase: v3.x · multi-project-scope evaluation
9
+ type: structural
10
+ review_date: 2027-05-29
11
+ ---
12
+
13
+ # ADR-032 — Linked-projects scope: GO on opt-in auto-detection (Option A, passive awareness)
14
+
15
+ ## Status
16
+
17
+ **Accepted** · 2026-05-29. Approves an opt-in auto-detection feature for
18
+ IDE-attached sibling repositories, scoped to **passive awareness** (Option A).
19
+ A same-day earlier draft recorded NO-GO; that verdict was reversed after the
20
+ proactivity-gap argument (below). Time-boxed: review on **2027-05-29** or
21
+ earlier if a kill-switch trigger fires.
22
+
23
+ Not to be confused with [`ADR-029`](ADR-029-multi-workspace-deferred.md): that
24
+ defers a restructure of the **package's own root layout**. This ADR is about
25
+ the **agent's working scope over a sibling project repository**.
26
+
27
+ ## Context
28
+
29
+ Developers routinely check out sibling repos that change together (e.g.
30
+ `galawork-api` + `galawork-web`) and attach them in the IDE. Detection is
31
+ deterministic from on-disk config (`.idea/modules.xml` + `vcs.xml`,
32
+ `*.code-workspace`).
33
+
34
+ A Phase-0 spike found Claude Code can already read/write a sibling outside its
35
+ working directory **unconditionally** — no rule needed. An initial reading
36
+ concluded the feature was therefore only an "awareness signal" a doc could
37
+ deliver, and drafted NO-GO.
38
+
39
+ ## The reversal — proactivity gap
40
+
41
+ That NO-GO mis-framed the value. The point is **not** capability (the agent can
42
+ write everywhere); it is **proactivity**: the agent does **not** consider a
43
+ sibling unless explicitly told, so cross-repo dependencies — an API change that
44
+ breaks the frontend, a shared type that drifts — are missed by default. A
45
+ manual doc/snippet presupposes the very awareness the target user lacks: the
46
+ developer who needs this most is exactly the one who won't think to write the
47
+ note. **Auto-detection is zero-knowledge** — it reads the relationship the
48
+ developer already encoded by attaching the repo in their IDE.
49
+
50
+ AI Council (anthropic/claude-sonnet-4-5 + openai/gpt-4o, 3 rounds + Karpathy
51
+ peer-review, 2026-05-29) flipped to **GO** on this reasoning.
52
+
53
+ ## Decision — GO, scoped to Option A (passive awareness)
54
+
55
+ Build an **opt-in** auto-detection feature:
56
+
57
+ 1. **Detect** IDE-attached siblings from on-disk config (config-driven only;
58
+ never arbitrary adjacent directories).
59
+ 2. **Opt-in once** per sibling; persist the choice **local-only** in
60
+ `.agent-settings.local.yml` (in agents/settings/) (gitignored, per-machine — sibling paths differ
61
+ per developer and must never be committed).
62
+ 3. **Behavioral directive** for in-scope siblings: proactively check cross-repo
63
+ impact on relevant changes (API contract, shared types) and **warn**.
64
+ **Do NOT bulk-include** the sibling's files (interpretation C — token
65
+ blowup — stays **out of scope**). Out-of-root writes still pass the host
66
+ agent's own permission gate.
67
+
68
+ ### A/B/C scoping
69
+
70
+ - **A — passive awareness (CHOSEN):** know + warn, no bulk inclusion. Cheap, low risk.
71
+ - **B — proactive dependency scanning:** auto-scan on every change. Deferred (needs heuristics).
72
+ - **C — implicit inclusion of all sibling files:** **rejected** — token blowup, context pollution.
73
+
74
+ ### Fork resolutions
75
+
76
+ - **Fork A** — `.agent-settings.local.yml` (in agents/settings/), deepest cascade layer reusing `_deep_merge` (not a bespoke override).
77
+ - **Fork B** — key `linked_projects` (avoids ADR-007 "scope"/"workspace", ADR-029 "multi-workspace").
78
+ - **Fork C** — cross-cwd writes documented, never auto-configured; host permission gate applies.
79
+
80
+ ## Consequences
81
+
82
+ - New: detector (`scripts/_lib/linked_projects.py`), the
83
+ `.agent-settings.local.yml` (in agents/settings/) cascade layer, a committed-local lint, and the
84
+ `linked-projects-onboarding-gate` rule (tier-2b, **experimental**, **removable**).
85
+ - The intra-repo module system (`enumerate_modules()`) is untouched.
86
+ - Size never excludes a sibling — a real frontend (galawork-web ≈ 38k files)
87
+ must surface; it is flagged `large` (awareness only). The council's literal
88
+ "skip >20k files" guardrail was corrected: it conflated Option C's
89
+ file-inclusion cost with Option A, under which repo size is cost-irrelevant.
90
+ - Per install decision **D2**, the installer does not touch the consumer
91
+ `.gitignore`; consumers gitignore `.agent-settings.local.yml` (in agents/settings/) themselves
92
+ (documented in the guide).
93
+
94
+ ## Kill-switch
95
+
96
+ Experimental + removable by construction. If opt-in is consistently declined or
97
+ siblings are never cited in practice, remove the rule. Signal stays local — no
98
+ telemetry.
99
+
100
+ ## Open follow-ups
101
+
102
+ - **Consumer detector reachability:** the detector lives in `scripts/_lib/`;
103
+ exposing it as an `agent-config` CLI subcommand for consumer installs is a
104
+ follow-up. Import-reachable in this repo / co-located maintainer setups today.
105
+ - **Multi-agent verification:** only Claude Code was empirically validated.
106
+ Cursor / Augment / Copilot are unverified — the guide's manual snippet covers
107
+ them until an interactive per-IDE test is run.
108
+
109
+ ## Alternatives considered
110
+
111
+ - **NO-GO + docs only** — rejected: a manual note fails the target user who lacks the awareness to write it.
112
+ - **Build Option C** — rejected: token blowup.
113
+
114
+ ## References
115
+
116
+ - [`docs/guides/cross-repo-linked-projects.md`](../guides/cross-repo-linked-projects.md)
117
+ - [`ADR-007`](ADR-007-agent-discovery-scopes.md) — owns "scope"/"workspace".
118
+ - [`ADR-029`](ADR-029-multi-workspace-deferred.md) — unrelated package-root multi-workspace defer.
@@ -0,0 +1,81 @@
1
+ ---
2
+ adr: 033
3
+ status: accepted
4
+ date: 2026-05-29
5
+ decision: distribution-identity-npm-primary
6
+ supersedes: —
7
+ superseded_by: —
8
+ phase: distribution-identity
9
+ type: structural
10
+ review_date: 2026-08-29
11
+ ---
12
+
13
+ # ADR-033 — Distribution identity: npm-primary, Packagist deprecated-in-place
14
+
15
+ ## Status
16
+
17
+ **Accepted** · 2026-05-29. AI Council (claude-sonnet-4-5 + gpt-4o, analysis lens, 2026-05-29) converged on **npm-primary with Packagist deprecated-in-place**. No maintainer veto surfaced; the de-facto evidence and the operational-honesty argument both point to a single answer. Review date 2026-08-29.
18
+
19
+ ## Context
20
+
21
+ External feedback rounds 10 / 12 / 13 returned to two recurring symptoms — *"Packagist still shows 1.0.4"* and *"two major bumps in six days with no breaking-change signal"*. A council deliberation (lens: analysis) re-framed both as **one distribution-identity question** the repo has answered in practice but never recorded:
22
+
23
+ - **`package.json` at `5.1.0`** — the package is past the unified-setup 4.0.0 major and a subsequent 5.0.0 line; semver discipline (per `CONTRIBUTING.md § Versioning policy`) treats installer-layout changes as major, so the cadence is policy-correct.
24
+ - **`scripts/release.py` invokes `npm publish` exclusively** — the only publish surface that ships from a release run.
25
+ - **No `composer.json` exists in this repo** — the file was removed during the npm pivot (pre-3.x era). The Packagist `event4u/agent-config` 1.0.4 listing is a zombie pointing at a repository state that no longer exists.
26
+ - **No automation syncs the Composer surface** — none has existed since the pivot. Pretending the channel is supported is a maintenance lie.
27
+ - **ADR-027** already locks the changelog-machine path (`scripts/release.py` derives `CHANGELOG.md § Breaking` from Conventional Commits). The "no breaking-change signal" symptom is stale against that surface — what is missing is a one-click pointer for consumers.
28
+
29
+ Council framing — two perspectives surfaced:
30
+
31
+ **Lens A — Strategic / consumer-positioning.** A consumer who lands on Packagist sees 1.0.4 and installs an artefact the repo cannot support. The honest signal is deprecation; dual-track without resources is worse than a single declared channel.
32
+
33
+ **Lens B — Technical / operational-honesty.** A channel is "supported" only if a release pipeline publishes to it. Composer/Packagist has had no such pipeline since the npm pivot. The simpler invariant — *one channel, one truth* — wins on every measurable axis: maintenance load, consumer trust, audit clarity.
34
+
35
+ Both lenses converged.
36
+
37
+ ## Decision
38
+
39
+ 1. **The package is npm-primary.** The canonical install path is `npm install @event4u/agent-config` (and the consumer-facing `npx @event4u/agent-config install` wizard). All release tooling (`scripts/release.py`, `package.json`, `CONTRIBUTING.md § Versioning policy`) already aligns with this surface — this ADR records the policy, no code changes follow from the declaration itself.
40
+
41
+ 2. **Composer / Packagist is deprecated-in-place.** No `composer.json` ships from this repo; no PHP autoload surface is supported; the Packagist `event4u/agent-config` 1.0.4 listing is treated as legacy and gets a deprecation pointer through the only mechanism available to a deleted-composer-json repo: a registry-side claim/archive action by the maintainer. The corresponding human-owner item is surfaced in `docs/distribution/registries.md` (see Phase 2 of the [`road-to-distribution-identity.md`](../../agents/roadmaps/road-to-distribution-identity.md) roadmap).
42
+
43
+ 3. **Breaking-change communication uses `CHANGELOG.md § Breaking`.** ADR-027 locked the auto-generated changelog from Conventional Commits. This ADR adds one consumer-facing affordance — a README / distribution-doc pointer linking that section — so a consumer who sees a major-version bump has a one-click path to *what broke, what to do*. No new `BREAKING_CHANGES.md` file unless the maintainer prefers one.
44
+
45
+ 4. **Commit-subject hygiene is enforced in CI.** Because the changelog generator reads commit subjects verbatim, sloppy subjects (`leftover`, `wip`, `temp`, `fixup`, or sub-10-character one-word entries) leak directly into the public changelog. A CI lint (`task lint-commit-subjects` or sibling) rejects those subjects on PR. This is wired in Phase 3 of the same roadmap and is the one hygiene item that ties directly to distribution identity.
46
+
47
+ ## Consequences
48
+
49
+ **Positive:**
50
+
51
+ - The distribution story is **single-channel, single-truth**. Anyone scanning the package — consumer, contributor, auditor, package-registry crawler — gets one consistent answer.
52
+ - The zombie Packagist listing no longer misdirects PHP-shop consumers (once the maintainer files the registry-side archive action).
53
+ - Breaking-change discoverability is two clicks from the README: "release notes" → `CHANGELOG.md § Breaking`. No bespoke `BREAKING_CHANGES.md` to maintain.
54
+ - CI catches sloppy commit subjects before they become public changelog lines — the changelog is as clean as the gate that feeds it.
55
+
56
+ **Negative / accepted costs:**
57
+
58
+ - A PHP-shop consumer who depends on the 1.0.4 Packagist release is left behind. Mitigation: clear deprecation notice + npm install pointer in the registry-side archive.
59
+ - The maintainer must perform a one-time login at `packagist.org/packages/event4u/agent-config` to claim or archive the listing — autonomous tooling cannot do that.
60
+ - Future re-entry into the Composer ecosystem would require a new ADR superseding this one. That cost is acceptable: a re-entry would be a strategic redirect, not a quiet re-add.
61
+
62
+ **Operationally neutral:**
63
+
64
+ - `scripts/release.py` already does the right thing; no script change follows from this ADR.
65
+ - `CONTRIBUTING.md § Versioning policy` already does the right thing; no policy change follows from this ADR. The major bumps that prompted the feedback (4.0.0 unified-setup, 5.0.0 follow-up) were policy-correct under the existing rule.
66
+
67
+ ## Alternatives considered
68
+
69
+ - **Dual-track with auto-sync.** Rejected. The repo carries no Composer surface to sync; restoring one would mean re-introducing a `composer.json` + a sync pipeline + a PHP-side autoload story, all to support a consumer base nobody has named. The carrying cost outweighs the demand signal.
70
+ - **Dual-track without auto-sync (status quo).** Rejected. This is the failure mode that produced the external feedback in the first place — a stale 1.0.4 listing pretends a channel is supported when it is not. Operational-honesty argument carries.
71
+ - **Re-introduce composer.json as a stub linking to npm.** Rejected. A stub on Packagist is still a Packagist artifact; consumers may still try to `composer require` it and hit a non-functional package. Registry-side archive is the cleaner signal.
72
+ - **A bespoke `BREAKING_CHANGES.md` file.** Rejected by default; ADR-027 already locks the machine-generated changelog as the breaking-change surface. Maintainer may revisit if the changelog format proves insufficient for end-of-life or migration-guide-shape communication.
73
+
74
+ ## References
75
+
76
+ - [`agents/roadmaps/road-to-distribution-identity.md`](../../agents/roadmaps/road-to-distribution-identity.md) — the work-item plan this ADR underwrites.
77
+ - [`ADR-027-changelog-machine-vs-manual.md`](ADR-027-changelog-machine-vs-manual.md) — the prior decision locking the auto-generated changelog.
78
+ - [`CONTRIBUTING.md § Versioning policy`](../../CONTRIBUTING.md) — the semver discipline this ADR confirms.
79
+ - [`docs/distribution/registries.md`](../distribution/registries.md) — external-registry submission posture; this ADR adds a distribution-channel-identity section to that file.
80
+ - `scripts/release.py` — the `npm publish` release pipeline.
81
+ - External feedback rounds 10 / 12 / 13 (private session transcripts; convergence summary inlined above to keep this ADR self-contained).
@@ -35,6 +35,8 @@ _Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._
35
35
  | [ADR-029](ADR-029-multi-workspace-deferred.md) | Multi Workspace Deferred | accepted | 2026-05-25 | — |
36
36
  | [ADR-030](ADR-030-claude-code-command-projection.md) | Claude Code Command Projection | accepted | 2026-05-28 | — |
37
37
  | [ADR-031](ADR-031-validation-severity-tiers-and-projection-roundtrip.md) | Validation Severity Tiers And Projection Roundtrip | accepted | 2026-05-29 | — |
38
+ | [ADR-032](ADR-032-linked-projects-scope.md) | Linked Projects Scope Go Option A | accepted | 2026-05-29 | — |
39
+ | [ADR-033](ADR-033-distribution-identity-npm-primary.md) | Distribution Identity Npm Primary | accepted | 2026-05-29 | — |
38
40
 
39
41
  ## Unnumbered (legacy)
40
42
 
@@ -4,6 +4,35 @@ Track third-party registries / directories we want this package to surface in. S
4
4
 
5
5
  > **Authority** — Phase 2 of [`road-to-product-adoption.md`](../../agents/roadmaps/road-to-product-adoption.md). The autonomous roadmap pass cannot open PRs in third-party repos; this file is the handoff.
6
6
 
7
+ ## Distribution channels — npm-primary
8
+
9
+ Per [`ADR-033`](../decisions/ADR-033-distribution-identity-npm-primary.md), the package is **npm-primary, Packagist deprecated-in-place**. Both lenses of the council deliberation (strategic / operational) converged on a single-channel posture: this is the canonical record of which registries we publish to vs. which we treat as legacy.
10
+
11
+ | Channel | Status | Canonical install | Notes |
12
+ |---|---|---|---|
13
+ | npm — `@event4u/agent-config` | **Primary** | `npm install @event4u/agent-config` or `npx @event4u/agent-config install` | The release pipeline (`scripts/release.py`) runs `npm publish` exclusively; `package.json` is the source of truth for the published version. |
14
+ | Packagist — `event4u/agent-config` | **Deprecated-in-place** | (do not install — see ADR-033) | The 1.0.4 listing is a legacy artefact from the pre-3.x repo namespace. No `composer.json` ships from this repo. Maintainer-side claim/archive action required (see below). |
15
+
16
+ ### Packagist deprecation — human-owner item
17
+
18
+ The Packagist `event4u/agent-config` listing pins at 1.0.4 from a repository state that no longer exists. The autonomous pipeline cannot retire that listing — it requires a maintainer login at `packagist.org/packages/event4u/agent-config`. Two paths exist; either is acceptable per ADR-033.
19
+
20
+ - [ ] **Claim + abandoned-flag the package.** Log in at packagist.org, claim the `event4u/agent-config` namespace, set the package to `abandoned` with the replacement pointer `event4u/agent-config` on npm (or the npm package URL as a body note where Packagist's abandoned-replacement field expects a Composer-namespace value, fall back to a `description` field redirect).
21
+ - [ ] **OR: leave the listing as legacy + add a description-field redirect.** If claim is blocked or out of scope, edit the listing description to add a one-line `> Deprecated — install via npm: \`@event4u/agent-config\`` so any consumer who lands there sees the correct path.
22
+
23
+ This item is **owner-owned**, not autonomous; the roadmap explicitly captures it as such (`road-to-distribution-identity.md` Phase 2 Step 1). Mark the chosen path with `[x]` once executed.
24
+
25
+ ### Breaking-change communication
26
+
27
+ Major-version bumps are policy-correct per [`CONTRIBUTING.md § Versioning policy`](../../CONTRIBUTING.md) (semver — installer-layout changes are major). The auto-generated `CHANGELOG.md § Breaking` section is the **single source of truth** for breaking changes; [`ADR-027`](../decisions/ADR-027-changelog-machine-vs-manual.md) locks the machine-generated path.
28
+
29
+ Consumers who see a major-version bump should follow:
30
+
31
+ 1. [`CHANGELOG.md § Breaking`](../../CHANGELOG.md#breaking) — the diff between the prior and the new major. Every breaking change carries a Conventional-Commits subject prefixed `feat!:` or with a `BREAKING CHANGE:` footer.
32
+ 2. The release-line link in the GitHub release entry for the new version (links the auto-generated changelog section).
33
+
34
+ No bespoke `BREAKING_CHANGES.md` is maintained — the changelog is the authoritative surface.
35
+
7
36
  ## Submission status
8
37
 
9
38
  | # | Registry | URL | Submission shape | Status | PR link |
@@ -106,7 +106,7 @@ Your agent is now:
106
106
  - **Respecting your codebase** — no conflicting patterns
107
107
  - **Following standards** — consistent code quality
108
108
 
109
- This is enforced automatically by 77 rules. No configuration needed.
109
+ This is enforced automatically by 78 rules. No configuration needed.
110
110
 
111
111
  ---
112
112
 
@@ -0,0 +1,86 @@
1
+ # Working across linked sibling projects
2
+
3
+ When two repositories change together — an API and its frontend, a service and
4
+ a shared library — a change in one can silently break the other. The agent can
5
+ already read and write a sibling repo, but it won't **proactively consider** one
6
+ unless it knows the sibling is relevant. This feature closes that gap: it
7
+ detects the sibling your IDE already attached and, after a one-time opt-in,
8
+ makes the agent flag cross-repo impact by default.
9
+
10
+ > **Scope — passive awareness (Option A).** The agent gains *awareness*: it
11
+ > warns about cross-repo impact on relevant changes and can read/edit the
12
+ > sibling on demand. It does **not** bulk-load the sibling's files into context
13
+ > (that would blow up token cost). See
14
+ > [ADR-032](../decisions/ADR-032-linked-projects-scope.md). Unrelated to
15
+ > [ADR-029](../decisions/ADR-029-multi-workspace-deferred.md) (package root
16
+ > layout).
17
+
18
+ ## Auto-detection (Claude Code — verified)
19
+
20
+ If you attach a sibling repo in your IDE, the agent detects it from on-disk
21
+ config and prompts **once** to opt it in:
22
+
23
+ - **PhpStorm / IntelliJ** — a sibling attached via `.idea/modules.xml` /
24
+ `.idea/vcs.xml` (e.g. `../galawork-web`).
25
+ - **VS Code** — folders in a `*.code-workspace`.
26
+
27
+ On the first turn (and on a new attachment) the agent asks per detected sibling:
28
+ include / decline / always / never-ask. Your choice is stored **local-only** in
29
+ `.agent-settings.local.yml` (in agents/settings/) (gitignored, per-machine — see below). A declined
30
+ sibling is never prompted again.
31
+
32
+ Once a sibling is in scope, the agent proactively checks it for impact when a
33
+ change here may affect it (API contract, shared types) and warns you — without
34
+ loading its files wholesale. Large siblings (a real frontend easily exceeds
35
+ 20 000 files) are flagged `large` and surfaced as awareness only, never skipped.
36
+
37
+ ## Manual setup (other agents / any editor)
38
+
39
+ Auto-detection is verified for Claude Code only. For Cursor, Augment, Copilot,
40
+ or any editor without IDE attachment, add the sibling by hand to
41
+ `.agent-settings.local.yml` (in agents/settings/):
42
+
43
+ ~~~yaml
44
+ linked_projects:
45
+ - path: /abs/path/to/web # or a path relative to the project
46
+ include: true
47
+ ~~~
48
+
49
+ Or, if your agent reads a rules file, drop a short note there:
50
+
51
+ ~~~markdown
52
+ ## Linked sibling project: ../web
53
+
54
+ `../web` changes alongside this repo. When an API/contract or shared-type
55
+ change here may affect it, check `../web` for impact and warn. Don't load its
56
+ files wholesale; access specific files on demand.
57
+ ~~~
58
+
59
+ ## Keep it local, never committed
60
+
61
+ `.agent-settings.local.yml` (in agents/settings/) is **gitignored on purpose** — sibling paths are
62
+ per-developer and per-machine. The installer does **not** touch your
63
+ `.gitignore` (decision D2 — you own your ignore file), so if your project does
64
+ not already ignore it, add:
65
+
66
+ ~~~gitignore
67
+ .agent-settings.local.yml
68
+ ~~~
69
+
70
+ ## Validate it works
71
+
72
+ Ask the agent:
73
+
74
+ > Read `package.json` (or `composer.json`) from the linked sibling and tell me the project name.
75
+
76
+ If it reports the name, cross-repo access works. An out-of-root edit will prompt
77
+ for confirmation, then succeed — that is expected (the agent's permission gate
78
+ still applies).
79
+
80
+ ## Tell us what works
81
+
82
+ Auto-detection is verified for Claude Code only. If you use Cursor, Augment, or
83
+ Copilot, please report whether the rule note alone worked, you needed to add the
84
+ folder to the IDE workspace, or neither — that evidence is the trigger to extend
85
+ verified auto-detection to your agent. See
86
+ [ADR-032](../decisions/ADR-032-linked-projects-scope.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "Universal AI Agent OS \u2014 audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -61,6 +61,21 @@ from . import user_global_paths
61
61
  logger = logging.getLogger(__name__)
62
62
 
63
63
  DEFAULT_PROJECT_FILE = ".agent-settings.yml"
64
+ #: Per-machine override file. Gitignored. A SINGLE project-level file living
65
+ #: under ``agents/settings/`` (with the rest of the project's settings layer,
66
+ #: not at the repo root). It is appended as the deepest cascade layer so a
67
+ #: developer's local values override every committed ``.agent-settings.yml``
68
+ #: without ever being committed. Missing file is harmless (read as {}).
69
+ LOCAL_PROJECT_FILE = ".agent-settings.local.yml"
70
+ #: Project-relative directory the local override lives in.
71
+ LOCAL_PROJECT_SUBDIR = ("agents", "settings")
72
+
73
+
74
+ def _local_settings_path(project_root: Path) -> Path:
75
+ """Single local override: ``<root>/agents/settings/.agent-settings.local.yml``."""
76
+ return project_root.joinpath(*LOCAL_PROJECT_SUBDIR, LOCAL_PROJECT_FILE)
77
+
78
+
64
79
  DEFAULT_TEAM_FILE = ".agent-project-settings.yml"
65
80
  USER_GLOBAL_FILENAME = "agent-settings.yml"
66
81
 
@@ -415,12 +430,12 @@ def _resolve_cascade_paths(
415
430
  """
416
431
  if cwd is None:
417
432
  legacy = Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE)
418
- return [legacy]
433
+ return [legacy, _local_settings_path(legacy.parent)]
419
434
 
420
435
  root = find_project_root(cwd)
421
436
  if root is None:
422
437
  legacy = Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE)
423
- return [legacy]
438
+ return [legacy, _local_settings_path(legacy.parent)]
424
439
 
425
440
  cwd_resolved = cwd.resolve()
426
441
  # Build the chain root → … → cwd (shallowest first, deepest last).
@@ -435,7 +450,9 @@ def _resolve_cascade_paths(
435
450
  break
436
451
  cursor = parent
437
452
  chain.reverse()
438
- return [d / DEFAULT_PROJECT_FILE for d in chain]
453
+ # Committed cascade root cwd, then the single project-level local override
454
+ # under agents/settings/ as the deepest (winning) layer.
455
+ return [d / DEFAULT_PROJECT_FILE for d in chain] + [_local_settings_path(root)]
439
456
 
440
457
 
441
458
  def load_agent_settings(
@@ -0,0 +1,238 @@
1
+ """Detect IDE-attached sibling projects (linked-projects scope, Option A).
2
+
3
+ Pure, dependency-free detector. Reads on-disk IDE config the developer already
4
+ created by attaching a sibling repo, and returns the sibling project roots that
5
+ sit *outside* the current project. Config-driven only — never guesses from
6
+ arbitrary adjacent directories.
7
+
8
+ Sources:
9
+ * PhpStorm / IntelliJ — ``.idea/modules.xml`` (``<module fileurl>``) and
10
+ ``.idea/vcs.xml`` (``<mapping directory>``).
11
+ * VS Code — ``*.code-workspace`` (``folders[].path``).
12
+
13
+ Guardrails (per the linked-projects council, Option A):
14
+ * a candidate must resolve OUTSIDE the project root, exist, and contain a
15
+ ``.git/`` directory;
16
+ * a candidate whose file count exceeds ``max_files`` (default 20000) is
17
+ **flagged** ``large: true`` — NOT excluded. Under Option A the agent only
18
+ carries a passive awareness note and never bulk-includes sibling files, so
19
+ repo size is cost-irrelevant to detection; a real frontend repo routinely
20
+ exceeds 20000 files (excluding node_modules) and must still be surfaced.
21
+ The flag lets the awareness note say "large repo — check targeted impact,
22
+ do not scan the whole tree";
23
+ * the bloat directories ``node_modules``/``.git``/``dist``/``build``/
24
+ ``.venv``/``target`` are never descended into while counting.
25
+
26
+ The detector returns awareness candidates; it does NOT include any sibling
27
+ files in context and does NOT persist anything. Opt-in + persistence is the
28
+ caller's job.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import logging
35
+ import re
36
+ import xml.etree.ElementTree as ET
37
+ from pathlib import Path
38
+ from typing import Any
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ #: File-count ceiling above which a sibling is skipped (token-blowup guard).
43
+ DEFAULT_MAX_FILES = 20000
44
+
45
+ #: Directories never descended into while counting a sibling's size.
46
+ SKIP_DIRS: frozenset[str] = frozenset(
47
+ {"node_modules", ".git", "dist", "build", ".venv", "target", ".idea"}
48
+ )
49
+
50
+
51
+ def detect_linked_projects(
52
+ project_root: Path | str,
53
+ *,
54
+ max_files: int = DEFAULT_MAX_FILES,
55
+ ) -> list[dict[str, Any]]:
56
+ """Return IDE-attached sibling projects outside ``project_root``.
57
+
58
+ Each entry is ``{"path": <absolute str>, "detected_via": <source>,
59
+ "large": <bool>}`` where source is one of ``phpstorm_modules`` /
60
+ ``phpstorm_vcs`` / ``vscode_workspace`` and ``large`` is true when the
61
+ sibling's file count (excluding bloat dirs) exceeds ``max_files``. Results
62
+ are de-duplicated by resolved path (first source wins) and sorted by path.
63
+ Size never excludes — see the module docstring.
64
+ """
65
+ root = Path(project_root).resolve()
66
+ if not root.is_dir():
67
+ logger.info("linked_projects: project_root %s is not a directory", root)
68
+ return []
69
+
70
+ candidates: list[tuple[Path, str]] = []
71
+ candidates.extend((p, "phpstorm_modules") for p in _phpstorm_modules(root))
72
+ candidates.extend((p, "phpstorm_vcs") for p in _phpstorm_vcs(root))
73
+ candidates.extend((p, "vscode_workspace") for p in _vscode_workspace(root))
74
+
75
+ seen: set[Path] = set()
76
+ out: list[dict[str, Any]] = []
77
+ for path, source in candidates:
78
+ try:
79
+ resolved = path.resolve()
80
+ except OSError:
81
+ logger.info("linked_projects: cannot resolve %s", path)
82
+ continue
83
+ if resolved in seen:
84
+ continue
85
+ if not _is_valid_sibling(resolved, root):
86
+ continue
87
+ large = _exceeds_size(resolved, max_files)
88
+ if large:
89
+ logger.info(
90
+ "linked_projects: %s exceeds %d files — flagged large (awareness only)",
91
+ resolved,
92
+ max_files,
93
+ )
94
+ seen.add(resolved)
95
+ out.append(
96
+ {"path": str(resolved), "detected_via": source, "large": large}
97
+ )
98
+
99
+ out.sort(key=lambda e: e["path"])
100
+ return out
101
+
102
+
103
+ def _is_valid_sibling(candidate: Path, root: Path) -> bool:
104
+ """A sibling must be outside the project root, exist, and be a git repo."""
105
+ try:
106
+ if candidate == root or root in candidate.parents:
107
+ return False # inside the project — that's the module system's job
108
+ if candidate in root.parents:
109
+ return False # an ancestor of the project, not a sibling
110
+ if not candidate.is_dir():
111
+ logger.info("linked_projects: candidate missing/not-a-dir %s", candidate)
112
+ return False
113
+ if not (candidate / ".git").exists():
114
+ logger.info("linked_projects: candidate not a git repo %s", candidate)
115
+ return False
116
+ except OSError:
117
+ logger.info("linked_projects: unreadable candidate %s", candidate)
118
+ return False
119
+ return True
120
+
121
+
122
+ def _exceeds_size(candidate: Path, max_files: int) -> bool:
123
+ """True if the tree (minus SKIP_DIRS) holds more than ``max_files`` files."""
124
+ import os
125
+
126
+ count = 0
127
+ for dirpath, dirnames, filenames in os.walk(candidate):
128
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
129
+ count += len(filenames)
130
+ if count > max_files:
131
+ return True
132
+ return False
133
+
134
+
135
+ def _phpstorm_modules(root: Path) -> list[Path]:
136
+ """Sibling roots from ``.idea/modules.xml`` ``<module fileurl>`` entries."""
137
+ path = root / ".idea" / "modules.xml"
138
+ elems = _iter_xml_attrs(path, "module", ("fileurl", "filepath"))
139
+ out: list[Path] = []
140
+ for attrs in elems:
141
+ raw = attrs.get("fileurl") or attrs.get("filepath")
142
+ if not raw:
143
+ continue
144
+ resolved = _resolve_idea_url(raw, root)
145
+ if resolved is None:
146
+ continue
147
+ # raw points at <sibling>/.idea/<name>.iml → sibling is .idea's parent.
148
+ if resolved.parent.name == ".idea":
149
+ out.append(resolved.parent.parent)
150
+ else:
151
+ out.append(resolved)
152
+ return out
153
+
154
+
155
+ def _phpstorm_vcs(root: Path) -> list[Path]:
156
+ """Sibling roots from ``.idea/vcs.xml`` ``<mapping directory>`` entries."""
157
+ path = root / ".idea" / "vcs.xml"
158
+ out: list[Path] = []
159
+ for attrs in _iter_xml_attrs(path, "mapping", ("directory",)):
160
+ raw = attrs.get("directory")
161
+ if not raw:
162
+ continue
163
+ resolved = _resolve_idea_url(raw, root)
164
+ if resolved is not None:
165
+ out.append(resolved)
166
+ return out
167
+
168
+
169
+ def _vscode_workspace(root: Path) -> list[Path]:
170
+ """Sibling roots from ``*.code-workspace`` ``folders[].path`` entries."""
171
+ out: list[Path] = []
172
+ try:
173
+ workspaces = sorted(root.glob("*.code-workspace"))
174
+ except OSError:
175
+ return out
176
+ for ws in workspaces:
177
+ data = _read_jsonc(ws)
178
+ if not isinstance(data, dict):
179
+ continue
180
+ folders = data.get("folders")
181
+ if not isinstance(folders, list):
182
+ continue
183
+ for folder in folders:
184
+ if not isinstance(folder, dict):
185
+ continue
186
+ rel = folder.get("path")
187
+ if not isinstance(rel, str) or not rel.strip():
188
+ continue
189
+ out.append((root / rel).resolve())
190
+ return out
191
+
192
+
193
+ def _resolve_idea_url(raw: str, root: Path) -> Path | None:
194
+ """Resolve a PhpStorm path token to an absolute Path, or None."""
195
+ value = raw.strip()
196
+ if value.startswith("file://"):
197
+ value = value[len("file://") :]
198
+ value = value.replace("$PROJECT_DIR$", str(root))
199
+ if not value:
200
+ return None
201
+ try:
202
+ return (Path(value) if Path(value).is_absolute() else root / value).resolve()
203
+ except OSError:
204
+ return None
205
+
206
+
207
+ def _iter_xml_attrs(
208
+ path: Path, tag: str, _attrs: tuple[str, ...]
209
+ ) -> list[dict[str, str]]:
210
+ """Return the attribute dicts of every ``<tag>`` in ``path`` (tolerant)."""
211
+ if not path.is_file():
212
+ return []
213
+ try:
214
+ tree = ET.parse(path)
215
+ except (ET.ParseError, OSError) as exc:
216
+ logger.info("linked_projects: malformed/unreadable %s (%s)", path, exc)
217
+ return []
218
+ return [dict(el.attrib) for el in tree.iter(tag)]
219
+
220
+
221
+ def _read_jsonc(path: Path) -> Any:
222
+ """Parse JSON that may carry ``//`` comments and trailing commas (VS Code)."""
223
+ try:
224
+ text = path.read_text(encoding="utf-8")
225
+ except OSError:
226
+ return None
227
+ try:
228
+ return json.loads(text)
229
+ except json.JSONDecodeError:
230
+ pass
231
+ # tolerant fallback: strip line comments + trailing commas, retry once.
232
+ stripped = re.sub(r"^\s*//.*$", "", text, flags=re.MULTILINE)
233
+ stripped = re.sub(r",(\s*[}\]])", r"\1", stripped)
234
+ try:
235
+ return json.loads(stripped)
236
+ except json.JSONDecodeError as exc:
237
+ logger.info("linked_projects: malformed workspace JSON %s (%s)", path, exc)
238
+ return None