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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ releases begin.
- `skills/stack-upgrade/SKILL.md` — `/stack-upgrade` command (gstack-style).
- `UPDATING.md` — the non-destructive update model ("reference, don't copy") + the
managed-block convention for the rare inlined case.
- `scripts/sync_inlined_method.py` — regenerates a `STACK:BEGIN/END` managed block
from `AGENTS.md` (with `--check` for CI). For agents without an `@file` import
(e.g. MiniMax/Mavis): the method is inlined and re-synced, never hand-forked.
- `tools/ai_docs/source_config.py` — replaces `source_exts.py`; now also exports
`EXCLUDE_DIRS`, the unified directory exclusion set shared by all tools.
- `tools/ai_docs/module_discovery.py` — shared `find_module()` function, eliminating
Expand Down
17 changes: 14 additions & 3 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,20 @@ For those, wrap the stack-managed region in markers and edit only outside them:
- ...
```

An updater replaces only the bytes between `STACK:BEGIN`/`STACK:END`; everything
outside survives. This is the fallback for the rare inlined case — **prefer the
`@AGENTS.md` reference**, which needs no markers and no merge at all.
Regenerate the block from the canonical source with:

```bash
python3 scripts/sync_inlined_method.py <target-file> # refresh the block
python3 scripts/sync_inlined_method.py <target-file> --check # CI/pre-commit: fail if stale
```

It replaces only the bytes between `STACK:BEGIN`/`STACK:END` (backing up the
target first); everything outside survives. This is the path for tools without
an import directive — e.g. **MiniMax/Mavis**, whose `agent.md` has no `@file`
include, so the method is inlined in a managed block and re-synced on update.

**Prefer the `@AGENTS.md` reference** wherever the tool supports it (Claude Code,
Cursor, Codex) — it needs no markers and no sync at all.

## For maintainers: cutting a release

Expand Down
100 changes: 100 additions & 0 deletions scripts/sync_inlined_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""sync_inlined_method.py — Refresh a STACK-managed block from AGENTS.md.

Some agents (e.g. MiniMax/Mavis) have no `@file` import — their config must
physically contain the method. To keep that copy from diverging, wrap the
region in markers and regenerate it from the canonical AGENTS.md:

<!-- STACK:BEGIN vX.Y.Z — managed by ai-native-dev-stack, do not edit inside -->
... (replaced wholesale on every sync) ...
<!-- STACK:END -->

Only the bytes between the markers are replaced; everything outside survives, so
the target's personal/tool-specific sections are never touched. This is the
non-destructive update path for inlined content (see UPDATING.md).

Usage:
python3 scripts/sync_inlined_method.py <target-file> [--source AGENTS.md] [--check]

--check : exit 1 if the block is stale (CI guard / pre-commit), write nothing.

If the target has no markers yet, the script prints where to add them and exits
non-zero — it never guesses an insertion point in a file it doesn't own.
"""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

BEGIN_RE = re.compile(r"^<!--\s*STACK:BEGIN.*?-->\s*$", re.MULTILINE)
END_RE = re.compile(r"^<!--\s*STACK:END\s*-->\s*$", re.MULTILINE)

STACK_ROOT = Path(__file__).resolve().parent.parent


def _stack_version(source: Path) -> str:
m = re.search(r"stack-version:\s*([0-9][0-9A-Za-z.\-]*)", source.read_text(encoding="utf-8"))
if m:
return m.group(1)
vf = STACK_ROOT / "VERSION"
return vf.read_text(encoding="utf-8").strip() if vf.exists() else "0.0.0"


def _build_block(source: Path) -> str:
version = _stack_version(source)
body = source.read_text(encoding="utf-8").rstrip("\n")
begin = (f"<!-- STACK:BEGIN v{version} — managed by ai-native-dev-stack from "
f"AGENTS.md, do not edit inside (run scripts/sync_inlined_method.py) -->")
end = "<!-- STACK:END -->"
return f"{begin}\n\n{body}\n\n{end}"


def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("target", help="File containing the STACK-managed block")
ap.add_argument("--source", default=str(STACK_ROOT / "AGENTS.md"),
help="Canonical method file (default: AGENTS.md)")
ap.add_argument("--check", action="store_true",
help="Exit 1 if the block is stale; write nothing")
args = ap.parse_args()

target = Path(args.target)
source = Path(args.source)
if not target.exists():
print(f"ERROR: target not found: {target}")
return 2
if not source.exists():
print(f"ERROR: source not found: {source}")
return 2

content = target.read_text(encoding="utf-8")
bm, em = BEGIN_RE.search(content), END_RE.search(content)
if not bm or not em or bm.start() >= em.start():
print("ERROR: no STACK:BEGIN/STACK:END block found in the target.")
print("Add these two markers around the region you want managed, then re-run:")
print(" <!-- STACK:BEGIN — managed by ai-native-dev-stack -->")
print(" <!-- STACK:END -->")
return 3

new_block = _build_block(source)
new_content = content[:bm.start()] + new_block + content[em.end():]

if new_content == content:
print(f"OK: {target.name} already in sync.")
return 0

if args.check:
print(f"STALE: {target.name} differs from {source.name} — run without --check to sync.")
return 1

backup = target.with_suffix(target.suffix + ".bak")
backup.write_text(content, encoding="utf-8")
target.write_text(new_content, encoding="utf-8")
print(f"SYNCED: {target.name} updated from {source.name} (backup: {backup.name}).")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading