The shape of the repo: where the code is, where the docs are. Not the code content, not the doc content.
Three shapes:
| Shape | Code | Docs | Code owned by this repo |
|---|---|---|---|
inline(default) |
Repo root | <仓库根>/docs/ |
Yes |
shell |
Under each git submodule | <仓库根>/docs/ |
Yes |
linked |
Local directory reference, inside or outside the repo | <仓库根>/docs/ |
No |
The docs root is docs/under all three shapes, which is a fixed invariant with no configuration field. Only where the code is and who owns it vary.
shell Serves two zero-pollution scenarios: maintaining open source projects (your own docs must not go into the upstream repo) and proprietary projects pending release.
linked Serves the scenario where "the code root does not belong to this repo": multi-repo joint debugging, temporary workspaces assembled on demand, wanting only the docs repo to remember where the related code is. What means to use to organize it (worktree / clone / symlink / just a directory) is up to the user; this skill only recognizes, does not create。
Three entry points
| Entry point | What it does | Does it write files? |
|---|---|---|
inspect |
Returns the topology context for other flows to consume | Only when the declaration is missing and cannot be determined: ask once and write the declaration; do not write when a declaration already exists |
reconcile |
Interactively maintain the declaration (idempotent, reentrant) | Yes |
migrate --to shell --mode manual|auto |
Repo transformation from single repo → shell layout | Yes |
Calling with no arguments is equivalent to reconcile。
When there is no declaration: ask, do not guess
If there is a workspace: declaration, use it directly.If there is no declaration, ask the user, write the answer into AGENTS.md, and never ask again after that.
1. 问 mode:inline / shell / linked 三选一
2. 按 mode 问代码根:
inline → 不用问,代码根就是仓库根
shell → 列出 .gitmodules 的条目,多选哪些是代码根
linked → 问路径,可多个(相对 / ~/ / 绝对都行)
3. 写进 AGENTS.md 的 workspace: 块
⛔ No automatic determination. The repo shape is a product decision, not a fact that can be inferred from the file layout — especially the linked code root can be outside the repo, and no in-repo scan can discover it.
Scanning is only used to order the presets of the options; guessing wrong does not matter:
| If scanned | Preset |
|---|---|
.gitmodules If there is an entry in |
shell , order it first, and list submodules as code root candidates |
| The repo root has its own files | inline , order it first |
| Neither | Lay out the three options flat |
To determine "the repo root has its own files", use git ls-files -s and exclude entries whose mode is 160000 — those are submodule records, not files of this repo. If they are not excluded, every shell repo will be preset as inline。
⚠️ Submodules may also be vendor / assets, so under shell ,whichsubmodules count as code roots must be asked; you cannot take all entries of .gitmodules at face value.
inspect
Read the declaration and return yaml-summary-v1, with the topology context written to summary.details。When a declaration already exists, it is purely read-only; when there is no declaration, ask once as in the previous section and write the declaration, and after that it is always read-only.
schema: workspace-context.v2
mode: inline | shell | linked
workspace_root: /abs/path
docs_dir: docs
code_roots:
- name: api
path: /abs/resolved/path # 解析后的绝对路径,消费方唯一要用的
declared_path: ../pool/api # 声明原文,供报告与诊断
missing_code_roots: # 声明了但解析不到实体
- name: web
declared_path: ../pool/web
Under all three modes, the code_roots element shapeis completely identical——Caller does not branch,inline is also a code root (single item,path = repository root absolute path).
inline when , the single item takes {name: ".", path: <仓根绝对路径>, declared_path: "."} —— name and declared_path preserve . as a sentinel, so consumers can distinguish based on it without reading mode。
pathgives the resolved absolute path, consumers do zero parsing;declared_pathis only for human viewing- Code roots that cannot be resolved do not go into
code_roots, they go intomissing_code_roots. Every item the consumer gets can directlycd, no need to probe liveness first missing_code_rootsnon-empty does not block the flow, ⚠️ just report it. The real failure is left until the consumer actually needs to use it- detected
kind/branch/upstreamdoes not go into the contract, only inreconcileand the report for human viewing—consumers only needcdto get in and work
How consumers get it
⛔ Consumers are not required to call this skill themselves. Most atomic skills' allowed-tools do not have Task, so they cannot dispatch sub-Agents; letting them read the declaration file themselves would leak implementation details.
Use constraints.md §3 's minimal handshake protocol:
- Orchestration layer(
pipeline/feature/bugfix/dev-workflow) callsinspectonce at the start of the flow, passingworkspace_contextdown with the handshake'sinputspass down - Atomic skill reads from
inputs.workspace_context, no need forTask - Standalone run(user directly calls an atomic skill, no orchestration layer): no
workspace_context→ followinlinedefaults + ℹ️ prompt "shell/linkedprojects should be called via the orchestration layer"
What consumers depend on is theinput parameter shape, not the declaration file, not .gitmodules。
reconcile
interactive maintenance and declaration.Idempotent: if the state is consistent, it is a no-op and produces no empty commit.
| Action | Semantics |
|---|---|
| Reconfirm | Ask once about mode and code roots per "## When there is no declaration: ask, don't guess", overwriting the existing declaration. ⛔ Do not auto-determine |
Adjust code_roots |
Declaration-only change |
| Existing submodule added to management scope | Declaration-only change |
| Add submodule | User provides URL and path → git submodule add → append code_roots |
Exit code_roots |
Declaration-only exit, meaning "no longer a working code root". ⛔ Do not touch .gitmodules, do not touch the local directory |
| legacy field migration | See "Metadata" below, one-time |
| Resume after interruption | Fromactual git statecontinue, without relying on a record of "which step was executed" |
linked path maintenance |
Ask the user for paths (may be multiple). Adding/removing is adeclaration-only change, ⛔ do not create a worktree, do not clone, do not touch the referenced directory |
Two boundaries that must be distinguished:
- Changing the declaration ≠ changing the real topology。
modefields can be changed freely, butinline → shellreal topology changes are repository restructuring, go throughmigrate。 - Exit
code_roots≠ delete submodules. The latter would modify.gitmodules, gitlink, and local directories; it is a destructive operation, and this skill does not do it, leaving it to the user to perform.
Self-verify after writing: immediately rerun the full verification table; any ⛔ → roll back this write and report. ⛔ "command exit code 0" must not be taken to mean completion.
shell → inline: provides manual planning only, does not promise automation—with multiple code roots, there is no safe default answer for "which repo to merge into / whether to preserve shell history / how to handle multiple remotes."
migrate
migrate only occurs between inline ↔ shell , in both directions.linked does not participate in any direction— the criterion is ownership; for the full rationale see references/migration.md. When conversion between linked and other forms is needed, the user performs the physical transformation themselves, then runs reconcile to redeclare.
Key points:
- triggered only by explicit user intent, this skill never proactively proposes it—that is a product decision, not a spec gap
- The boundary between the two modes iswho moves the code repo into the root directory: in manual mode the user handles this action that crosses the workspace boundary (parent directory write permission, cwd migration), while the rest is still completed by this skill
- The order is a hard constraint: create shell → whole repo
mvin →submodule addregister → copy docs → verify the copy is in place → only then delete the original → write declaration → push and wrap up - ⛔ Do not do rebase / filter / squash / force-push; ⛔ do not push silently; ⛔ do not automatically delete the original checkout; ⛔ do not migrate the code repo's own docs (API docs, README material) along with it
Metadata
The declaration is in the project root AGENTS.md frontmatter'sindependent workspace: block:
---
workspace:
mode: shell
code_roots: [web, api] # mode=shell / mode=linked 时必填。shell 下元素为 .gitmodules 的 submodule name;linked 下须给 path,见下方 linked 示例
devdocs: # keel 项目才有;非 keel 项目只有 workspace: 块
---
linked Form and object item syntax:
---
workspace:
mode: linked
code_roots:
- name: api
path: ../pool/api # linked 下必填
upstream: ssh://... # 可选,重建线索
branch: feature-x # 可选,重建线索
---
code_roots accepts both string items and object items; the string foo is equivalent to {name: foo}。shell name is sufficient (the path source of truth is .gitmodules),Existing declarations require zero changes;linked must provide path。
Why it is a separate block: this is a repository-level fact, not a keel fact. Hanging it under devdocs: would make "declaring a repository setting" have to go through the keel process.
The path source of truth is .gitmodules(only shell),code_roots records only the submodule name, and at runtime it is resolved via git config -f .gitmodules submodule.<name>.path . Changing a submodule's path only changes .gitmodules, and the declaration does not need to follow.linked Under , the declaration itself is the source of truth.
Ownership
| Who | owns what |
|---|---|
workspace-topology |
workspace: block |
agent-memory |
body + devdocs: block; ⛔ for workspace: blockpreserve as-is, do not overwrite or delete |
legacy compatibility
Older versions put fields under devdocs.workspace_mode / devdocs.code_roots。
| Situation | Behavior |
|---|---|
Only workspace: block |
Read normally |
| Only legacy fields | Read-only compatibility;reconcile When , migrate to workspace: and delete the old field |
| Both places coexist andconsistent | Migrate and remove old fields |
| Both places coexist andconflict | ⛔ Blocked; the user decides which place is authoritative |
The field is split across two places (mode in the new,code_roots in the old) |
⛔ Blocked—concatenating the two declarations leaves "which place is authoritative" without a single answer |
| neither has it | inline, zero impact |
code_roots comparison is bysetsemantics; order is meaningless.
Validation (fail-closed)
The whole table is split by mode; authority lies in references/protocol.md §3. The three most easily hit:
| Case | Behavior |
|---|---|
| The declared code root cannot be resolved | ⚠️ Classified under missing_code_roots, does not block the process |
Bare gitlink (tracks mode 160000 but .gitmodules has no entry) |
⛔ All modes—objectively corrupted; after clone it is an empty directory and cannot be recovered |
| The resolved path escapes the authorized working scope | ⛔ All modes—a path declaration is not write-permission authorization |
Leftover files at the repo root
shell Under, the repository root is not in docs/, does not belong to any code_root of thetracked files → ℹ️ List them and hand them to the user for judgment.
⛔ Do not guess which counts as source code. Upstream repo directory conventions vary; a static allowlist would cause false positives. List them and let people look.
Commit
Whoever does the work commits: this skill commits what it produces itself (.gitmodules、workspace: blocks, file moves during migration); it does not commit on behalf of other skills, nor is it used by other skills to commit on their behalf.
Cross-repo commit protocol (shell under, Commit 1 expands to N+1 repos) see references/protocol.md, executed by the working skill per the provisions.
Detailed protocol
- Full-table validation, zero-pollution write scope, N+1 repo commit protocol, gitlink exclusion:references/protocol.md
- Migration dual mode, probing steps, detached HEAD handling, pointer drift repair:references/migration.md