Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2e9a129
docs(design): add =self-doc sentinel and derivative-item exclusion
xen0n Sep 19, 2026
272e7f3
docs(template): teach agents =self-doc and derivative-item exclusion
xen0n Sep 19, 2026
6fe2687
feat(linter): track =self-doc sentinel identically to =trivial
xen0n Sep 19, 2026
b170981
docs(linter): reclassify stretched =trivial specs as =self-doc
xen0n Sep 19, 2026
a187cc3
docs(linter): drop derivative test specs from context sidecar
xen0n Sep 19, 2026
c2ec28e
docs(design): treat sentinel specs as reviewed and staleness-tracked
xen0n Sep 19, 2026
00661de
docs(design): define =trivial as a claim about simple expressions
xen0n Sep 19, 2026
c43279d
docs(design): drop test module from =self-doc examples
xen0n Sep 19, 2026
0149cbf
docs(template): exclude sentinels from adversarial test generation
xen0n Sep 19, 2026
45b564d
docs(template): add rule-conflict escalation to agent instructions
xen0n Sep 19, 2026
1bd5146
fix(template): correct nonexistent liyi approve flag
xen0n Sep 19, 2026
f934f38
docs(design): sync agent-skill rules and schema with AGENTS.md
xen0n Sep 19, 2026
98b73eb
docs(design): stop hard-coding the behavioral-rule count
xen0n Sep 19, 2026
27146e9
docs(template): sync key protocol principles into agent instructions
xen0n Sep 19, 2026
76c3ba1
build(meta): add template-sync gate to make lint
xen0n Sep 19, 2026
dd3f0e1
docs(meta): document the template-sync gate
xen0n Sep 19, 2026
903ba95
ci(meta): run template-sync in CI
xen0n Sep 19, 2026
18c0934
refactor(meta): share one liyi:template block between the docs
xen0n Sep 19, 2026
668a7b0
refactor(template): move the section heading outside the template block
xen0n Sep 19, 2026
7635a52
refactor(meta): give the template-sync targets verb-first names
xen0n Sep 19, 2026
2e31564
refactor(template): keep the heading in AGENTS.md, bridge it in tooling
xen0n Sep 19, 2026
b14681e
refactor: tweak the AGENTS.md section heading and related wording
xen0n Sep 19, 2026
33ff727
fix(linter): update stale AGENTS.md heading assertion
xen0n Sep 19, 2026
7ad7230
fix(linter): point template-sync related marker at the real requirement
xen0n Sep 19, 2026
970e871
feat(linter): resolve Python module-level constants in tree_path
xen0n Sep 19, 2026
7e82b65
refactor: manually fix scripts/check-template-sync.py.liyi.jsonc
xen0n Sep 19, 2026
97f9071
refactor(template): adopt the versioned liyi agent-instructions pragma
xen0n Sep 19, 2026
c33fca5
docs(template): shorten the pragma reminder to one 80-column line
xen0n Sep 19, 2026
395b4cf
feat(linter): ship the versioned agent-instructions pragma from liyi …
xen0n Sep 19, 2026
463d5a5
feat(linter): migrate agent-instruction blocks with liyi migrate
xen0n Sep 19, 2026
ae61c75
docs: liyi approve
xen0n Sep 19, 2026
5e5e00b
docs: link the agent instruction block delimiters to the requirement too
xen0n Sep 19, 2026
44a5500
test(linter): cover liyi init agent-instructions behavior
xen0n Sep 19, 2026
1d021a1
test(linter): cover liyi migrate target discovery and rewrites
xen0n Sep 19, 2026
747b009
fix(linter): target migrate strictly like init, without recursion
xen0n Sep 19, 2026
f498db8
docs(design): record the monorepo governance boundary gap
xen0n Sep 19, 2026
c473d57
docs(monorepo-governance): fix commit id after human DCO
xen0n Sep 19, 2026
1252d2b
docs(meta): state the AGENTS.md harness convention in the README
xen0n Sep 19, 2026
77d716e
feat(linter): announce the AGENTS.md convention from liyi init
xen0n Sep 19, 2026
621a8a6
docs(README): manually tweak the Harness Compatibility section
xen0n Sep 19, 2026
d4b47ef
docs: liyi approve
xen0n Sep 19, 2026
ad18fe3
docs(docs): drop shipped items from the next-steps roadmap
xen0n Sep 19, 2026
4e72120
docs(docs): refresh stale next-steps context
xen0n Sep 19, 2026
971125b
docs(docs): refresh the next-steps baseline banner
xen0n Sep 19, 2026
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

name: CI

on:

Check notice on line 5 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / Liyi self-check

立意 · on

intent "=trivial"

Check notice on line 5 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / Liyi self-check

立意 · on

intent "=trivial"
push:
branches: [main]
pull_request:
Expand Down Expand Up @@ -71,3 +71,10 @@
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo run -p liyi-cli -- check --root .

check-template-sync:
name: Check template sync
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: python3 scripts/check-template-sync.py
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml.liyi.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@
"tree_path": "key.jobs::key.\"liyi-check\"",
"source_hash": "sha256:991baeb9c5382d8fab9e2c777bd2af27a7c6c9fcc08b64263f7b5963f7ba0909",
"source_anchor": " liyi-check:"
},
{
"item": "jobs::check-template-sync",
"reviewed": true,
"intent": "Verify that AGENTS.md is derived from docs/liyi-design.md's liyi agent-instructions block, with the section heading added. Must fail the PR if the two diverge.",
"source_span": [
75,
80
],
"tree_path": "key.jobs::key.\"check-template-sync\"",
"source_hash": "sha256:fa834b9241a1a982101e82894486ecafd3aae730ca0021efcd91925205d87182",
"source_anchor": " check-template-sync:"
}
]
}
47 changes: 32 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ For Rust code:
- Conform to official rustfmt style.
- Keep free from Clippy lints.

Before committing, run `make lint` (rustfmt check, Clippy, and the linter's
own self-check) and ensure it passes, or `make verify` to also run the test
suite. Both gates abort at the first failure and end with a success banner
(`lint: all gates passed` / `verify: all gates passed`), so you can confirm
success by the final line — no need to capture the exit code or redirect
output to a log.
Before committing, run `make lint` (rustfmt check, Clippy, the linter's own
self-check, and the agent-template sync check) and ensure it passes, or
`make verify` to also run the test suite. Both gates abort at the first
failure and end with a success banner (`lint: all gates passed` /
`verify: all gates passed`), so you can confirm success by the final line —
no need to capture the exit code or redirect output to a log.

**If you are an AI agent, prefer the quiet variants `make lint-short` and
`make verify-short`.** They run the exact same gates but suppress normal
Expand Down Expand Up @@ -69,27 +69,44 @@ See the contributing guide for full details and examples.

---

<!-- liyi:template:start -->
## 立意 (Intent Specs)
<!-- START liyi agent instructions rev. 1 -->
<!-- DON'T EDIT THIS BLOCK. REFRESH WITH `liyi migrate` FROM A NEWER LIYI. -->
## The 立意 (Intent Specs) design pattern for agents

When writing or modifying code:

