Project Config - alexherrero/agentm GitHub Wiki

Project config reference

You configure each project in .harness/project.json. This file holds vault_project, github, and env. It also adds an enablement block. This block includes type, skills, hooks, registered_at, registered_via, operator_overrides, and last_redetect_at. It records your repo's detected and approved configuration. You keep the verification ledger in a separate file. You leave features.json and its passes untouched.

โšก Quick Reference

Question Answer
Where does enablement config live? .harness/project.json โ€” NOT features.json (locked DC-1).
Who writes it? scripts/project_config.py โ€” register() calls merge_enablement(), which preserves every other key (vault_project / github / env) and overwrites only the enablement keys.
Where does the file resolve to? Vault-resident post-V4 #26: the project directory's own desk/project.json (agentm-vault ยง Projects and tasks โ€” desk/ is the project's home for machine files since the projects migration, plan 15 retired the older _harness/project.json fallback rung). The writer routes through harness_memory.write_machine_file (.project-mode-aware โ€” routes to the repo-local home <repo>/.harness/project.json for local-mode projects, or any project with no vault; distinct from the migrate-nagging legacy fallback).
What counts as "registered"? is_registered() = project.json carries a non-empty skills block OR the repo has a repo_registry entry.
What's the default type? "coding" (the type taxonomy is deferred to V5).
Related pages Detection rules, Configure a new project, Repo layout

Enablement block schema

The build_enablement_block function emits 7 keys. The merge_enablement function overwrites these keys. You preserve everything else in project.json verbatim during the merge.

Field Type Default Meaning
type string "coding" Project type. The taxonomy beyond "coding" is deferred to V5.
skills object the 6 enableable skills, all enabled Map <name> โ†’ TargetState. Names: memory, design, diataxis-author, pii-scrubber, ship-release, dependabot-fixer.
hooks object the 7 enableable hooks, all enabled Map <name> โ†’ TargetState. Names: kill-switch, steer, commit-on-stop, memory-recall-session-start, memory-recall-prompt-submit, memory-reflect-idle, memory-reflect-stop.
registered_at string (ISO-8601 UTC) now, at write time When the block was first written, e.g. 2026-05-29T14:03:00Z.
registered_via string "auto-detect" How the repo was registered โ€” "auto-detect" (the flow) or "manual".
operator_overrides array [] [{at, skill_or_hook, action, reason}] โ€” one entry per opt-out recorded by apply_override (action defaults to "disabled-at-registration").
last_redetect_at string | null null When re-detection last scanned this repo. Stays null until the first redetect run; a --dry-run preview leaves it alone.

TargetState shape (each entry in skills / hooks)

Field Type Meaning
enabled bool Whether the skill/hook is active for this project (false after an opt-out).
auto_detected bool true if a rule matched this target; false if it carries the default rationale only.
rationale string Why this target is relevant โ€” a detection reason (when matched) or a default reason.
rule_id string | null The matching rule's id (e.g. R-wiki), or null when default-enabled.
operator_action string | null null until an opt-out flips it to "disabled-at-registration".

Re-detection

You re-scan a configured repo with redetect. It diffs a fresh detection result against the stored block and prints what moved. It writes no enablement key unless you pass --apply.

python3 scripts/project_config.py redetect .                    # surface the diff
python3 scripts/project_config.py redetect . --format json      # machine-readable
python3 scripts/project_config.py redetect . --dry-run          # read-only; no stamp
python3 scripts/project_config.py redetect . --apply            # refresh the rationale
Change category What moved What --apply does
newly-detected A rule matches a target that carried only the default rationale. Writes the rule's rule_id and rationale onto the entry.
no-longer-detected The rule that justified a target stopped matching. Returns the entry to auto_detected: false with the default rationale.
rule-changed A different rule justifies the target now. Writes the new rule_id and rationale.
new-target The harness added an enableable your config predates. Adds the entry with operator_action: null.
retired-target The harness dropped an enableable your config still lists. Removes the stale entry.

Three rules govern the flow:

  • Applying refreshes rationale, never enablement. --apply writes auto_detected, rule_id, and rationale. It carries enabled and operator_action over from the stored entry untouched, so re-detection can never turn a skill off behind you. Detection surfaces and never gates (DC-7), so a lapsed rule returns a target to its default rationale rather than disabling it. You disable things through the override path instead.
  • An operator_overrides entry suppresses suggestions about its target. Those land in a separate suppressed list that names the override and your original reason, so a decline you already made never comes back as a fresh choice. A retired-target is the one exception โ€” a dead config entry is housekeeping, not a re-suggestion.
  • --apply re-scans from scratch. There is no stored diff to go stale between the run that shows you a change and the run that applies it.

Exit codes: 0 the config still matches the repo, 1 changes were surfaced or applied, 2 re-detect could not run (no enablement block yet, or the repo is the harness source and detection bypasses it).

Pre-existing keys (unchanged)

You add the enablement block. The merge-writer preserves these existing keys.

Key Meaning
vault_project MemoryVault slug for this repo.
github GitHub Projects linkage (owner/number/url/repo). See GitHub Projects integration.
env Per-project environment settings. Pre-existing; untouched by the enablement merge.

Related

โš ๏ธ **GitHub.com Fallback** โš ๏ธ