Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,24 @@ That a `<user>-wip` workspace repository confers anything either: it holds no
code, elects nothing and is membership of nothing — it is one person's index
of their own unfinished work, and nothing here creates one.

**What you DO tell them instead:** that the Triad — this shape, an assembly
root with a spec leg and a code leg — is the PREFERRED project shape; that a
project choosing a single repository is reviewed identically and owes no
declaration; and that the shape still confers nothing (Brett Heap, 2026-10-06,
openxFactory `prefer-triad-project-shape`). Preferred is not required.

Starting work for a person in a repository that is not a Triad, say so ONCE in
the session, in a line or two, then carry on with what you were asked:
`adopt-project.py` converts a repository in place when a person deciding for it
runs it, and a project that stays single can say so in
`single-repository.yaml`. The doctor's `advisory` row and the `triad` lines the
scaffold and `adopt-project.py plan`/`check` print are that sentence; relay
them once. Say nothing in an assembly root, a family holder, a `<user>-wip`
repository or one carrying `single-repository.yaml`; send a leg clone to its
assembly root instead; say it everywhere else, without guessing a class. A
subagent or an unattended run says nothing. Never convert, never write the
record for them, never block, and never put the shape in a review or a gate.

## Testing your changes to this repository

`python3 -m pytest tests -q`. The suite scaffolds into bare repositories in a
Expand Down
50 changes: 45 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ both.
> travels in the grants rather than in the layout. Review lanes and
> wallet-carried authority are OVERLAYS an org adds later; a project with no
> overlays is fully conformant.
>
> **The Triad is the PREFERRED shape, and still confers nothing.** The Triad is
> this shape — an assembly root with a spec leg and a code leg; "three-leg" and
> "three-repository project" mean the same, and no machine key changed. Ruled by
> **Brett Heap on 2026-10-06**: *"ratify 1249 as recommended"*, ratifying
> openxFactory's `prefer-triad-project-shape`. It is the recommended default for
> a new project and the recommended target for an existing single repository.
> Preferred is not required: electing it stays a person's decision per project,
> converting stays `adopt-project.py` run by that person, and a project kept in
> one repository is reviewed identically and owes no declaration.

**Where the preference is said, and where it is not.** At the start of work,
once, in a line or two, and never as a stop: `scaffold-project.py`'s plan names
the Triad as what it creates, `adopt-project.py plan` and `check` say it about
the repository they read, and `shape-doctor.py` gives a single repository one
`advisory` row. None blocks, converts, writes or changes an exit status on its
account, and nothing records that it was given. It is never a review input: no
review lane, required check, validator, floor, council or merge gate reads the
shape, and a check built on the doctor's `NOT A SHAPE ROOT` is defective. It is
silent in an elected Triad's assembly root, a leg clone (which is told to work
from its root instead), a family holder, a `<user>-wip` workspace repository
and a repository carrying `single-repository.yaml`. Every other repository —
an aggregation, a configuration repository, a vendored fork — hears it, because
no class is guessed.

**`single-repository.yaml` is OPTIONAL and never owed.** A project that has
decided to stay a single repository may say so once, at its root, and stop
hearing the advisory: `schema_version: 1`, `kind: single-repository-record`,
`decided_by`, `decided_on`, `reason` and an optional `revisit_on`. Its schema
is `contracts/single-repository-record.yaml` and its template
`templates/single-repository/single-repository.yaml`; nothing here writes one
for anybody. It confers nothing and the advisory is its only reader. `kind:`
decides: one that does not parse, or declares another kind, silences nothing
and is reported beside the advisory, and nothing fails on its account.

