As implemented in v0.1. Every field listed here is both written by something and read by something. A field that loses one of those is removed, not kept "for later".
Conventions:
- YAML for anything a person edits. JSON Lines for anything only Majordomus appends to.
- Unknown keys are errors at every level.
- Identity fields (
repository_id,branch,head,working_tree,changed_files) are always computed from git at write time. A worker or a person supplying them is an error, not an override. - Timestamps are UTC ISO 8601 with seconds:
2026-09-03T19:30:12Z.
.ai/manifest.yaml
The section registry of the repository's AI layer. Discovery reads this file and never
walks .ai/; a file under repo/ that no section covers is not context and carries no
authority. init writes it from the skeleton; the format version is what an executable
checks before reading anything else, and one it does not read is refused with the reason.
schema: ai-repository/v1
repo:
path: repo # tracked canonical context
local:
path: local # this checkout's own; ignored by git
tracked: false
implicit_context: false
sections: # relative to .ai/; each one resolves to a path the tool reads
policy: repo/policy.yaml
profiles: repo/profiles
rules: repo/rules
prompts: repo/prompts
skills: repo/skills
workflows: repo/workflows
knowledge: repo/knowledge
adrs: repo/adrs
project: repo/project
Every key is required except that project may resolve to a directory that does not
exist, which is a repository with no plan. Unknown keys are errors (share/allow/manifest.txt).
doctor fails when a section the manifest names is absent, when local/ is not ignored
or carries a tracked file, or when pre-.ai project data still sits under .majordomus/.
.ai/repo/policy.yaml
The one canonical, provider-neutral policy.
version: 1 # schema version; only 1 is valid in v0.1
context:
always_loaded_budget_lines: 150 # hard cap on the always-loaded projection
builder_budget_lines: 300 # hard cap on what `majordomus context` prints
recent_decisions: 5 # decisions offered to a worker, newest first
max_list_items: 20 # cap on any list inside the assembled context
strategy: minimum-sufficient # documentation of intent; projected into instructions
transcript_is_state: false # projected as a rule; never true in v0.1
profiles:
default: implementation # must name a file in profiles/
checkpoint_interval_default: 15m # profiles may override
verification:
required_for_changes: true # finish requires --verify-command when files changed
finish_requires: # the contract; order is display order
- scope_respected
- verification_ran
- state_updated
- no_open_blockers
- note_present
checkpoint:
max_body_lines: 40 # a body over this is refused, not truncated
retention_max_files: 500 # doctor reports when exceeded
handover:
required_sections: [Objective, Current State, Next Action]
retention_max_files: 200 # doctor reports when exceeded
ledger:
retention_max_lines: 5000
benchmark:
samples: 10 # warm samples per target in `majordomus bench`
warmup: 2 # unsampled runs before them
regression: # fractions over the baseline that `bench --check` refuses
p50: 0.5
p95: 0.5
p99: 0.6
budget: # doctor and watch report their own wall time against these (WARN, never the exit code)
doctor_ms: 3000
watch_ms: 3000
enforcement: # what doctor reconciles; each must be wired
- name: doctor-on-commit
path: majordomus # on PATH, repo-relative, absolute, or named in the hook line
args: [doctor]
wired_by: git-hook:pre-commit
- name: finish-on-push
path: majordomus
args: [finish, --check]
wired_by: git-hook:pre-push
projections:
- provider: claude-code
target: CLAUDE.md
mode: region # file (default) | region
always_loaded: true # exactly one projection may be always_loaded
- provider: codex
target: AGENTS.md
- provider: gemini
target: GEMINI.md
- provider: generic
target: docs/AI_INSTRUCTIONS.md
mode: file generates the whole target and owns every byte of it. mode: region
generates only the text between two markers and owns nothing else in the file:
Whatever the repository already wrote. Majordomus never reads or rewrites this.
<!-- majordomus:begin 7b88abe0f22a -->
... generated from the policy ...
<!-- majordomus:end -->
The hash in the begin marker names the policy the region came from; it is not part of
the hashed content, so re-running update after an unrelated policy edit is not a hand
edit. update replaces an existing region in place and appends one to a file that has
none. Markers that are unclosed, out of order, or repeated are refused (15), never
guessed at. fingerprints.yaml records mode alongside the hash, and for a region the
hash covers the region — an edit outside the markers is the repository's business and is
never reported as drift.
wired_by values in v0.1: git-hook:<name> (resolved through core.hooksPath or
.git/hooks/), ci:<path> (a file that must exist and contain the invocation),
manual (documented, not verified; doctor lists it as unverified, never as wired).
The hook line must not swallow the exit code (|| true, || exit 0).
.ai/repo/scope.yaml
The repository scope: what a worker reads and what it never reads. Named by the
manifest's scope section; a manifest naming none means the distribution's default
applies (share/skeleton/ai/repo/scope.yaml). The schema is
share/schemas/majordomus/scope/scope.v1.schema.json; the shell tool's allow-list is generated from it.
SCOPE.md explains the judgement.
version: 1
in: # pathspecs anchored at the root; a trailing / is the directory and everything beneath
- .ai/repo/**
- src/**
out: # wins over in; every key optional
paths: ['.git/', '.ai/local/', '**/target/']
binary: true # a NUL byte in the first 8 KiB
max_bytes: 1048576
archive: { names: [...] } # written as a block mapping; the subset has no flow maps
image: { names: [...] }
video: { names: [...] }
pdf: { names: [...] }
database_dump: { names: [...] }
generated: { paths: [...], names: [...] }
secret: { paths: [...], names: [...] }
fixtures: { paths: [...], names: [...], max_bytes: 65536 }
names are matched against the file name alone and may not carry a slash; a pattern
with a leading slash, a :( prefix or a .. segment is refused with the key named.
.ai/repo/profiles/<name>.yaml
Five independent axes. Nothing here names a vendor model.
name: debugging
description: reproduce, isolate, fix, and prove a defect fixed
capability: strong # fast | standard | strong | strongest — projections map this
effort: high # low | medium | high | xhigh | max; omit to inherit the default
effort_escalation: # optional; projected as guidance, not enforced in v0.1
after_blocked_attempts: 2
to: xhigh
verbosity: concise # terse | concise | detailed
presentation: engineering # machine | engineering | summary
context: # toggles; projections turn them into loading guidance
task: true
current_state: true
decisions: true
relevant_files: true
failing_output: true
recent_history_depth: 50 # commits; 0 disables
architecture_notes: false
verification:
verify_command_required: true
regression_test_required: true
decision_record_required: false
checkpoint_interval: 15m
output_contract: # fields a completion note must carry for this profile
- outcome
- changes
- verification
- blockers
- next_action
The four shipped profiles:
routine | implementation | debugging | deep-work | |
|---|---|---|---|---|
| capability | fast | standard | strong | strongest |
| effort | low | medium | high | high, escalates to xhigh |
| verbosity | terse | concise | concise | detailed |
| presentation | machine | engineering | engineering | engineering |
| context | task, state | + decisions, files | + failing output, history 50 | + architecture, history 200 |
| verify command | if files changed | required | required | required |
| regression test | no | no | required | no |
| decision record | no | no | no | required |
| checkpoint | 30m | 15m | 15m | 30m |
.ai/repo/rules/**/*.md — rule objects
A rule is a Markdown file with YAML front matter. The baseline the tool ships lives in
share/standard/majordomus/ and is vendored into the repository under
.ai/repo/rules/vendor/majordomus/ (a manifest.yaml naming every file with its hash,
and rules/*.md); the repository's own rules live under .ai/repo/rules/project/. See
DOCTRINE.md for the model and .ai/repo/rules/README.md for the format
as a repository reads it.
---
id: majordomus.scope-integrity # identity, with version; never the file name
version: 1
kind: rule
title: Scope integrity
description: A task touches only the paths it claimed; work found elsewhere is not accepted as done.
statement: Work outside the paths a task claimed is not accepted as that task's work.
status: active # active | deprecated
class: blocking # blocking | advisory — nothing else parses
depends_on: [majordomus.one-worker-one-scope@1] # exact id@version references, or []
tags: [scope, verification]
x-majordomus: # present only on a rule the tool enforces
validator: scope # runs mj_validate_scope; a name with no function is a failure
category: scope # the finding category a violation is reported under
enforced_by: [check, finish, watch]
policy_key: scope_respected # finish doctrines only: the name used in verification.finish_requires
exit_code: 10 # from the existing contract; a rule invents no code
claims: [scope-enforcement, scoped-task] # ids in docs/CLAIMS.yaml
tests: [test/cases/04_start_check.sh]
---
# Rationale
...
# Required behaviour
...
# Failure behaviour
...
# Verification
...| field | required | meaning |
|---|---|---|
id, version | yes | identity; a project rule may not use the majordomus. namespace. The URL slug on the site is the id without its namespace |
kind | yes | rule |
title, description, statement | yes | one line each, rendered verbatim |
status | yes | active or deprecated; a deprecated rule is not in the effective set and may not be depended on |
class | yes | blocking stops the command; advisory reports and lets it pass. Any other value fails the whole set closed |
depends_on | yes | exact id@version references; a missing one or a cycle fails the set |
tags | no | free labels; principle marks the ten principles |
x-majordomus.validator | with the block | the suffix of mj_validate_<validator>; the function must exist in lib/ |
x-majordomus.category | with the block | the finding category, so output stays one vocabulary |
x-majordomus.enforced_by | with the block | commands that dispatch it; each must exist and call mj_doctrine_dispatch |
x-majordomus.exit_code | with the block | 0 for advisory, otherwise a code from the exit-code contract |
x-majordomus.policy_key | no | present only on doctrines a repository can select in verification.finish_requires |
x-majordomus.claims | no | claim ids this doctrine backs; each must exist in docs/CLAIMS.yaml |
x-majordomus.tests | with the block | the cases that prove the behaviour; each file must exist |
The allowed keys are share/allow/rule.txt; any other key is an error. majordomus doctor
verifies every one of those constraints against the source, that the vendored package
matches its manifest, and additionally that no mj_validate_* function exists which no
rule declares.
.ai/**/README.md — context documents
A context document is a Markdown file under the .ai/ tree (minus local/ and
rules/vendor/) whose front matter declares the contract below. The manifest's
context.documents names the file names that must carry it wherever they appear in the
tree; today that is README.md. See CONTEXT.md for the model.
---
schema: context/v1
id: ai.repo.rules # identity; ^[a-z][a-z0-9-]*(\.[a-z0-9-]+)*$ ; unique across the tree; survives a move
kind: context
title: Repository rules
description: One sentence.
status: active # active | deprecated (discovered and listed, never applied)
scope: subtree # directory | subtree | explicit
paths: [] # explicit only: repo-relative directories inside the tree
providers: ["*"] # "*" or provider names from the policy's projections
audience: [human, agent] # who it addresses; resolve --audience filters
composition: extend # extend | replace | final
order: 100 # integer; ties within one depth are broken by path
supersedes: [] # replace only: ids of ancestor-chain documents, none of them final
tracks: [lib/rules.sh] # git pathspecs this document describes
children: # subtree only: what the directories below owe
require_contract: true # true | false; the default where nothing declares it is true
exempt: [] # subtrees this layer carries but does not author
---| field | required | meaning |
|---|---|---|
schema | yes | context/v1; a newer value is refused, never guessed at |
id | yes | identity, unique across the tree; the file name is a convention |
kind | yes | context; another kind is another kind of file |
title, description | yes | one line each |
status | yes | active or deprecated |
scope | yes | directory, subtree or explicit |
paths | with explicit | repository-relative directories inside the tree the document applies to |
providers | yes | "*" or names of projections in the policy; an unknown name fails validation |
audience | no | human, agent; a filter, never a permission |
composition | yes | extend, replace or final |
order | yes | an integer; less is earlier within one depth |
supersedes | with replace | ids in the ancestor chain this document stands in for; a final ancestor cannot be named |
tracks | no | pathspecs whose change names this document for review |
children.require_contract | no | subtree only: whether every directory below this one owes a context document. Absent everywhere above a directory means true. A descendant may raise false to true; lowering an inherited true is illegal-override |
children.exempt | no | subtree only: repository-relative directories inside this document's own scope that owe no context document, and nor does anything below them — a subtree the layer carries but does not author, such as an installed package whose integrity is its own manifest's business. Naming a directory outside the document's scope, or its own directory, is illegal-override; a directory that does not exist is broken-reference |
The allowed keys are share/allow/context.txt; any other key is an error. There are no
defaults: a required key that is missing is invalid-front-matter, not a silent value.
majordomus context validate checks every constraint over the whole tree, and
majordomus doctor dispatches the same check through majordomus.context-integrity.
.ai/repo/sessions/<stamp>--<id>--<branch>--<head>--<digest>.md
One closed execution episode, written by majordomus session close and by nothing else. A
shared object of the layer: discovered by the source class session, projected to the
index, majordomus://session/<id>, the object routes, the graph and the site, with no list
of sessions kept anywhere (ADR 0014).
---
schema: session/v1 # the contract; a version this executable does not read is refused
kind: session
session_id: s-20260906035523-7b6c # identity, allocated at open, never reused
created_at: 2026-09-06T03:55:24Z # when the record was written: the close
started_at: 2026-09-06T03:55:23Z
closed_at: 2026-09-06T03:55:24Z
outcome: closed # closed | interrupted
title: "Session s-… on feature/x" # one line for a listing; the task's title when it had one
task_id: none # the task the episode belonged to, or none
profile: none
worker: "claude-code/opus-5" # what did the work, as it identified itself
repository_id: git@github.com:… # the remote; `local:<hash>` when there is none
worktree_id: e2d0ee3a34301bd5 # the working copy, named without naming a path
branch: feature/session-records
start_head: 4658856… # the commit the episode started from
head: 4658856… # the commit it ended at
start_working_tree: dirty # clean | dirty, at open
working_tree: dirty # and at close
commits: [] # between the two heads, oldest first
changed_files: [] # repository-relative, as git reported them
tasks: [] # every list below is read from the ledger's events
issues: [] # for this episode: nothing here is authored
milestones: []
checkpoints: []
handovers: []
decisions: []
questions: []
evidence: []
---
The body: a summary of the work, given on standard input at close.| Key | Required | Meaning |
|---|---|---|
schema | yes | session/v1 |
kind | yes | session |
session_id | yes | identity; the file name is a convenience |
started_at, closed_at | yes | RFC 3339, UTC |
outcome | yes | closed or interrupted |
title, task_id, profile, worker | no | what the episode was and what worked it |
repository_id, worktree_id | no | where it ran, without naming a path |
branch, start_head, head | no | from git |
start_working_tree, working_tree | no | clean or dirty |
commits, changed_files | no | from git |
tasks, issues, milestones, checkpoints, handovers, decisions, questions, evidence | no | from the ledger's events for this episode |
Unknown keys are errors, and no value may be an absolute path: a shared record carries what
the repository can prove, never a fact about the machine that ran it. There is no field for
a conversation, which is project.never-store-transcripts enforced by the contract rather
than by a habit. The open episode's own state file is a different object with its own schema
(session), and it stays in the checkout-local half.
.ai/repo/rules/vendor/majordomus/manifest.yaml
The package manifest: every rule file the vendored baseline holds, its identity and the
hash of the file. Written by init and by majordomus rules vendor update, never by
hand; doctor fails when a listed file is absent, differs from its hash, declares another
identity, or when a file under rules/ is not listed.
vendor: majordomus
package: majordomus-standard-rules
version: 1
format: ai-rules/v1
source_revision: 0.1.0 # the executable version the package was taken from
rules:
- id: majordomus.scope-integrity
version: 1
file: rules/scope-integrity.v1.md
sha256: 6b737b27...
The same manifest describes the package the distribution ships in
share/standard/majordomus/; rules vendor status compares the two and reports a newer
distribution package without applying it.
.ai/repo/project/project.yaml
The canonical project model's root. Present only in a repository that plans through
milestones and issues; the model is opt-in and doctor skips it where it is absent.
schema_version: 1
name: Prismatic Majordomus
repository: korczis/prismatic-majordomus # owner/name; the projection target
default_branch: master
Nothing derived is stored here. There is no active_milestone field: the active milestone
is the lowest-ordered one that is ACTIVE, or failing that the lowest-ordered one that is
not finished, computed on every read.
.ai/repo/project/milestones/<ID>.yaml
One executable specification of an outcome. The filename is the id; a record whose id
disagrees with its filename is refused rather than reconciled.
id: M000
title: Milestone and DAG driven development
slug: milestone-dag-driven-development
order: 0 # integer; decides which milestone is active
priority: p0 # p0 | p1 | p2 | p3
problem: "What is wrong today." # single-line scalars; the YAML subset has no folding
outcome: "What is true when it ends."
current_state: "Where it stands."
desired_state: "Where it is going."
scope: [...] # what this outcome covers
non_scope: [...] # what it deliberately does not
acceptance_criteria: [...] # what would make the outcome true
validation: [...] # commands that demonstrate it
evidence_required: [...] # tokens gating milestone acceptance
risks: [...]
cancelled: false # optional
evidence: # appended; each entry covers one required token
- covers: suite
type: test # test | build | ci | artifact | manual
command: "bash test/run.sh"
result: "every case passed"
artifact: "…" # optional: a path, URL or hash
commit: 60f83e3… # written by the tool
recorded_at: 2026-09-04T03:14:00Z # written by the tool
created_at: 2026-09-04
updated_at: 2026-09-04
There is no issues: list. An issue names its milestone and the relation is read in that
one direction, so the two records cannot disagree about which issues belong to the outcome.
There is no status: field; see below.
.ai/repo/project/issues/<ID>.yaml
One bounded execution contract, carrying enough for a worker with no conversation history.
id: I0007
milestone: M000
title: Enforce the canonical model as doctrine
slug: model-doctrine
priority: p1
profile: implementation # which execution profile suits this work
parallel_safe: true # false forces serialisation regardless of the graph
objective: "What this issue produces."
why: "Why it exists."
current_state: "…"
desired_state: "…"
scope: [...] # the paths this issue may touch
non_scope: [...]
depends_on: [I0005] # issue ids; the edges of the DAG
acceptance_criteria: [...] # required; an issue without one is a placeholder
validation: [...] # required; the commands that demonstrate it
evidence_required: [...] # tokens that gate completion
risk: "…"
owner: alice # optional
completion: "…" # optional one-line completion report
# lifecycle markers — written only by `majordomus plan`, never by hand
started_at: 2026-09-04T03:00:00Z
verified_at: 2026-09-04T03:20:00Z
completed_at: 2026-09-04T03:31:00Z
cancelled: false
evidence: # same shape as a milestone's
- covers: doctrine_test
type: test
command: "bash test/run.sh 44_model_doctrine"
result: "1 passed, 0 failed"
commit: aa90a8b…
recorded_at: 2026-09-04T03:30:11Z
There is no status field on either record. BLOCKED, READY, ACTIVE, VERIFY,
DONE and CANCELLED are derived by lib/project.awk from the lifecycle markers, the
evidence coverage, and the state of the issue's dependencies; a milestone's status is
derived from its issues and its own evidence. Writing status: into either file is an
unknown key and fails majordomus plan validate and majordomus doctor.
Unknown keys are checked against share/allow/project.txt, share/allow/milestone.txt and
share/allow/issue.txt, the same mechanism the policy and the profiles use.
.ai/local/state/current.yaml
The one active task. Absent when nothing is active.
id: t-20260903-193012-a4f1 # t-<utc compact>-<4 hex>
task: fix OAuth callback
profile: debugging
owner: alice # free-form string
scope: # normalised at start; no trailing slashes
- lib/auth
started_at: 2026-09-03T19:30:12Z
checkpoint_at: 2026-09-03T20:02:41Z # updated by check --checkpoint and by finish
outcome: active # active | completed | partial | blocked | no_match | failed | handed_over
# computed from git at start; refreshed at finish; never authored
repository_id: /abs/path/.git # git rev-parse --git-common-dir
worktree: /abs/path # the checkout this task belongs to
branch: main
head: 3f2a9c1e... # full SHA
working_tree: dirty # clean | dirty
.ai/local/state/ is never tracked, so this record stays with the checkout that wrote it. A copy that reaches another
worktree on the same branch reads it and must not be held to a scope it never claimed, so
worktree says which checkout the task belongs to. check, finish --check and watch
report a record from another checkout and enforce nothing from it; finish refuses to write
to it at all. A record written before this field existed has no opinion and is treated as
local, so upgrading does not turn an existing installation red.
outcome and checkpoint_at are the only fields a command changes after start.
active refuses a new start; every other outcome lets start archive the record to
state/archive/<id>.yaml and begin a new task.
.ai/local/state/decisions.md
Append-only, dated, one entry per decision. Human-authored. Read by decision list,
by context (which prints as many entries as context.recent_decisions allows, for this
task or for the repository depending on the profile), by search, and by a prompt asset
that uses {{DECISIONS}}. check reads it only to report entries that no task or head can
be attributed to. Projections reference it by path and never inline it.
Append-only. decision add writes one entry with Task and Head computed; the
remaining fields come from its options. --why is required, because a decision with no
recorded reason cannot be reviewed later, only re-argued. Absent optional fields are
written as - rather than omitted, so every entry has the same shape.
An entry is never edited or deleted. --supersedes records that a later decision replaced
an earlier one, and refuses text that matches no recorded decision.
## 2026-09-03 — token refresh uses the existing session store
Task: t-20260903193012-a4f1
Head: 9b1e2d4f8c3a5e7b1d0f2a4c6e8b0d3f5a7c9e1b
Why: one source of truth for expiry; a second cache would mean two expiry clocks
Rejected: a separate refresh cache
Evidence: lib/auth/session_store.rb#expiry
Supersedes: -
check, doctor and watch report an entry missing Task, Head or Why as a
warning: the file is hand-editable by design, and an entry nothing can attribute is a
decision that no gate will ever find. Text inside an HTML comment is the file's own
template and is not an entry.
.ai/local/state/open-questions.md
Things blocked on a human. finish --outcome completed refuses while any entry for the
current task is unresolved.
Written by question add and rewritten in place by question resolve. Resolving edits the
line because an index of what is still open must not accumulate; the append-only record of
every opening and resolution, with its answer, is the ledger.
- [unresolved] t-20260903193012-a4f1 — token refresh window: 15 or 60 minutes? (2026-09-03)
- [resolved 2026-09-02] t-20260902120000-b3d1 — keep the legacy callback path — yes, until Q4
Exactly two line shapes are valid:
- [unresolved] <task id> — <question> (<YYYY-MM-DD>)
- [resolved <YYYY-MM-DD>] <task id> — <question> — <answer>
— separates the fields, so a question may not contain it. Any other - [ line is a
failure in check, doctor and watch, not a warning: this file is a gate on
acceptance, and a gate that cannot read an entry is a gate that can be bypassed by
mistyping one.
.ai/local/state/handovers/<file>.md
Filename: <utc-compact>--<branch-key>--<short-head>--<16 hex>.md, e.g.
20260903T201455Z--main--9b1e2d4--c0ffee1234567890.md. branch-key is the branch
with any character outside [A-Za-z0-9._-] replaced by -, or DETACHED.
---
schema_version: 1
created_at: 2026-09-03T20:14:55Z
task_id: t-20260903-193012-a4f1
profile: debugging
owner: "alice"
repository_id: /abs/path/.git
worktree: /abs/path
branch: main
head: 9b1e2d4f...
working_tree: clean
changed_files:
- lib/auth/oauth.rb
- test/auth/oauth_test.rb
---
# Objective
Fix the OAuth callback dropping state on redirect.
# Current State
Root cause found: state param stripped by the proxy rewrite. Fix in oauth.rb applied,
regression test added, suite green.
# Next Action
Decide the refresh window (open question) and then finish.
# Decisions
See decisions.md 2026-09-03.
# Verification
make test — exit 0, 41s, at head 9b1e2d4.
Front matter is written by Majordomus. A body containing a line that looks like a
front-matter key is rejected. Mode 0600. Created atomically. Never staged.
.ai/local/state/checkpoints/<file>.md
Filename and front matter are exactly a handover's; only the directory and the body rules
differ. Written by checkpoint, mode 0600, created atomically, never staged.
---
schema_version: 1
created_at: 2026-09-03T19:45:00Z
task_id: t-20260903193012-a4f1
profile: debugging
owner: "alice"
repository_id: /abs/path/.git
worktree: /abs/path
branch: main
head: 9b1e2d4f8c3a5e7b1d0f2a4c6e8b0d3f5a7c9e1b
working_tree: dirty
changed_files:
- lib/auth/oauth.rb
---
OAuth state mismatch reproduced with test/fixtures/callback.json.
The cause is in callback normalisation, not in the comparison.
Next: regression test before touching the implementation.
The body is free text, not sections, and is capped at checkpoint.max_body_lines lines. A
body over the cap is refused, not truncated, with the suggestion to write a handover
instead: the cap is what keeps a checkpoint short enough to quote whole into the next
briefing rather than becoming a second kind of report.
A body containing any identity field is refused, as for a handover. An empty body is
allowed and writes no file — it updates checkpoint_at only, which is what
check --checkpoint does.
Read by checkpoint --show and --list, and by context, all through the same resolver
as handovers: same worktree and branch, else same branch, else nothing.
.ai/local/state/session-current.yaml
The open session in this worktree. One at a time; session start refuses while one is
open rather than replacing it. Removed by session close, which is the only writer that
removes it.
session_id: s-20260904153733-fc51
started_at: 2026-09-04T15:37:33Z
owner: "alice"
worker: "some-provider/some-model" # optional; recorded only when supplied
# computed from git; never authored
repository_id: /abs/path/.git
worktree: /abs/path
branch: master
start_head: 9b1e2d4f8c3a5e7b1d0f2a4c6e8b0d3f5a7c9e1b
start_working_tree: dirty
session_id is s- followed by the compact UTC timestamp with its T and Z removed
and four hex characters, which is the shape a task id already has. worker is a free-form
string and is the only field a person or a worker supplies beyond owner; nothing
validates it, and nothing infers it when it is absent — an unrecorded worker stays
unrecorded rather than becoming a plausible guess.
Unknown keys are an error, as in every other Majordomus YAML file.
This file is not tracked. The other state files are, because something outside the checkout reads them: a task record carries the scope claim other worktrees compare against, the question store is scoped to a branch by version control, and the append-only records have to travel. An open session carries none of that, so committing one would only make every checkout on the branch inherit an episode it did not open. Closed sessions are durable records and are tracked.
The defence stays in place regardless: a session record from another worktree is reported and is not treated as this checkout's open session, exactly as a foreign task record is.
.ai/local/session-contexts/<stamp>--<session-id>.md
The bounded working context of one episode, written by session start and appended to by
session close. Its contract is majordomus.session-context/v1
(share/schemas/majordomus/session-context/); the kind is declared in share/kinds.yaml
like every other, and no source class discovers it.
---
schema: session-context/v1
kind: session-context
session_id: s-20260906035523-7b6c
opened_at: 2026-09-06T03:55:23Z
opened_by: hook # hook | hand
provider: claude-code # only when a provider's event opened it
provider_session: "abc-123" # the provider's own identity, as it sent it
branch: master
head: 9b1e2d4f8c3a5e7b1d0f2a4c6e8b0d3f5a7c9e1b
task_id: none
profile: none
worker: "some-provider/some-model" # optional; recorded only when supplied
---
# Working context of session s-20260906035523-7b6c
## Context at open
<the context builder's output, verbatim>
## Notes
<the worker's own account of the work>
## Close # appended by `session close`
- closed_at: 2026-09-06T05:12:04Z
- outcome: closed
- head: 9b1e2d4f…
- record: .ai/repo/sessions/20260906T051204Z--s-…--master--9b1e2d4--c0ffee1234567890.md
opened_by is hook exactly when the open named the provider that delivered the event,
which only something running inside that provider's hook can do; that is what makes it a
fact rather than a claim. provider_session is the string that ties the episode to the
prompt archive, whose records carry the same one.
The document is appended to, never rewritten: the front matter describes the open, and the close adds a section, so whatever a worker typed between the two events survives. It is not tracked, and unlike the other local state it is not tracked for a second reason as well: it is a snapshot of a projection, so re-resolving it later produces a different document and no surface can reproduce it (ADR 0015).
It is never a transcript. The derived half is the builder's output and the authored half
summarises the work; a front-matter key naming a message list, a completion or a model's
reply is refused by the majordomus.session-lifecycle doctrine, which is how
project.never-store-transcripts is kept mechanically here rather than by memory.
.ai/repo/sessions/<file>.md
The immutable record of a closed session. Filename:
<utc-compact>--<session-id>--<branch-key>--<short-head>--<16 hex>.md, e.g.
20260904T171402Z--s-20260904153733-fc51--master--3c9ba2f--c0ffee1234567890.md.
The grammar is the handover's with the session id inserted after the timestamp, and it
carries the same four properties. The leading UTC timestamp makes lexicographic order
chronological order. branch-key is branch-safe by construction. The short head says
which history the record was written against. The sixteen random hex characters plus an
atomic hard-link publish make it collision-safe: a name already in use is retried, never
overwritten. Nothing reads filesystem modification time, which does not survive a clone
and is not the time the record asserts.
---
schema_version: 1
created_at: 2026-09-04T17:14:02Z
task_id: t-20260904153733-fc51
profile: deep-work
owner: "alice"
repository_id: /abs/path/.git
worktree: /abs/path
branch: master
head: 3c9ba2f1d0e5a7b9c3f5e7a9b1d3f5e7a9b1d3f5
working_tree: dirty
changed_files:
- lib/session.sh
session_id: s-20260904153733-fc51
started_at: 2026-09-04T15:37:33Z
closed_at: 2026-09-04T17:14:02Z
outcome: closed
worker: "some-provider/some-model"
start_head: 9b1e2d4f8c3a5e7b1d0f2a4c6e8b0d3f5a7c9e1b
start_working_tree: dirty
commits:
- 2c4dc6f
- 3c9ba2f
tasks:
- t-20260904153733-fc51
issues:
- I0801
milestones:
- M003
checkpoints:
- .ai/local/state/checkpoints/20260904T161122Z--master--2c4dc6f--a1b2c3d4e5f60718.md
handovers: []
decisions:
- "2026-09-04 — The session envelope is derived from the ledger, not accumulated"
questions: []
evidence:
- I0801:boundary_rewritten
---
Optional. Free text, written by whoever closed the session, or absent.
created_at, head and working_tree describe the close, so the record reads back
through the same resolver and the same divergence label as a handover: head is the
commit the session ended at, and the label compares it with the current one. start_head
and start_working_tree describe the open.
commits is the list of commits between start_head and head, shortest form. When the
start commit is not an ancestor of the end commit — a rebase during the episode — the
value is the single entry diverged rather than a list computed across a history that no
longer connects.
The reference lists carry the identity each kind actually has, and nothing invents one:
| Field | Identity used | Why that one |
|---|---|---|
tasks | task id | It exists and is stable. |
issues, milestones | canonical id | Same. |
checkpoints, handovers | repository-relative path | The files are immutable, so the path is the identity. |
decisions | the dated title | decisions.md has no id field; the dated title is the heading it already uses, and it is what the ledger records. |
questions | the question text | open-questions.md has no id field either; the text is what its line format keys on. |
evidence | <issue>:<token> | The pair an evidence record is attached to. |
Two of those are weaker identities than the rest, and that is recorded rather than papered over: a decision or a question is referenced by text, so editing that text breaks the reference, and session validation reports it as a dangling reference rather than silently resolving to nothing.
The lists are derived, not accumulated. They are computed at close time by reading
ledger.jsonl, and by nothing else. No other command writes to the session record;
checkpoint, decision, question and plan are unchanged and know nothing about
sessions.
The alternative — every command appending its reference to the open session file as it runs — was rejected twice over: it puts a write on the hot path of commands that currently only append one ledger line, and it creates a second mutable store of facts the ledger already holds, which is the thing this record exists to avoid being.
Selection is by the session stamp on each line, not by a time range. Every ledger line
carries a session field naming the episode that wrote it, alongside the head and
branch it already carried; all three are computed, none is authored. A time range was
tried first and was wrong: the ledger is one file per repository, and the first real run
of it collected another worker's tasks, checkpoints and handovers, because nothing in a
timestamp tells two concurrent workers apart.
A line carrying no session belongs to no episode, and no envelope claims it. That is the
correct answer rather than a gap — sessions are optional, and work done outside one is
attributed to nobody instead of to whoever happened to have a session open nearby.
The cost is that the ledger becomes load-bearing for a second purpose. It is append-only,
written only by Majordomus, and already validated by majordomus.ledger-integrity, which is what
makes it a safe thing to derive from. Ledger line order is preserved, so two events
written inside the same second need no tiebreak at all.
Front matter is written by Majordomus. A body containing a line that looks like a
front-matter key is rejected, as for a handover. Mode 0600. Created atomically. Never
overwritten, never edited after the fact: a closed session is superseded by later
information, not corrected.
.ai/repo/prompts/<name>.md
A reusable framing, versioned with the repository. Front matter is authored, not computed — these are not records of work.
---
name: debug
description: frame a defect so that the fix is proven, not asserted
profile: debugging
---
Task {{TASK_ID}} on branch {{BRANCH}} at {{HEAD}}: {{TASK}}
Open questions that block acceptance:
{{OPEN_QUESTIONS}}| Key | Required | Meaning |
|---|---|---|
name | yes | must equal the filename without .md |
description | yes | non-empty; shown by prompt list |
profile | no | the profile this framing suits; documentation only, nothing selects on it |
Unknown keys are errors, as everywhere else.
Tokens are a closed set. Inline, substituted anywhere in a line: {{TASK}},
{{TASK_ID}}, {{PROFILE}}, {{SCOPE}}, {{OWNER}}, {{BRANCH}}, {{HEAD}},
{{WORKING_TREE}}, {{REPOSITORY}}, {{NOW}}. Block, valid only alone on a line:
{{OPEN_QUESTIONS}}, {{DECISIONS}}, {{CHECKPOINT}}, {{HANDOVER}}, {{CONTEXT}}.
Any other {{...}}, or a block token used inline, is an error. There is no templating
language: no conditionals, no loops, no includes, no shell. A prompt that silently renders
{{TSAK}} as literal text is worse than one that refuses. doctor and watch validate
every asset for the same reason.
An asset whose body contains {{CONTEXT}} is excluded from context --prompt rather than
rendered, and the exclusion is named: the result would be the same context nested inside
itself, which the budget then pays for twice.
.ai/repo/skills/<id>/SKILL.md
A skill: a provider-neutral procedure for one bounded kind of work. Front matter is
authored, validated against share/schemas/majordomus/skill/skill.v1.schema.json (the allow-list
share/allow/skill.txt is generated from it); the body is the procedure. The skill's
identity is the id, which must equal the directory name, and its MCP URI is
majordomus://skill/<id>. Discovery is the source class skill in
.ai/repo/knowledge/sources.yaml; nothing else registers a skill.
---
schema: skill/v1
id: repo-review
version: 1
title: Repository review
description: An evidence-driven review of the actual state of the repository before work is recommended or done.
status: active
tags: [review, evidence]
related: []
inputs:
- a checkout of the repository with git history available
outputs:
- findings, each with severity, evidence, location and the smallest correct fix
---
# Purpose
...
# Procedure
...
# Output
...| Key | Required | Meaning |
|---|---|---|
schema | yes | skill/v1 |
id | yes | ^[a-z][a-z0-9-]*$; must equal the directory name |
version | yes | an integer of at least 1 |
title | yes | non-empty |
description | yes | one sentence; shown by every listing |
status | yes | draft, active or deprecated |
tags | no | ids, same pattern as id |
related | no | ids of other skills; every one must exist |
inputs, outputs | no | one string per item; what the worker needs and what the procedure leaves |
Unknown keys are errors. The body carries non-empty level-one sections # Purpose,
# Procedure and # Output; majordomus skills check refuses a skill without them.
examples/*.md beside the file are optional documents, each opening with a level-one
heading. See CLI.md for majordomus skills.
.ai/repo/adrs/<NNNN>-<slug>.md
An architecture decision: what was decided, why, and what it costs. Front matter is
authored or proposed by the tool, validated against share/schemas/majordomus/adr/adr.v1.schema.json (the
allow-list share/allow/adr.txt is generated from it); the body is the narrative. The
identity is adr-NNNN, allocated once, never reused, and it fixes the file-name prefix, so
a retitle moves the slug and never the number. Discovery is the source class adr in
.ai/repo/knowledge/sources.yaml; nothing else registers a decision.
---
schema: adr/v1
id: adr-0004
kind: adr
title: One canonical declaration, composed modules, derived projections
status: accepted
date: 2026-09-05
tags: [architecture, capabilities]
supersedes: [adr-0002]
related:
- rule:majordomus.capability-registry
- claim:capability-registry
- file:apps/majordomus-cli/src/capability
- test:test/cases/91_canonical_architecture.sh
provenance:
origin: extracted
derived_from:
- decision:t-2026-09-05-a
- file:docs/CAPABILITIES.md
---
# Context
...
# Decision
...
# Consequences
...| Key | Required | Meaning |
|---|---|---|
schema | yes | adr/v1 |
id | yes | ^adr-[0-9]{4}$; the number equals the file-name prefix |
kind | yes | adr |
title | yes | one line, the decision rather than the topic |
status | yes | proposed, accepted, superseded or rejected |
date | yes | YYYY-MM-DD, the day it reached that status |
tags | no | ids, same pattern as elsewhere |
supersedes | no | decisions this one stands in for; each must exist and name this one back |
superseded_by | no | present exactly when the status is superseded |
related | no | what the decision put in force: rule:<id>, claim:<id>, file:<path>, test:<path>. Each is validated, and the knowledge graph turns it into an edge — declares, supports, references, tested_by — so the reverse direction is a query, never a second edge to maintain |
provenance.origin | no | authored (a person wrote it) or extracted (adr propose derived it) |
provenance.derived_from | no | typed references: decision:, session:, commit:, issue:, file:, test: |
Unknown keys are errors. The body carries non-empty level-one sections # Context,
# Decision and # Consequences; # Alternatives rejected is optional.
propose never writes accepted. It writes status: proposed and has no flag that
says otherwise, because acceptance is the person's act: a tool that can write accepted
can turn its own inference into repository truth, and a reader months later cannot tell
which. An extracted record must name what it was derived from — a candidate without
evidence is an assertion — and is refused if it claims accepted. It may be superseded
without ever having been accepted, because that is something a later record did to it
rather than a decision it claims for itself.
Supersession is reciprocal. A chain walkable from one end only is not a chain:
supersedes on one record requires superseded_by on the other, and adr check reports
either half missing. Identities are allocated under a lock over the section directory, so
two worktrees proposing at the same moment get two numbers rather than one number twice.
See CLI.md for majordomus adr.
.ai/repo/deployments/<id>.yaml
One deployment of this repository's executable, and the only authoritative statement of
it. The container image definition, its ignore file and the provider configuration
(fly.toml) are generated from this object; each carries a provenance header naming
this file and the command that regenerates it, and majordomus generate --check fails
when one is edited by hand. Contract:
share/schemas/majordomus/deployment/deployment.v1.schema.json
(deployment/v1); keys are closed by share/allow/deployment.txt, generated from it.
schema: deployment/v1
kind: deployment
id: majordomus # identity, [a-z][a-z0-9-]*
title: ... # one line, for a listing
description: ... # one line: what it serves and to whom
status: declared # declared | active | retired
application: majordomus # the application's name at the provider
build:
package: majordomus-cli # the Cargo package built
binary: majordomus # the binary target the image runs
profile: release # the Cargo profile
inputs: [apps/majordomus-cli, share, .ai, Cargo.toml, Cargo.lock]
site: site/public # what the canonical site pipeline writes; absent = no site
listen:
port: 8080 # >= 1024: the process runs as a non-root user
interface: all # loopback | all — `all` is stated intent, not a suppressed warning
health:
liveness: /api/v1/live # must be a route a capability registers
readiness: /api/v1/ready # likewise
grace_seconds: 2
interval_seconds: 15
timeout_seconds: 2
resources:
cpu_kind: shared # shared | performance
cpus: 1
memory_mb: 256 # a hypothesis until a measured run under it passes
machines:
count: 1 # the deployed inventory is asserted against this
min_running: 0 # 0 with autostop: the cheap profile
autostart: true
autostop: true
region: fra # the provider's own vocabulary
budgets: # each written by the run that measured it; none guessed
image_bytes: ...
binary_bytes: ...
build_context_bytes: ...
cold_start_ms: ...
resident_memory_mb: ...
blocking_check_ms: ...
request_p99_ms: ...
provider:
name: fly # provider-specific facts live here and nowhere else
fly:
org: ...
force_https: true
concurrency: { soft_limit: 20, hard_limit: 40 }
The port is stated once. The process, the image and the provider configuration all read this one field; a literal port anywhere else is the drift this kind exists to prevent. The same holds for the region, the resources, the machine count, the health routes and the build inputs.
No credential belongs here. The token that authorises a deployment comes from outside the repository and is never written into the object, into a generated file, into an image layer or into a log.
The schema decides shape; majordomus deploy doctor decides sense — a health route no
capability registers, a package or binary the workspace does not contain, a build input
that does not resolve, a min_running above count — and each refusal names the file, the
key, the value found and the correction.
.ai/repo/workspaces/<id>.yaml
One external workspace this repository is authorised to read: a body of content held by
another vendor — a ChatGPT Project, and later a Claude Project or its equivalent — over
which the operator is already authenticated. A workspace is not a provider: a provider
is a tool that works in this repository (ADR 0024, share/providers.yaml), and a
workspace is the arrow pointing the other way. Both are named after the same vendors, which
is why the distinction is written down rather than inferred. Contract:
share/schemas/majordomus/workspace/workspace.v1.schema.json
(workspace/v1); keys are closed by share/allow/workspace.txt, generated from it.
schema: workspace/v1
kind: workspace
id: chatgpt-majordomus # identity, [a-z][a-z0-9-]*; stable across a rename upstream
title: ... # one line, for a listing
description: ... # one line: what it holds and why this repository reads it
status: declared # declared | active | retired
vendor:
id: chatgpt # [a-z][a-z0-9-]*
name: ChatGPT
origins: # the only origins an adapter may observe; scheme and host
- https://chatgpt.com
upstream:
id: g-p-... # the vendor's own identifier, carried opaquely
kind: project # the vendor's word for this shape of container
url: https://... # where a person opens it; nothing resolves it
authorisation:
asserted_by: ... # who asserts it
asserted_at: 2026-09-09
statement: "..." # what is authorised, in a sentence a person would defend
write: false # false until a separate decision says otherwise
access:
browser_profile: default # a label the operator's configuration resolves, outside
# this repository — never a path, a variable or a secret
transports: [browser] # official | browser | private_http, in preference order
capabilities: # expectation, not permission
- workspace.read
- conversations.read
- messages.read
- instructions.read
- files.read
The content is not here and never will be. A workspace's conversations, messages and
attachments are synced into .ai/local/workspaces/<id>/, which is this checkout's state
and never a source (ADR 0005, ADR 0025). Nothing under it is indexed, generated from,
published or carried by a branch. A piece of it becomes the repository's own statement only
when a person promotes it into .ai/repo/, deliberately, one piece at a time.
No credential belongs here either. access.browser_profile is constrained by the
schema to ^[a-z][a-z0-9-]*$ so that a filesystem path, an environment variable or a token
cannot be written where a label belongs; what the label resolves to lives outside the
repository. test/cases/93_workspace_kind.sh reads every declaration and refuses one that
carries a credential-shaped key or value.
The declaration bounds the adapter before anything is fetched. vendor.origins is the
scope of observation rather than a hint — traffic to anything not named there is never
recorded, not filtered afterwards. authorisation.statement is a person's assertion in
their own words and the adapter may not exceed it. capabilities is an expectation: one
the vendor withdrew is a diagnostic, and an adapter that cannot provide one reports it
unavailable rather than returning an empty success.
.ai/repo/knowledge/sources.yaml
The repository's declared knowledge sources: which tracked files are knowledge, in which
class. Discovery goes through the version-control index with the pathspec given here,
never through a filesystem walk, so an untracked file, build output or a vendored tree is
never a source and two machines report the same list in the same order. The tool ships
the operational classes (the records it writes under the state directory) in
share/knowledge-sources.yaml, with paths relative to that directory; a class's scope
(shared or operational) is decided by which of the two files declared it.
version: 1
sources:
- id: policy # the class, unique; also the order sources are reported in
kind: policy # the node kind every source in this class produces
discovery: vcs # the tracked files matching the pathspec
pathspec: ':(glob).ai/repo/policy.yaml'
required: true # a class that discovers nothing is a finding, not a silence
Every vcs pathspec carries the :(glob) prefix so that * never crosses a directory
separator and no two classes claim one file. Read back with majordomus knowledge sources.
.ai/local/state/ledger.jsonl
Append-only. Written only by Majordomus. One JSON object per line. Retention-capped;
doctor reports when over cap, and history --rotate moves the oldest lines to
ledger.<utc>.jsonl.archived (never deletes).
One line is one history event: what happened, when, for which task, at which head, and
in which session. majordomus history reads them back.
Common envelope:
{"ts":"2026-09-03T19:30:12Z","event":"task.started","head":"3f2a9c1e...","branch":"main","by":"majordomus/0.1.0","session":"s-20260903193010-a4f1","task_id":"t-20260903193012-b7c2"}
session is present when a session is open in this worktree and absent otherwise. Like
head and branch it is computed, never authored. It is what a closed session's
reference lists are selected by, and it exists because the alternative does not work: the
ledger is one file per repository, so a selection by time range cannot tell two concurrent
workers apart, and the first real run of one claimed another worker's records. A line with
no session belongs to no episode, which is the honest answer for work done outside one.
The event vocabulary is closed. share/events.yaml declares every name the ledger
accepts, what writes it, and the payload keys it must carry; mj_ledger_append refuses
an unregistered name, history --event refuses to filter on one, and
history --validate reports a stored line whose name no reader recognises. Adding an
event means adding an entry there — the same rule that makes an unknown policy key an
error.
Events and their extra fields:
| event | extra fields |
|---|---|
task.started | profile, scope[], owner |
task.checkpoint | checkpoint_path when a body was written; absent when only checkpoint_at moved |
task.finished | outcome, contract (object of doctrine id → pass/fail/skipped), verify (command, exit, seconds) or null, checkpoints (count) |
task.handed_over | handover_path, closed (true with --close) |
decision.recorded | decision (the entry's title) |
question.opened | question |
question.resolved | question, answer |
session.started | owner, worker when one was supplied |
session.closed | outcome, session_path |
ledger.rotated | archived (lines moved), kept, archive (path) |
projections.updated | policy_sha256, targets (count) |
use_cases.ran | ran, failed (counts; the evidence under .ai/local/evidence/use-cases/ carries the steps) |
plan_start | issue |
plan_verify | issue |
plan_evidence | issue, covers (the requirement it satisfies), type |
plan_done | issue |
layout.migrated | from, to, backup (the copy of local state made before it moved, or empty) |
rules.vendored | package (the revision of the package now vendored) |
adr.proposed | adr (the identity written), title; never written for an acceptance, which is a person's edit to the file |
doctor, check (without --checkpoint), watch, context, history, search, and
prompt write nothing, the ledger included.
Every line must be a JSON object carrying ts and event. A line that is not is a
failure in history --validate, check, doctor and watch: a ledger the tool
cannot parse is a ledger that cannot be used as evidence. The reader skips such a line
rather than crashing, so a corrupted ledger is still readable while it is being reported.
Because created_at has second resolution, two records written inside one second would
resolve in an order decided by their random filename suffixes. The ledger is append-only
and written in the order the commands ran, which makes it the tiebreak the resolver uses —
the one portable monotonic ordering available without sub-second timestamps.
history --rotate moves all but the newest ledger.retention_max_lines lines into
ledger.<utc-compact>.jsonl.archived and appends a ledger.rotated event. It never
deletes and refuses to overwrite an existing archive.
.ai/local/benchmarks/runs/<run-id>.json — a benchmark run
Written by majordomus bench, one file per run, plus latest.json (the newest run) and
history.jsonl (one compact line per run). Local evidence under the ignored half of the
layer; never a baseline.
{"schema":"majordomus/benchmark-result/v1",
"run_id":"b-20260905T031200Z-9f1c","recorded_at":"2026-09-05T03:12:00Z",
"repository":{"commit":"<40 hex>","branch":"main","dirty":false},
"environment":{"os":"Darwin","arch":"arm64","bash":"5.3.15","clock":"epochrealtime"},
"profile":{"samples":10,"warmup":2,"mode":"both"},
"results":[{"command":"doctor","mode":"warm","class":"read-only","scenario":"fixture not-wired",
"status":"ok","samples":10,"min_ms":2601,"p50_ms":2640,"p90_ms":2760,"p95_ms":2790,
"p99_ms":2790,"max_ms":2790,"mean_ms":2655.2,"stddev_ms":58.1}]}
status is ok, setup-failed, or exit-<code> when the command exited with a code its
scenario does not accept. Percentiles are nearest-rank over the sorted samples. clock
names the source of the milliseconds (epochrealtime, perl or seconds).
baseline.json under .ai/repo/benchmarks/ — the accepted baseline
The same document with schema majordomus/benchmark-baseline/v1, written only by
majordomus bench --write-baseline on a clean tree (or with --force), tracked and
reviewed like any other change. bench --check compares a fresh run's p50, p95 and p99 per
target and mode against it under the policy's benchmark.regression fractions; a baseline
with another schema is reported as not comparable, never as a number.
Projection stamp
Every generated target describes itself; there is no provenance file beside it, so a fresh clone carries the same evidence as the checkout that generated it.
A file-mode target begins:
<!-- generated by `majordomus update` from .ai/repo/policy.yaml (policy 8e1d2c3b4a5f, content 4b2f9a01c7d3e6f8) — do not edit; edit the policy and regenerate -->
policy is the hash of the policy and every profile, concatenated, at generation time.
content is the hash of everything below the stamp line. A region-mode target carries
the same two values in its begin marker:
<!-- majordomus:begin 8e1d2c3b4a5f 4b2f9a01c7d3e6f8 -->
...generated region...
<!-- majordomus:end -->
and content then covers the region body only, never the host document around it.
doctor fails on any target whose content differs from the hash its stamp names
(hand-edited) and on a target with no stamp at all (not written by update). watch
reports policy drift when a stamp names a policy hash that is no longer the policy on
disk, and update refuses to overwrite a target whose content matches neither its stamp
nor the new output.
Generated artifact header
The provider bootstraps carry the stamp above, which is majordomus update's. Every other
generated artifact — everything majordomus generate writes — carries a provenance header
in the form its encoding allows, and the three lines say the same thing in every one:
<!-- GENERATED FILE — DO NOT EDIT DIRECTLY
Source: <what it was derived from>; regenerate with `majordomus generate`
Generator: majordomus-cli <version> --># GENERATED FILE — DO NOT EDIT DIRECTLY
# Source: <what it was derived from>; regenerate with `majordomus generate`
# Generator: majordomus-cli <version>
A JSON document carries them as members instead, schema first when it has a contract:
{
"schema": "majordomus/capability-registry/v1",
"generated": "GENERATED FILE — DO NOT EDIT DIRECTLY; source: …; regenerate with `majordomus generate`",
"generator": "majordomus-cli 0.1.0"
}
and a document whose own specification fixes its member names — the OpenAPI document is
the only one — carries x-majordomus-generated and x-majordomus-generator instead. The
line-oriented text artifacts (share/allow/*.txt) take the # form; every reader of one
skips comment lines.
No header carries a timestamp, an absolute path or a fingerprint that moves with an
unrelated edit. The index of every artifact, with the document each projects, its encoding,
its contract, its source, its size and its hash, is docs/generated/artifacts.json (schema
majordomus/generated-artifacts/v1), and it is itself generated; the contracts of the
generated documents are share/schemas/generated/*.schema.json, each pinned to its
document by the const of the document's schema member. The rule is
project.generated-artifacts-are-typed@1.
Worktree coordination
There is no sidecar registry. start and check --overlap read git worktree list
and each worktree's own .ai/local/state/current.yaml. Git is the authority; nothing
can rot.
Completion note (when no handover is written)
finish accepts --note <file>. For completed it needs the handover's required
sections; for partial/blocked a # Next Action; for no_match/failed a
# Reason. The profile's output_contract fields may lead as a YAML block; v0.1 records
the note but does not validate that block. On success the note is copied to
state/completed/<id>.md.
---
outcome: completed
changes: 2
verification: make test — exit 0
blockers: []
next_action: none
---
# Objective
...
# Current State
...
# Next Action
none