@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.
- package/.agent-src/contexts/execution/roadmap-process-loop.md +30 -4
- package/.agent-src/rules/linked-projects-onboarding-gate.md +82 -0
- package/.agent-src/rules/roadmap-progress-sync.md +39 -5
- package/.agent-src/scripts/update_roadmap_progress.py +63 -7
- package/.agent-src/skills/roadmap-management/SKILL.md +121 -21
- package/.agent-src/skills/roadmap-writing/SKILL.md +63 -0
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +22 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +20 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +6 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +33 -11
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/router.json +1 -1
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +3 -2
- package/docs/decisions/ADR-032-linked-projects-scope.md +118 -0
- package/docs/decisions/ADR-033-distribution-identity-npm-primary.md +81 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/distribution/registries.md +29 -0
- package/docs/getting-started.md +1 -1
- package/docs/guides/cross-repo-linked-projects.md +86 -0
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +20 -3
- package/scripts/_lib/linked_projects.py +238 -0
- package/scripts/check_no_local_settings_committed.py +51 -0
- 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).
|
package/docs/decisions/INDEX.md
CHANGED
|
@@ -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 |
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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.
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|