Project State Reader
Thin wrapper around ${CLAUDE_PLUGIN_ROOT}/scripts/project-state-read.sh. The script parses the project's project_state.md header block and emits structured JSON. This skill exists to give it a Skill-tool-callable name and to document the contract.
Contract
Input: one argument — absolute path to a project folder (the one containing project_state.md).
Output: single JSON object to stdout. Exit code always 0.
Fields:
project_name— from the H1 line inproject_state.md, or folder basename fallbackcodePath— absolute path (string) if declared and resolves, ornullif docs-only / unknownfolder— the absolute path passed inplaybookSets— (v1.1+) array of dev-guides path slugs the project subscribes to (e.g.,["<framework>/best-practices/<author>"]). Falls back to the plugin'sdefaults.jsonplaybookSetswhen field absent. Empty array when explicitnone.playbookSetsSource— (v1.1+)"explicit"(field present with values) |"explicit-none"(field is literalnone) |"default"(field absent, a NON-EMPTYdefaults.jsonlist applied) |"default-empty"(v5.35.7+) (field absent and thedefaults.jsonfallback is itself empty — the project never chose and there is nothing to fall back to; this is what the shippeddefaults.jsonyields)userPlaybook— (v1.1+) absolute path to project-local playbook file, ornullwhen state isunsetordocs-only-no-playbookuserPlaybookState— (v1.1+)"unset"|"docs-only-no-playbook"|"set"playbookResolutions— (v1.1+) array of{topic, set}entries recording per-topic multi-set contradiction resolutionsworktreeByDefault— (v1.2+) boolean. Whentrue,/implementalways recommends a worktree. Defaults tofalsewhen field absent.warnings— array of{code, detail}entries
Defensive posture (never throws)
| Input state | Warning code |
|---|---|
| Project folder does not exist | folder_missing |
Folder exists, project_state.md missing |
project_state_md_missing |
project_state.md has no **Code path:** line |
code_path_unknown |
**Code path:** /some/dir but that directory doesn't exist |
code_path_missing |
Declared (docs-only) sentinel |
(none — legitimate docs-only state, codePath is null) |
Code path sentinels in project_state.md
Accepted on the one-line **Code path:** metadata entry:
**Code path:** /absolute/path— non-null string; path normalized viarealpath -m**Code path:** (docs-only)— null; explicit docs-only declaration- (line absent) — null; first-use prompt should fire
Case-insensitive match on the label.
Playbook fields in project_state.md (v1.1+)
**Playbook Sets:** <framework>/best-practices/<author1>, <framework>/best-practices/<author2>
**User Playbook:** /home/me/projects/idexx/docs/playbook.md
**User Playbook State:** set
**Playbook Resolutions:**
- font-sizing → <framework>/best-practices/<author1>
- bem-methodology → <framework>/best-practices/<author2>
| Line | Semantics |
|---|---|
**Playbook Sets:** <ids,...> |
Comma-separated set IDs |
**Playbook Sets:** none |
Explicit opt-out — empty list, source explicit-none |
| (line absent) | Use the plugin's defaults.json playbookSets; source default when that list is non-empty, default-empty when it is not |
**User Playbook:** <abs path> |
Project-local playbook file |
**User Playbook State:** unset | docs-only-no-playbook | set |
3-state field; mirrors Code Path State precedent |
**Playbook Resolutions:** (multi-line list) |
Per-topic multi-set choices |
Invocation
"${CLAUDE_PLUGIN_ROOT}/scripts/project-state-read.sh" "/abs/path/to/project/folder"
Parse with jq. Example:
OUTPUT=$("${CLAUDE_PLUGIN_ROOT}/scripts/project-state-read.sh" "$PROJECT_DIR")
CODE_PATH=$(jq -r '.codePath // empty' <<<"$OUTPUT")
UNKNOWN=$(jq -e '[.warnings[] | select(.code == "code_path_unknown")] | length > 0' <<<"$OUTPUT" >/dev/null && echo true || echo false)
Consumers (v3.11.0+)
/ai-dev-assistant:set-code-path— read current value to show in confirm prompt/ai-dev-assistant:propose-epics— get codePath before invoking analysis agent/ai-dev-assistant:research— pre-analysis hook needs codePath for strong-signal checkanalysis-agent— inputscodePath(resolved by caller; agent doesn't call this skill directly)
Future consumers that need project-level metadata should call this skill rather than parsing project_state.md directly.
Do NOT
- Do not write to
project_state.mdfrom this skill. Reading only. Writes go through/set-code-pathcommand or the/newcreation flow. - Do not treat non-empty
warnings[]as a blocking error — warnings are observations. - Do not duplicate the parsing logic elsewhere. Call this skill or the script.
See also
${CLAUDE_PLUGIN_ROOT}/scripts/project-state-read.sh— the scripttask-frontmatter-readerskill (v2.0.0) — same design pattern, task-level metadata instead of project-level