feat(agents-config): add root AGENTS.md thin-pointer specification (#159)

Root AGENTS.md pointer file per the architecture decision in discussion #78: documents both config roots (.agents/skills/ portable portal skills, .claude/ orchestration) and the profile entry points, carries a framework_version marker registered in both version tools.

Design case made by @erikpr1994 in the #78 architecture thread; implementation by @jovin-nicholas.
This commit is contained in:
Jovin Nicholas
2026-07-15 22:33:42 +02:00
committed by GitHub
parent 37595187d6
commit 1db48568b3
3 changed files with 41 additions and 16 deletions
+19
View File
@@ -0,0 +1,19 @@
---
framework_version: 1.0.0
---
# Agent Guidelines: AI Job Search
This workspace is structured to manage job search activities, scraper tools, CVs, cover letters, and interview preparation.
## Thin-Pointer Design (Single Source of Truth)
To prevent duplication and configuration drift across different AI agent frameworks (Claude Code, Google Antigravity, Codex, Cursor, Gemini CLI, etc.), this workspace uses a unified thin-pointer design. All agent runtimes should load the canonical specifications and candidate profiles from the files and directories below:
1. **Personal Candidate Profile:**
- The candidate profile, contact details, education, and target preferences are defined in [CLAUDE.md](CLAUDE.md) and the individual profile methodology files under [.claude/skills/job-application-assistant/](.claude/skills/job-application-assistant/) (specifically `01-*.md` etc.).
2. **Canonical Workflow Specifications:**
- The step-by-step instructions and triggers for tasks (setup, scrape, rank, apply, upskill, interview) are defined in the [.claude/](.claude/) directory (specifically under `.claude/skills/` and `.claude/commands/`).
- Do not duplicate these rules or specifications. Treat `.claude/` files as the single source of truth.
3. **Portal Search Skills:**
- Job-portal search CLIs live under [.agents/skills/](.agents/skills/) in the portable Agent Skills format (with a `SKILL.md` per portal). Codex and Antigravity discover these automatically; the `/scrape` workflow in [.claude/skills/job-scraper/](.claude/skills/job-scraper/) orchestrates them.
+5
View File
@@ -17,6 +17,11 @@ ROOT = Path(__file__).resolve().parent.parent
SKILL_DIR = ROOT / ".claude/skills/job-application-assistant" SKILL_DIR = ROOT / ".claude/skills/job-application-assistant"
FRAMEWORK_FILES = sorted(SKILL_DIR.glob("*.md")) FRAMEWORK_FILES = sorted(SKILL_DIR.glob("*.md"))
# Add root AGENTS.md if it exists
root_agents = ROOT / "AGENTS.md"
if root_agents.exists():
FRAMEWORK_FILES.append(root_agents)
def run_git(args: list[str]) -> tuple[int, str, str]: def run_git(args: list[str]) -> tuple[int, str, str]:
res = subprocess.run(["git"] + args, cwd=str(ROOT), capture_output=True, text=True) res = subprocess.run(["git"] + args, cwd=str(ROOT), capture_output=True, text=True)
return res.returncode, res.stdout, res.stderr return res.returncode, res.stdout, res.stderr
+17 -16
View File
@@ -20,16 +20,16 @@ import sys
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
SKILL_DIR = ".claude/skills/job-application-assistant"
FRAMEWORK_FILES = [ FRAMEWORK_FILES = [
"01-candidate-profile.md", ".claude/skills/job-application-assistant/01-candidate-profile.md",
"02-behavioral-profile.md", ".claude/skills/job-application-assistant/02-behavioral-profile.md",
"03-writing-style.md", ".claude/skills/job-application-assistant/03-writing-style.md",
"04-job-evaluation.md", ".claude/skills/job-application-assistant/04-job-evaluation.md",
"05-cv-templates.md", ".claude/skills/job-application-assistant/05-cv-templates.md",
"06-cover-letter-templates.md", ".claude/skills/job-application-assistant/06-cover-letter-templates.md",
"07-interview-prep.md", ".claude/skills/job-application-assistant/07-interview-prep.md",
"SKILL.md", ".claude/skills/job-application-assistant/SKILL.md",
"AGENTS.md",
] ]
def run_git(args: list[str]) -> tuple[int, str, str]: def run_git(args: list[str]) -> tuple[int, str, str]:
@@ -94,10 +94,10 @@ def main() -> int:
updates_available = [] updates_available = []
errors = [] errors = []
for filename in FRAMEWORK_FILES: for rel_path in FRAMEWORK_FILES:
local_path = ROOT / SKILL_DIR / filename local_path = ROOT / rel_path
if not local_path.exists(): if not local_path.exists():
print(f"Local file missing: {SKILL_DIR}/{filename}") print(f"Local file missing: {rel_path}")
continue continue
# Get local version # Get local version
@@ -105,7 +105,7 @@ def main() -> int:
local_ver = get_framework_version_from_text(local_text) local_ver = get_framework_version_from_text(local_text)
# Get upstream version # Get upstream version
rc, upstream_text, _ = run_git(["show", f"{ref}:{SKILL_DIR}/{filename}"]) rc, upstream_text, _ = run_git(["show", f"{ref}:{rel_path}"])
if rc != 0: if rc != 0:
# File might not exist upstream yet # File might not exist upstream yet
continue continue
@@ -113,19 +113,20 @@ def main() -> int:
upstream_ver = get_framework_version_from_text(upstream_text) upstream_ver = get_framework_version_from_text(upstream_text)
if not local_ver: if not local_ver:
errors.append(f"Local file {filename} is missing 'framework_version' in frontmatter.") errors.append(f"Local file {rel_path} is missing 'framework_version' in frontmatter.")
continue continue
if not upstream_ver: if not upstream_ver:
continue continue
if parse_semver(upstream_ver) > parse_semver(local_ver): if parse_semver(upstream_ver) > parse_semver(local_ver):
updates_available.append({ updates_available.append({
"filename": filename, "filename": Path(rel_path).name,
"local": local_ver, "local": local_ver,
"upstream": upstream_ver, "upstream": upstream_ver,
"path": f"{SKILL_DIR}/{filename}" "path": rel_path
}) })
if errors: if errors:
print("Configuration errors:") print("Configuration errors:")
for err in errors: for err in errors: