name: pharaoh-decision-record description: Use when recording a canonical decision, fact, or preference in the shared Papyrus workspace with automatic dedup on (type, canonical_name). Returns {action: wrote|duplicate, papyrus_id}. Generalizes pharaoh-finding-record beyond audit findings.
pharaoh-decision-record
When to use
Invoke from any authoring or multi-agent coordination workflow that needs to record a canonical named item (architecture decision, domain fact, style preference) in a way that survives concurrent writers without duplication. Typical callers: pharaoh-req-from-code (when a type or concept is first surfaced), pharaoh-decide chains, multi-agent reverse-engineering fan-out.
Do NOT invoke for informational or non-canonical observations, and do NOT invoke when pharaoh-finding-record is a better fit (audit-finding category + subject_id tuple).
Atomicity
- (a) Indivisible — single write-or-dedup action. Does not author, classify, or retrieve.
- (b) Input:
{type: "dec"|"fact"|"pref", canonical_name: str, body: str, tags?: list[str], reporter_id: str}. Output:{action: "wrote"|"duplicate"|"error", papyrus_id: str, dup_of?: str, message?: str}. - (c) Reward: deterministic — two writers for the same
(type, canonical_name)must produce exactly 1"wrote"+ 1"duplicate"; measured via fixture. - (d) Reusable: any multi-agent workflow needing canonical-vocabulary coordination; ADR capture; design-decision dedup.
- (e) Composable: Papyrus write-only; never modifies artefact files or invokes other skills.
Input
type: one ofdec(decision),fact(domain fact),pref(preference). Controls the Papyrus need type.canonical_name: canonical identifier for the subject. Style (snake_case, CamelCase, etc.) MUST match the frozen vocabulary if one exists for the project. If the caller is unsure, it MUST first invokepharaoh-context-gatherand reuse an existing canonical before coining a new one.body: 1-3 sentence description. Stored verbatim as the Papyrus need body.tags: optional list of free-form tags.reporter_id: caller identifier (e.g.req-from-code:health_monitor.cpp). Stored in the Papyrus needsourcefield for traceability.
Output
Exactly one single-line JSON object, no prose:
{"action": "wrote", "papyrus_id": "FACT_HealthMonitor"}
or:
{"action": "duplicate", "papyrus_id": "FACT_HealthMonitor", "dup_of": "FACT_HealthMonitor"}
or, on subprocess failure:
{"action": "error", "papyrus_id": "FACT_HealthMonitor", "message": "<stderr-first-line>"}
Process
Step 1: Construct deterministic ID
papyrus_id = uppercase(type) + "_" + sanitize(canonical_name)
sanitize replaces non-alphanumeric characters with underscores, collapses consecutive underscores, strips leading/trailing underscores. Case is PRESERVED (do not lowercase).
Examples:
("fact", "HealthMonitor")→FACT_HealthMonitor("fact", "heartbeat_timeout")→FACT_heartbeat_timeout("dec", "use thread pool for monitors")→DEC_use_thread_pool_for_monitors
Step 2: Attempt papyrus add
papyrus --workspace .papyrus add <papyrus_need_type> \
"<canonical_name>" \
--id <papyrus_id> \
--body "<body>" \
--tags "canonical:<canonical_name>,<joined_tags>" \
--source "<reporter_id>" \
--scope local
The <papyrus_need_type> argument maps as: dec → decision, fact → fact, pref → preference.
Step 3: Interpret result
- Exit 0 → emit
{"action": "wrote", "papyrus_id": "<id>"}. - Exit non-zero with stderr containing
"already exists"→ emit{"action": "duplicate", "papyrus_id": "<id>", "dup_of": "<id>"}. - Any other non-zero exit → emit
{"action": "error", "papyrus_id": "<id>", "message": "<stderr-first-line>"}and return; the caller must not retry.
No surrounding prose. Emit exactly one JSON object per invocation.
Dedup semantics
- Match key is
(type, canonical_name). Body and tag differences do NOT suppress dedup — first writer wins and sets canonical body. reporter_iddifference does NOT suppress dedup — two callers arriving at the same concept from different files still collapse to one record.- Concurrent writes for the same
papyrus_idare serialized by the PapyrusFileLock; only one succeeds, the others get"duplicate". - Case is significant in the ID:
FACT_HealthMonitorandFACT_healthmonitordo NOT dedup. The caller is responsible for consistent casing (viapharaoh-context-gatherlookup before coining).
Failure modes
papyrusbinary missing → emit{"action": "error", "message": "papyrus CLI not found"}..papyrus/workspace missing → emit{"action": "error", "message": "no .papyrus/ workspace in cwd"}.- Any other subprocess failure → emit
{"action": "error", "message": "<stderr-first-line>"}.
Relationship to pharaoh-finding-record
pharaoh-finding-record is the audit-specialized sibling: it constrains category to a known enum and derives IDs from (category, subject_id). pharaoh-decision-record accepts any (type, canonical_name) pair. Existing Phase 4b audit fan-out continues to use pharaoh-finding-record; Phase 4c's reverse-engineering fan-out uses pharaoh-decision-record.
A future cleanup may reimplement pharaoh-finding-record as a thin wrapper over pharaoh-decision-record; that refactor is out of scope for Phase 4c.
Last step
After emitting the artefact, invoke pharaoh-decision-review on it. Pass the emitted artefact (or its need_id) as target. Attach the returned review JSON to the skill's output under the key review. If the review emits any axis with score: 0 or severity: critical, return a non-success status with the review findings verbatim and do NOT finalize the artefact — the caller must regenerate (via pharaoh-decision-regenerate if available, or by re-invoking this skill with the findings as input).
See shared/self-review-invariant.md for the rationale and enforcement mechanism. Coverage is mechanically enforced by pharaoh-self-review-coverage-check in pharaoh-quality-gate.
Composition
Each caller invokes this skill once per canonical subject surfaced. The orchestrator or harness then reads the final Papyrus workspace via papyrus recall for the aggregated vocabulary.