You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Surfaced 2026-05-15. While listing proposals, the operator caught two files claiming the same id: PROP-022 in their frontmatter:
PROP-022-per-fleet-domain-brainstorm-skills-234500.md — created 2026-05-14 23:45 (canonical id: PROP-022-per-fleet-domain-brainstorm-skills-234500, correct form).
PROP-022-tokens-alongside-usd-reporting-235748.md — created 2026-05-15 00:57 by a separate session (id: PROP-022, short form, written manually without invoking /proposal-create).
Two distinct failure modes combined to produce the collision:
Off-skill creation. The second proposal file was written directly with the Write tool rather than invoking /claude-code-hermit:proposal-create. The skill's "list existing PROPs, take max NNN, add 1" rule never fired, so the NNN was assigned by reading the proposal list captured earlier in the session — which predated the per-fleet-domain proposal. The skill's filename-collision guard (the a/b suffix from PROP-008) only triggers on same-second filename collisions, not NNN reuse across different slugs.
Short-form id. The frontmatter used id: PROP-022 rather than the canonical id: PROP-022-tokens-alongside-usd-reporting-235748. The canonical form is unique by construction (slug + HHMMSS); the short form is not. Even if the NNN had been unique, the short-form id violates the schema documented in proposal-create/SKILL.md step 67 ("the canonical ID PROP-NNN-<slug>-HHMMSS — equals the filename stem without .md"). Most existing PROPs (PROP-001 through PROP-013, PROP-018, PROP-019, PROP-020, PROP-021, PROP-022) use short-form ids, so this is a long-standing schema drift, not new with this incident.
Already resolved manually: the tokens proposal was renamed to PROP-023 and its frontmatter id updated to PROP-023-tokens-alongside-usd-reporting-235748.
Problem
Three concrete risks if this stays unaddressed:
Off-skill proposal creation is the realistic path, not the exception. The model (or an operator) often writes a PROP file directly because the file is short and the skill prose isn't loaded. The skill's NNN-assignment rule lives in prose; it has zero enforcement teeth. Two parallel sessions or one inattentive session can collide.
Short-form id is the de-facto convention even though the schema requires canonical form. Reading the on-disk proposals: most files use id: PROP-NNN not id: PROP-NNN-<slug>-HHMMSS. The two coexisting conventions confuse downstream consumers (proposal-list table, /proposal-act resolution algorithm, cortex Dataview views, metrics dedup). PROP-008 split into PROP-008-103800 and PROP-008 exactly because of this drift.
Detection happens by eyeball. The operator caught this only because the proposal-list table showed two PROP-022 rows. Without that incidental surfacing, both proposals could've lived in parallel for weeks until one got accepted/dismissed and the other was treated as referring to the same decision.
Proposed Solution
Three coordinated layers — each cheap, each independently useful, together robust:
Layer A — next-prop-id.js helper script (eliminates eyeball NNN)
New script at plugins/claude-code-hermit/scripts/next-prop-id.js. Takes a slug and prints the next reserved PROP-NNN-<slug>-HHMMSS to stdout. Implementation:
Glob .claude-code-hermit/proposals/PROP-*.md.
Extract NNN from each filename (regex ^PROP-(\d+)), take max, +1, zero-pad to 3.
Append current HHMMSS (timezone from config.json).
Probe for filename collisions; append a/b/... if needed.
Reserve atomically: create an empty <filename>.lock sentinel file with O_CREAT|O_EXCL (fails if it already exists). If the lock exists, increment HHMMSS by 1s and retry; cap retries at 3.
Print canonical id to stdout.
Caller writes the proposal file, then deletes the .lock sentinel.
This isn't a true cross-process lock (no flock for cross-host safety), but for the realistic concurrency level (operator + at most one hermit session at a time), it closes 95% of the race window. The remaining 5% (two processes racing for the same O_CREAT|O_EXCL) is bounded — one fails, increments, retries.
Update proposal-create/SKILL.md step 1 to:
Run node ${CLAUDE_PLUGIN_ROOT}/scripts/next-prop-id.js <slug> and capture stdout as the canonical proposal id.
Update state-templates/CLAUDE-APPEND.md to add a "Proposal creation" note: Never write .claude-code-hermit/proposals/PROP-*.md files by hand. Always invoke /claude-code-hermit:proposal-create (or, in skill prose, run scripts/next-prop-id.js).
Layer B — validate-proposals.js integrity check (catches off-skill writes)
New script at plugins/claude-code-hermit/scripts/validate-proposals.js. Reads every PROP file's frontmatter; flags:
NNN duplicates: two or more files share the same NNN prefix in their filename or frontmatter id.
id ≠ filename stem: frontmatter id must equal filename without .md. Catches short-form ids (id: PROP-022 in a file named PROP-022-tokens-...md).
id format violations: must match ^PROP-\d{3}-[a-z0-9-]+-\d{6}[a-z]?$.
Exit code 0 on clean, 1 on any violation; prints offending files + reason to stderr.
A pre-write hook (PreToolUse on Write/Edit, matcher .claude-code-hermit/proposals/**): if the script returns 1 after the planned write, refuse the write and surface the error. (Approximation: today's enforce-deny-patterns hook is the closest pattern.) Optional — Layer A handles prevention; Layer B is detection.
Layer C — Migrate existing short-form ids to canonical (cleanup, not gating)
One-off: a script that scans all existing PROP files, computes canonical ids from the filename, and rewrites short-form id: PROP-NNN to id: PROP-NNN-<slug>-HHMMSS in frontmatter. Idempotent — no-op if already canonical.
Out of scope for the routine flow; one-time chore the operator runs after merging this PROP.
What's NOT in scope
True cross-host locking (overkill; the realistic concurrency is single-host).
Mutex via state/runtime.json (more complex than the sentinel-file approach; same race semantics).
Renaming filenames on collision detection (too destructive; flag and let operator decide).
Effort: Layer A ~40 lines of JS + 1-line skill edit. Layer B ~60 lines of JS + 1-line doctor wiring. Layer C ~30 lines, one-time. Total ~2h.
Risk: Very low. Layer A only changes id assignment, which was already prose-defined. Layer B is read-only validation. Layer C runs once.
Benefit:
Eliminates the off-skill NNN reuse path (Layer A).
Surfaces drift at every hermit-doctor run (Layer B).
Aligns existing files with the documented schema (Layer C).
Closes the loop on PROP-008's "collision-safe proposal IDs" theme — that earlier PROP added the a/b filename suffix; this one prevents the more common NNN-reuse-across-slugs case it didn't cover.
Open Questions
Should Layer A also write a stub file at reserve time, not just a .lock sentinel? Writing the actual file with empty body would guarantee ls-visibility for downstream max-NNN scans. Lean: yes, stub the file with frontmatter only, mark status: drafting; let the proposal-create skill fill in the body. Simpler than a separate .lock.
Should /proposal-list reject or warn-and-show on duplicates? Lean: warn-and-show — the operator (or the doctor check) needs to see them to fix; silently rejecting risks hiding work.
What about MP- micro-proposals? They use a different id scheme (MP-YYYYMMDD-N). Out of scope here; same theme worth a follow-up if reuse happens there too.
Filed via hermit-scribe · proposal=PROP-024 · session=null
Context
Surfaced 2026-05-15. While listing proposals, the operator caught two files claiming the same
id: PROP-022in their frontmatter:PROP-022-per-fleet-domain-brainstorm-skills-234500.md— created 2026-05-14 23:45 (canonical id:PROP-022-per-fleet-domain-brainstorm-skills-234500, correct form).PROP-022-tokens-alongside-usd-reporting-235748.md— created 2026-05-15 00:57 by a separate session (id:PROP-022, short form, written manually without invoking/proposal-create).Two distinct failure modes combined to produce the collision:
/claude-code-hermit:proposal-create. The skill's "list existing PROPs, take max NNN, add 1" rule never fired, so the NNN was assigned by reading the proposal list captured earlier in the session — which predated the per-fleet-domain proposal. The skill's filename-collision guard (thea/bsuffix from PROP-008) only triggers on same-second filename collisions, not NNN reuse across different slugs.id: PROP-022rather than the canonicalid: PROP-022-tokens-alongside-usd-reporting-235748. The canonical form is unique by construction (slug + HHMMSS); the short form is not. Even if the NNN had been unique, the short-form id violates the schema documented inproposal-create/SKILL.mdstep 67 ("the canonical IDPROP-NNN-<slug>-HHMMSS— equals the filename stem without.md"). Most existing PROPs (PROP-001 through PROP-013, PROP-018, PROP-019, PROP-020, PROP-021, PROP-022) use short-form ids, so this is a long-standing schema drift, not new with this incident.Already resolved manually: the tokens proposal was renamed to PROP-023 and its frontmatter id updated to
PROP-023-tokens-alongside-usd-reporting-235748.Problem
Three concrete risks if this stays unaddressed:
id: PROP-NNNnotid: PROP-NNN-<slug>-HHMMSS. The two coexisting conventions confuse downstream consumers (proposal-list table, /proposal-act resolution algorithm, cortex Dataview views, metrics dedup). PROP-008 split intoPROP-008-103800andPROP-008exactly because of this drift.Proposed Solution
Three coordinated layers — each cheap, each independently useful, together robust:
Layer A —
next-prop-id.jshelper script (eliminates eyeball NNN)New script at
plugins/claude-code-hermit/scripts/next-prop-id.js. Takes a slug and prints the next reservedPROP-NNN-<slug>-HHMMSSto stdout. Implementation:.claude-code-hermit/proposals/PROP-*.md.^PROP-(\d+)), take max, +1, zero-pad to 3.HHMMSS(timezone from config.json).a/b/... if needed.<filename>.locksentinel file withO_CREAT|O_EXCL(fails if it already exists). If the lock exists, increment HHMMSS by 1s and retry; cap retries at 3..locksentinel.This isn't a true cross-process lock (no
flockfor cross-host safety), but for the realistic concurrency level (operator + at most one hermit session at a time), it closes 95% of the race window. The remaining 5% (two processes racing for the sameO_CREAT|O_EXCL) is bounded — one fails, increments, retries.Update
proposal-create/SKILL.mdstep 1 to:Update
state-templates/CLAUDE-APPEND.mdto add a "Proposal creation" note: Never write.claude-code-hermit/proposals/PROP-*.mdfiles by hand. Always invoke/claude-code-hermit:proposal-create(or, in skill prose, runscripts/next-prop-id.js).Layer B —
validate-proposals.jsintegrity check (catches off-skill writes)New script at
plugins/claude-code-hermit/scripts/validate-proposals.js. Reads every PROP file's frontmatter; flags:idmust equal filename without.md. Catches short-form ids (id: PROP-022in a file namedPROP-022-tokens-...md).^PROP-\d{3}-[a-z0-9-]+-\d{6}[a-z]?$.Exit code 0 on clean, 1 on any violation; prints offending files + reason to stderr.
Wire as:
proposals_integritycheck inhermit-doctor(becomes check release(claude-code-dev-hermit): v0.2.3 #9, between cost budget and proposal health)..claude-code-hermit/proposals/**): if the script returns 1 after the planned write, refuse the write and surface the error. (Approximation: today'senforce-deny-patternshook is the closest pattern.) Optional — Layer A handles prevention; Layer B is detection.Layer C — Migrate existing short-form ids to canonical (cleanup, not gating)
One-off: a script that scans all existing PROP files, computes canonical ids from the filename, and rewrites short-form
id: PROP-NNNtoid: PROP-NNN-<slug>-HHMMSSin frontmatter. Idempotent — no-op if already canonical.Out of scope for the routine flow; one-time chore the operator runs after merging this PROP.
What's NOT in scope
state/runtime.json(more complex than the sentinel-file approach; same race semantics).proposal-metrics.jsonlhistorical entries to canonical ids (history record; don't rewrite).Impact
hermit-doctorrun (Layer B).a/bfilename suffix; this one prevents the more common NNN-reuse-across-slugs case it didn't cover.Open Questions
.locksentinel? Writing the actual file with empty body would guaranteels-visibility for downstream max-NNN scans. Lean: yes, stub the file with frontmatter only, markstatus: drafting; let the proposal-create skill fill in the body. Simpler than a separate.lock./proposal-listreject or warn-and-show on duplicates? Lean: warn-and-show — the operator (or the doctor check) needs to see them to fix; silently rejecting risks hiding work.MP-micro-proposals? They use a different id scheme (MP-YYYYMMDD-N). Out of scope here; same theme worth a follow-up if reuse happens there too.Filed via hermit-scribe · proposal=PROP-024 · session=null