1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `<source_filename>.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone.
1. For each non-trivial item (function, struct, macro invocation, decorated endpoint, etc.), infer what it SHOULD do (not what it does). Write intent to a sidecar file named `<source_filename>.liyi.jsonc` (e.g., `money.rs` → `money.rs.liyi.jsonc`). Record `source_span` (start/end lines). Do not write `source_hash` or `source_anchor` — the tool fills them in. Do not write `"reviewed"` — that is set by the human via CLI or IDE. **If you change the `intent` text of a spec that has `"reviewed": true`, you must set `"reviewed"` to `false`** (or delete the field). The human reviewed the *old* intent; your rewrite has not been reviewed. Failing to unset it silently self-approves agent-written intent, bypassing human review. Use `"intent": "=doc"` only when the docstring contains behavioral requirements (constraints, error conditions, properties), not just a functional summary — a docstring that says "Returns the sum" is not adequate; one that says "Must reject mismatched currencies with an error" is. For trivial items (simple getters, one-line wrappers, and simple expressions with no load-bearing operand order, short-circuit side effects, nested-call effects, or cost), annotate with `@liyi:trivial` instead of writing a spec. Alternatively, when working sidecar-first, use `"intent": "=trivial"` to mark items as trivial without requiring a source annotation. Use `"intent": "=self-doc"` for an item that is **not** trivial (it may be substantial — a plain data struct, a recursive helper, a container module) but whose intent is fully intrinsic to its surface form, such that any prose would tautologically restate the name/signature/fields. `=trivial` is a *risk* claim (small enough that nothing hides); `=self-doc` is an *epistemic* claim (nothing implicit hides behind the form) — do not stretch `=trivial` onto substantial code. A `=self-doc` claim holds ONLY if a reader competent in the language but a stranger to this codebase, domain, and session — holding only the item's name, signature, and durable co-located context (enclosing module, in-scope types, a governing `@liyi:note`) — could recover every property any observer (caller, attacker, scheduler, profiler, regulator) could rely on, INCLUDING non-value-domain ones: timing, resource use, termination, information flow, nondeterminism, ordering, idempotency. Side effects are presumptively disqualifying; a sequence `A; B; C` is not self-documenting when its ordering, non-idempotency, or partial-failure semantics are load-bearing and invisible in the sequence. Claiming `=self-doc` obliges you to have checked for these disqualifiers and found none — a false `=self-doc` is a lie about work done, not a judgment call. When `liyi init` emits `_hints` in a sidecar scaffold, use them to prioritize which items warrant deeper investigation (e.g., `git log`, reading tests) within your token/call budget; for unhinted items, infer from source alone.
2. When module-level invariants or governing context are apparent, write a `@liyi:note` block — in the directory's existing module doc (`README.md`, `doc.go`, `mod.rs` doc comment, etc.) or in a dedicated `LIYI.md`. Use the doc markup language's comment syntax for the marker, and close embedded blocks with `@liyi:end-note`. A note is a **context primitive, not a tracked spec**: it is marker-only — never write it to a `.liyi.jsonc`, never hash it, never add a `"reviewed"` flag. Notes apply to their directory subtree (shadowed by deeper notes); to pull a named note into a specific item's context regardless of location, annotate the item with `@liyi:see <name>`. (Optionally name a note: `@liyi:note <name>`; same-named blocks aggregate.)
3. If a source item has a `@liyi:related <name>` annotation, record the dependency in `.liyi.jsonc` as `"related": {"<name>": null}`. The tool fills in the requirement's current hash.
4. For each `@liyi:requirement <name>` block encountered (closed by `@liyi:end-requirement <name>`), ensure it has a corresponding entry in the co-located `.liyi.jsonc` with `"requirement"` and `"source_span"`. (The tool fills in `"source_hash"`.)
5. If a spec has `"related"` edges referencing a requirement, do not overwrite the requirement text during inference. Update the spec (update `source_span`) but preserve the `"related"` edges. Do not write `source_hash` — the tool fills it in.
6. Only generate adversarial tests from items that have a `@liyi:intent` annotation in source or `"reviewed": true` in the sidecar (i.e., human-reviewed intent). When `@liyi:intent` is present in source, use its prose (or the docstring for `=doc`) as the authoritative intent for test generation.
6. Only generate adversarial tests from items that have a `@liyi:intent` annotation in source or `"reviewed": true` in the sidecar (i.e., human-reviewed intent), excluding sentinel intents (`=trivial`, `=self-doc`), which carry no agent-authored intent for a second model to attack. When `@liyi:intent` is present in source, use its prose (or the docstring for `=doc`) as the authoritative intent for test generation.
7. Tests should target boundary conditions, error-handling gaps, property violations, and semantic mismatches. Prioritize tests a subtly wrong implementation would fail.
8. Skip items annotated with `@liyi:ignore` or `@liyi:trivial`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial`. Honor `@liyi:file language=<lang>` as the authoritative language for a file when present (it overrides extension-based detection).
8. Skip items annotated with `@liyi:ignore` or `@liyi:trivial`, files matched by `.liyiignore`, and files bearing a `@liyi:file ignore` directive. Respect `@liyi:nontrivial` — if present, always infer a spec for that item and never override with `@liyi:trivial` or `"=self-doc"`. Honor `@liyi:file language=<lang>` as the authoritative language for a file when present (it overrides extension-based detection). **Exclude clearly-derivative items from inference entirely** — write no sidecar entry for an item whose whole purpose is to exercise or assert another already-specced item, adding no invariant of its own (chiefly tests: a test named for the behavior it checks derives its intent from that behavior's spec). This targets the derivative *shape*, not "tests" by name — a test that encodes a novel invariant the code does not otherwise state does carry independent intent and should be specced. The end state for a derivative item is no entry; do not reach for `"=trivial"` or `"=self-doc"` to file it away.
9. Use a different model for test generation than the one that wrote the code, when possible.
10. When `liyi check` reports stale items, choose one of two paths:
- **Direct re-inference** (preferred during interactive editing with few stale items): re-read the source, update `source_span` and `intent` in the sidecar, leave `"reviewed"` unset. Appropriate when you are the agent that just made the change, the number of stale items is small, and the changes are straightforward.
- **Triage** (preferred for batch workflows, CI, or when many items are stale): assess each item — is the change cosmetic, semantic, or an intent violation? Write the assessment to `.liyi/triage.json` following the triage report schema. For cosmetic changes, run `liyi triage --apply` to auto-fix. For semantic changes, propose updated intent in `suggested_intent`. For intent violations, flag for human review. Prefer triage when stale items have `"reviewed": true` or `@liyi:intent` in source — these carry human-vouched intent that deserves explicit assessment, not silent re-inference.

In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve --rehash` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority.
In either path, **never compute `source_hash` manually** and **never leave `"reviewed": true` on a spec whose `intent` you changed** (see rule 1). When `liyi check --fix` cannot auto-rehash a stale item (e.g., because it has `"reviewed": true`), update `source_span` and `intent`, **set `"reviewed"` to `false`**, and **delete** the `source_hash` field (or set it to `null`). The human will run `liyi check --fix` or `liyi approve` after reviewing the updated intent. This rule exists because agents consistently produce wrong hashes (trailing-newline differences, encoding mismatches) — the tool reads the actual bytes and is the only authority.
11. Before committing, run `liyi check`. If it reports coverage gaps (missing requirement specs, missing related edges), resolve **all** gaps in the same commit. When running as an agent, prefer `liyi check --prompt` for structured JSON output with per-gap resolution instructions. Do not commit with unresolved coverage gaps — CI will reject it.

### Key principles of the intent protocol

- **Adversarial, not confirmatory.** Find bugs, not confirm correctness.
- **Spec is the referee.** If the spec says one thing and the code does another, the test exposes the gap. The human decides who's right.
- **Model diversity.** Different model for tests than for code, when possible.
- **Never modify source code logic** during the protocol. Only create/update `.liyi.jsonc` files, `@liyi:note` blocks (in docs or doc comments), test files, and annotation comments (`@liyi:trivial`, `@liyi:ignore`, `@liyi:requirement`, `@liyi:related`). Annotation comments are metadata, not logic — adding them does not change program behavior.

### Resolving rule conflicts

When two normative instructions here appear to conflict, or a task's correct completion turns on an ambiguity these instructions do not resolve:

1. **Do not guess.** Name the conflicting rules (and any design-doc sections they reference).
2. **If a human is reachable:** ask once — state the candidate resolutions and your recommendation. Consolidate into a single question rather than a series.
3. **If no human is reachable** (batch, CI): take the most conservative reading, leave the affected specs `"reviewed": false`, complete the unambiguous parts, and report the unresolved conflict prominently.
4. **Do not over-apply this.** Where the standard delegates judgment to you — e.g. whether an item qualifies for `=self-doc` under the observer test — reason it out and state your reasoning so a human can veto. That is not a conflict.

### `.liyi.jsonc` Schema (v0.1)

Sidecar files must conform to the following JSON Schema. The top-level object has three required fields: `"version"` (must be `"0.1"`), `"source"` (repo-relative path to the source file), and `"specs"` (array of item or requirement entries). Each spec entry is either an **item spec** or a **requirement spec**, distinguished by the presence of `"item"` vs `"requirement"`.
Expand Down Expand Up @@ -147,11 +164,11 @@ Sidecar files must conform to the following JSON Schema. The top-level object ha
"reviewed": {
"type": "boolean",
"default": false,
"description": "Optional. Whether a human has reviewed and accepted this intent via sidecar approval. Defaults to false when absent. The linter also considers an item reviewed if \u0040liyi:intent is present in source."
"description": "Optional. Whether a human has reviewed and accepted this intent via sidecar approval. Defaults to false when absent. The linter also considers an item reviewed if @liyi:intent is present in source."
},
"intent": {
"type": "string",
"description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, or '=trivial' meaning the item is trivial and needs no intent spec."
"description": "Natural-language description of what the item SHOULD do, or the sentinel value '=doc' meaning the source docstring captures intent, '=trivial' meaning the item is trivial and needs no intent spec, or '=self-doc' meaning the item is self-documenting (intent intrinsic to its surface form) and needs no intent spec."
},
"source_span": { "$ref": "#/$defs/sourceSpan" },
"tree_path": {
Expand Down Expand Up @@ -388,4 +405,4 @@ When `liyi check` reports stale items, the agent assesses each and writes the re
}
}
```
<!-- liyi:template:end -->
<!-- END liyi agent instructions -->
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ structure, code style, commit conventions, and the AIGC policy.
1. **Open an issue first** for non-trivial changes so we can discuss scope.
2. Keep PRs small and focused — one logical change per PR.
3. Ensure `make verify` passes — it runs the lint gates (rustfmt check,
Clippy, and the project's own linter, which dogfoods itself) plus
Clippy, the project's own linter, which dogfoods itself, and the
agent-template sync check) plus
`cargo test --workspace`, aborting at the first failure and ending with a
`verify: all gates passed` banner. (`make lint` runs just the lint gates.)
AI agents should prefer the quiet `make verify-short` / `make lint-short`
Expand Down
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 OR MIT

.PHONY: verify lint fmt-check clippy liyi-check test \
.PHONY: verify lint fmt-check clippy liyi-check check-template-sync sync-template test \
verify-short lint-short test-short

# Run every pre-commit gate (lint + tests) in one shot.
Expand All @@ -14,7 +14,7 @@ verify: lint test
@echo "verify: all gates passed"

# Run every pre-commit lint gate (matches CI). Ends with a success banner.
lint: fmt-check clippy liyi-check
lint: fmt-check clippy liyi-check check-template-sync
@echo "lint: all gates passed"

fmt-check:
Expand All @@ -27,6 +27,17 @@ clippy:
liyi-check:
cargo run -p liyi-cli -- check --root .

# Verify AGENTS.md materializes docs/liyi-design.md's liyi:template block.
# The design doc is the source; AGENTS.md adds the section heading so
# `liyi init` can ship the block unchanged.
check-template-sync:
python3 scripts/check-template-sync.py

# Rewrite AGENTS.md's liyi:template block from the design doc's, adding
# the section heading. Review the resulting diff before committing.
sync-template:
python3 scripts/check-template-sync.py --sync

test:
cargo test --workspace
@echo "test: passed"
Expand Down Expand Up @@ -58,6 +69,10 @@ lint-short:
tail -n 20 $$LOG; rm -f $$LOG; \
echo "lint-short: liyi check FAILED — re-run \`make liyi-check\` for the full report"; exit 1; \
fi; \
if ! python3 scripts/check-template-sync.py >$$LOG 2>&1; then \
tail -n 20 $$LOG; rm -f $$LOG; \
echo "lint-short: check-template-sync FAILED — re-run \`make check-template-sync\` for the full output"; exit 1; \
fi; \
rm -f $$LOG; \
echo "lint-short: all gates passed"

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ liyi check --root .
3. **`liyi migrate`** — upgrades sidecar files when the schema version changes. Idempotent.
4. **Human reviews** — sets `"reviewed": true` in the sidecar to approve, or adds `@liyi:intent` in source to provide the authoritative human version.

## Harness compatibility

立意 adopts the [AGENTS.md convention](https://agents.md) and writes its agent
instructions to `AGENTS.md` by default.
Any harness that reads `AGENTS.md` should work out of the box.
If your harness does not read `AGENTS.md`, it is your responsibility to find
another suitable place for the instruction block and make sure it is actually
read by your harness.

## Progressive Adoption

| Level | What you do | What you get |
Expand Down
Loading
Loading