diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8ca58f..80c59fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,3 +54,37 @@ jobs: if (-not (Test-Path "$root/learning-flow")) { throw "Installer did not create the learning-flow component." } + - name: Run global and linked scope smoke test + shell: pwsh + run: | + $globalRoot = Join-Path $env:RUNNER_TEMP "learning-flow-global" + $linked = Join-Path $env:RUNNER_TEMP "learning-flow-linked" + New-Item -ItemType Directory -Force -Path $globalRoot, $linked | Out-Null + $env:CODEBASE_LEARNING_FLOW_HOME = $globalRoot + & "$PWD/scripts/install.ps1" ` + -Scope Global ` + -Repository $env:GITHUB_REPOSITORY ` + -Ref $env:GITHUB_SHA ` + -Profile full ` + -Mode fail + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$PWD/scripts/install.ps1" ` + -Target $linked ` + -Scope Linked ` + -Repository $env:GITHUB_REPOSITORY ` + -Ref $env:GITHUB_SHA ` + -Mode fail ` + -SkipRootAgents + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (-not (Test-Path "$globalRoot/skills/repository-learning/SKILL.md")) { + throw "Global scope did not install managed skills." + } + if (Test-Path "$globalRoot/learning-flow/MAP.md") { + throw "Repository-authored MAP.md reached the global root." + } + if (-not (Test-Path "$linked/learning-flow/MAP.md")) { + throw "Linked scope did not install repository learning state." + } + if (Test-Path "$linked/agentic-flow/AGENTS.md") { + throw "Framework instructions were duplicated into the linked repository." + } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7985087..de72f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 1.4.0 + +Install scopes: the framework can now be installed once into `%USERPROFILE%\.agents\` (`~/.agents/`) and shared by every repository, while each repository keeps its own learning state locally. Repository-scoped installation is unchanged and remains the default. + +### Added + +- `--scope repository|global|linked` / `-Scope Repository|Global|Linked` on both installers. `global` installs framework-owned instructions and managed skills into `$HOME/.agents` (skills at `/skills/`, where host agents already look) and writes no repository state — no `.local/`, no `.gitignore` entry, no home-directory `AGENTS.md`. `linked` installs only a repository's own learning state and reads everything else from the global installation. +- `.repository-files` manifests in `sample/common/agentic-flow/` and both profiles' `learning-flow/`, naming the repository-authored seeds (`SETTINGS.md`, `DECISIONS.md`, `MAP.md`, `TAKEAWAYS.md`, and `REPOSITORIES.md` in the full profile). The framework-owned half was already declared in `.managed-files`; this makes the other half explicit instead of implicit in what `update` happens not to overwrite. +- "Framework root" section in `agentic-flow/AGENTS.md`: the single canonical rule that `agentic-flow/` and `learning-flow/` resolve at the repository root first and at `~/.agents/` second, that a repository copy always wins, that the two roots are never merged, and that repository state is never read from the global root. +- `learning-flow/.install-scope` marker recording the scope and framework version of each installation, plus the global version a linked repository was linked against. The installer compares them and warns on drift. This reintroduces a version marker under the three conditions `docs/DESIGN_NOTES.md` set in 1.3 when it removed `.template-version`: a documented reader, a stated compatibility rule, and a CI check. +- Install-scope classification check in `scripts/ci-validate.py`: every packaged file in a component must appear in exactly one of its managed/repository manifests, so a new file cannot ship without a declared install scope. +- Global and linked scenarios in `scripts/ci-install-test.sh`, `scripts/ci-release-test.sh`, and the PowerShell CI job, including assertions that neither root holds the other's content and that the two scope markers agree on the version. +- `CODEBASE_LEARNING_FLOW_HOME` environment override for the global root. +- Two routing cases in `skill-evals/agentic-cases.yaml` covering framework-root resolution and the rule that repository knowledge is never written to the global root. + +### Changed + +- Scope conversion is supported in both directions. `--scope linked --mode update` removes a repository's framework copies through their own manifests and leaves authored state in place; `--scope repository --mode update` restores them at the profile and extension the global installation was providing. +- The root pointer block and lean root template now state the framework-root resolution rule, so one pointer stays correct whether the framework files are local, global, or later moved between the two. +- `scripts/ci-validate.py` now validates `.extension-managed-files` and `.extension-managed-skills` the same way it already validated their non-extension counterparts. +- Both installers report the resolved scope and root in their closing summary, and warn when a repository-scoped install would make the host agent discover every managed skill twice. +- `agentic-workflow`'s discovery step now detects `learning-flow/.install-scope` and reports which root the framework files resolve from; `agentic-flow/LEARN.md`'s setup explanation names that root too, so "explain my agentic setup" stays accurate in a linked repository. +- `learning-closure` states that every destination it recommends belongs to the repository being worked on, closing the one place a skill could write repository knowledge to a global root. +- The regulatory extension is now detected by skill name rather than by the literal path `.agents/skills/regulatory-knowledge/`, which resolves differently under a global installation and would have made the extension look absent in a linked repository. + +### Fixed + +- `agentic-flow/ROOT_INTEGRATION.md`'s documented pointer-block example, which had again fallen behind `sample/root/AGENTS.pointer.md`. +- `docs/EXAMPLE_WALKTHROUGH.md` was missing from `MANIFEST.txt`, so released packages shipped a `docs/README.md` whose link to it was broken. + + ## 1.3.0 Architectural simplification of the `full` profile: reduced meta-ceremony while preserving every behavior the framework depends on (repository authority, selective learning, hypothesis-first proposals, consequential-change reasoning, private continuity, optional regulatory guidance). diff --git a/MANIFEST.txt b/MANIFEST.txt index 7c5e000..7bbf605 100644 --- a/MANIFEST.txt +++ b/MANIFEST.txt @@ -1,17 +1,18 @@ .gitattributes 40 .gitignore 324 AGENTS.md 2995 -CHANGELOG.md 26666 +CHANGELOG.md 31134 LICENSE 2213 -README.md 9023 +README.md 12031 adoption/ADOPT.md 4177 adoption/README.md 2445 -docs/AGENTIC_WORKFLOW_SANITY.md 9802 -docs/ARCHITECTURE.md 7131 -docs/DESIGN_NOTES.md 37828 +docs/AGENTIC_WORKFLOW_SANITY.md 10021 +docs/ARCHITECTURE.md 8577 +docs/DESIGN_NOTES.md 45480 +docs/EXAMPLE_WALKTHROUGH.md 3614 docs/EDUCATION_MODEL.md 5452 -docs/INITIALIZE_LEARNING_FLOW.md 10880 -docs/README.md 4016 +docs/INITIALIZE_LEARNING_FLOW.md 12199 +docs/README.md 4257 docs/references/REFERENCE_INTEGRATION.md 4551 docs/references/REFERENCE_REVIEW_AWESOME_AGENT_SKILLS.md 2317 docs/references/REFERENCE_REVIEW_BEST_OF_AGENT_HARNESSES.md 2540 @@ -19,14 +20,14 @@ docs/references/REFERENCE_REVIEW_GOOSE.md 2486 docs/references/REFERENCE_REVIEW_LEARNING_FLOW_ADJUSTMENT.md 9505 docs/references/REFERENCE_REVIEW_LITT.md 2069 docs/references/REFERENCE_REVIEW_POCOK.md 2155 -sample/README.md 3065 -sample/common/.agents/skills/agentic-workflow/SKILL.md 2686 -sample/common/.agents/skills/learn-anything/SKILL.md 1756 +sample/README.md 3382 +sample/common/.agents/skills/agentic-workflow/SKILL.md 2828 +sample/common/.agents/skills/learn-anything/SKILL.md 1793 sample/common/.agents/skills/learn-anything/agents/openai.yaml 246 -sample/common/.agents/skills/learning-closure/SKILL.md 3605 +sample/common/.agents/skills/learning-closure/SKILL.md 3709 sample/common/.agents/skills/learning-freshness/SKILL.md 1908 -sample/common/.agents/skills/repository-learning/SKILL.md 4904 -sample/common/.agents/skills/structured-change/SKILL.md 3059 +sample/common/.agents/skills/repository-learning/SKILL.md 4941 +sample/common/.agents/skills/structured-change/SKILL.md 3096 sample/common/.agents/skills/structured-change/knowledge/engineering/ai-collaboration.md 2533 sample/common/.agents/skills/structured-change/knowledge/engineering/architecture.md 1386 sample/common/.agents/skills/structured-change/knowledge/engineering/dependency-management.md 1300 @@ -37,19 +38,20 @@ sample/common/.agents/skills/structured-change/knowledge/engineering/testing.md sample/common/.agents/skills/structured-change/templates/design.md 649 sample/common/.agents/skills/structured-change/templates/exploration.md 368 sample/common/.agents/skills/structured-change/templates/structure.md 243 -sample/common/agentic-flow/.managed-files 152 +sample/common/agentic-flow/.managed-files 170 sample/common/agentic-flow/.managed-skills 106 -sample/common/agentic-flow/AGENTS.md 5995 -sample/common/agentic-flow/CONFIGURE.md 4475 +sample/common/agentic-flow/.repository-files 25 +sample/common/agentic-flow/AGENTS.md 6825 +sample/common/agentic-flow/CONFIGURE.md 4469 sample/common/agentic-flow/DECISIONS.md 963 sample/common/agentic-flow/EDUCATION.md 5232 -sample/common/agentic-flow/LEARN.md 1822 -sample/common/agentic-flow/LOCAL.md 4179 +sample/common/agentic-flow/LEARN.md 1910 +sample/common/agentic-flow/LOCAL.md 4366 sample/common/agentic-flow/README.md 2897 sample/common/agentic-flow/REFERENCE_INTEGRATION.md 2183 -sample/common/agentic-flow/ROOT_INTEGRATION.md 3392 +sample/common/agentic-flow/ROOT_INTEGRATION.md 3803 sample/common/agentic-flow/SETTINGS.md 544 -sample/common/agentic-flow/WORKFLOW.md 6086 +sample/common/agentic-flow/WORKFLOW.md 6080 sample/common/local/learning-history.md 1528 sample/extensions/regulatory/.agents/skills/regulatory-knowledge/SKILL.md 3530 sample/extensions/regulatory/.agents/skills/regulatory-knowledge/knowledge/regulatory/audit-trails.md 1444 @@ -67,36 +69,38 @@ sample/extensions/regulatory/.agents/skills/regulatory-knowledge/knowledge/regul sample/extensions/regulatory/learning-flow/.extension-managed-files 81 sample/extensions/regulatory/learning-flow/.extension-managed-skills 21 sample/extensions/regulatory/learning-flow/.extension-name 11 -sample/extensions/regulatory/learning-flow/REGULATORY.md 1882 -sample/profiles/full/.agents/skills/change-explainer/SKILL.md 1308 +sample/extensions/regulatory/learning-flow/REGULATORY.md 1827 +sample/profiles/full/.agents/skills/change-explainer/SKILL.md 1345 sample/profiles/full/.agents/skills/change-explainer/templates/change-explainer.md 353 -sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md 1491 -sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md 1513 +sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md 1528 +sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md 1550 sample/profiles/full/.agents/skills/ticket-learning-path/templates/ticket-learning-path.md 405 -sample/profiles/full/learning-flow/.managed-files 69 +sample/profiles/full/learning-flow/.managed-files 87 sample/profiles/full/learning-flow/.managed-skills 57 +sample/profiles/full/learning-flow/.repository-files 36 sample/profiles/full/learning-flow/.template-profile 5 -sample/profiles/full/learning-flow/AGENTS.md 3198 +sample/profiles/full/learning-flow/AGENTS.md 3360 sample/profiles/full/learning-flow/MAP.md 1464 sample/profiles/full/learning-flow/README.md 2383 sample/profiles/full/learning-flow/REPOSITORIES.md 439 sample/profiles/full/learning-flow/TAKEAWAYS.md 970 -sample/profiles/minimal/learning-flow/.managed-files 69 +sample/profiles/minimal/learning-flow/.managed-files 87 sample/profiles/minimal/learning-flow/.managed-skills 0 +sample/profiles/minimal/learning-flow/.repository-files 20 sample/profiles/minimal/learning-flow/.template-profile 8 -sample/profiles/minimal/learning-flow/AGENTS.md 1362 +sample/profiles/minimal/learning-flow/AGENTS.md 1525 sample/profiles/minimal/learning-flow/MAP.md 1229 sample/profiles/minimal/learning-flow/README.md 1974 sample/profiles/minimal/learning-flow/TAKEAWAYS.md 973 -sample/root/AGENTS.md 2102 -sample/root/AGENTS.pointer.md 551 -scripts/README.md 6771 +sample/root/AGENTS.md 2263 +sample/root/AGENTS.pointer.md 712 +scripts/README.md 10227 scripts/install.bat 1477 -scripts/install.ps1 40477 -scripts/install.sh 34980 +scripts/install.ps1 55583 +scripts/install.sh 47145 skill-evals/README.md 1689 skill-evals/adoption-cases.yaml 1337 -skill-evals/agentic-cases.yaml 8895 +skill-evals/agentic-cases.yaml 9830 skill-evals/conversation-cases.yaml 3653 skill-evals/full-cases.yaml 5001 skill-evals/minimal-cases.yaml 3913 diff --git a/README.md b/README.md index 13d92f0..20d443c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,39 @@ curl -fsSL https://raw.githubusercontent.com/legrab/codebase-learning-flow/main/ *Contributing to the framework itself? Run `scripts/install.sh` from your local checkout instead of curling the remote copy above.* +### One installation for every repository + +By default the framework installs into a single repository. `--scope global` instead installs the framework-owned instructions and skills once, into `%USERPROFILE%\.agents\` (`~/.agents/` elsewhere), where every repository picks them up: + +```powershell +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/legrab/codebase-learning-flow/main/scripts/install.ps1))) -Scope Global -Profile Full +``` + +```sh +sh install.sh --scope global --profile full +``` + +That alone is enough to work in any repository. What it deliberately does not do is create repository state: a global installation writes no `.local/`, no `.gitignore` entry, and no `AGENTS.md` in your home directory. + +When a repository should keep its own durable learning — a map of the system, verified takeaways, its own collaboration settings and decision record — run `--scope linked` inside it: + +```sh +sh install.sh --scope linked +``` + +The repository then holds only what it authors; the instructions and skills stay global and shared. + +| Scope | Root | Holds | +|---|---|---| +| `repository` (default) | the repository | everything, self-contained | +| `global` | `~/.agents/` | instructions and skills, shared by every repository | +| `linked` | the repository | that repository's own learning state only | + +> [!NOTE] +> Instructions resolve at the repository root first and fall back to `~/.agents/`, so a repository with its own copy always wins. Repository state — `.local/`, `MAP.md`, `TAKEAWAYS.md`, `SETTINGS.md`, `DECISIONS.md` — is never read from the global root. + +An existing installation can move between scopes: `--scope linked --mode update` strips the framework copies out of a repository and leaves its authored state behind, and `--scope repository --mode update` puts them back. +
Profiles, extensions, and update modes @@ -197,6 +230,28 @@ learning-flow/ Task-specific templates live inside their owning skills and are materialized only when justified. +
+Where each file lands under a global installation + +The split follows the manifests the installer already used to decide what `update` may overwrite: `.managed-files` and `.managed-skills` name framework-owned content, `.repository-files` names the repository-authored seeds. + +```text +~/.agents/ / +├── agentic-flow/ ├── agentic-flow/ +│ ├── AGENTS.md │ ├── SETTINGS.md +│ ├── WORKFLOW.md │ └── DECISIONS.md +│ ├── EDUCATION.md ├── learning-flow/ +│ └── ... │ ├── MAP.md +├── learning-flow/ │ ├── TAKEAWAYS.md +│ ├── AGENTS.md │ └── REPOSITORIES.md +│ └── README.md ├── .local/ +└── skills/ └── AGENTS.md +``` + +`learning-flow/.install-scope` in each root records the scope and the framework version it was installed at. A linked repository also records the version of the global installation it was linked against, and the installer warns when the two drift apart. + +
+ ## Documentation - [`docs/EDUCATION_MODEL.md`](docs/EDUCATION_MODEL.md) · learning and ownership model diff --git a/docs/AGENTIC_WORKFLOW_SANITY.md b/docs/AGENTIC_WORKFLOW_SANITY.md index a28ec48..9b7f12e 100644 --- a/docs/AGENTIC_WORKFLOW_SANITY.md +++ b/docs/AGENTIC_WORKFLOW_SANITY.md @@ -39,6 +39,8 @@ one narrowly relevant knowledge/reference file, if needed Learning and regulatory material are conditional branches, not part of the universal baseline. +Resolving the framework root — repository first, then `~/.agents/` — happens once, when the first framework file is opened. It is not a step, does not add a read, and must not turn into an inventory of both roots. + ## Sanity scenarios ### 1. Typo-only change diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 691c065..193927a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -116,6 +116,30 @@ This separation is a trust and context boundary as well as an installation bound
+## Install roots + +The three layers are content boundaries. Cutting across them is a second, independent boundary: whether a given file describes *the framework* or *one repository*. + +| Kind | Example | Root | +|---|---|---| +| Framework-owned | `agentic-flow/AGENTS.md`, `learning-flow/AGENTS.md`, every managed skill | repository or `~/.agents/` | +| Repository-authored | `MAP.md`, `TAKEAWAYS.md`, `REPOSITORIES.md`, `SETTINGS.md`, `DECISIONS.md`, `.local/` | always the repository | + +Framework-owned content is identical in every repository, so it can be installed once globally and shared. Repository-authored content describes one system and cannot be. This is the same line the installer's `.managed-files` manifests already drew to decide what `update` may overwrite; `.repository-files` names the other side of it explicitly so both can be installed independently. + +Instructions resolve at the repository root first and fall back to the global root, so a self-contained repository never consults the global installation. There is no merging between roots: whichever answers first is the one that applies. + +```mermaid +flowchart LR + T[Task in a repository] --> R{Repository has agentic-flow/?} + R -->|yes| L[Read repository copy] + R -->|no| G[Read ~/.agents copy] + L --> S[Repository state: MAP, TAKEAWAYS, SETTINGS, .local] + G --> S +``` + +Repository state sits below the fork because it is read from the repository either way. + ## Runtime instruction flow The path an agent actually walks for one task, independent of profile: diff --git a/docs/DESIGN_NOTES.md b/docs/DESIGN_NOTES.md index 5099ec9..cdff9bf 100644 --- a/docs/DESIGN_NOTES.md +++ b/docs/DESIGN_NOTES.md @@ -4,6 +4,48 @@ The harness should keep a developer able to reason about a repository while collaborating with an agent, and let any learner use the same lightweight methods for a general subject. It should improve delivery, code and architecture understanding, domain reasoning, debugging, ownership growth, and conversational learning without making workflow administration or learning administration the primary activity. +## v1.4 install scopes: one framework, many repositories + +Until 1.4 the framework had exactly one install root. A developer who wanted this behavior in fifteen repositories installed and updated fifteen byte-identical copies of `agentic-flow/`, `learning-flow/`, and every managed skill, and had no way at all to get the behavior in a repository they could not or should not modify. The layer architecture was already right; the *deployment* model assumed the repository was the only place content could live. + +### The split already existed as data + +The important finding of this pass was that the global/local boundary did not need to be invented. `.managed-files` and `.managed-skills` already named exactly the framework-owned, repository-independent content — that is precisely what makes those files safe for `update` to overwrite. Everything the package shipped but did not list (`SETTINGS.md`, `DECISIONS.md`, `MAP.md`, `TAKEAWAYS.md`, `REPOSITORIES.md`) was repository-authored, which is why `update` deliberately left it alone. + +So the boundary was real and load-bearing, but only expressed negatively: as *the set of files update happens not to touch*. A new file added to a component belonged to that set by omission, with nothing to catch the mistake. + +`.repository-files` names the other side of the line explicitly. It costs one small manifest per component, and it turns an implicit convention into something `scripts/ci-validate.py` can enforce: every packaged file in a component must appear in exactly one of its manifests, never both and never neither. A file with no declared install scope would otherwise ship to whichever root the payload happened to be copied into. + +This is also why `--scope` is orthogonal to profile and extension rather than a fourth value of one of them. Profile selects *how much* is installed; extension selects *what additional lens*; scope selects *where each half goes*. They compose without interacting. + +### Path resolution is the part that actually needed designing + +Roughly thirty-five instruction and skill references were written as bare repository-relative paths (``follow `agentic-flow/AGENTS.md` ``). With one root that is unambiguous. With two it is not, and relative paths cannot fix it: a skill at `/.agents/skills/x/` reaches `agentic-flow/` through `../../../`, while the same skill at `~/.agents/skills/x/` reaches it through `../../`. There is no single relative form, so the rule has to be stated. + +It is stated once, under "Framework root" in `agentic-flow/AGENTS.md`: resolve at the repository root first, then at `~/.agents/`; a repository copy always wins; never merge the two; repository state is never read from the global root. + +Each skill additionally carries a five-word parenthetical (`repository root, else ~/.agents/`) on its first reference. This is deliberate duplication, against the one-canonical-owner discipline the rest of this document enforces, and the reason is a genuine chicken-and-egg: a skill is an *entry point* that a host agent may invoke before any other framework file has been read, so it cannot delegate "where is the framework root" to a file whose location is exactly what the rule resolves. The parenthetical is kept to the minimum that makes a skill self-sufficient; the full rule, with its precedence and non-merging semantics, has one owner. + +### The version marker, reintroduced under the conditions v1.3 set + +v1.3 removed the `.template-version` files because nothing read them, and stated three conditions for bringing a version marker back: a documented reader, a stated compatibility rule, and a CI check that fails when the value goes stale. A global root at one version with repositories linked against another is the first situation where those conditions can all be met. + +`learning-flow/.install-scope` records `scope` and `version`, and a linked repository also records the `global-version` it was linked against. The reader is the installer's own skew check; the rule is that the two must agree; the CI check is a paired global-then-linked install in `ci-install-test.sh` and `ci-release-test.sh` asserting they do. The value is installer-generated rather than hand-maintained, so it cannot drift the way the old per-component constants did — the failure mode this marker guards is drift between two *installations*, not between a file and its own repository. + +### Boundary decisions + +- **Global skills land in `/skills/`, not `/.agents/skills/`.** The global root *is* the `.agents` directory a host agent scans. Nesting another `.agents` inside it would put skills where nothing looks for them. +- **A global installation writes no repository state.** No `.local/`, no `.gitignore` entry, no `AGENTS.md` in the home directory. A tool that quietly creates dotfiles in `$HOME` beyond the directory it was asked to manage has exceeded its mandate, and `.local/` in particular is meaningless outside a repository: it holds continuity *about a system*. +- **`linked` requires an existing global installation and refuses without one.** The alternative — falling back to a download — would let a repository be seeded from a different version than the instructions it will actually read, which is the exact skew the marker exists to catch. Failing with a one-line instruction is better than silently producing the inconsistent state. +- **`linked` inherits profile and extension rather than accepting its own.** A repository seeded for `full` while reading `minimal` routing is incoherent, so a conflicting `--profile` is an error rather than a silent override. +- **Repository-authored seeds are copy-if-missing in every mode, including `replace`.** In a repository install, `replace` resets framework directories, which is a defensible destructive reset because the content is framework-owned and refreshable. In a linked repository there is no framework content at all — everything present was authored locally — so the same mode would only destroy the user's learning. Modes describe what may happen to framework content; where there is none, they have nothing to do. +- **Scope conversion is supported in both directions** because existing installations are all repository-scoped and would otherwise need manual deletion to adopt this. `repository` → `linked` requires `update` or `replace` for the same reason a destructive profile switch does: it removes files, and `merge` never removes anything. It removes them through the repository's own recorded manifests, so nothing outside the framework's declared ownership is touched. +- **Installing `--scope repository` alongside a global installation warns rather than fails.** The host agent then discovers every managed skill twice, which is a real problem, but a repository that deliberately pins its own copy is a legitimate choice. + +### Deliberately not built + +A global `.local/`, a global `SETTINGS.md` supplying default collaboration preferences, symlinking instead of copying, and any form of automatic global-to-repository synchronization. The first two would move repository-specific state out of the repository; the third breaks on Windows without developer mode and makes `update` semantics unclear; the fourth reintroduces the unpinned-`latest` problem the release-distribution section already rejected. + ## v1.3 consolidation and ownership boundaries The 1.3 cleanup makes three small maintenance boundaries explicit: @@ -308,7 +350,8 @@ Design decisions specific to this boundary: - a proliferating `explorations/`/`designs/`/`decisions/`-per-item directory scaffold in place of the existing durable-file surfaces; - mandatory evaluation-matrix scoring or commit traceability blocks for ordinary, low-risk work; - a design challenge for a one-line or obviously reversible change; -- a clarifying question about something repository evidence already answers. +- a clarifying question about something repository evidence already answers; +- repository-specific state at a global install root, or automatic synchronization between roots. ## Final review checklist @@ -326,3 +369,5 @@ Design decisions specific to this boundary: 12. No workflow requires contributor identity unless the user explicitly wants personal tracking. 13. An installed extension never changes what a profile means, and adding or removing one never touches unrelated framework or repository content. 14. A checkout install and a packaged-release install are never ambiguous about which one ran: the installer states its version and trust boundary, and there is no path that silently resolves an unpinned "latest" release. +15. Every installable file declares whether the framework or the repository owns it, and no install scope can place one at the other's root. +16. A repository copy of the instructions always wins over the global one, and the two are never merged. diff --git a/docs/INITIALIZE_LEARNING_FLOW.md b/docs/INITIALIZE_LEARNING_FLOW.md index 1359137..4ef1454 100644 --- a/docs/INITIALIZE_LEARNING_FLOW.md +++ b/docs/INITIALIZE_LEARNING_FLOW.md @@ -123,6 +123,10 @@ Update only manifest-owned framework files and managed skills. Preserve reposito Support an orthogonal, additive extension dimension (currently one: a regulatory extension adding traceability, validation, risk-management, and audit-trail knowledge). An extension must install identically under either profile, never change what a profile means, track its own managed-file and managed-skill manifests under names distinct from the profile's own, and be fully removable in `update` or `replace` mode without touching profile or repository content. New installs default to no extension; existing installations retain their selection automatically. +Support a second orthogonal dimension, install scope: `repository` (default, self-contained), `global` (framework-owned instructions and managed skills only, installed once under `$HOME/.agents` and shared by every repository), and `linked` (one repository's own learning state only, reading everything else from a global installation). Each component must declare both halves — framework-owned in a managed-file manifest, repository-authored in a repository-file manifest — so no file can be installed to the wrong root, and structural validation must reject a packaged file that is declared in neither or in both. A global installation must write no repository state: no `.local/`, no ignore rule, no home-directory root instructions. A linked installation must inherit the global installation's profile and extension, must copy its seeds only when missing in every mode, and must refuse to run when no global installation exists rather than seeding from a possibly different version. + +Instruction files must resolve `agentic-flow/` and `learning-flow/` at the repository root first and the global root second, never merging the two. Record the scope and framework version of each installation, and the global version a linked repository was linked against, so version drift between the two roots is detectable. + Fresh setup must create `.local/{sessions,follow-ups}`, seed `.local/learning-history.md` only when missing, and ensure `/.local/` is ignored. Repeated setup must preserve all existing local content. Retire only framework-owned legacy contributor placeholders automatically; move contributor-authored tracked state only through an explicit verified migration. ## Safety and restraint diff --git a/docs/README.md b/docs/README.md index 61cb6ce..c99a356 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,7 +76,10 @@ Use [`AGENTIC_WORKFLOW_SANITY.md`](AGENTIC_WORKFLOW_SANITY.md) when changing com 6. Preserve repository-authored maps, takeaways, settings, and unrelated skills during updates. 7. Validate both minimal and full installations after changing manifests or - managed files. + managed files, and both the global and linked scopes after changing which + files a component owns. + A new file under a component must be added to its `.managed-files` or its + `.repository-files`; structural validation rejects one that is in neither. 8. Review the human entry points after structural changes. The root README should remain useful before any agent-facing detail is read. diff --git a/sample/README.md b/sample/README.md index 37013cd..0d572aa 100644 --- a/sample/README.md +++ b/sample/README.md @@ -42,6 +42,8 @@ sample/root/ | `extensions/regulatory` | optional, additive traceability/validation/risk-management knowledge | | `.local/` | private sessions, attempts, checks, progress, and follow-ups | +Each component declares which of its files the framework owns and which the installing repository authors, in `.managed-files` and `.repository-files` respectively. That split is what lets `--scope global` install the first set once for every repository while `--scope linked` keeps the second set where it belongs. + > [!IMPORTANT] > Learning routes share educational principles but not repository assumptions. General learning remains safe for history, science, languages, arts, teaching, and other non-code topics. diff --git a/sample/common/.agents/skills/agentic-workflow/SKILL.md b/sample/common/.agents/skills/agentic-workflow/SKILL.md index ca47251..461b6ab 100644 --- a/sample/common/.agents/skills/agentic-workflow/SKILL.md +++ b/sample/common/.agents/skills/agentic-workflow/SKILL.md @@ -5,13 +5,13 @@ description: Initialize, integrate, configure, explain, review, or improve a rep # Agentic workflow -Read `agentic-flow/README.md` and `AGENTS.md` first. Read `SETTINGS.md` only when configuration matters. Load only the documents needed for the current operation. +Read `agentic-flow/README.md` and `AGENTS.md` (repository root, else `~/.agents/`) first. Read `SETTINGS.md` only when configuration matters. Load only the documents needed for the current operation. ## Discover 1. Inspect root and nested instructions plus tool-specific files. 2. Find skills, prompts, plans, sessions, records, and refresh rules. -3. Detect managed template markers. +3. Detect managed template markers, including `learning-flow/.install-scope`, and note which root the framework files actually resolve from. 4. Inspect custom additions, overrides, conflicts, and precedence. 5. Keep stable policy, task procedures, shared learning, and private `.local/` state distinct. 6. Keep context narrow. diff --git a/sample/common/.agents/skills/learn-anything/SKILL.md b/sample/common/.agents/skills/learn-anything/SKILL.md index 312c71a..079e93e 100644 --- a/sample/common/.agents/skills/learn-anything/SKILL.md +++ b/sample/common/.agents/skills/learn-anything/SKILL.md @@ -5,7 +5,7 @@ description: Guide concise, adaptive learning conversations for general topics. # Learn Anything -Do not inspect repository code. Read only the relevant parts of `agentic-flow/EDUCATION.md`. +Do not inspect repository code. Read only the relevant parts of `agentic-flow/EDUCATION.md` (repository root, else `~/.agents/`). ## Loop diff --git a/sample/common/.agents/skills/learning-closure/SKILL.md b/sample/common/.agents/skills/learning-closure/SKILL.md index 94535f0..de71251 100644 --- a/sample/common/.agents/skills/learning-closure/SKILL.md +++ b/sample/common/.agents/skills/learning-closure/SKILL.md @@ -38,7 +38,7 @@ If this session corrected an existing `learning-flow/MAP.md` or `TAKEAWAYS.md` e | Module README/documentation | knowledge belongs specifically with a module | | Skill/workflow documentation | knowledge changes a reusable framework or repository workflow | -Prefer the module's own documentation when it is necessary to use or extend that module correctly. Prefer learning-flow surfaces for cross-cutting repository understanding. +Prefer the module's own documentation when it is necessary to use or extend that module correctly. Prefer learning-flow surfaces for cross-cutting repository understanding. Every destination above belongs to the repository being worked on, never to a global installation root. ## Ask at meaningful closure diff --git a/sample/common/.agents/skills/repository-learning/SKILL.md b/sample/common/.agents/skills/repository-learning/SKILL.md index 4859598..e53aaa6 100644 --- a/sample/common/.agents/skills/repository-learning/SKILL.md +++ b/sample/common/.agents/skills/repository-learning/SKILL.md @@ -5,7 +5,7 @@ description: Understand or work on a repository through one focused route for or # Repository learning -Follow `agentic-flow/AGENTS.md` and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` selectively and only the relevant shared records. +Follow `agentic-flow/AGENTS.md` (repository root, else `~/.agents/`) and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` selectively and only the relevant shared records. ## Shared route diff --git a/sample/common/.agents/skills/structured-change/SKILL.md b/sample/common/.agents/skills/structured-change/SKILL.md index 1886133..e552143 100644 --- a/sample/common/.agents/skills/structured-change/SKILL.md +++ b/sample/common/.agents/skills/structured-change/SKILL.md @@ -5,7 +5,7 @@ description: Sequence Explore, Design, and Approve for one consequential, ambigu # Structured change -Read `agentic-flow/AGENTS.md` and the relevant `WORKFLOW.md` sections. Read `SETTINGS.md` only when the preset affects the decision. +Read `agentic-flow/AGENTS.md` (repository root, else `~/.agents/`) and the relevant `WORKFLOW.md` sections. Read `SETTINGS.md` only when the preset affects the decision. ## Use when diff --git a/sample/common/agentic-flow/.managed-files b/sample/common/agentic-flow/.managed-files index e74f70c..e5f0fe3 100644 --- a/sample/common/agentic-flow/.managed-files +++ b/sample/common/agentic-flow/.managed-files @@ -1,5 +1,6 @@ .managed-files .managed-skills +.repository-files README.md AGENTS.md WORKFLOW.md diff --git a/sample/common/agentic-flow/.repository-files b/sample/common/agentic-flow/.repository-files new file mode 100644 index 0000000..210291a --- /dev/null +++ b/sample/common/agentic-flow/.repository-files @@ -0,0 +1,2 @@ +SETTINGS.md +DECISIONS.md diff --git a/sample/common/agentic-flow/AGENTS.md b/sample/common/agentic-flow/AGENTS.md index 374b0f4..e38a78d 100644 --- a/sample/common/agentic-flow/AGENTS.md +++ b/sample/common/agentic-flow/AGENTS.md @@ -4,6 +4,19 @@ This is the common Agentic Delivery layer. It owns collaboration behavior, routi Learning & Ownership and Optional Risk Lenses add guidance without becoming a second workflow. +## Framework root + +`agentic-flow/` and `learning-flow/` resolve against the first root that contains them: + +1. the repository root, for a repository-scoped installation; +2. `~/.agents/` (`%USERPROFILE%\.agents\` on Windows), for a global installation shared by every repository. + +A repository copy always wins over the global one. Never merge the two: read whichever root answered first. + +Repository state is never read from the global root. `.local/`, `learning-flow/MAP.md`, `learning-flow/TAKEAWAYS.md`, `learning-flow/REPOSITORIES.md`, `agentic-flow/SETTINGS.md`, and `agentic-flow/DECISIONS.md` describe one repository and always live in that repository, even when every instruction file is global. + +When neither root has a file this layer names, the guidance simply does not apply. Do not create it to satisfy a reference. + ## Route 1. Follow root, nested, and tool-specific repository instructions first. diff --git a/sample/common/agentic-flow/CONFIGURE.md b/sample/common/agentic-flow/CONFIGURE.md index d62842a..5a2dfec 100644 --- a/sample/common/agentic-flow/CONFIGURE.md +++ b/sample/common/agentic-flow/CONFIGURE.md @@ -13,7 +13,7 @@ Configuration is optional. Use `balanced` by default and begin work immediately. `balanced` is the default. A task-specific request such as “move fast,” “teach me as we work,” or “pause before implementation” overrides the stored preset for that task without rewriting settings. -`gated` sets the default posture; `structured-change` is what actually runs the Explore → Design → Approve sequence for one change, regardless of preset, when the change itself warrants it. When `.agents/skills/regulatory-knowledge/` is installed (the `regulatory` extension), that lens is available to `structured-change` automatically; it does not change the default preset. +`gated` sets the default posture; `structured-change` is what actually runs the Explore → Design → Approve sequence for one change, regardless of preset, when the change itself warrants it. When the `regulatory-knowledge` skill is available (the `regulatory` extension), that lens is available to `structured-change` automatically; it does not change the default preset.
Advanced overrides and optional learner context diff --git a/sample/common/agentic-flow/LEARN.md b/sample/common/agentic-flow/LEARN.md index a65bd34..c706032 100644 --- a/sample/common/agentic-flow/LEARN.md +++ b/sample/common/agentic-flow/LEARN.md @@ -18,7 +18,7 @@ Private learning and temporary state in .local/ Show: -- which files provide stable rules; +- which files provide stable rules, and whether they resolve from this repository or a global installation at `~/.agents/`; - which workflow governs planning, autonomy, validation, and handoff; - which skills load only for particular tasks; - where learning support lives; diff --git a/sample/common/agentic-flow/LOCAL.md b/sample/common/agentic-flow/LOCAL.md index ec6cd9b..8eedc89 100644 --- a/sample/common/agentic-flow/LOCAL.md +++ b/sample/common/agentic-flow/LOCAL.md @@ -4,7 +4,7 @@ Use one rule: > Learn locally first. Promote only reusable knowledge deliberately. -The repository-root `.local/` directory owns private learning continuity. The installer creates it, adds `/.local/` to the root `.gitignore`, and never overwrites existing local files. +The repository-root `.local/` directory owns private learning continuity. The installer creates it, adds `/.local/` to the root `.gitignore`, and never overwrites existing local files. This stays true for a global installation: instructions and skills may live in `~/.agents/`, but `.local/` always belongs to the repository being worked on. There is no global `.local/`. This framework's own source checkout follows the same model lazily: when `.local/` is missing, create the two directories below and copy `sample/common/local/learning-history.md` only if the local history file does not exist. diff --git a/sample/common/agentic-flow/ROOT_INTEGRATION.md b/sample/common/agentic-flow/ROOT_INTEGRATION.md index 3ef80bb..b6c90be 100644 --- a/sample/common/agentic-flow/ROOT_INTEGRATION.md +++ b/sample/common/agentic-flow/ROOT_INTEGRATION.md @@ -50,6 +50,8 @@ The installer and the `agentic-workflow` skill may add this idempotent block (ve ```md For collaboration behavior, planning, validation, and handoff, follow `agentic-flow/AGENTS.md`. +Resolve `agentic-flow/` and `learning-flow/` at the repository root first, then at `~/.agents/` +(`%USERPROFILE%\.agents\` on Windows) for a global installation. **Learning is automatic:** `help me understand`, `explain`, `teach me`, and similar general questions use `learn-anything`; questions about the current repository use repository @@ -62,6 +64,8 @@ Do not turn mechanical work into a lesson. Keep the user's actual task primary. Never append a second copy. Existing root content remains repository-owned. +The block is written the same way for every install scope. A repository that carries its own `agentic-flow/` never consults the global root, so one pointer stays correct whether the framework files are local, global, or moved between the two later. + ## Later changes The same choices remain available after installation. Use the `agentic-workflow` skill to integrate, review, reconfigure, or explain the setup at any time. diff --git a/sample/common/agentic-flow/WORKFLOW.md b/sample/common/agentic-flow/WORKFLOW.md index 81e4043..c86bb8c 100644 --- a/sample/common/agentic-flow/WORKFLOW.md +++ b/sample/common/agentic-flow/WORKFLOW.md @@ -102,7 +102,7 @@ Traceability: - Documentation: Updated / Not required ``` -When `.agents/skills/regulatory-knowledge/` is installed and relevant, append: +When the `regulatory-knowledge` skill is available and relevant, append: ```text Regulatory: diff --git a/sample/extensions/regulatory/learning-flow/REGULATORY.md b/sample/extensions/regulatory/learning-flow/REGULATORY.md index 712927f..5bfe794 100644 --- a/sample/extensions/regulatory/learning-flow/REGULATORY.md +++ b/sample/extensions/regulatory/learning-flow/REGULATORY.md @@ -7,7 +7,7 @@ Installed by `--extension regulatory`. This file is additive: it never turns ord ## What this extension adds -- The `regulatory-knowledge` skill (`.agents/skills/regulatory-knowledge/`): practical, non-standard-reproducing guidance on traceability, validation, risk management, audit trails, and change control, plus short orientation to ISO 9001, ISO 13485, ISO 14971, ISO 17025, IEC 62304, and 21 CFR Part 11. +- The `regulatory-knowledge` skill: practical, non-standard-reproducing guidance on traceability, validation, risk management, audit trails, and change control, plus short orientation to ISO 9001, ISO 13485, ISO 14971, ISO 17025, IEC 62304, and 21 CFR Part 11. - A regulatory lens inside `structured-change`'s Design step, and the `Regulatory notes` section of `structured-change/templates/design.md`. - An optional `Regulatory` addition to the commit traceability guidance in `agentic-flow/WORKFLOW.md`, used only when it adds real value. @@ -17,7 +17,7 @@ Apply it when the change affects validated software, safety-relevant behavior, a ## Human ownership -Traceability, validation, and risk records exist to support the accountable human, not to substitute for them. This extension never signs anything and never represents agent output as a regulatory determination. Read `.agents/skills/regulatory-knowledge/SKILL.md` before using it on a real decision. +Traceability, validation, and risk records exist to support the accountable human, not to substitute for them. This extension never signs anything and never represents agent output as a regulatory determination. Read the `regulatory-knowledge` skill before using it on a real decision. ## Removing this extension diff --git a/sample/profiles/full/.agents/skills/change-explainer/SKILL.md b/sample/profiles/full/.agents/skills/change-explainer/SKILL.md index 1d853ae..1f5089a 100644 --- a/sample/profiles/full/.agents/skills/change-explainer/SKILL.md +++ b/sample/profiles/full/.agents/skills/change-explainer/SKILL.md @@ -5,7 +5,7 @@ description: Explain a non-trivial diff, pull request, or machine-generated impl # Change explainer -Follow `agentic-flow/AGENTS.md` and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. +Follow `agentic-flow/AGENTS.md` (repository root, else `~/.agents/`) and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. 1. Establish the exact change range and intended real-world outcome. 2. Explain the governing rules, system boundary, and relevant architecture before the diff. diff --git a/sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md b/sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md index ed48a78..7667766 100644 --- a/sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md +++ b/sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md @@ -5,7 +5,7 @@ description: Create or refresh a compact repository baseline covering the real s # Learning bootstrap -Follow `agentic-flow/AGENTS.md` and `agentic-flow/SETTINGS.md` for collaboration behavior. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. +Follow `agentic-flow/AGENTS.md` (repository root, else `~/.agents/`) and `agentic-flow/SETTINGS.md` for collaboration behavior. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. 1. Inspect repository instructions, shape, entry points, build and proof commands, and relevant configuration. 2. Recognize managed template markers quietly. diff --git a/sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md b/sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md index b5e1265..9ea0ead 100644 --- a/sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md +++ b/sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md @@ -5,7 +5,7 @@ description: Turn a real issue or planned change into a short repository learnin # Ticket learning path -Follow `agentic-flow/AGENTS.md` and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. +Follow `agentic-flow/AGENTS.md` (repository root, else `~/.agents/`) and `agentic-flow/SETTINGS.md`. Read `agentic-flow/EDUCATION.md` and `learning-flow/AGENTS.md`. 1. Read and classify the ticket. 2. State the actor, intended outcome, capability, and governing rule. diff --git a/sample/profiles/full/learning-flow/.managed-files b/sample/profiles/full/learning-flow/.managed-files index 2767dfc..6c748f9 100644 --- a/sample/profiles/full/learning-flow/.managed-files +++ b/sample/profiles/full/learning-flow/.managed-files @@ -1,5 +1,6 @@ .managed-files .managed-skills +.repository-files .template-profile README.md AGENTS.md diff --git a/sample/profiles/full/learning-flow/.repository-files b/sample/profiles/full/learning-flow/.repository-files new file mode 100644 index 0000000..4d36ca3 --- /dev/null +++ b/sample/profiles/full/learning-flow/.repository-files @@ -0,0 +1,3 @@ +MAP.md +TAKEAWAYS.md +REPOSITORIES.md diff --git a/sample/profiles/full/learning-flow/AGENTS.md b/sample/profiles/full/learning-flow/AGENTS.md index 46d9e23..3d5c644 100644 --- a/sample/profiles/full/learning-flow/AGENTS.md +++ b/sample/profiles/full/learning-flow/AGENTS.md @@ -50,4 +50,6 @@ Use at most one consequential understanding check per task. A declined check nev - `REPOSITORIES.md`: repository identity, baseline, and access boundary. - `.local/`: private sessions, attempts, uncertainty, and follow-ups. +All four describe one repository and live in that repository, under its own `learning-flow/` and `.local/`, even when these instructions are installed globally. + Promote only verified, repository-specific, reusable, non-sensitive findings that are costly enough to rediscover. diff --git a/sample/profiles/minimal/learning-flow/.managed-files b/sample/profiles/minimal/learning-flow/.managed-files index 2767dfc..6c748f9 100644 --- a/sample/profiles/minimal/learning-flow/.managed-files +++ b/sample/profiles/minimal/learning-flow/.managed-files @@ -1,5 +1,6 @@ .managed-files .managed-skills +.repository-files .template-profile README.md AGENTS.md diff --git a/sample/profiles/minimal/learning-flow/.repository-files b/sample/profiles/minimal/learning-flow/.repository-files new file mode 100644 index 0000000..3afe775 --- /dev/null +++ b/sample/profiles/minimal/learning-flow/.repository-files @@ -0,0 +1,2 @@ +MAP.md +TAKEAWAYS.md diff --git a/sample/profiles/minimal/learning-flow/AGENTS.md b/sample/profiles/minimal/learning-flow/AGENTS.md index c3c0fb2..1b4136c 100644 --- a/sample/profiles/minimal/learning-flow/AGENTS.md +++ b/sample/profiles/minimal/learning-flow/AGENTS.md @@ -16,6 +16,8 @@ Select one `repository-learning` branch: Orientation, Bug, Feature, or Refactor. - `TAKEAWAYS.md`: verified reusable lessons that are costly to rediscover. - `.local/`: private attempts, progress, uncertainty, checks, and session continuity. +All three describe one repository and live in that repository, under its own `learning-flow/` and `.local/`, even when these instructions are installed globally. + The common Agentic Delivery and Education instructions own collaboration, context economy, verification, understanding checks, evidence language, and handoff. Do not duplicate those rules here. Promote only verified, repository-specific, reusable, non-sensitive knowledge that is costly enough to rediscover. diff --git a/sample/root/AGENTS.md b/sample/root/AGENTS.md index 77825ca..5735cd7 100644 --- a/sample/root/AGENTS.md +++ b/sample/root/AGENTS.md @@ -22,7 +22,7 @@ Use the repository's current evidence as truth. Keep changes within scope and di ## Collaboration layers -For collaboration behavior, planning, validation, and handoff, follow `agentic-flow/AGENTS.md`. +For collaboration behavior, planning, validation, and handoff, follow `agentic-flow/AGENTS.md`. Resolve `agentic-flow/` and `learning-flow/` at the repository root first, then at `~/.agents/` (`%USERPROFILE%\.agents\` on Windows) for a global installation. For repository learning, onboarding, explanation, and durable understanding, follow `learning-flow/AGENTS.md` when relevant. diff --git a/sample/root/AGENTS.pointer.md b/sample/root/AGENTS.pointer.md index ddd60ff..dca53bb 100644 --- a/sample/root/AGENTS.pointer.md +++ b/sample/root/AGENTS.pointer.md @@ -1,5 +1,5 @@ -For collaboration behavior, planning, validation, and handoff, follow `agentic-flow/AGENTS.md`. +For collaboration behavior, planning, validation, and handoff, follow `agentic-flow/AGENTS.md`. Resolve `agentic-flow/` and `learning-flow/` at the repository root first, then at `~/.agents/` (`%USERPROFILE%\.agents\` on Windows) for a global installation. **Learning is automatic:** `help me understand`, `explain`, `teach me`, and similar general questions use `learn-anything`; questions about the current repository use repository learning. Build a compact model, use a useful example when helpful, and check back for understanding when it adds value. diff --git a/scripts/README.md b/scripts/README.md index ceab37c..a409569 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -15,15 +15,19 @@ path rather than a production one. ```mermaid flowchart LR - D[Resolve source: checkout ref or pinned release] --> P[Select profile] + D[Resolve source: checkout ref or pinned release] --> Sc[Resolve scope and root] + Sc --> P[Select profile] P --> C[Install common agentic flow] C --> L[Install learning profile] L --> Ext[Install or remove regulatory extension] Ext --> S[Install managed skills] S --> X[Initialize ignored .local] X --> R[Integrate or preserve root AGENTS] + R --> M[Record scope and version marker] ``` +Under `--scope global` the steps that write repository state — `.local/`, `.gitignore`, root `AGENTS.md` — are skipped. Under `--scope linked` the steps that write framework content are skipped instead. + ## Installing a packaged release ```text @@ -71,6 +75,8 @@ never ships something CI has not already installed and exercised. ## Installed components +Under `--scope repository`, all six; under `global`, only the framework-owned parts of 1–4; under `linked`, only the repository-authored parts of 1 and 3 plus 5 and 6. + 1. common `agentic-flow/`; 2. common `agentic-workflow`, `learn-anything`, and `structured-change` skills unless skipped; 3. the selected minimal or full `learning-flow/` profile and its managed skills; @@ -83,6 +89,51 @@ The local workspace contains `learning-history.md`, `sessions/`, and `follow-ups > [!IMPORTANT] > `update` owns framework files listed in managed manifests. Repository-authored maps, takeaways, settings, local history, and unrelated skills remain outside destructive refresh behavior. +## Scopes + +```text +--scope repository|global|linked +-Scope Repository|Global|Linked +``` + +| Scope | Default root | Installs | Skips | +|---|---|---|---| +| `repository` | the current directory | everything | nothing | +| `global` | `$HOME/.agents` or `%USERPROFILE%\.agents` | managed files and managed skills | `.local/`, `.gitignore`, root `AGENTS.md` | +| `linked` | the current directory | repository-authored seeds, `.local/`, root `AGENTS.md` | managed files and managed skills | + +`repository` is the default, so an existing command line keeps behaving exactly as before. + +Which files belong to which scope is declared, not inferred: `.managed-files` and `.managed-skills` name framework-owned content, and `.repository-files` names the seeds a repository authors afterward (`SETTINGS.md`, `DECISIONS.md`, `MAP.md`, `TAKEAWAYS.md`, and `REPOSITORIES.md` in the full profile). `scripts/ci-validate.py` fails if a packaged file appears in neither manifest or in both. + +Under `global`, managed skills install to `/skills/` rather than `/.agents/skills/`, because the global root is itself the `.agents` directory a host agent scans. Skills the framework does not manage are never touched. + +`--target`/`-TargetPath` overrides the global root when given. `CODEBASE_LEARNING_FLOW_HOME` overrides the default location for every scope's global lookup. + +
+Linked-scope rules and scope conversion + +- `linked` requires an existing global installation and refuses to run without one, rather than silently downloading a possibly different version into the repository. +- `linked` inherits the global installation's profile and extension. Passing a conflicting `--profile` or `--extension` is an error: the repository would be seeded for a routing contract it does not read. +- Repository-authored seeds are copied only when missing, in every mode. There is no framework content in a linked repository for `update` or `replace` to refresh, so those modes cannot destroy authored learning state. +- `repository` → `linked` requires `update` or `replace`. It removes the repository's managed files and managed skills through their own manifests and leaves authored files in place. +- `linked` → `repository` requires `merge`, `update`, or `replace`, and inherits the profile and extension the global installation was providing. +- Installing `--scope repository` while a global installation exists is allowed but warned about: the host agent would discover every managed skill twice. + +
+ +## Version and scope marker + +Each root records `learning-flow/.install-scope`: + +```text +scope: linked +version: v1.4.0 +global-version: v1.4.0 +``` + +The installer is the reader. On a `linked` install it compares the version being written against the global installation's own and warns when they differ; `scripts/ci-install-test.sh` and `scripts/ci-release-test.sh` assert the two agree after a paired install. Installations predating this marker are treated as `repository`. + ## Profiles | Profile | Default | Intended use | diff --git a/scripts/ci-install-test.sh b/scripts/ci-install-test.sh index e07d8b5..9cea196 100755 --- a/scripts/ci-install-test.sh +++ b/scripts/ci-install-test.sh @@ -13,7 +13,37 @@ bash "$repo_root/scripts/install.sh" --target "$target" --repository "$repositor grep -Fxq "CI sentinel" "$target/.local/ci-sentinel" full_target="$(mktemp -d)" -trap 'rm -rf "$target" "$full_target"' EXIT +global_root="$(mktemp -d)" +linked_target="$(mktemp -d)" +trap 'rm -rf "$target" "$full_target" "$global_root" "$linked_target"' EXIT bash "$repo_root/scripts/install.sh" --target "$full_target" --repository "$repository" --ref "$ref" --profile full --mode fail --skip-root-agents test -f "$full_target/.agents/skills/repository-learning/SKILL.md" -echo "Installer smoke test passed for minimal and full profiles." + +# A global installation owns framework files only; a linked repository owns +# only what it authors. The two must never hold the other's content. +export CODEBASE_LEARNING_FLOW_HOME="$global_root" +bash "$repo_root/scripts/install.sh" --scope global --repository "$repository" --ref "$ref" --profile full --mode fail +test -f "$global_root/agentic-flow/AGENTS.md" +test -f "$global_root/skills/repository-learning/SKILL.md" +test ! -e "$global_root/agentic-flow/SETTINGS.md" +test ! -e "$global_root/learning-flow/MAP.md" +test ! -e "$global_root/.local" +test ! -e "$global_root/AGENTS.md" + +bash "$repo_root/scripts/install.sh" --target "$linked_target" --scope linked --repository "$repository" --ref "$ref" --mode fail --skip-root-agents +test -f "$linked_target/learning-flow/MAP.md" +test -f "$linked_target/agentic-flow/SETTINGS.md" +test -f "$linked_target/.local/learning-history.md" +test ! -e "$linked_target/agentic-flow/AGENTS.md" +test ! -e "$linked_target/.agents/skills" + +# The scope markers are the only reader of the recorded framework version, so +# CI asserts they agree instead of letting the value drift unnoticed. +global_version="$(sed -n 's/^version:[[:space:]]*//p' "$global_root/learning-flow/.install-scope")" +linked_version="$(sed -n 's/^global-version:[[:space:]]*//p' "$linked_target/learning-flow/.install-scope")" +test -n "$global_version" +test "$global_version" = "$linked_version" +grep -Fxq "scope: global" "$global_root/learning-flow/.install-scope" +grep -Fxq "scope: linked" "$linked_target/learning-flow/.install-scope" + +echo "Installer smoke test passed for minimal, full, global, and linked scopes." diff --git a/scripts/ci-release-test.sh b/scripts/ci-release-test.sh index 65b9495..2b1fdf5 100755 --- a/scripts/ci-release-test.sh +++ b/scripts/ci-release-test.sh @@ -70,4 +70,43 @@ if (cd "$WORK_ROOT/minimal" && sh "$INSTALL_SH" --package-file "$PACKAGE_PATH" - fi echo "OK: fail mode refuses to overwrite an existing installation" +# --- Global and linked scopes ---------------------------------------------- +# The split is only correct if neither root holds the other's content, so +# these assertions are stated as absences as well as presences. +CODEBASE_LEARNING_FLOW_HOME="$WORK_ROOT/global" +export CODEBASE_LEARNING_FLOW_HOME + +if (cd "$WORK_ROOT" && sh "$INSTALL_SH" --package-file "$PACKAGE_PATH" --scope linked --target "$WORK_ROOT/too-early") 2>/dev/null; then + fail "linked scope unexpectedly succeeded without a global installation" +fi +echo "OK: linked scope refuses to run before a global installation exists" + +run_install "global" "$WORK_ROOT/global" --scope global --profile full --extension regulatory +[ -f "$WORK_ROOT/global/agentic-flow/AGENTS.md" ] || fail "global install has no agentic-flow/AGENTS.md" +[ -f "$WORK_ROOT/global/skills/repository-learning/SKILL.md" ] || fail "global install has no managed skills" +[ -f "$WORK_ROOT/global/skills/regulatory-knowledge/SKILL.md" ] || fail "global install has no extension skill" +[ ! -e "$WORK_ROOT/global/agentic-flow/SETTINGS.md" ] || fail "repository-authored SETTINGS.md reached the global root" +[ ! -e "$WORK_ROOT/global/learning-flow/MAP.md" ] || fail "repository-authored MAP.md reached the global root" +[ ! -e "$WORK_ROOT/global/.local" ] || fail "global install created a .local/ workspace" +[ ! -e "$WORK_ROOT/global/.gitignore" ] || fail "global install wrote a .gitignore" +[ ! -e "$WORK_ROOT/global/AGENTS.md" ] || fail "global install wrote a root AGENTS.md" +echo "OK: global scope installs framework content only" + +run_install "linked" "$WORK_ROOT/linked" --scope linked --skip-root-agents +[ -f "$WORK_ROOT/linked/learning-flow/MAP.md" ] || fail "linked install has no MAP.md" +[ -f "$WORK_ROOT/linked/learning-flow/REPOSITORIES.md" ] || fail "linked install did not inherit the full profile" +[ -f "$WORK_ROOT/linked/agentic-flow/SETTINGS.md" ] || fail "linked install has no SETTINGS.md" +[ -f "$WORK_ROOT/linked/.local/learning-history.md" ] || fail "linked install has no .local/ workspace" +[ ! -e "$WORK_ROOT/linked/agentic-flow/AGENTS.md" ] || fail "framework instructions were duplicated into the linked repository" +[ ! -e "$WORK_ROOT/linked/.agents/skills" ] || fail "managed skills were duplicated into the linked repository" +echo "OK: linked scope installs repository state only" + +# The recorded framework version has exactly one reader, the skew check +# between these two markers, so CI asserts they agree. +global_version="$(sed -n 's/^version:[[:space:]]*//p' "$WORK_ROOT/global/learning-flow/.install-scope")" +linked_global_version="$(sed -n 's/^global-version:[[:space:]]*//p' "$WORK_ROOT/linked/learning-flow/.install-scope")" +[ -n "$global_version" ] || fail "global installation recorded no framework version" +[ "$global_version" = "$linked_global_version" ] || fail "linked repository recorded $linked_global_version against a global installation at $global_version" +echo "OK: global and linked scope markers agree on the framework version" + echo "All packaged-release checks passed for $PACKAGE_PATH" diff --git a/scripts/ci-validate.py b/scripts/ci-validate.py index be8bbdc..b1bc054 100644 --- a/scripts/ci-validate.py +++ b/scripts/ci-validate.py @@ -71,40 +71,92 @@ def check_skill_structure(): ) +FILE_MANIFESTS = ( + ".managed-files", + ".repository-files", + ".extension-managed-files", + ".extension-repository-files", +) + +SKILL_MANIFESTS = ( + ".managed-skills", + ".extension-managed-skills", +) + + +def manifest_entries(path): + if not path.is_file(): + return [] + + return [ + line.strip() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.strip().startswith("#") + ] + + def check_managed(): for path in repo_files(): - if path.name == ".managed-files": - for line in path.read_text(encoding="utf-8").splitlines(): - entry = line.strip() - - if ( - entry - and not entry.startswith("#") - and not (path.parent / entry).is_file() - ): + if path.name in FILE_MANIFESTS: + for entry in manifest_entries(path): + if not (path.parent / entry).is_file(): fail( f"{path.relative_to(ROOT)} " f"references missing file {entry!r}" ) - elif path.name == ".managed-skills": - for line in path.read_text(encoding="utf-8").splitlines(): - name = line.strip() - - if ( - name - and not name.startswith("#") - and not re.fullmatch( - r"[A-Za-z0-9.\_-]+", - name, - ) - ): + elif path.name in SKILL_MANIFESTS: + for name in manifest_entries(path): + if not re.fullmatch(r"[A-Za-z0-9.\_-]+", name): fail( f"Unsafe skill name in " f"{path.relative_to(ROOT)}: {name!r}" ) +def check_install_scope_classification(): + """Every installable file must declare where it belongs. + + The managed/repository split is what the installer reads to decide which + files a global root owns and which ones stay behind in each repository. + A file in neither manifest has no defined install scope, so it would + silently ship to whichever root the payload happened to be copied into. + """ + for path in repo_files(): + if path.name not in (".managed-files", ".extension-managed-files"): + continue + + component = path.parent + repository_manifest = ( + ".repository-files" + if path.name == ".managed-files" + else ".extension-repository-files" + ) + + managed = set(manifest_entries(path)) + authored = set(manifest_entries(component / repository_manifest)) + + overlap = sorted(managed & authored) + if overlap: + fail( + f"{component.relative_to(ROOT)} declares {overlap} as both " + f"framework-managed and repository-authored" + ) + + for member in sorted(component.rglob("*")): + if not member.is_file(): + continue + + relative = member.relative_to(component).as_posix() + + if relative not in managed | authored: + fail( + f"{component.relative_to(ROOT)}/{relative} appears in " + f"neither {path.name} nor {repository_manifest}; every " + f"installable file needs a declared install scope" + ) + + def check_markdown_links(): pattern = re.compile(r"\[[^\]]+\]\(([^)]+)\)") root = ROOT.resolve() @@ -166,6 +218,7 @@ def check_zip_paths(): check_yaml, check_skill_structure, check_managed, + check_install_scope_classification, check_markdown_links, check_zip_paths, ): diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 6bebf79..02fa2fc 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -11,6 +11,13 @@ param( # Internal/CI hook: install directly from an already-built local release # package without touching the network. Not part of the public contract. [string]$PackageFile = "", + # Where framework content is installed: Repository (default) installs + # everything into one repository; Global installs framework-owned + # instructions and skills into %USERPROFILE%\.agents so every repository + # shares them; Linked adds only this repository's own learning state and + # reads everything else from an existing global installation. + [ValidateSet("Repository", "Global", "Linked")] + [string]$Scope = "Repository", [ValidateSet("Auto", "Minimal", "Full")] [string]$Profile = "Auto", [ValidateSet("Auto", "None", "Regulatory")] @@ -44,6 +51,49 @@ function Write-Step([string]$Message) { Write-Host "[learning-flow] $Message" } +function Resolve-GlobalRoot { + if (-not [string]::IsNullOrWhiteSpace($env:CODEBASE_LEARNING_FLOW_HOME)) { + return $env:CODEBASE_LEARNING_FLOW_HOME + } + $home_directory = $env:USERPROFILE + if ([string]::IsNullOrWhiteSpace($home_directory)) { $home_directory = $env:HOME } + if ([string]::IsNullOrWhiteSpace($home_directory)) { + throw "Cannot resolve the global root: neither USERPROFILE nor HOME is set. Pass -TargetPath or set CODEBASE_LEARNING_FLOW_HOME." + } + return (Join-Path $home_directory ".agents") +} + +function Read-MarkerField([string]$MarkerPath, [string]$Field) { + if (-not (Test-Path -LiteralPath $MarkerPath -PathType Leaf)) { return "" } + foreach ($line in Get-Content -LiteralPath $MarkerPath) { + if ($line -match "^$([regex]::Escape($Field))\s*:\s*(.*?)\s*$") { + return $Matches[1] + } + } + return "" +} + +function Write-InstallScopeMarker( + [string]$MarkerPath, + [string]$ScopeValue, + [string]$VersionValue, + [string]$GlobalVersionValue +) { + $parent = Split-Path -Parent $MarkerPath + if (-not (Test-Path -LiteralPath $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + $lines = @("scope: $ScopeValue", "version: $VersionValue") + if (-not [string]::IsNullOrWhiteSpace($GlobalVersionValue)) { + $lines += "global-version: $GlobalVersionValue" + } + [System.IO.File]::WriteAllText( + $MarkerPath, + ([string]::Join("`n", $lines) + "`n"), + [System.Text.UTF8Encoding]::new($false) + ) +} + function Get-Sha256([string]$Path) { (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } @@ -253,6 +303,103 @@ function Copy-ManagedFiles([string]$Source, [string]$Destination, [string]$Manif return $copied } +# Shared manifest walker for the two non-overwriting copies: framework files in +# Merge mode, and repository-authored seeds in every mode. +function Copy-ManifestFilesIfMissing( + [string]$Source, + [string]$Destination, + [string]$ManifestPath, + [string]$Kind +) { + $sourceRoot = [System.IO.Path]::GetFullPath($Source).TrimEnd([char[]]@('\', '/')) + [System.IO.Path]::DirectorySeparatorChar + $destinationRoot = [System.IO.Path]::GetFullPath($Destination).TrimEnd([char[]]@('\', '/')) + [System.IO.Path]::DirectorySeparatorChar + $copied = 0 + $preserved = 0 + + if (-not (Test-Path -LiteralPath $Destination -PathType Container)) { + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + } + + foreach ($rawLine in Get-Content -LiteralPath $ManifestPath) { + $relative = $rawLine.Trim() + if ([string]::IsNullOrWhiteSpace($relative) -or $relative.StartsWith('#')) { continue } + $normalized = $relative.Replace('/', [string][System.IO.Path]::DirectorySeparatorChar) + $sourceFile = [System.IO.Path]::GetFullPath((Join-Path $Source $normalized)) + $targetFile = [System.IO.Path]::GetFullPath((Join-Path $Destination $normalized)) + if (-not $sourceFile.StartsWith($sourceRoot, [System.StringComparison]::OrdinalIgnoreCase) -or + -not $targetFile.StartsWith($destinationRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Unsafe path in $Kind manifest: $relative" + } + if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) { + throw "Manifest source file is missing: $relative" + } + if (Test-Path -LiteralPath $targetFile -PathType Container) { + throw "Manifest target path is a directory, expected a file: $relative" + } + $parent = Split-Path -Parent $targetFile + if (-not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + if (Test-Path -LiteralPath $targetFile) { + $preserved += 1 + } + else { + Copy-Item -LiteralPath $sourceFile -Destination $targetFile + $copied += 1 + } + } + + Write-Step "Added $copied and preserved $preserved $Kind files in $(Split-Path -Leaf $Destination)" +} + +# Global scope installs only what the managed manifest declares: the framework +# owns every file at the global root, so repository-authored seeds must not +# follow the payload there. +function Install-ManagedComponent( + [string]$Name, + [string]$Source, + [string]$Destination, + [string]$ManagedFiles, + [string]$InstallMode +) { + if ($InstallMode -eq "Replace" -and (Test-Path -LiteralPath $Destination)) { + Write-Step "Removing existing $Name directory" + Remove-Item -LiteralPath $Destination -Recurse -Force + } + if (-not (Test-Path -LiteralPath $Destination -PathType Container)) { + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + } + + if ($InstallMode -eq "Merge") { + Write-Step "Merging missing $Name files" + Copy-ManifestFilesIfMissing -Source $Source -Destination $Destination -ManifestPath $ManagedFiles -Kind "managed" + } + else { + if ($InstallMode -eq "Update") { + $retired = Remove-RetiredManagedFiles -Destination $Destination -PreviousManifestPath (Join-Path $Destination ".managed-files") -CurrentManifestPath $ManagedFiles + if ($retired -gt 0) { Write-Step "Removed $retired retired managed files from $Name" } + } + Write-Step "Installing $Name" + $count = Copy-ManagedFiles -Source $Source -Destination $Destination -ManifestPath $ManagedFiles + Write-Step "Updated $count managed files in $Name" + } +} + +# Removing every entry of a previously installed manifest is expressed as +# "retire against an empty manifest", which is how extension removal already +# works. Scope conversion reuses it for the profile's own managed files. +function Remove-ManifestFiles([string]$Destination, [string]$PreviousManifestPath) { + if (-not (Test-Path -LiteralPath $PreviousManifestPath -PathType Leaf)) { return 0 } + $emptyManifest = Join-Path ([System.IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString("N") + ".txt") + New-Item -ItemType File -Path $emptyManifest -Force | Out-Null + try { + return Remove-RetiredManagedFiles -Destination $Destination -PreviousManifestPath $PreviousManifestPath -CurrentManifestPath $emptyManifest + } + finally { + Remove-Item -LiteralPath $emptyManifest -Force -ErrorAction SilentlyContinue + } +} + function Remove-RetiredManagedFiles([string]$Destination, [string]$PreviousManifestPath, [string]$CurrentManifestPath) { if (-not (Test-Path -LiteralPath $PreviousManifestPath -PathType Leaf)) { return 0 } @@ -506,6 +653,13 @@ if ($Repository -like "__GITHUB_OWNER__/*") { throw "Replace __GITHUB_OWNER__ in the installer or pass -Repository owner/codebase-learning-flow." } +# Resolved before self-refresh because the re-invocation always passes +# -TargetPath explicitly, which would hide "the user did not choose a target" +# from the child process. +if ($Scope -eq "Global" -and -not $PSBoundParameters.ContainsKey('TargetPath')) { + $TargetPath = Resolve-GlobalRoot +} + $resolvedCommit = Resolve-RemoteCommit -RepositoryName $Repository -RequestedRef $Ref $headers = @{ "Cache-Control" = "no-cache, no-store, max-age=0"; "Pragma" = "no-cache" } @@ -529,6 +683,7 @@ if (-not $SkipSelfRefresh) { -Repository $Repository ` -Release $Release ` -PackageFile $PackageFile ` + -Scope $Scope ` -Profile $Profile ` -Extension $Extension ` -Mode $Mode ` @@ -543,6 +698,7 @@ if (-not $SkipSelfRefresh) { -Repository $Repository ` -Ref $resolvedCommit ` -PackageFile $PackageFile ` + -Scope $Scope ` -Profile $Profile ` -Extension $Extension ` -Mode $Mode ` @@ -567,8 +723,88 @@ if (-not (Test-Path -LiteralPath $resolvedTarget)) { $targetAgentic = Join-Path $resolvedTarget "agentic-flow" $targetLearning = Join-Path $resolvedTarget "learning-flow" -$targetSkills = Join-Path $resolvedTarget ".agents/skills" -$installedProfile = Get-InstalledProfile $targetLearning +$scopeName = $Scope.ToLowerInvariant() +if ($scopeName -eq "global") { + # The global root is itself the .agents directory host agents already scan, + # so managed skills belong directly beneath it rather than one level deeper. + $globalRoot = $resolvedTarget + $targetSkills = Join-Path $resolvedTarget "skills" +} +else { + $globalRoot = Resolve-GlobalRoot + $targetSkills = Join-Path $resolvedTarget ".agents/skills" +} +$globalLearning = Join-Path $globalRoot "learning-flow" +$globalMarker = Join-Path $globalLearning ".install-scope" + +$installedScope = Read-MarkerField -MarkerPath (Join-Path $targetLearning ".install-scope") -Field "scope" +if ($installedScope -notin @("", "repository", "global", "linked")) { + throw "Invalid installed scope marker: $installedScope" +} +if ([string]::IsNullOrWhiteSpace($installedScope) -and (Test-DirectoryHasContent $targetLearning)) { + # Installations from before scopes existed are repository-scoped. + $installedScope = "repository" +} + +if (-not [string]::IsNullOrWhiteSpace($installedScope) -and $installedScope -ne $scopeName) { + if ($installedScope -eq "repository" -and $scopeName -eq "linked") { + if ($Mode -notin @("Update", "Replace")) { + throw "Scope change repository -> linked is not supported in mode '$Mode'. Use Update or Replace." + } + Write-Step "Converting repository-scoped installation to linked; framework files move to $globalRoot" + } + elseif ($installedScope -eq "linked" -and $scopeName -eq "repository") { + if ($Mode -notin @("Merge", "Update", "Replace")) { + throw "Scope change linked -> repository is not supported in mode '$Mode'. Use Merge, Update, or Replace." + } + Write-Step "Converting linked installation to a self-contained repository installation" + } + else { + throw "$resolvedTarget holds a '$installedScope' installation and cannot be reused as '$scopeName'." + } +} + +$globalVersion = "" +$globalProfile = "" +$globalExtension = "" +if ((Read-MarkerField -MarkerPath $globalMarker -Field "scope") -eq "global") { + $globalVersion = Read-MarkerField -MarkerPath $globalMarker -Field "version" + $globalProfile = Get-InstalledProfile $globalLearning + $globalExtension = Get-InstalledExtension $globalLearning + if ([string]::IsNullOrWhiteSpace($globalExtension)) { $globalExtension = "none" } +} +elseif ($scopeName -eq "linked") { + throw "-Scope Linked requires a global installation at $globalRoot. Run the installer once with -Scope Global first." +} + +if ($scopeName -eq "linked") { + if ([string]::IsNullOrWhiteSpace($globalProfile)) { + throw "The global installation at $globalRoot is missing its profile marker. Reinstall it with -Scope Global." + } + # The global installation owns the routing contract. A linked repository + # only adds its own state, so it cannot select a different profile or + # extension than the instructions it will actually read. + if ($Profile -ne "Auto" -and $Profile.ToLowerInvariant() -ne $globalProfile) { + throw "The global installation uses the '$globalProfile' profile. A linked repository cannot select '$($Profile.ToLowerInvariant())'; change the global installation instead." + } + $Profile = $globalProfile + if ($Extension -ne "Auto" -and $Extension.ToLowerInvariant() -ne $globalExtension) { + throw "The global installation uses extension '$globalExtension'. A linked repository cannot select '$($Extension.ToLowerInvariant())'; change the global installation instead." + } + $Extension = $globalExtension +} +elseif ($installedScope -eq "linked") { + # A linked repository carries no profile or extension marker of its own, so + # converting it back to self-contained inherits what the global + # installation was providing rather than the fresh-install defaults. + if ($Profile -eq "Auto" -and -not [string]::IsNullOrWhiteSpace($globalProfile)) { $Profile = $globalProfile } + if ($Extension -eq "Auto" -and -not [string]::IsNullOrWhiteSpace($globalExtension)) { $Extension = $globalExtension } +} + +# A linked repository never carries .template-profile: the global installation +# owns the profile, so the legacy "content but no marker means full" fallback +# would misread it. +$installedProfile = if ($installedScope -eq "linked") { $null } else { Get-InstalledProfile $targetLearning } $requestedProfile = $Profile.ToLowerInvariant() $selectedProfile = if ($requestedProfile -eq "auto") { if ([string]::IsNullOrWhiteSpace($installedProfile)) { "minimal" } else { $installedProfile } @@ -670,6 +906,8 @@ try { $sourceProfileSkills = Join-Path $sourceProfile ".agents/skills" $sourceLearningManagedFiles = Join-Path $sourceLearning ".managed-files" $sourceLearningManagedSkills = Join-Path $sourceLearning ".managed-skills" + $sourceAgenticRepositoryFiles = Join-Path $sourceAgentic ".repository-files" + $sourceLearningRepositoryFiles = Join-Path $sourceLearning ".repository-files" $sourceRootAgents = Join-Path $archiveRoot "sample/root/AGENTS.md" $sourceRootPointer = Join-Path $archiveRoot "sample/root/AGENTS.pointer.md" @@ -684,12 +922,16 @@ try { throw "Required framework directory is missing: $requiredDirectory" } } - foreach ($requiredFile in @($sourceAgenticManagedFiles, $sourceAgenticManagedSkills, $sourceLearningManagedFiles, $sourceLearningManagedSkills, $sourceLocalHistory)) { + foreach ($requiredFile in @($sourceAgenticManagedFiles, $sourceAgenticManagedSkills, $sourceLearningManagedFiles, $sourceLearningManagedSkills, $sourceAgenticRepositoryFiles, $sourceLearningRepositoryFiles, $sourceLocalHistory)) { if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) { throw "Required framework manifest is missing: $requiredFile" } } - if (-not $SkipSkills) { + if ($scopeName -eq "global") { + $SkipRootAgents = [switch]$true + $RootAgents = "Skip" + } + if (-not $SkipSkills -and $scopeName -ne "linked") { if (-not (Test-Path -LiteralPath $sourceCommonSkills -PathType Container)) { throw "Common skill directory is missing." } $profileManagedSkills = @(Get-Content -LiteralPath $sourceLearningManagedSkills | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and -not $_.Trim().StartsWith("#") }) if ($profileManagedSkills.Count -gt 0 -and -not (Test-Path -LiteralPath $sourceProfileSkills -PathType Container)) { @@ -714,7 +956,7 @@ try { if ((Test-DirectoryHasContent $targetAgentic) -or (Test-DirectoryHasContent $targetLearning)) { throw "agentic-flow or learning-flow already contains files. Use -Mode Merge, -Mode Update, or -Mode Replace." } - if (-not $SkipSkills) { + if (-not $SkipSkills -and $scopeName -ne "linked") { $allNames = @( Get-ManagedSkillNames $sourceAgenticManagedSkills Get-ManagedSkillNames $sourceLearningManagedSkills @@ -729,27 +971,52 @@ try { } } - Install-Component -Name "agentic-flow" -Source $sourceAgentic -Destination $targetAgentic -ManagedFiles $sourceAgenticManagedFiles -InstallMode $Mode - Install-Component -Name "learning-flow/$selectedProfile" -Source $sourceLearning -Destination $targetLearning -ManagedFiles $sourceLearningManagedFiles -InstallMode $Mode - - if ($selectedExtension -eq "regulatory") { - Install-ExtensionOverlay -Name "learning-flow/regulatory (extension)" -Source $sourceExtensionLearning -Destination $targetLearning -ManagedFiles $sourceExtensionManagedFiles -InstallMode $Mode + if ($scopeName -eq "linked") { + # A linked repository owns only the files it authors. Everything the + # framework owns is read from the global root, so the seeds are copied + # without overwriting in every mode: there is no framework content here + # for Update or Replace to refresh. + if ($installedScope -eq "repository") { + Remove-ManifestFiles -Destination $targetLearning -PreviousManifestPath (Join-Path $targetLearning ".extension-managed-files") | Out-Null + Remove-ManifestFiles -Destination $targetAgentic -PreviousManifestPath (Join-Path $targetAgentic ".managed-files") | Out-Null + Remove-ManifestFiles -Destination $targetLearning -PreviousManifestPath (Join-Path $targetLearning ".managed-files") | Out-Null + Write-Step "Removed repository-scoped framework files now owned by $globalRoot" + } + Write-Step "Installing repository learning state" + Copy-ManifestFilesIfMissing -Source $sourceAgentic -Destination $targetAgentic -ManifestPath $sourceAgenticRepositoryFiles -Kind "repository" + Copy-ManifestFilesIfMissing -Source $sourceLearning -Destination $targetLearning -ManifestPath $sourceLearningRepositoryFiles -Kind "repository" } - elseif (-not [string]::IsNullOrWhiteSpace($installedExtension) -and ($Mode -eq "Update" -or $Mode -eq "Replace")) { - $removedExtensionFiles = Join-Path ([System.IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString("N") + ".txt") - New-Item -ItemType File -Path $removedExtensionFiles -Force | Out-Null - try { - Remove-RetiredManagedFiles -Destination $targetLearning -PreviousManifestPath (Join-Path $targetLearning ".extension-managed-files") -CurrentManifestPath $removedExtensionFiles | Out-Null + elseif ($scopeName -eq "global") { + Install-ManagedComponent -Name "agentic-flow" -Source $sourceAgentic -Destination $targetAgentic -ManagedFiles $sourceAgenticManagedFiles -InstallMode $Mode + Install-ManagedComponent -Name "learning-flow/$selectedProfile" -Source $sourceLearning -Destination $targetLearning -ManagedFiles $sourceLearningManagedFiles -InstallMode $Mode + } + else { + Install-Component -Name "agentic-flow" -Source $sourceAgentic -Destination $targetAgentic -ManagedFiles $sourceAgenticManagedFiles -InstallMode $Mode + Install-Component -Name "learning-flow/$selectedProfile" -Source $sourceLearning -Destination $targetLearning -ManagedFiles $sourceLearningManagedFiles -InstallMode $Mode + } + + if ($scopeName -ne "linked") { + if ($selectedExtension -eq "regulatory") { + Install-ExtensionOverlay -Name "learning-flow/regulatory (extension)" -Source $sourceExtensionLearning -Destination $targetLearning -ManagedFiles $sourceExtensionManagedFiles -InstallMode $Mode } - finally { - Remove-Item -LiteralPath $removedExtensionFiles -Force -ErrorAction SilentlyContinue + elseif (-not [string]::IsNullOrWhiteSpace($installedExtension) -and ($Mode -eq "Update" -or $Mode -eq "Replace")) { + Remove-ManifestFiles -Destination $targetLearning -PreviousManifestPath (Join-Path $targetLearning ".extension-managed-files") | Out-Null + Write-Step "Removed $installedExtension extension" } - Write-Step "Removed $installedExtension extension" } - Initialize-LocalLearningWorkspace -TargetRoot $resolvedTarget -HistoryTemplate $sourceLocalHistory + if ($scopeName -ne "global") { + Initialize-LocalLearningWorkspace -TargetRoot $resolvedTarget -HistoryTemplate $sourceLocalHistory + } + + if ($scopeName -eq "linked" -and $installedScope -eq "repository" -and -not $SkipSkills) { + Remove-ManagedSkills -ManifestPath $sourceAgenticManagedSkills -TargetSkills $targetSkills + Remove-ManagedSkills -ManifestPath (Join-Path $archiveRoot "sample/profiles/minimal/learning-flow/.managed-skills") -TargetSkills $targetSkills + Remove-ManagedSkills -ManifestPath (Join-Path $archiveRoot "sample/profiles/full/learning-flow/.managed-skills") -TargetSkills $targetSkills + Remove-ManagedSkills -ManifestPath $sourceExtensionManagedSkills -TargetSkills $targetSkills + } - if (-not $SkipSkills) { + if (-not $SkipSkills -and $scopeName -ne "linked") { New-Item -ItemType Directory -Path $targetSkills -Force | Out-Null if ($Mode -eq "Replace") { @@ -811,6 +1078,20 @@ try { Set-RootIntegrationState -SettingsPath (Join-Path $targetAgentic "SETTINGS.md") -ResolvedMode $resolvedRootAgents + $frameworkVersion = if (-not [string]::IsNullOrWhiteSpace($Release)) { $Release } else { $resolvedCommit } + Write-InstallScopeMarker ` + -MarkerPath (Join-Path $targetLearning ".install-scope") ` + -ScopeValue $scopeName ` + -VersionValue $frameworkVersion ` + -GlobalVersionValue $(if ($scopeName -eq "linked") { $globalVersion } else { "" }) + + if ($scopeName -eq "linked" -and -not [string]::IsNullOrWhiteSpace($globalVersion) -and $globalVersion -ne $frameworkVersion) { + Write-Step "WARNING: this repository was linked at $frameworkVersion but $globalRoot holds $globalVersion. Reinstall one of them so the routing contract and the repository state agree." + } + if ($scopeName -eq "repository" -and (Read-MarkerField -MarkerPath $globalMarker -Field "scope") -eq "global") { + Write-Step "WARNING: a global installation exists at $globalRoot. This repository now carries its own copy of every managed skill, so the host agent will discover each one twice. Use -Scope Linked instead unless the duplication is intended." + } + Write-Host "" Write-Host "Codebase Learning Flow" if (-not [string]::IsNullOrWhiteSpace($Release)) { @@ -826,11 +1107,18 @@ try { Write-Host "Version: $resolvedCommit (ref: $Ref)" Write-Host "Source: development checkout (mutable unless ref is a commit or tag)" } + Write-Host "Scope: $scopeName ($resolvedTarget)" - Write-Step "Installation complete: profile=$selectedProfile extension=$selectedExtension mode=$($Mode.ToLowerInvariant()) root-agents=$($resolvedRootAgents.ToLowerInvariant())" + Write-Step "Installation complete: scope=$scopeName profile=$selectedProfile extension=$selectedExtension mode=$($Mode.ToLowerInvariant()) root-agents=$($resolvedRootAgents.ToLowerInvariant())" Write-Host "" - Write-Host "Suggested first instruction:" - Write-Host "Start with my current task. Quietly verify the installed workflow, surface only meaningful instruction conflicts, teach the relevant code and domain path while working, and persist only verified findings that will be useful again." + if ($scopeName -eq "global") { + Write-Host "Next step:" + Write-Host "Run the installer with -Scope Linked inside a repository to give it its own learning state, or start working: the managed skills in $targetSkills already apply everywhere." + } + else { + Write-Host "Suggested first instruction:" + Write-Host "Start with my current task. Quietly verify the installed workflow, surface only meaningful instruction conflicts, teach the relevant code and domain path while working, and persist only verified findings that will be useful again." + } } finally { if (Test-Path -LiteralPath $tempRoot) { diff --git a/scripts/install.sh b/scripts/install.sh index f439d48..040d285 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,11 +2,13 @@ set -eu TARGET_PATH="$(pwd)" +USER_SET_TARGET="false" REPOSITORY="${CODEBASE_LEARNING_FLOW_REPOSITORY:-legrab/codebase-learning-flow}" REF="${CODEBASE_LEARNING_FLOW_REF:-main}" RELEASE_TAG="" PACKAGE_FILE="" MODE="fail" +SCOPE="repository" PROFILE="auto" EXTENSION="auto" SKIP_ROOT_AGENTS="false" @@ -18,21 +20,29 @@ usage() { Usage: install.sh [options] Options: - --target PATH Target repository directory + --target PATH Target directory; defaults to the current repository, or to the + global root when --scope global is used --repository OWNER/REPO Public template repository --ref REF Branch, tag, or commit reference (development checkout path) --release TAG Exact published release tag (e.g. v0.9.0); preferred for team/enterprise installs. Downloads the packaged, checksum-verified release artifact instead of a mutable source snapshot. "latest" is deliberately not supported: pin an exact tag. + --scope repository|global|linked Where framework content is installed: + repository (default) installs everything into one repository; + global installs framework-owned instructions and skills into + $HOME/.agents so every repository shares them; + linked adds only this repository's own learning state and uses + an existing global installation for everything else --profile auto|minimal|full Learning profile; auto keeps an existing profile and defaults new installs to minimal --extension auto|none|regulatory Additive installation dimension; auto keeps an existing extension and defaults new installs to none --mode fail|merge|update|replace Existing-framework behavior --root-agents MODE auto|integrate|initialize|preserve|skip --skip-root-agents Alias for --root-agents skip - --skip-skills Do not install or update .agents/skills + --skip-skills Do not install or update managed skills -h, --help Show this help --ref and --release are mutually exclusive. +The global root can be overridden with CODEBASE_LEARNING_FLOW_HOME. EOF } @@ -40,6 +50,41 @@ log() { printf '%s\n' "[learning-flow] $*" } +resolve_global_root() { + if [ -n "${CODEBASE_LEARNING_FLOW_HOME:-}" ]; then + printf '%s\n' "$CODEBASE_LEARNING_FLOW_HOME" + return + fi + home_directory="${HOME:-}" + [ -n "$home_directory" ] || home_directory="${USERPROFILE:-}" + if [ -z "$home_directory" ]; then + echo "Cannot resolve the global root: neither HOME nor USERPROFILE is set. Pass --target or set CODEBASE_LEARNING_FLOW_HOME." >&2 + exit 1 + fi + printf '%s\n' "$home_directory/.agents" +} + +read_marker_field() { + marker="$1" + field="$2" + [ -f "$marker" ] || return 0 + sed -n "s/^$field:[[:space:]]*//p" "$marker" | sed -n '1{s/[[:space:]]*$//;p;}' +} + +write_install_scope_marker() { + marker="$1" + scope_value="$2" + version_value="$3" + global_version_value="$4" + + mkdir -p "$(dirname "$marker")" + { + printf 'scope: %s\n' "$scope_value" + printf 'version: %s\n' "$version_value" + [ -z "$global_version_value" ] || printf 'global-version: %s\n' "$global_version_value" + } > "$marker" +} + initialize_local_learning_workspace() { target_root="$1" history_template="$2" @@ -417,6 +462,75 @@ copy_managed_files() { log "Updated $copied managed files in $(basename "$target_root")" } +# Shared manifest walker for the two non-overwriting copies: framework files in +# merge mode, and repository-authored seeds in every mode. +copy_manifest_files_if_missing() { + source_root="$1" + target_root="$2" + manifest="$3" + kind="$4" + copied=0 + preserved=0 + + while IFS= read -r relative || [ -n "$relative" ]; do + case "$relative" in + ''|'#'*) continue ;; + /*|..|../*|*/../*|*/..) + echo "Unsafe path in $kind manifest: $relative" >&2 + exit 1 + ;; + esac + + source_file="$source_root/$relative" + target_file="$target_root/$relative" + [ -f "$source_file" ] || { + echo "Manifest source file is missing: $relative" >&2 + exit 1 + } + [ ! -d "$target_file" ] || { + echo "Manifest target path is a directory, expected a file: $relative" >&2 + exit 1 + } + mkdir -p "$(dirname "$target_file")" + if [ -e "$target_file" ]; then + preserved=$((preserved + 1)) + else + cp "$source_file" "$target_file" + copied=$((copied + 1)) + fi + done < "$manifest" + + log "Added $copied and preserved $preserved $kind files in $(basename "$target_root")" +} + +# Global scope installs only what the managed manifest declares: the framework +# owns every file at the global root, so repository-authored seeds must not +# follow the payload there. +install_managed_component() { + component_name="$1" + source_root="$2" + target_root="$3" + managed_files="$4" + previous_manifest="$5" + + if [ "$MODE" = "replace" ] && [ -e "$target_root" ]; then + log "Removing existing $component_name directory" + rm -rf "$target_root" + fi + mkdir -p "$target_root" + + if [ "$MODE" = "merge" ]; then + log "Merging missing $component_name files" + copy_manifest_files_if_missing "$source_root" "$target_root" "$managed_files" "managed" + else + if [ "$MODE" = "update" ]; then + remove_retired_managed_files "$target_root" "$previous_manifest" "$managed_files" + fi + log "Installing $component_name" + copy_managed_files "$source_root" "$target_root" "$managed_files" + fi +} + remove_retired_managed_files() { target_root="$1" previous_manifest="$2" @@ -613,6 +727,7 @@ while [ "$#" -gt 0 ]; do --target) require_value "$1" "$#" TARGET_PATH="$2" + USER_SET_TARGET="true" shift 2 ;; --repository) @@ -650,6 +765,11 @@ while [ "$#" -gt 0 ]; do MODE="$(printf '%s' "$2" | tr 'A-Z' 'a-z')" shift 2 ;; + --scope) + require_value "$1" "$#" + SCOPE="$(printf '%s' "$2" | tr 'A-Z' 'a-z')" + shift 2 + ;; --root-agents) require_value "$1" "$#" ROOT_AGENTS_MODE="$(printf '%s' "$2" | tr 'A-Z' 'a-z')" @@ -677,21 +797,110 @@ while [ "$#" -gt 0 ]; do done case "$MODE" in fail|merge|update|replace) ;; *) echo "Invalid mode: $MODE" >&2; exit 2 ;; esac +case "$SCOPE" in repository|global|linked) ;; *) echo "Invalid scope: $SCOPE" >&2; exit 2 ;; esac case "$PROFILE" in auto|minimal|full) ;; *) echo "Invalid profile: $PROFILE" >&2; exit 2 ;; esac case "$EXTENSION" in auto|none|regulatory) ;; *) echo "Invalid extension: $EXTENSION" >&2; exit 2 ;; esac case "$ROOT_AGENTS_MODE" in auto|integrate|initialize|preserve|skip) ;; *) echo "Invalid root agents mode: $ROOT_AGENTS_MODE" >&2; exit 2 ;; esac command -v unzip >/dev/null 2>&1 || { echo "The installer requires unzip." >&2; exit 1; } +GLOBAL_ROOT="$(resolve_global_root)" +if [ "$SCOPE" = "global" ] && [ "$USER_SET_TARGET" = "false" ]; then + TARGET_PATH="$GLOBAL_ROOT" +fi + mkdir -p "$TARGET_PATH" TARGET_PATH="$(cd "$TARGET_PATH" && pwd)" TARGET_AGENTIC="$TARGET_PATH/agentic-flow" TARGET_LEARNING="$TARGET_PATH/learning-flow" -TARGET_SKILLS="$TARGET_PATH/.agents/skills" +if [ "$SCOPE" = "global" ]; then + # The global root is itself the .agents directory host agents already scan, + # so managed skills belong directly beneath it rather than one level deeper. + GLOBAL_ROOT="$TARGET_PATH" + TARGET_SKILLS="$TARGET_PATH/skills" +else + TARGET_SKILLS="$TARGET_PATH/.agents/skills" +fi +GLOBAL_LEARNING="$GLOBAL_ROOT/learning-flow" +GLOBAL_MARKER="$GLOBAL_LEARNING/.install-scope" + +INSTALLED_SCOPE="$(read_marker_field "$TARGET_LEARNING/.install-scope" scope)" +case "$INSTALLED_SCOPE" in ''|repository|global|linked) ;; *) echo "Invalid installed scope marker: $INSTALLED_SCOPE" >&2; exit 1 ;; esac +if [ -z "$INSTALLED_SCOPE" ] && has_content "$TARGET_LEARNING"; then + # Installations from before scopes existed are repository-scoped. + INSTALLED_SCOPE="repository" +fi + +if [ -n "$INSTALLED_SCOPE" ] && [ "$INSTALLED_SCOPE" != "$SCOPE" ]; then + case "$INSTALLED_SCOPE/$SCOPE" in + repository/linked) + case "$MODE" in + update|replace) log "Converting repository-scoped installation to linked; framework files move to $GLOBAL_ROOT" ;; + *) echo "Scope change repository -> linked is not supported in mode '$MODE'. Use update or replace." >&2; exit 1 ;; + esac + ;; + linked/repository) + case "$MODE" in + merge|update|replace) log "Converting linked installation to a self-contained repository installation" ;; + *) echo "Scope change linked -> repository is not supported in mode '$MODE'. Use merge, update, or replace." >&2; exit 1 ;; + esac + ;; + *) + echo "$TARGET_PATH holds a '$INSTALLED_SCOPE' installation and cannot be reused as '$SCOPE'." >&2 + exit 1 + ;; + esac +fi + +GLOBAL_VERSION="" +GLOBAL_PROFILE="" +GLOBAL_EXTENSION="" +if [ "$(read_marker_field "$GLOBAL_MARKER" scope)" = "global" ]; then + GLOBAL_VERSION="$(read_marker_field "$GLOBAL_MARKER" version)" + GLOBAL_PROFILE="$(read_profile_file "$GLOBAL_LEARNING/.template-profile")" + GLOBAL_EXTENSION="$(read_profile_file "$GLOBAL_LEARNING/.extension-name")" + [ -n "$GLOBAL_EXTENSION" ] || GLOBAL_EXTENSION="none" +elif [ "$SCOPE" = "linked" ]; then + echo "--scope linked requires a global installation at $GLOBAL_ROOT. Run the installer once with --scope global first." >&2 + exit 1 +fi + +if [ "$SCOPE" = "linked" ]; then + if [ -z "$GLOBAL_PROFILE" ]; then + echo "The global installation at $GLOBAL_ROOT is missing its profile marker. Reinstall it with --scope global." >&2 + exit 1 + fi + # The global installation owns the routing contract. A linked repository + # only adds its own state, so it cannot select a different profile or + # extension than the instructions it will actually read. + if [ "$PROFILE" != "auto" ] && [ "$PROFILE" != "$GLOBAL_PROFILE" ]; then + echo "The global installation uses the '$GLOBAL_PROFILE' profile. A linked repository cannot select '$PROFILE'; change the global installation instead." >&2 + exit 1 + fi + PROFILE="$GLOBAL_PROFILE" + if [ "$EXTENSION" != "auto" ] && [ "$EXTENSION" != "$GLOBAL_EXTENSION" ]; then + echo "The global installation uses extension '$GLOBAL_EXTENSION'. A linked repository cannot select '$EXTENSION'; change the global installation instead." >&2 + exit 1 + fi + EXTENSION="$GLOBAL_EXTENSION" +elif [ "$INSTALLED_SCOPE" = "linked" ]; then + # A linked repository carries no profile or extension marker of its own, so + # converting it back to self-contained inherits what the global + # installation was providing rather than the fresh-install defaults. + if [ "$PROFILE" = "auto" ] && [ -n "$GLOBAL_PROFILE" ]; then + PROFILE="$GLOBAL_PROFILE" + fi + if [ "$EXTENSION" = "auto" ] && [ -n "$GLOBAL_EXTENSION" ]; then + EXTENSION="$GLOBAL_EXTENSION" + fi +fi INSTALLED_PROFILE="$(read_profile_file "$TARGET_LEARNING/.template-profile")" case "$INSTALLED_PROFILE" in ''|minimal|full) ;; *) echo "Invalid installed profile marker: $INSTALLED_PROFILE" >&2; exit 1 ;; esac -if [ -z "$INSTALLED_PROFILE" ] && has_content "$TARGET_LEARNING"; then +# A linked repository never carries .template-profile: the global installation +# owns the profile, so the legacy "content but no marker means full" fallback +# would misread it. +if [ -z "$INSTALLED_PROFILE" ] && [ "$INSTALLED_SCOPE" != "linked" ] && has_content "$TARGET_LEARNING"; then INSTALLED_PROFILE="full" fi @@ -794,6 +1003,8 @@ SOURCE_LEARNING="$SOURCE_PROFILE/learning-flow" SOURCE_PROFILE_SKILLS="$SOURCE_PROFILE/.agents/skills" SOURCE_LEARNING_MANAGED_FILES="$SOURCE_LEARNING/.managed-files" SOURCE_LEARNING_MANAGED_SKILLS="$SOURCE_LEARNING/.managed-skills" +SOURCE_AGENTIC_REPOSITORY_FILES="$SOURCE_AGENTIC/.repository-files" +SOURCE_LEARNING_REPOSITORY_FILES="$SOURCE_LEARNING/.repository-files" SOURCE_ROOT_AGENTS="$ARCHIVE_ROOT/sample/root/AGENTS.md" SOURCE_ROOT_POINTER="$ARCHIVE_ROOT/sample/root/AGENTS.pointer.md" SOURCE_EXTENSION="$ARCHIVE_ROOT/sample/extensions/regulatory" @@ -805,10 +1016,14 @@ SOURCE_EXTENSION_MANAGED_SKILLS="$SOURCE_EXTENSION_LEARNING/.extension-managed-s for required in "$SOURCE_AGENTIC" "$SOURCE_LEARNING"; do [ -d "$required" ] || { echo "Required framework directory is missing: $required" >&2; exit 1; } done -for required in "$SOURCE_AGENTIC_MANAGED_FILES" "$SOURCE_AGENTIC_MANAGED_SKILLS" "$SOURCE_LEARNING_MANAGED_FILES" "$SOURCE_LEARNING_MANAGED_SKILLS" "$SOURCE_LOCAL_HISTORY"; do +for required in "$SOURCE_AGENTIC_MANAGED_FILES" "$SOURCE_AGENTIC_MANAGED_SKILLS" "$SOURCE_LEARNING_MANAGED_FILES" "$SOURCE_LEARNING_MANAGED_SKILLS" "$SOURCE_AGENTIC_REPOSITORY_FILES" "$SOURCE_LEARNING_REPOSITORY_FILES" "$SOURCE_LOCAL_HISTORY"; do [ -f "$required" ] || { echo "Required framework manifest is missing: $required" >&2; exit 1; } done -if [ "$SKIP_SKILLS" != "true" ]; then +if [ "$SCOPE" = "global" ]; then + SKIP_ROOT_AGENTS="true" + ROOT_AGENTS_MODE="skip" +fi +if [ "$SKIP_SKILLS" != "true" ] && [ "$SCOPE" != "linked" ]; then [ -d "$SOURCE_COMMON_SKILLS" ] || { echo "Common skill directory is missing." >&2; exit 1; } if has_content "$SOURCE_LEARNING_MANAGED_SKILLS"; then [ -d "$SOURCE_PROFILE_SKILLS" ] || { echo "Profile skill directory is missing." >&2; exit 1; } @@ -830,7 +1045,7 @@ if [ "$MODE" = "fail" ]; then exit 1 fi - if [ "$SKIP_SKILLS" != "true" ]; then + if [ "$SKIP_SKILLS" != "true" ] && [ "$SCOPE" != "linked" ]; then conflicts="" for manifest in "$SOURCE_AGENTIC_MANAGED_SKILLS" "$SOURCE_LEARNING_MANAGED_SKILLS"; do while IFS= read -r skill_name || [ -n "$skill_name" ]; do @@ -855,21 +1070,51 @@ if [ "$MODE" = "fail" ]; then fi fi -install_component "agentic-flow" "$SOURCE_AGENTIC" "$TARGET_AGENTIC" "$SOURCE_AGENTIC_MANAGED_FILES" -install_component "learning-flow/$SELECTED_PROFILE" "$SOURCE_LEARNING" "$TARGET_LEARNING" "$SOURCE_LEARNING_MANAGED_FILES" - -if [ "$SELECTED_EXTENSION" = "regulatory" ]; then - install_extension_overlay "learning-flow/regulatory (extension)" "$SOURCE_EXTENSION_LEARNING" "$TARGET_LEARNING" "$SOURCE_EXTENSION_MANAGED_FILES" -elif [ -n "$INSTALLED_EXTENSION" ]; then - if [ "$MODE" = "update" ] || [ "$MODE" = "replace" ]; then +if [ "$SCOPE" = "linked" ]; then + # A linked repository owns only the files it authors. Everything the + # framework owns is read from the global root, so the seeds are copied + # without overwriting in every mode: there is no framework content here + # for update or replace to refresh. + if [ "$INSTALLED_SCOPE" = "repository" ]; then remove_retired_managed_files "$TARGET_LEARNING" "$TARGET_LEARNING/.extension-managed-files" /dev/null - log "Removed $INSTALLED_EXTENSION extension" + remove_retired_managed_files "$TARGET_AGENTIC" "$TARGET_AGENTIC/.managed-files" /dev/null + remove_retired_managed_files "$TARGET_LEARNING" "$TARGET_LEARNING/.managed-files" /dev/null + log "Removed repository-scoped framework files now owned by $GLOBAL_ROOT" + fi + log "Installing repository learning state" + copy_manifest_files_if_missing "$SOURCE_AGENTIC" "$TARGET_AGENTIC" "$SOURCE_AGENTIC_REPOSITORY_FILES" "repository" + copy_manifest_files_if_missing "$SOURCE_LEARNING" "$TARGET_LEARNING" "$SOURCE_LEARNING_REPOSITORY_FILES" "repository" +elif [ "$SCOPE" = "global" ]; then + install_managed_component "agentic-flow" "$SOURCE_AGENTIC" "$TARGET_AGENTIC" "$SOURCE_AGENTIC_MANAGED_FILES" "$TARGET_AGENTIC/.managed-files" + install_managed_component "learning-flow/$SELECTED_PROFILE" "$SOURCE_LEARNING" "$TARGET_LEARNING" "$SOURCE_LEARNING_MANAGED_FILES" "$TARGET_LEARNING/.managed-files" +else + install_component "agentic-flow" "$SOURCE_AGENTIC" "$TARGET_AGENTIC" "$SOURCE_AGENTIC_MANAGED_FILES" + install_component "learning-flow/$SELECTED_PROFILE" "$SOURCE_LEARNING" "$TARGET_LEARNING" "$SOURCE_LEARNING_MANAGED_FILES" +fi + +if [ "$SCOPE" != "linked" ]; then + if [ "$SELECTED_EXTENSION" = "regulatory" ]; then + install_extension_overlay "learning-flow/regulatory (extension)" "$SOURCE_EXTENSION_LEARNING" "$TARGET_LEARNING" "$SOURCE_EXTENSION_MANAGED_FILES" + elif [ -n "$INSTALLED_EXTENSION" ]; then + if [ "$MODE" = "update" ] || [ "$MODE" = "replace" ]; then + remove_retired_managed_files "$TARGET_LEARNING" "$TARGET_LEARNING/.extension-managed-files" /dev/null + log "Removed $INSTALLED_EXTENSION extension" + fi fi fi -initialize_local_learning_workspace "$TARGET_PATH" "$SOURCE_LOCAL_HISTORY" +if [ "$SCOPE" != "global" ]; then + initialize_local_learning_workspace "$TARGET_PATH" "$SOURCE_LOCAL_HISTORY" +fi -if [ "$SKIP_SKILLS" != "true" ]; then +if [ "$SCOPE" = "linked" ] && [ "$INSTALLED_SCOPE" = "repository" ] && [ "$SKIP_SKILLS" != "true" ]; then + remove_skills_from_manifest "$SOURCE_AGENTIC_MANAGED_SKILLS" "$TARGET_SKILLS" + remove_skills_from_manifest "$ARCHIVE_ROOT/sample/profiles/minimal/learning-flow/.managed-skills" "$TARGET_SKILLS" + remove_skills_from_manifest "$ARCHIVE_ROOT/sample/profiles/full/learning-flow/.managed-skills" "$TARGET_SKILLS" + remove_skills_from_manifest "$SOURCE_EXTENSION_MANAGED_SKILLS" "$TARGET_SKILLS" +fi + +if [ "$SKIP_SKILLS" != "true" ] && [ "$SCOPE" != "linked" ]; then mkdir -p "$TARGET_SKILLS" if [ "$MODE" = "replace" ]; then @@ -928,6 +1173,21 @@ esac set_root_integration_state "$TARGET_AGENTIC/SETTINGS.md" "$RESOLVED_ROOT_AGENTS_MODE" +if [ -n "$RELEASE_TAG" ]; then + FRAMEWORK_VERSION="$RELEASE_TAG" +else + FRAMEWORK_VERSION="${RESOLVED_COMMIT:-unknown}" +fi + +write_install_scope_marker "$TARGET_LEARNING/.install-scope" "$SCOPE" "$FRAMEWORK_VERSION" "$GLOBAL_VERSION" + +if [ "$SCOPE" = "linked" ] && [ -n "$GLOBAL_VERSION" ] && [ "$GLOBAL_VERSION" != "$FRAMEWORK_VERSION" ]; then + log "WARNING: this repository was linked at $FRAMEWORK_VERSION but $GLOBAL_ROOT holds $GLOBAL_VERSION. Reinstall one of them so the routing contract and the repository state agree." +fi +if [ "$SCOPE" = "repository" ] && [ "$(read_marker_field "$GLOBAL_MARKER" scope)" = "global" ]; then + log "WARNING: a global installation exists at $GLOBAL_ROOT. This repository now carries its own copy of every managed skill, so the host agent will discover each one twice. Use --scope linked instead unless the duplication is intended." +fi + printf '\n%s\n' "Codebase Learning Flow" if [ -n "$RELEASE_TAG" ]; then printf 'Version: %s\n' "$RELEASE_TAG" @@ -940,7 +1200,13 @@ else printf 'Version: %s (ref: %s)\n' "$RESOLVED_COMMIT" "$REF" printf 'Source: development checkout (mutable unless ref is a commit or tag)\n' fi +printf 'Scope: %s (%s)\n' "$SCOPE" "$TARGET_PATH" -log "Installation complete: profile=$SELECTED_PROFILE extension=$SELECTED_EXTENSION mode=$MODE root-agents=$RESOLVED_ROOT_AGENTS_MODE" +log "Installation complete: scope=$SCOPE profile=$SELECTED_PROFILE extension=$SELECTED_EXTENSION mode=$MODE root-agents=$RESOLVED_ROOT_AGENTS_MODE" +if [ "$SCOPE" = "global" ]; then + printf '\n%s\n' "Next step:" + printf '%s\n' "Run the installer with --scope linked inside a repository to give it its own learning state, or start working: the managed skills in $TARGET_SKILLS already apply everywhere." + exit 0 +fi printf '\n%s\n' "Suggested first instruction:" printf '%s\n' "Start with my current task. Quietly verify the installed workflow, surface only meaningful instruction conflicts, teach the relevant code and domain path while working, and persist only verified findings that will be useful again." diff --git a/skill-evals/agentic-cases.yaml b/skill-evals/agentic-cases.yaml index c4abf4e..8a29eb2 100644 --- a/skill-evals/agentic-cases.yaml +++ b/skill-evals/agentic-cases.yaml @@ -74,6 +74,26 @@ cases: - treat catalogue presence publisher name or popularity as security approval - install a broad bundle by default + - prompt: "This repository has no agentic-flow/ directory, but the framework is installed globally. Start work on my task." + expected_behavior: resolve_framework_root_before_reading + must_include: + - repository root checked first then the global root + - read whichever root answered + - repository state stays in the repository + must_not: + - report the instructions as missing + - merge instructions from both roots + - create agentic-flow/ in the repository to satisfy a reference + + - prompt: "Promote what we verified today into durable knowledge. The framework instructions live in my home directory." + expected_behavior: repository_state_never_written_to_the_global_root + must_include: + - MAP.md and TAKEAWAYS.md in this repository + - .local/ in this repository + must_not: + - write repository knowledge under the global root + - create a global .local/ workspace + - prompt: "Explain this full-profile repository to me; I am not sure how much background you should assume." expected_skill: agentic-workflow must_include: