<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">PROP-007: Workspace — multi-package projects, recursive nesting, selective publish</title>
  <status stage="impl" state="done" comment="C 2026-07-25: M1.17 phases 1-5 shipped 2026-05-21 and M1.18 phase 5 shipped the install piece (Workspace::discover in plan.rs + apply.rs)"/>
  <p p="1"><fact id="milestone-line" status="impl/done">**Milestone:** `M1.17` ([`ROADMAP.md`](../../../ROADMAP.md)) — **shipped**, implementation-locked.</fact></p>
  <p p="2"><fact id="status-line" status="impl/done">**Status: IMPLEMENTED.** M1.17 Phases 1–5 landed 2026-05-21 — the workspace data model, the `vibe-workspace` discovery engine, path-source dependencies, `[workspace.versions]` placeholders, and `vibe workspace publish` (clippy-clean, fully tested). Workspace-aware `vibe install`, once "the remaining piece", shipped the next day with M1.18 Phase 5: PROP-009 §2.7 subsumed the design fork and `Workspace::discover` runs in both the install plan and apply paths (`vibe-install/plan.rs`, `apply.rs`).</fact></p>
  <p p="3"><fact id="related" status="spec/done">**Related:** [`VIBEVM-SPEC.md` §4.2 / §7 / §8](../../../VIBEVM-SPEC.md); [PROP-002](../vibe-registry/PROP-002-decentralized-registry.xml) (identity, registry, git-source, override); [PROP-008](../vibe-registry/PROP-008-qualified-naming.xml) (qualified naming — companion document, same design session); [PROP-003 §2.5](../vibe-resolver/PROP-003-dep-evolution.xml) (subskills — a *distinct* concept, see §4); [PROP-005](../vibe-index/PROP-005-package-index.xml) (index); [PROP-009](PROP-009-loading-model.xml) (loading model — answers §6 question 3).</fact></p>
  <p p="4"><fact id="design-rationale" status="spec/done">**Design rationale:** [`vibevm/vibespecs/design/workspace-and-qualified-naming.xml`](../../design/workspace-and-qualified-naming.xml) — the *why* and the lore behind this PROP: the owner's mental model, the fork-by-fork decision record, the Cargo-vs-Maven precedents. Non-normative; this PROP is the contract.</fact></p>
  <p p="5"><fact id="OWNER-SANCTION" status="impl/done">**Owner sanction:** the owner granted (2026-05-20) explicit sanction to edit any specification — including the owner-frozen `VIBEVM-SPEC.md` — for this refactor. PROP-007 + PROP-008 are the requirements record; the `VIBEVM-SPEC.md` edits (§4.2 layout, §7.3–7.5 schemas) land at implementation time.</fact></p>
  <section id="motivation" title="1. Motivation">
    <p p="6"><fact id="two-roles-lead" status="impl/done">vibevm today knows two manifest roles, carried by two different files:</fact></p>
    <list ordered="false" p="7">
      <item><fact id="role-consumer" status="impl/done">`vibe.toml` — the **consumer** manifest. Lives at the root of a project under development. Carries `[project]`, `[requires]`, `[[registry]]`, `[active]`, `[llm]`.</fact></item>
      <item><fact id="role-artifact" status="impl/done">`vibe-package.toml` — the **publishable artifact** manifest. Lives at the root of a package directory (what `vibe registry publish &lt;path&gt;` consumes, what a registry repo carries). Carries `[package]`, `[writes]`, `[provides]`, `[requires]`, `[obsoletes]`, `[conflicts]`.</fact></item>
    </list>
    <p p="8"><fact id="no-composition" status="impl/done">There is no notion of a *project composed of several modules*. A project is one consumer; a package is one artifact; the two never compose. Publishing is `vibe registry publish &lt;one-path&gt;` — one package at a time, by hand.</fact></p>
    <p p="9"><fact id="OWNER-REQUEST" status="impl/done">The owner's request (design session 2026-05-20): the Maven-multi-module + cargo-workspace shape. A project should decompose naturally into modules; each module publishes independently — or not at all, by choice; the whole structure is declared in the project manifest. Both extremes must be first-class: a project entirely local (nothing ever published, the source tree never leaves the developer's machine) and a project entirely published (every sub-package and the root in registries).</fact></p>
    <p p="10"><fact id="prior-art" status="impl/done">Prior art: cargo `[workspace]` (`members = [...]`, one `Cargo.lock`, `cargo publish -p`), Maven multi-module (`&lt;modules&gt;`, reactor build, per-module `&lt;skip&gt;`).</fact></p>
    <p p="11"><fact id="workspace-axis" status="impl/done">PROP-007 covers the workspace axis. The companion [PROP-008](../vibe-registry/PROP-008-qualified-naming.xml) covers qualified naming (`group`, short aliases, collision detection); the two were specified together and cross-reference each other but ship as separate milestones.</fact></p>
  </section>
  <section id="decisions" title="2. Decisions">
    <section id="workspace-section" title="2.1 The `[workspace]` section">
      <p p="12"><fact id="req-workspace-section" status="impl/done">`req r1`</fact></p>
      <p p="13"><fact id="WORKSPACE-TABLE" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** A `vibe.toml` may carry a `[workspace]` table declaring member packages:</fact></p>
      <fence lang="toml" p="14">[workspace]
members = [
  "packages/flow-wal",
  "packages/feat-auth",
  "packages/stack-*",          # glob permitted
]</fence>
      <list ordered="false" p="15">
        <item><fact id="MEMBERS-PATHS" status="impl/done">`members` — paths relative to the manifest. Glob patterns are permitted (`packages/*`).</fact></item>
        <item><fact id="MEMBER-IS-NODE" status="impl/done">Each member is a directory carrying its own `vibe.toml` (§2.2).</fact></item>
        <item><fact id="EXPLICIT-MEMBERSHIP" status="impl/done" action="continue" actionstage="doc" audience="user">Membership is **explicit** — there is no auto-discovery of directories that happen to carry a `vibe.toml`. The structure is declared, per the owner's "the whole structure is in the project description" requirement.</fact></item>
      </list>
    </section>
    <section id="unified-manifest" title="2.2 Unified manifest — one `vibe.toml`">
      <p p="16"><fact id="req-unified-manifest" status="impl/done">`req r1`</fact></p>
      <p p="17"><fact id="ONE-MANIFEST" status="impl/done" action="continue" actionstage="doc" audience="user,author">**Decision.** `vibe-package.toml` is **retired as a distinct filename**. Every node — project root, workspace member, published package — carries a single `vibe.toml`; the role is expressed by which sections are present. This is the cargo model: one `Cargo.toml` carries `[package]` and/or `[workspace]`.</fact></p>
      <p p="18"><fact id="section-roles-lead" status="impl/done">Section roles:</fact></p>
      <table p="19">
        <tr>
          <td>Section</td>
          <td>Presence</td>
          <td>Meaning</td>
        </tr>
        <tr>
          <td><fact id="ROW-SECTION-PACKAGE" status="impl/done">`[package]`</fact></td>
          <td><fact id="ROW-SECTION-PACKAGE-PRESENCE" status="impl/done">optional</fact></td>
          <td><fact id="ROW-SECTION-PACKAGE-MEANING" status="impl/done">The node is a publishable artifact (`kind`, `name`, `group`, `version`, …).</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SECTION-PROJECT" status="impl/done">`[project]`</fact></td>
          <td><fact id="ROW-SECTION-PROJECT-PRESENCE" status="impl/done">optional</fact></td>
          <td><fact id="ROW-SECTION-PROJECT-MEANING" status="impl/done">The node is a non-publishable consumer/root.</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SECTION-WORKSPACE" status="impl/done">`[workspace]`</fact></td>
          <td><fact id="ROW-SECTION-WORKSPACE-PRESENCE" status="impl/done">optional</fact></td>
          <td><fact id="ROW-SECTION-WORKSPACE-MEANING" status="impl/done">The node coordinates members (§2.1).</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SECTION-CONSUMER" status="impl/done">`[requires]`, `[[registry]]`, `[active]`, `[llm]`</fact></td>
          <td><fact id="ROW-SECTION-CONSUMER-PRESENCE" status="impl/done">optional</fact></td>
          <td><fact id="ROW-SECTION-CONSUMER-MEANING" status="impl/done">Consumer-side configuration.</fact></td>
        </tr>
      </table>
      <list ordered="false" p="20">
        <item><fact id="PACKAGE-XOR-PROJECT" status="impl/done" action="continue" actionstage="doc" audience="user,author">`[package]` and `[project]` are **mutually exclusive** in one file — a node is either a publishable package or a plain project, not both. (Decision 7-α from the design session: keep the two sections distinct rather than folding `[project]` into a `[package]` with optional `kind`. Explicitness wins; `kind` stays strictly mandatory wherever `[package]` appears.)</fact></item>
        <item><fact id="WORKSPACE-COMPOSES" status="impl/done">`[workspace]` composes with `[package]`, with `[project]`, or with neither (a virtual workspace root — just a coordinator).</fact></item>
      </list>
      <list ordered="false" p="21">
        <item><fact id="one-file-why" status="spec/done">**Why one file.** A workspace member is *simultaneously* a locally-developed node and a publishable artifact. Two files would give every member both, each carrying its own `[requires]` — duplication that drifts. One `vibe.toml` with a variable section set is the only coherent shape.</fact></item>
        <item><fact id="one-file-secondary" status="spec/done">The owner's secondary reason, recorded verbatim: reading one file is easier for a human — and for a small/weak LLM agent — than chasing many.</fact></item>
      </list>
      <p p="22"><fact id="MIGRATION-CONSEQUENCE" status="impl/done">**Consequence.** Registry repositories migrate `vibe-package.toml` → `vibe.toml` (a published package is a `vibe.toml` with `[package]`, no `[workspace]`). The redirect-stub marker `vibe-redirect.toml` (PROP-002 §2.4.2) is a separate concern and is unaffected. Migration detail: [PROP-008 §3](../vibe-registry/PROP-008-qualified-naming.xml#migration).</fact></p>
    </section>
    <section id="nesting" title="2.3 Recursive nesting">
      <p p="23"><fact id="req-nesting" status="impl/done">`req r1`</fact></p>
      <p p="24"><fact id="NESTED-WORKSPACES" status="impl/done">**Decision.** Nested workspaces are permitted to arbitrary depth — a member may itself carry a `[workspace]` section.</fact></p>
      <p p="25"><fact id="nesting-principle-lead" status="impl/done">The load-bearing principle that keeps this from becoming chaos:</fact></p>
      <p p="26"><fact id="NESTING-PRINCIPLE" status="impl/done" action="continue" actionstage="doc" audience="user">Nesting is **hierarchical grouping**, not independent resolution domains. The lockfile and unified resolution always live at the *absolute root* of the workspace tree. A nested `[workspace]` provides (a) the `[workspace.versions]` matryoshka (§2.6) and (b) logical grouping of members — never its own lockfile, never its own resolution pass.</fact></p>
      <list ordered="false" p="27">
        <item><fact id="ROOT-DISCOVERY" status="impl/done">**Root discovery.** A command run inside a node walks up the directory tree, collects every `vibe.toml` carrying `[workspace]`, and selects the *topmost one that transitively includes the current node*. The lockfile lives there.</fact></item>
        <item><fact id="STANDALONE-NODE" status="impl/done">**Standalone node.** If no enclosing `[workspace]` exists above a node (it was cloned on its own — e.g. it is just a published package), the node is its own absolute root. This is the same rule as §2.4's command-bubbling and matches cargo's behaviour for a crate cloned outside any workspace.</fact></item>
        <item><fact id="EXPLICIT-NESTING" status="impl/done">**Explicit nesting.** A parent `[workspace].members` lists the nested sub-workspace among its members. No nesting is inferred from the directory tree alone.</fact></item>
      </list>
      <p p="28"><fact id="nesting-cost" status="spec/done">**Cost.** Cargo forbids nested workspaces precisely to avoid "which workspace is mine" ambiguity. vibevm permits them because the "lock always at the absolute root" rule resolves that ambiguity deterministically. The price is recursion in three places — parent-chain discovery, transitive member aggregation, and placeholder resolution (§2.6) — which the implementation estimate for M1.17 must absorb.</fact></p>
    </section>
    <section id="lockfile" title="2.4 Single lockfile at the absolute root">
      <p p="29"><fact id="req-lockfile" status="impl/done">`req r1`</fact></p>
      <p p="30"><fact id="ONE-LOCKFILE" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** One `vibe.lock`, at the absolute root of the workspace tree (§2.3). No per-member lockfiles.</fact></p>
      <list ordered="false" p="31">
        <item><fact id="UNIFIED-RESOLUTION" status="impl/done">**Unified resolution.** All members resolve together: one version of each external dependency across the whole workspace. A "diamond" inside a workspace is impossible by construction. This is the cargo model; Maven's nearest equivalent is `&lt;dependencyManagement&gt;` in the parent POM (Maven has no lockfile at all — a known reproducibility gap vibevm does not inherit, since the lockfile is already load-bearing for content-hash integrity per PROP-002 §2.1).</fact></item>
        <item><fact id="COMMAND-BUBBLING" status="impl/done" action="continue" actionstage="doc" audience="user">**Command bubbling.** A command (`vibe install`, `vibe build`) run inside a member's directory walks up to the absolute root, finds `vibe.lock`, and operates against it. The member "does not notice" it is part of something larger — this realises the owner's requirement that a developer can work inside a sub-project unaware of the surrounding workspace.</fact></item>
      </list>
    </section>
    <section id="path-source" title="2.5 Cross-member dependencies — the `path` source">
      <p p="32"><fact id="req-path-source" status="impl/done">`req r1`</fact></p>
      <p p="33"><fact id="PATH-SOURCE" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** A third dependency source-kind joins registry-resolved (PROP-002 §2.2) and git-source (PROP-002 §2.4.1): **path-source**.</fact></p>
      <fence lang="toml" p="34">[requires.packages]
"org.vibevm.world/wal" = { path = "../flow-wal" }
# dual-form (recommended for any member that is itself published):
"org.vibevm.world/wal" = { path = "../flow-wal", version = "^0.1" }</fence>
      <list ordered="false" p="35">
        <item><fact id="DUAL-FORM" status="impl/done" action="continue" actionstage="doc" audience="user,author">**Dual-form.** `path` is used during local development inside the workspace; `version` takes effect when the consuming node is itself published — the published copy references `org.vibevm.world/wal@^0.1` from a registry, not `../flow-wal` (which an external consumer does not have). This is cargo's `{ path = ..., version = ... }` shape. Dual-form is **required** for any path-dep whose consumer is publishable.</fact></item>
        <item><fact id="PATH-PRIORITY" status="impl/done">**Resolution priority.** `[[override]]` &gt; path &gt; git-source &gt; registry-walk. Path sits below override (override is a deliberate patch) and above git-source (path is the most local, most authoritative declaration).</fact></item>
        <item><fact id="PATH-LOCKFILE" status="impl/done">**Lockfile.** New `source_kind = "path"`. For a workspace-member path-dep the lockfile records a reference to the member by id within the workspace, not an external `source_url` — so the lockfile stays portable across machines (an absolute path would not).</fact></item>
        <item><fact id="PATH-OUTSIDE" status="impl/done">**path outside the workspace.** A `path` pointing at a directory that is not a member of this workspace is permitted, but a node depending on it via path-only (no `version`) is not publishable — the published copy would dangle.</fact></item>
      </list>
    </section>
    <section id="versions" title="2.6 Version placeholders — `[workspace.versions]`">
      <p p="36"><fact id="req-versions" status="impl/done">`req r1`</fact></p>
      <p p="37"><fact id="VERSION-PLACEHOLDERS" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** Named version placeholders, the equivalent of Maven `&lt;properties&gt;`:</fact></p>
      <fence lang="toml" p="38"># in a [workspace] manifest:
[workspace.versions]
core = "0.0.1"
ui   = "^0.3"</fence>
      <fence lang="toml" p="39"># in a member:
[requires.packages]
"org.vibevm/auth" = { version.var = "core" }</fence>
      <list ordered="false" p="40">
        <item><fact id="MATRYOSHKA-RESOLUTION" status="impl/done">**Recursive resolution (matryoshka).** A `version.var = "core"` reference is resolved bottom-up: search `[workspace.versions]` of the node's nearest enclosing workspace, then its parent, then upward to the absolute root. First hit wins — a nearer level overrides a farther one. This is the arbitrary-depth nesting the owner asked for; it depends on §2.3 permitting nested workspaces.</fact></item>
        <item><fact id="VERSION-INHERITANCE" status="spec/done">**Version inheritance.** A member may write `version = { workspace = true }` in `[package]` to inherit its own version from the nearest `[workspace]` — cargo's `version.workspace = true`. Independent per-member versions remain the default; inheritance is opt-in.</fact></item>
        <item><fact id="workspace-dependencies-noted" status="spec/done">A companion mechanism, `[workspace.dependencies]` (cargo-style centralised per-pkgref defaults, ≈ Maven `&lt;dependencyManagement&gt;`), is noted as a possible addition but **not** the primary surface — named placeholders were the owner's explicit request and cover the stated use case ("write `0.0.1` once, reference it by name everywhere").</fact></item>
      </list>
    </section>
    <section id="selective-publish" title="2.7 Selective publish">
      <p p="41"><fact id="req-selective-publish" status="impl/done">`req r1`</fact></p>
      <p p="42"><fact id="PUBLISH-POSTURE" status="impl/done" action="continue" actionstage="doc" audience="user,author">**Decision.** Each publishable node declares its publish posture in `[package]`:</fact></p>
      <fence lang="toml" p="43">[package]
publish = false                 # never published — workspace-internal
# or
publish = true                  # default
# or
publish = ["vibespecs"]         # only into these named registries</fence>
      <list ordered="false" p="44">
        <item><fact id="PUBLISH-TOPOLOGICAL" status="impl/done" action="continue" actionstage="doc" audience="user">`vibe workspace publish [--member &lt;m&gt;]` walks members in **topological order** (dependency-first) and skips `publish = false`.</fact></item>
        <item><fact id="PUBLISH-NOT-ATOMIC" status="impl/done" action="continue" actionstage="doc" audience="user">Publish is **not atomic**: on the first failure the command stops and reports what was already published and what remains. (Distributed publishing across N independent host repos has no transaction; a rollback would be a worse lie than a clear partial-progress report.)</fact></item>
        <item><fact id="PUBLISH-EXTREMES" status="impl/done">Extremes: every member `publish = false` → the project is entirely invisible, nothing leaves the machine. Every member `publish = true` → the whole project, root included (§2.9), is published.</fact></item>
      </list>
    </section>
    <section id="published-repos" title="2.8 Published package repositories">
      <p p="45"><fact id="req-published-repos" status="impl/done">`req r1`</fact></p>
      <p p="46"><fact id="ONE-SOURCE-TREE" status="impl/done" action="continue" actionstage="doc" audience="user">**Decision.** The development tree is **one** source tree (one git repository, or not in git at all if the project is private). Workspace members are subdirectories; the split into packages is logical, at the vibevm resolver level. **Publishing is a separate operation that copies the content of a package's directory into a new, separate repository** in the registry org and tags the version — exactly what `vibe registry publish` does today for one package, repeated per self-published member by `vibe workspace publish`.</fact></p>
      <fence p="47">DEVELOPMENT — one tree, one git repo (or no git):
  my-project/
  ├── vibe.toml             [workspace] members = ["packages/X", "packages/Y"]
  └── packages/
      ├── X/  vibe.toml     [package] org.vibevm/X, publish = true
      └── Y/  vibe.toml     [package] org.vibevm/Y, publish = true

PUBLISH (`vibe workspace publish`) — splits into separate repos:
  packages/X/  --content copy--&gt;  &lt;registry-org&gt;/org.vibevm.X   tag v…
  packages/Y/  --content copy--&gt;  &lt;registry-org&gt;/org.vibevm.Y   tag v…
  The development tree is NOT modified. It stays a monorepo.</fence>
      <p p="48"><fact id="COPY-NOT-MOVE" status="impl/done">A nested package does **not** "surface" by moving files — only a *copy of its content* is published, into its own repository, at publish time. The source tree stays unified.</fact></p>
      <p p="49"><fact id="PUBLISHED-REPO-TERM" status="impl/done">**Terminology.** The published copy is a **published package repository**; the source of truth is the **workspace** (the development monorepo). This is *not* a `[[mirror]]` (PROP-002 §2.3 — that term means an availability copy of a registry).</fact></p>
      <p p="50"><fact id="ORIGIN-MARKER" status="impl/done">**Origin marker.** The published copy carries a machine-readable marker in its `vibe.toml`:</fact></p>
      <fence lang="toml" p="51">[origin]
upstream     = "https://github.com/you/my-project"   # the monorepo
path         = "packages/flow-wal"                   # path within it
generated_by = "vibe 0.x"
generated_at = "2026-…"</fence>
      <p p="52"><fact id="SIGNALLING-LAYERS" status="impl/done">**"Do not contribute here" signalling.** A published copy whose source of truth is a monorepo should tell humans not to send pull requests. GitHub offers no "disable PRs only" switch, so the signal is layered. `vibe workspace publish` default applies layers 1–4; `--archive` adds layer 5:</fact></p>
      <table p="53">
        <tr>
          <td>Layer</td>
          <td>Visibility</td>
          <td>Cost</td>
        </tr>
        <tr>
          <td><fact id="ROW-SIGNAL-README" status="impl/done">README banner as the first block (vibevm already generates such banners — `build_redirect_readme` for stubs)</fact></td>
          <td><fact id="ROW-SIGNAL-README-VISIBILITY" status="impl/done">seen immediately on opening the repo</fact></td>
          <td><fact id="ROW-SIGNAL-README-COST" status="impl/done">free</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SIGNAL-DESCRIPTION" status="impl/done">repo `description` = "Generated copy of `&lt;pkgref&gt;` — contribute at `&lt;upstream&gt;`"</fact></td>
          <td><fact id="ROW-SIGNAL-DESCRIPTION-VISIBILITY" status="impl/done">visible in the repo header</fact></td>
          <td><fact id="ROW-SIGNAL-DESCRIPTION-COST" status="impl/done">one API call at create</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SIGNAL-ISSUES" status="spec/done">Issues disabled (`has_issues = false`)</fact></td>
          <td><fact id="ROW-SIGNAL-ISSUES-VISIBILITY" status="spec/done">Issues tab disappears</fact></td>
          <td><fact id="ROW-SIGNAL-ISSUES-COST" status="spec/done">one API call</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SIGNAL-PR-TEMPLATE" status="impl/done">`.github/PULL_REQUEST_TEMPLATE.md` with a STOP notice</fact></td>
          <td><fact id="ROW-SIGNAL-PR-TEMPLATE-VISIBILITY" status="impl/done">fires at PR-creation time</fact></td>
          <td><fact id="ROW-SIGNAL-PR-TEMPLATE-COST" status="impl/done">free</fact></td>
        </tr>
        <tr>
          <td><fact id="ROW-SIGNAL-ARCHIVE" status="spec/done">`archived = true` (`--archive`)</fact></td>
          <td><fact id="ROW-SIGNAL-ARCHIVE-VISIBILITY" status="spec/done">yellow "Public archive" banner, PR/issues/push all blocked</fact></td>
          <td><fact id="ROW-SIGNAL-ARCHIVE-COST" status="spec/done">re-publish needs unarchive→push→archive; vibevm drives that cycle</fact></td>
        </tr>
      </table>
      <p p="54"><fact id="PUBLISHED-REPOS-TOGGLE" status="spec/done">A `[workspace]`-level setting `published_repos = "read-only" | "open"` (default `"read-only"` for workspace members) lets an operator opt into the inverse model where the split repo *is* the canonical contribution target.</fact></p>
      <list ordered="false" p="55">
        <item><fact id="FLAT-LAYOUT" status="impl/done">**Layout recommendation.** Keep members as siblings (flat under `packages/`), not physically nested. Logical hierarchy ("X is built from Y") is expressed by a path-dependency (§2.5), not by nesting directories.</fact></item>
        <item><fact id="SUBTREE-EXCISION" status="spec/done">If a member *is* physically inside another's directory, publishing the outer package must excise the inner sub-package's subtree from the outer's content (cargo does this with nested crates) — supported, but discouraged for the "holes in the tree" complexity it adds.</fact></item>
      </list>
    </section>
    <section id="root-package" title="2.9 Root as a publishable package">
      <p p="56"><fact id="req-root-package" status="impl/done">`req r1`</fact></p>
      <p p="57"><fact id="ROOT-PUBLISHABLE" status="impl/done">**Decision.** The root `vibe.toml` may itself carry `[package]` alongside `[workspace]` — cargo-style. The workspace coordinator can also be a publishable artifact in its own right. (Maven's parent POM cannot; cargo's root crate can. vibevm follows cargo.)</fact></p>
    </section>
  </section>
  <section id="surface" title="3. Command and crate surface">
    <p p="58"><fact id="design-surface" status="impl/done">`design r1`</fact></p>
    <list ordered="false" p="59">
      <item><fact id="SURF-PUBLISH" status="impl/done">`vibe workspace publish [--member &lt;m&gt;] [--archive]` — topological publish of self-publishing members (§2.7), origin-marker + signalling (§2.8).</fact></item>
      <item><fact id="SURF-BUBBLE" status="impl/done">`vibe install` / `vibe build` bubble up to the absolute root (§2.4); `-p &lt;member&gt;` targets one member; run inside a member's directory they address that member's `[requires]`.</fact></item>
      <item><fact id="SURF-CRATE" status="impl/done">A new `vibe-workspace` crate, or workspace functions inside `vibe-core` — decided at implementation time.</fact></item>
    </list>
    <p p="60"><fact id="LOCK-V4" status="impl/done">`vibe.lock` schema bumps to **v4** (v3 was git-source `source_kind`, PROP-002 §2.4.1) to carry `source_kind = "path"` and the member-reference shape (§2.5).</fact></p>
  </section>
  <section id="vs-subskills" title="4. Workspace members vs subskills">
    <p p="61"><fact id="design-vs-subskills" status="impl/done">`design r1`</fact></p>
    <p p="62"><fact id="easy-to-confuse" status="impl/done">These are easy to confuse; they are different objects.</fact></p>
    <table p="63">
      <tr>
        <td></td>
        <td>Workspace member (PROP-007)</td>
        <td>Subskill ([PROP-003 §2.5](../vibe-resolver/PROP-003-dep-evolution.xml))</td>
      </tr>
      <tr>
        <td><fact id="ROW-VS-WHAT" status="impl/done">What it is</fact></td>
        <td><fact id="ROW-VS-WHAT-WORKSPACE-MEMBER-PROP-00" status="impl/done">A separate package</fact></td>
        <td><fact id="ROW-VS-WHAT-SUBSKILL-PROP-003-2-5-VI" status="impl/done">A sub-document *inside* one package</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-VS-VERSIONING" status="impl/done">Versioning</fact></td>
        <td><fact id="ROW-VS-VERSIONING-WORKSPACE-MEMBER-PROP-00" status="impl/done">Its own `version`; published independently</fact></td>
        <td><fact id="ROW-VS-VERSIONING-SUBSKILL-PROP-003-2-5-VI" status="impl/done">Versioned together with its parent package</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-VS-PUBLICATION" status="impl/done">Publication</fact></td>
        <td><fact id="ROW-VS-PUBLICATION-WORKSPACE-MEMBER-PROP-00" status="impl/done">Becomes its own repository (§2.8)</fact></td>
        <td><fact id="ROW-VS-PUBLICATION-SUBSKILL-PROP-003-2-5-VI" status="impl/done">Never published separately</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-VS-IDENTITY" status="impl/done">Identity</fact></td>
        <td><fact id="ROW-VS-IDENTITY-WORKSPACE-MEMBER-PROP-00" status="impl/done">Own `(group, name, version, content_hash)`</fact></td>
        <td><fact id="ROW-VS-IDENTITY-SUBSKILL-PROP-003-2-5-VI" status="impl/done">No independent identity</fact></td>
      </tr>
    </table>
    <p p="64"><fact id="MEMBER-VS-SUBSKILL" status="impl/done">A workspace member is a package. A subskill is content granularity within a package. PROP-007 does not touch the subskill design.</fact></p>
  </section>
  <section id="rejected" title="5. Rejected / deferred alternatives">
    <list ordered="false" p="65">
      <item><fact id="REJ-PER-MEMBER-LOCKS" status="spec/done">**Per-member lockfiles.** Rejected. Independent resolution per member loses unified resolution and reintroduces intra-workspace diamonds. One lock at the absolute root (§2.4) is the cargo-proven shape.</fact></item>
      <item><fact id="REJ-TWO-FILES" status="spec/done">**Two files (`vibe.toml` + `vibe-package.toml`) side by side.** Rejected. A member needs both consumer and publishable roles; two files duplicate `[requires]` and drift. §2.2 unifies into one file.</fact></item>
      <item><fact id="REJ-PHYSICAL-NESTING" status="spec/done">**Physical nesting of members by default.** Discouraged, not forbidden. §2.8 recommends a flat sibling layout; physical nesting is supported with subtree excision but adds avoidable complexity.</fact></item>
      <item><fact id="REJ-ATOMIC-PUBLISH" status="spec/done">**Atomic `vibe workspace publish`.** Deferred / rejected as infeasible — no transaction spans N independent host repos. §2.7 ships stop-on-first-failure with a clear partial-progress report instead.</fact></item>
    </list>
  </section>
  <section id="open" title="6. Open questions">
    <p p="66"><fact id="resolved-lead" status="impl/done">**Resolved during M1.17 implementation:**</fact></p>
    <list ordered="true" p="67">
      <item><fact id="RES-ORIGIN-FIELDS" status="impl/done">`[origin]` field set (§2.8) — **resolved**: `upstream`, `path`, `commit` (optional — present when the monorepo is a git repo), `generated_by`, `generated_at`.</fact></item>
      <item><fact id="RES-LOCK-SHAPE" status="impl/done">`vibe.lock` v4 path-member shape (§2.5) — **resolved**: `source_kind = "path"`, with `source_url` carrying the member's path relative to the workspace root. No separate `workspace_member` field — the relative path is portable and one field suffices.</fact></item>
    </list>
    <p p="68"><fact id="open-lead" status="spec/work">**Open:**</fact></p>
    <list ordered="true" p="69">
      <item><fact id="OPEN-WORKSPACE-INSTALL" status="impl/done">**Workspace-aware `vibe install` / `vibe build`.** The remaining M1.17 milestone. §2.4 / §3 sketch the intent — bubble to the absolute root, unified multi-member resolution, `-p &lt;member&gt;` — but the concrete behaviour turns on a per-member **materialisation target**: when a dependency is resolved for member M, which member's `spec/` does its content land in? That decision is unspecified and wants explicit owner input. The path-source resolver capability this builds on is already implemented and tested (Phase 3). **→ Resolved 2026-05-21 by [PROP-009](PROP-009-loading-model.xml):** the question proved to be a loading-model redesign — separate authored / dependency trees, a computed per-node effective boot sequence, generated `STATIC.md` / `INDEX.md` artifacts — and the new framing supersedes "which member's `spec/`".</fact></item>
      <item><fact id="OPEN-VERSION-INHERITANCE" status="spec/work">`version = { workspace = true }` member-version inheritance (§2.6) — **deferred**. §2.6 names it but defines no source for the inherited version: cargo reads `[workspace.package].version`, a table PROP-007 does not specify. Needs an explicit spec decision before implementation. The `[workspace.versions]` named placeholders (shipped) cover the owner's stated "write the version once" use case.</fact></item>
      <item><fact id="OPEN-WORKSPACE-DEPS" status="spec/work">Whether `[workspace.dependencies]` (§2.6) ships alongside named placeholders or is deferred until a concrete need surfaces.</fact></item>
    </list>
  </section>
  <section id="phases" title="7. Phase plan">
    <p p="70"><fact id="phase-plan" status="impl/done">PROP-007 (workspace) has **no dependency on the index** and can be implemented first. The companion [PROP-008](../vibe-registry/PROP-008-qualified-naming.xml) (qualified naming) depends on [PROP-005](../vibe-index/PROP-005-package-index.xml) being implemented for short-name resolution. Suggested order: PROP-007 → PROP-005 implementation → PROP-008. PROP-007 alone delivers multi-package projects, local cross-member deps, selective publish, and both "entirely local" / "entirely published" extremes — the bulk of the owner's request.</fact></p>
    <p p="71"><fact id="spec-edits-note" status="impl/done">`VIBEVM-SPEC.md` edits (§4.2 directory layout, §7.3–7.5 manifest/lockfile schemas) land in the PROP-007 implementation milestone under the owner sanction recorded above.</fact></p>
  </section>
  <section id="history" title="8. Version history">
    <list ordered="false" p="72">
      <item><fact id="HISTORY-DRAFT-1" status="spec/done">**2026-05-20 — draft 1.** Initial proposal. Requirements locked in an owner design session (decisions on workspace shape, recursive nesting, unified manifest, path-source, version placeholders, selective publish, published-repo signalling). Open for review.</fact></item>
      <item><fact id="HISTORY-PHASES-1-5" status="spec/done">**2026-05-21 — Phases 1–5 implemented (M1.17).** The unified `vibe.toml` manifest (all legacy removed), the `vibe-workspace` discovery crate (`Workspace::discover`, recursive nesting, glob, cycle detection), path-source dependencies with `vibe.lock` schema v4, `[workspace.versions]` recursive placeholders, and `vibe workspace publish` — each shipped clippy-clean and fully tested. Workspace-aware `vibe install` (§6 question 3) remains. Detailed record: §9.</fact></item>
    </list>
  </section>
  <section id="impl" title="9. Implementation record">
    <p p="73"><fact id="impl-record-intro" status="impl/done">M1.17 implemented this proposal across six phases on the `m1.17-workspace` branch, 2026-05-21. This section is the non-normative record of *what was built* — the contract is §1–§7, and where the two disagree the contract wins. Phases 1–5 are the workspace data model and tooling; Phase 6 is documentation. One piece — workspace-aware `vibe install` — is deferred (§9.3).</fact></p>
    <section id="impl-phases" title="9.1 What each phase delivered">
      <p p="74"><fact id="IMPL-PHASE-1" status="impl/done">**Phase 1 — the unified manifest** (`vibe-core`; commits `b794e7a`, `9a190ff`). The two manifest types — `ProjectManifest` (read from `vibe.toml`) and `PackageManifest` (read from `vibe-package.toml`) — collapse into one `Manifest` type for a single `vibe.toml` per node. Sections are optional; the role is the set of sections present. `Manifest::validate` enforces: `[project]` ⊕ `[package]` (never both); at least one of `[project]` / `[package]` / `[workspace]`; package-role sections (`[writes]`, `[provides]`, `[boot_snippet]`, `[[requires_any]]`, `[obsoletes]`, `[conflicts]`, `[compatibility]`, `[features]`, `[target.*]`) require `[package]`. The new `[workspace]`, `[origin]` and `[package].publish` are parsed here and consumed by later phases. New module `manifest/document.rs`. The ~190 downstream call-sites across `vibe-registry`, `vibe-resolver`, `vibe-install`, `vibe-publish`, `vibe-check`, `vibe-cli`, `vibe-mcp` and the `vibe-index` service were migrated; `CachedPackage.manifest` is a `Manifest` with a `package_meta()` accessor; eight registry / manual-test fixtures were renamed `vibe-package.toml` → `vibe.toml`.</fact></p>
      <p p="75"><fact id="IMPL-PHASE-2" status="impl/done">**Phase 2 — workspace discovery** (new `vibe-workspace` crate; commit `ece30a6`). `Workspace::discover(start)` walks up from any directory to the topmost `[workspace]` transitively enclosing the start node — the absolute root. `Workspace::load(root)` reads the root manifest and expands members recursively: `members` patterns are glob-expanded against the filesystem; an explicit (non-glob) member that does not resolve is an error, a glob that sweeps a non-package directory skips it; a workspace that transitively lists itself is a `NestingCycle`. A node with no enclosing `[workspace]` is a standalone workspace with zero members — so `discover` is the universal entry point and degenerates cleanly for every existing single-package project. Members carry a portable `rel_path` (forward-slashed, relative to the root); absolute paths exist only in memory during the walk.</fact></p>
      <p p="76"><fact id="IMPL-PHASE-3" status="impl/done">**Phase 3 — path-source dependencies + lockfile v4** (`vibe-core`, `vibe-registry`, `vibe-install`; commits `ff21de3`, `e9a15d2`). A `[requires.packages]` entry `{ path = "../sibling", version = "^0.1" }` is a third source-kind. `vibe-core` gains `PathPackageDep` and the `Requires.path_packages` bucket. The resolver dispatches `[[override]]` &gt; path-source &gt; git-source &gt; registry-walk; `resolve_path_source` reads the package off the local directory, `fetch_path_source` copies it into the cache (no clone). `vibe.lock` bumps to schema v4: a new `source_kind = "path"`, whose `source_url` carries the member's path relative to the workspace root. All legacy lockfile readers — the `source` field alias, the v1 heuristic, the schema-version default — were deleted; `Lockfile::read` rejects any `schema_version` other than 4.</fact></p>
      <p p="77"><fact id="IMPL-PHASE-4" status="impl/done">**Phase 4 — `[workspace.versions]` placeholders** (`vibe-core`, `vibe-workspace`; commit `98795e8`). A `[workspace]` may carry a `[workspace.versions]` table of named version constraints. A member references one as `{ version.var = "core" }`, parsed into `Requires.var_packages`. The `vibe-workspace` loader, after discovering all members, resolves each placeholder bottom-up — the node's own `[workspace.versions]` if it is itself a workspace, then its declaring workspace, up to the absolute root; first hit wins — folding each into a concrete `PackageRef`. `WorkspaceMember` gained a `parent` link to make the enclosing-workspace walk possible.</fact></p>
      <p p="78"><fact id="IMPL-PHASE-5" status="impl/done">**Phase 5 — selective publish** (`vibe-cli`, `vibe-workspace`; commit `b673d2b`). `vibe workspace publish` selects the self-publishing nodes (those carrying `[package]` whose `PublishPosture` admits the primary registry), orders them dependency-first over the inter-member path-deps, and publishes each from a staged copy — the developer's tree is never modified. Each staged copy carries the `[origin]` provenance marker, a "generated copy — contribute upstream" README banner, a `PULL_REQUEST_TEMPLATE.md` STOP notice, and a generated-copy description. Publishing is non-atomic: on the first failure the command stops and reports what published and what remains.</fact></p>
      <p p="79"><fact id="IMPL-PHASE-6" status="impl/done">**Phase 6 — documentation** (commits `047f92d`, `10406a1`, `3cb2a03`). `VIBEVM-SPEC.md` §4.2 / §7.6, this §9 and the §6 / §8 updates, `ROADMAP.md`, `CHANGELOG.md`, the `docs/` and `manual-tests/` sweep, the new `docs/commands/workspace-publish.md`, and the WAL checkpoint.</fact></p>
    </section>
    <section id="impl-decisions" title="9.2 Decisions pinned during implementation">
      <list ordered="false" p="80">
        <item><fact id="IMPL-HARD-BREAK" status="impl/done">**Hard compatibility break — no legacy, no migration.** vibevm is pre-release; rather than carry compatibility shims, every legacy form was deleted: the `vibe-package.toml` filename, the `[dependencies]` section, the array-form `packages = ["…"]`, the singleton `[registry]` table, and the `vibe.lock` v1/v2/v3 readers. A manifest or lockfile using a removed form is a hard error. This is the owner's directive for the milestone, recorded here.</fact></item>
        <item><fact id="IMPL-Q2-RESOLVED" status="impl/done">**§6 question 2 — the lockfile path-member shape.** Resolved: `source_kind = "path"` with `source_url` holding the member's workspace-root-relative path. No separate `workspace_member` field — a relative path is portable and one field suffices.</fact></item>
        <item><fact id="IMPL-Q1-RESOLVED" status="impl/done">**§6 question 1 — the `[origin]` field set.** Resolved: `upstream`, `path`, `commit` (optional — present when the monorepo is a git repo), `generated_by`, `generated_at`.</fact></item>
        <item><fact id="IMPL-VAR-SCOPE" status="impl/done">**`version.var` scope.** Supported on registry-resolved dependencies only; a git-source or path-source `version` must be a concrete constraint. The placeholder mechanism serves the many registry deps that share a version — the owner's stated use case.</fact></item>
      </list>
    </section>
    <section id="impl-deferred" title="9.3 Deferred work">
      <list ordered="false" p="81">
        <item><fact id="DEF-WORKSPACE-INSTALL" status="impl/done">**Workspace-aware `vibe install` / `vibe build` — RESOLVED, no longer deferred.** This was PROP-007's one remaining piece: §2.4 / §3 described command-bubbling and unified multi-member resolution, but the concrete behaviour turned on a **per-member materialisation target** (when a dependency is resolved for member M, into which member's `spec/` does its content land?) — a genuine design fork left to an explicit owner decision (§6 question 3). [PROP-009 §2.7](PROP-009-loading-model.xml) answered it and M1.18 Phase 5 shipped it: `Workspace::discover` runs in the install plan and apply paths (`vibe-install/plan.rs`, `apply.rs`), gathering every member's `[requires]` into one resolve. Standalone single-package projects were unaffected throughout.</fact></item>
        <item><fact id="DEF-VERSION-INHERITANCE" status="spec/done">**`version = { workspace = true }`** (§2.6) — member-version inheritance. Deferred because §2.6 names no source table for the inherited version (cargo reads `[workspace.package].version`; PROP-007 defines no such table). Shipping it means extending the spec — a decision to take explicitly. `[workspace.versions]` already covers the "write the version once" use case (§6 question 4).</fact></item>
        <item><fact id="DEF-SIGNALLING-POLISH" status="spec/done">**Publish-signalling polish** (§2.8) — `--archive` (the GitHub `archived = true` lockdown and its unarchive→push→archive re-publish cycle), `has_issues = false` at repo creation, the `published_repos = "read-only" | "open"` toggle, and multi-registry fan-out. The `[origin]` marker + README banner + PR template + description already make a published copy unmistakably a generated read-only copy; these remaining layers are incremental host-API hardening.</fact></item>
      </list>
    </section>
    <section id="impl-map" title="9.4 Crate and module map">
      <list ordered="false" p="82">
        <item><fact id="MAP-CORE" status="impl/done">`vibe-core` — `manifest/document.rs` (`Manifest`, `WorkspaceSection`, `OriginSection`, `validate`); `manifest/package.rs` (`PackageMeta` + `PublishPosture`, `Requires` + `PathPackageDep` + `VarRegistryDep`, the wire forms); `manifest/lockfile.rs` (schema v4, `SourceKind::Path`).</fact></item>
        <item><fact id="MAP-WORKSPACE" status="impl/done">`vibe-workspace` — new crate. `lib.rs` (`Workspace`, `WorkspaceMember`, `discover` / `load`, the `[workspace.versions]` finalize pass); `publish.rs` (publishable-node selection, topological order, node staging).</fact></item>
        <item><fact id="MAP-REGISTRY" status="impl/done">`vibe-registry` — `multi_registry_resolver.rs` (`ResolvedPathDep`, `with_path_packages`, `resolve_path_source` / `fetch_path_source`, the priority dispatch).</fact></item>
        <item><fact id="MAP-CLI" status="impl/done">`vibe-cli` — `commands/workspace.rs` (the `vibe workspace publish` command).</fact></item>
        <item><fact id="MAP-INSTALL" status="impl/done">`vibe-install` — the `is_path_source` → `SourceKind::Path` lockfile mapping.</fact></item>
      </list>
    </section>
    <section id="impl-gates" title="9.5 Quality gates">
      <p p="83"><fact id="IMPL-GATES" status="impl/done">Every phase landed with `cargo clippy --workspace --all-targets -- -D warnings` clean and its full test suite green — 703 hermetic tests across the workspace at the close of M1.17, plus `vibe check` reporting 0/0/0. `vibe-install`'s tests pass — 18 of them — but only when the test binary is run under a name without the substring `install`: `os error 740` on the normally-named `vibe_install-&lt;hash&gt;.exe` is **Windows UAC installer detection** (a heuristic that treats an unsigned, unmanifested `*install*.exe` as a legacy installer requiring elevation), not Windows Defender, and not a code defect.</fact></p>
    </section>
  </section>
</spec>
