Record, query, and conflict-check architectural and design decisions across the OmniNode platform
Resources
4Install
npx skillscat add omninode-ai/omniclaude/decision-store Install via the SkillsCat registry.
decision-store skill
Skill ID: onex:decision-store
Version: 1.1.0
Owner: omniclaude
Tickets: OMN-2768 (base), OMN-2769 (Slack gate + status events)
Purpose
Record, query, and conflict-check architectural and design decisions across the OmniNode
platform. Decisions are persisted by NodeDecisionStoreEffect (OMN-2765) and queried
via NodeDecisionStoreQueryCompute (OMN-2767).
Conflicts are detected in two stages:
- Structural check (sync, pure function) — always runs
- Semantic check (async, LLM via DeepSeek-R1) — only if structural confidence >= 0.6
HIGH-severity conflicts trigger the Slack conflict resolution gate (OMN-2769), which blocks
the pipeline until an operator resolves each conflict via Slack reply.
Sub-operations
record
Write a new decision entry, run structural conflict check against all existing entries
in the same domain, and route to Slack if any conflict is HIGH severity.
/decision-store record
--type TECH_STACK_CHOICE|DESIGN_PATTERN|API_CONTRACT|SCOPE_BOUNDARY|REQUIREMENT_CHOICE
--domain <domain>
--layer architecture|design|planning
--services <svc1,svc2,...> # omit = platform-wide
--summary "<one-line summary>"
--rationale "<full rationale>"
[--dry-run]Behavior:
- Validate required fields.
- Call
NodeDecisionStoreEffectto persist the entry. - Run
structural_confidence()against all existing entries in the same domain. - For each conflict with confidence > 0.0, call
compute_severity(). - If any severity is HIGH: post Slack gate via
slack-gateskill (HIGH_RISK tier) and
wait for operator resolution via Slack command grammar. - If structural confidence >= 0.6: fire async
semantic_check_async()— non-blocking. - Emit
decision-conflict-status-changed.v1event for each new conflict (status = OPEN). - Emit
decision-conflict-status-changed.v1on each subsequent status transition
(OPEN -> RESOLVED or OPEN -> DISMISSED) after Slack resolution.
query
Retrieve existing decisions with optional filters and cursor-based pagination.
/decision-store query
[--domain <domain>]
[--layer architecture|design|planning]
[--type <decision_type>]
[--service <service_name>]
[--status open|resolved|dismissed]
[--cursor <cursor_token>]
[--limit 20]Behavior:
- Call
NodeDecisionStoreQueryComputewith filter params. - Display paginated results with conflict indicators.
- If
--cursorprovided, fetch next page.
check-conflicts
Dry-run structural conflict check for a hypothetical new decision, without writing anything.
/decision-store check-conflicts
--type <decision_type>
--domain <domain>
--layer architecture|design|planning
--services <svc1,svc2,...>
--summary "<one-line summary>"Behavior:
- Run
structural_confidence()andcompute_severity()against all existing entries. - Display conflict report: confidence scores, severities, affected entries.
- Does NOT write to the store. Does NOT trigger semantic check. Does NOT emit events.
Severity Matrix
| Decision Type | Layer | Base Severity |
|---|---|---|
| TECH_STACK_CHOICE | architecture | HIGH |
| TECH_STACK_CHOICE | design | HIGH |
| TECH_STACK_CHOICE | planning | MEDIUM |
| DESIGN_PATTERN | architecture | HIGH |
| DESIGN_PATTERN | design | MEDIUM |
| DESIGN_PATTERN | planning | LOW |
| API_CONTRACT | architecture | HIGH |
| API_CONTRACT | design | MEDIUM |
| API_CONTRACT | planning | LOW |
| SCOPE_BOUNDARY | architecture | MEDIUM |
| SCOPE_BOUNDARY | design | MEDIUM |
| SCOPE_BOUNDARY | planning | LOW |
| REQUIREMENT_CHOICE | architecture | MEDIUM |
| REQUIREMENT_CHOICE | design | LOW |
| REQUIREMENT_CHOICE | planning | LOW |
Modifiers (applied in order after base):
- Platform-wide scope (either entry has
scope_services = []): floor at MEDIUM - Architecture layer (either entry has
scope_layer = "architecture"): floor at HIGH - Semantic shift (from DeepSeek-R1): +/-1 step, clamped to [LOW, HIGH]
Hard rules:
structural_confidence == 0.0(cross-domain): no conflict, semantic check MUST NOT runstructural_confidence == 0.3(disjoint services): semantic check CANNOT escalate to HIGH- Semantic check only runs if
structural_confidence >= 0.6
Structural Confidence Values
| Value | Meaning |
|---|---|
| 0.0 | Cross-domain — no possible conflict (semantic blocked) |
| 0.3 | Disjoint service sets — low probability (semantic capped at MEDIUM) |
| 0.4 | Same domain, different layer |
| 0.7 | Overlapping service sets |
| 0.8 | One platform-wide vs specific |
| 0.9 | Both platform-wide |
| 1.0 | Identical service scope |
Conflict Resolution (Slack Gate)
When a HIGH severity conflict is detected after record, the pipeline invokes theslack-gate skill with HIGH_RISK tier, posting a conflict notification to Slack and
blocking until an operator resolves the conflict.
Integration with slack-gate
HIGH conflicts use the existing slack-gate HIGH_RISK gate interface:
- See
omniclaude/plugins/onex/skills/slack-gate/SKILL.mdfor gate interface details - The conflict notification is posted as a HIGH_RISK gate
- All other HIGH_RISK gates (e.g. auto-merge) are unaffected; only decision conflicts use
this grammar
Slack Command Grammar
Operators resolve conflicts by replying in the Slack gate thread:
Format: <action> <conflict_id> [note]
Actions:
proceed <id> [note] — mark RESOLVED, record approver + note, continue pipeline
hold <id> — pipeline stays paused (gate remains open)
dismiss <id> — mark DISMISSED permanently (no further checks for this pair)proceed rules:
proceed <id>with no note ->resolution_note = "proceed (no note provided)"proceed <id> <note>->resolution_note = <note text>
hold rules:
- Pipeline stays paused; no state change; no event emitted
dismiss rules:
- Conflict marked DISMISSED permanently
- This decision pair will NEVER trigger a Slack gate again (even if re-recorded)
resolution_noteisNonefor dismissals
Multiple conflicts:
- Each conflict must be resolved separately in its own reply
- All OPEN conflicts must be resolved before the pipeline resumes
- RESOLVED and DISMISSED conflicts do not block the pipeline
Unrecognized commands:
- Re-prompt operator with usage text; no state changes
Conflict Workflow (Full Sequence)
1. record sub-operation detects HIGH conflict
2. Invoke slack-gate skill (HIGH_RISK tier) -> post conflict message to Slack
3. Pipeline blocks (polling for Slack replies)
4. Operator replies with: proceed | hold | dismiss
5. For proceed:
- Update conflict: status=RESOLVED, resolved_by, resolved_at, resolution_note
- Emit decision-conflict-status-changed.v1 (OPEN -> RESOLVED)
- If all conflicts resolved: resume pipeline
6. For hold:
- No state change, no event
- Continue polling
7. For dismiss:
- Update conflict: status=DISMISSED
- Emit decision-conflict-status-changed.v1 (OPEN -> DISMISSED)
- Mark pair as permanently suppressed
- If all conflicts resolved/dismissed: resume pipeline
8. Pipeline resumes after all blocking conflicts are RESOLVED or DISMISSEDStatus Change Events
decision-conflict-status-changed.v1 is emitted on EVERY conflict status transition.
Topic constant: TopicBase.DECISION_CONFLICT_STATUS_CHANGED
Wire topic: onex.evt.omniclaude.decision-conflict-status-changed.v1
Source: omniclaude/src/omniclaude/hooks/topics.py (added in OMN-2766)
Transitions that emit:
None -> OPEN(conflict first detected duringrecord)OPEN -> RESOLVED(viaproceedcommand)OPEN -> DISMISSED(viadismisscommand)
Payload:
{
"conflict_id": "uuid",
"old_status": "OPEN | null",
"new_status": "OPEN | RESOLVED | DISMISSED",
"resolved_by": "slack_user_id | null",
"resolved_at": "ISO8601 | null",
"resolution_note": "string | null"
}Note: old_status is null for the initial OPEN event (no previous status).resolution_note is null for dismiss actions.
Files
| File | Purpose |
|---|---|
SKILL.md |
This document — usage, severity matrix, conflict resolution workflow |
prompt.md |
Operational prompt followed by the agent |
detect_conflicts.py |
structural_confidence() and compute_severity() pure functions |
semantic_check.py |
Async LLM check via DeepSeek-R1 (only if structural >= 0.6) |
examples/record_decision.md |
Example: recording a tech stack choice |
examples/query_decisions.md |
Example: querying decisions with filters |
examples/conflict_resolution.md |
Example: Slack conflict resolution walkthrough |
Dependencies
| Ticket | Component | Role |
|---|---|---|
| OMN-2763 | ModelDecisionStoreEntry, ModelDecisionConflict |
Data models |
| OMN-2765 | NodeDecisionStoreEffect |
Write node |
| OMN-2767 | NodeDecisionStoreQueryCompute |
Query node |
| OMN-2766 | TopicBase.DECISION_CONFLICT_STATUS_CHANGED in topics.py |
Event emission |
| OMN-2769 | Slack command grammar + status change events | This ticket |
Event Schema
decision-conflict-status-changed.v1 (emitted on every status transition):
{
"conflict_id": "uuid",
"old_status": "OPEN | RESOLVED | DISMISSED | null",
"new_status": "OPEN | RESOLVED | DISMISSED",
"resolved_by": "string | null",
"resolved_at": "ISO8601 | null",
"resolution_note": "string | null"
}Note: The event schema was simplified in OMN-2769. Fields decision_a_id,decision_b_id, and approver were consolidated: approver is now resolved_by,
and decision pair IDs are carried by the conflict record (not the status event).
The status event is intentionally minimal — consumers query the conflict record for
full decision pair context.