- [Starting a project in a new organisation](#starting-a-project-in-a-new-organisation)
- [Quick start for a first-time user](#quick-start-for-a-first-time-user)
Expand Down Expand Up @@ -1090,11 +1124,14 @@ every leg, against the path policy), **agent files**, and **machine**. A
FAMILY holder gets **family**, **manifest kinds**, **shape currency**,
**placement** (`n/a` — a holder has no legs of its own), **members** and
**machine**; a directory that is neither gets what it is CALLED under the
naming policy, what IS in it, and the way in.

Every row is `ok`, `note`, `FINDING` or `n/a`, and every one that is not
`ok` names the exact command that fixes it. **Only a `FINDING` moves the
verdict**, and `FINDING` means exactly *something else asserts this* — a
naming policy, what IS in it, and the way in — and a repository the advisory
is not silent about gets the **Triad** row, status `advisory`: the preference
said once and stepped over by the verdict (a leg gets its assembly root).

Every row is `ok`, `note`, `FINDING` or `n/a` — or `advisory`, the Triad row
alone — and every one that is not `ok` names the exact command that fixes it,
except the Triad row, which names none in any status. **Only a `FINDING`
moves the verdict**, and `FINDING` means exactly *something else asserts this* — a
validator, a pin row, a manifest, a gitlink. A difference nothing asserts is
a `note`: printed, in `--json`, and stepped over by the verdict. That line
was drawn after this command answered `INVALID` about a live estate whose
Expand Down Expand Up @@ -1339,7 +1376,9 @@ only: a required check in the repository that owns the object is what confers.
```
contracts/repository-naming.yaml the six naming families, as data
contracts/path-classification.yaml which leg a path belongs in, as data
contracts/single-repository-record.yaml the staying-single record's schema
scripts/repo_shape.py shared helpers: YAML subset reader, digests
scripts/shape_advisory.py the Triad advisory: who hears it, and when not
scripts/path_classify.py the classifier over the path policy
scripts/shape_materialize.py the ONE materializer, used by both tools
scripts/validate-repository-naming.py
Expand All @@ -1358,6 +1397,7 @@ templates/family-root/ the skeleton for a FAMILY holder
templates/spec-root/ the skeleton for <Project>-spec
templates/code-root/ the skeleton for <Project>-code
templates/workspace-root/ the skeleton for <user>-wip
templates/single-repository/ single-repository.yaml, to fill in by hand
templates/*/AGENTS-shape.md the rules of the shape, for an agent (PINNED)
templates/*/.gitattributes LF in every clone, so a copy digests as pinned
AGENTS.md the procedure an AI assistant follows
Expand Down
54 changes: 54 additions & 0 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
election_date, env_commit, git_init_commit, materialize_assembly_root,
naming_block, run, write_lf,
)
import shape_advisory # noqa: E402

#: The naming policy this tool classifies leg names against. One constant,
#: because three spellings of the same path is how the second one goes stale.
Expand Down Expand Up @@ -607,6 +608,54 @@ def _checked_plan_inputs(args, source: Source,
return names, pins


def _print_triad_advisory(path: Path, commit: str | None = None) -> None:
"""The Triad advisory, once, about the repository this run reads.

RULED BY BRETT HEAP ON 2026-10-06 ("ratify 1249 as recommended",
openxFactory `prefer-triad-project-shape`): the Triad is the preferred
project shape, it stays elective and confers nothing, and this tool's
`plan` and `check` say so beside what they already print. THIS TOOL IS
THE CONVERSION, so it names the preference rather than sending anybody to
itself, and says what it does about it: nothing until `execute`, on a
person's word for this project. `scripts/shape_advisory.py` decides where
it is silent -- an elected Triad, a family holder, a `<user>-wip`
workspace, a project that recorded staying single -- and a leg clone gets
the existing instruction to work from its assembly root instead.

PRINTED, NEVER WRITTEN. Nothing goes into the plan file, the split commit
or the manifest on its account, and no exit code changes: the advisory is
a sentence said to a person and not a fact about their project.

ABOUT THE COMMIT BEING PLANNED. `commit` is the one this plan is written
against, and the record, the manifests and a leg's `AGENTS.md` are read
out of it rather than out of the working tree: a `single-repository.yaml`
that exists only on a feature branch, or only uncommitted, is not in the
repository being split and silences nothing about it.

AND IT CANNOT FAIL THE RUN. Every line is flattened to ASCII first, as
`shape-doctor.py` flattens its rows -- a record's `kind:` is a person's
text, and a console that cannot encode it would otherwise raise in the
middle of `plan`, after the plan file is already written -- and anything
left is swallowed: a run that lost the advisory has lost one optional
sentence, and one that crashed on it would have changed an exit status,
which the ratified rule forbids.
"""
try:
reading = shape_advisory.read(path, commit=commit)
if reading.state == shape_advisory.LEG_CLONE:
lines = [f"NOTE {shape_advisory.leg_instruction(reading.leg)}"]
elif reading.advise:
first, second = shape_advisory.adopt_lines()
lines = [f"triad {first}", f" {second}"]
lines += [f"NOTE {line}" for line in
shape_advisory.record_report(reading.record_problems)]
else:
return
print("\n".join(shape_advisory.printable(line) for line in lines))
except Exception: # noqa: BLE001 - see the docstring
return


def _print_plan_report(args, source: Source, tree: list, entries: list,
follow_ups: list, out: Path) -> None:
"""What `plan` says to the terminal once the file is on disk.
Expand All @@ -620,6 +669,7 @@ def _print_plan_report(args, source: Source, tree: list, entries: list,
f"{source.commit[:12]} ({source.branch}), "
f"{len(tree)} files, {source.commit_count()} commits")
print(f"project {args.project} ({args.id}) in {args.org}, mode in-place")
_print_triad_advisory(source.path, source.commit)
_print_entries(entries)
_print_summary(entries)
print(f"\nfollow-ups ({len(follow_ups)}):")
Expand Down Expand Up @@ -964,6 +1014,10 @@ def cmd_check(args) -> int:
source = plan.open_source(args.source, work_root)
naming = NamingPolicy.load(NAMING_POLICY)
findings: list[str] = []
# FIRST, because it is said where work starts; it is never one of the
# findings below and never moves this command's exit code.
_print_triad_advisory(source.path,
plan.source_commit or source.commit)

if plan.source_commit and source.commit != plan.source_commit:
findings.append(
Expand Down
115 changes: 115 additions & 0 deletions contracts/single-repository-record.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
schema_version: 1
kind: single-repository-record-schema

# ===========================================================================
# single-repository-record.yaml — the SCHEMA of `single-repository.yaml`, the
# OPTIONAL record that a project is staying a single repository.
#
# WHY IT EXISTS. Ruled by Brett Heap on 2026-10-06: "ratify 1249 as
# recommended", ratifying openxFactory's `prefer-triad-project-shape`. The
# Triad — this standard's three-repository shape, an assembly root with a
# spec leg and a code leg — is the PREFERRED project shape, and a person
# starting work in a repository that has not elected it is told so once (see
# `scripts/shape_advisory.py`). A project that has DECIDED to stay a single
# repository can say so here, once, and stop meeting that advisory. That is
# the record's whole purpose; the ruling on that change's OQ-1 put it in a
# file of its own at the repository root, with this schema, owned here.
#
# OPTIONAL, AND NEVER OWED. A project that declines the Triad and writes
# nothing owes nothing: it is reviewed identically and meets one advisory line
# per session. Nothing in this standard writes the record on anybody's
# behalf — not the advisory, not the doctor, not adopt — because staying
# single is the decision of a person deciding for that project, as electing
# the Triad would have been.
#
# IT CONFERS NOTHING. No gate, no floor, no grant, no clearance eligibility,
# no lifecycle state and no review difference, exactly as `project.yaml`
# confers nothing. A consumer deriving a permission from its presence, its
# absence or any field of it is defective.
#
# ITS ONLY READER IS THE ADVISORY. `scripts/shape_advisory.py` reads it and
# nothing else does — there is deliberately no validator script for it, and
# no review lane, required check, floor, council or merge gate may read it.
# `kind:` DECIDES, not the filename: a `single-repository.yaml` that parses
# and declares `kind: single-repository-record` silences the advisory; one
# that does not parse, or declares any other kind, silences nothing and is
# reported beside the advisory, and nothing fails on its account. The fields
# below are checked against what a record of the right kind carries, and a
# record that leaves one out is still silent — `shape-doctor.py` says which
# field, as a `note`.
#
# NEVER `project.yaml`, AND NEVER A FIELD OF IT. `project.yaml` declaring
# `kind: project-manifest` with a spec and a code leg IS how every surface
# detects a Triad — the shared agent protocol, the workstation bootstrap and
# `shape-doctor.py`, whose NOT A SHAPE ROOT keys on that file's absence — so
# a single repository carrying one would turn each of them from a presence
# test into a content test. The two files never collide: their names differ
# and their `kind`s differ.
# ===========================================================================

record_kind: single-repository-record

# Where a project puts it: ITS OWN ROOT, which is where the root read that
# looks for `project.yaml` already looks, so finding it costs no second walk.
file: single-repository.yaml
location: repository-root

# A copy to fill in by hand. Nothing in this standard renders it: replace
# every `{{PLACEHOLDER}}`, and a value still spelled that way is reported as
# a field nobody wrote.
template: templates/single-repository/single-repository.yaml

# Every field a record may carry, in the order the template writes them. A
# key not listed here is reported as not a field of the record. `type` is one
# of `integer`, `string` and `date` (an ISO `YYYY-MM-DD`); `value`, where
# present, is the one value the field may hold.
fields:
- name: schema_version
required: true
type: integer
value: 1
description: >-
The version of THIS schema the record was written against.
- name: kind
required: true
type: string
value: single-repository-record
description: >-
What decides whether the file is the record at all. Any other value
silences nothing.
- name: decided_by
required: true
type: string
description: >-
The person who decided, for this project, that it stays a single
repository — the same person an election of the Triad would have named
in `project.yaml`'s `elected_by`.
- name: decided_on
required: true
type: date
description: >-
The day that decision was taken.
- name: reason
required: true
type: string
description: >-
Why, in a sentence: a configuration repository, an aggregation, a
vendored fork, a project too small to split. Read by people; nothing
parses it.
- name: revisit_on
required: false
type: date
description: >-
OPTIONAL. A day the decision is worth looking at again. Nothing acts on
it: the advisory stays silent past it, and reconsidering is a person's
act, as the decision was.

# A complete record, filled in, for the reader who would rather copy an
# example than a template. Validated by `tests/test_shape_advisory.py`.
example:
schema_version: 1
kind: single-repository-record
decided_by: "Dana Okafor"
decided_on: 2026-10-06
reason: "A dotfiles repository: one person's configuration, with no specification to split from code."
revisit_on: 2027-04-01
Loading
Loading