Use this skill when the user wants to reconcile learnings into rules, run /reconcile, propose rules from learnings, turn learnings into .claude/rules/ entries, or review what rules would be generated from current session learnings. On-demand version of session-end Phase 3.6.8.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add Kanevry/session-orchestrator --skill reconcile --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Reconcile?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kanevry-reconcile-session-orchestrator)More formats (shields.io, HTML) on the badges page.
---
name: reconcile
user-invocable: true
tags: [learning, rules, intelligence, meta]
model: sonnet
model-preference: sonnet
model-preference-codex: gpt-5.4-mini
model-preference-cursor: claude-sonnet-4-6
args-schema:
- flag: --dry-run
description: "Print proposals without writing anything or rendering the approval AUQ"
description: >
Use this skill when the user wants to reconcile learnings into rules, run /reconcile, propose
rules from learnings, turn learnings into .claude/rules/ entries, or review what rules would be
generated from current session learnings. On-demand version of session-end Phase 3.6.8.
---
> **Platform Note:** State files use the platform's native directory: `.claude/` (Claude Code), `.codex/` (Codex CLI), or `.cursor/` (Cursor IDE). Shared metrics live in `.orchestrator/metrics/`. See `skills/_shared/platform-tools.md`.
# Reconcile Skill
On-demand version of the session-end Phase 3.6.8 reconciliation flow. Turns eligible learnings
from `.orchestrator/metrics/learnings.jsonl` into proposed `.claude/rules/<slug>.md` entries,
presenting each batch of 4 to the coordinator via AUQ multiSelect for operator approval before
any file is written. Advisory-only — rules are NEVER auto-applied.
## Posture Contract (load-bearing — read before executing)
- **Advisory-only.** No rule is ever written without explicit operator approval via AUQ.
The AUQ multiSelect is the mandatory gate; there is no bypass.
- **Never-always-on invariant.** The reconcile engine's emitter (`emitter.mjs`) throws
on any eligible learning that would produce an `alwaysApply: true` rule — the engine
structurally cannot emit always-on rules. This invariant is enforced upstream, not by
this skill.
- **Engine never writes `.claude/rules/`.** `runReconcile` computes proposals and records
them in the idempotency sidecar only. The only module that writes `.claude/rules/` is
`writer.mjs`, and only AFTER the operator approves proposals via AUQ.
- **The candidate store belongs to `mergeCandidates` — nothing else writes it.**
`.orchestrator/runtime/reconcile-candidates.jsonl` is a mutable work-queue whose only
sanctioned writer is `mergeCandidates` (`scripts/lib/reconcile/idempotency.mjs`); it is
not a scratch pad, and no report, analysis run, or agent may append to it by hand. A
hand-written record there corrupts downstream readers — the session-start reconcile nudge
banner derives "last run" from `created_at`, so a foreign-shaped record makes a non-empty
store report *no reconcile run on record*. Candidate analyses and dry-run reports write
their findings to `docs/reconcile/<date>-<topic>.md`, never into the store. (Since
2026-07-31 a read-side shape guard drops records lacking `learning_key`/`created_at` and
`mergeCandidates` reports the count as `skipped` — that guard is a backstop, not a licence.)
- **Same pipeline as session-end Phase 3.6.8.** This skill uses the identical engine and
writer seams as the automatic session-end reconciliation phase — operator experience is
consistent, and any fixes to the engine benefit both paths.
- **`reconcile.enabled` gates the AUTOMATIC session-end phase only.** `/reconcile` is an
on-demand command and runs regardless of `reconcile.enabled`. It still honours
`rule-expiry-days` and `confidence-floor` from the `reconcile` config block.
---
## Phase 0: Bootstrap Gate
Read `skills/_shared/bootstrap-gate.md` and execute the gate check. If the gate is CLOSED,
invoke `skills/bootstrap/SKILL.md` and wait for completion before proceeding. If the gate
is OPEN, continue to Phase 1.
<HARD-GATE>
Do NOT proceed past Phase 0 if GATE_CLOSED. There is no bypass. Refer to
`skills/_shared/bootstrap-gate.md` for the full HARD-GATE constraints.
</HARD-GATE>
---
## Phase 1: Config & Argument Loading
### 1.1 Read Session Config
Read and parse Session Config per `skills/_shared/config-reading.md`. Store result as `$CONFIG`.
### 1.2 Extract Reconcile Config
Extract the `reconcile` block from `$CONFIG`:
```bash
# rule-expiry-days defaults to EMPTY (not a number) so the engine falls back to
# its per-type TTL (deriveExpiresAt, default 60d). A numeric override forces flat
# N-day expiry — matching the `null` default of the reconcile: config resolver.
RULE_EXPIRY_DAYS=$(echo "$CONFIG" | jq -r '.reconcile["rule-expiry-days"] // empty')
CONFIDENCE_FLOOR=$(echo "$CONFIG" | jq -r '.reconcile["confidence-floor"] // 0.5')
RECONCILE_MODE=$(echo "$CONFIG" | jq -r '.reconcile.mode // "warn"')
MIN_RULE_DAYS=$(echo "$CONFIG" | jq -r '.reconcile["min-rule-days"] // 7')
MIN_INSIGHT_CHARS=$(echo "$CONFIG" | jq -r '.reconcile["min-insight-chars"] // 24')
MAX_PROPOSALS_PER_RUN=$(echo "$CONFIG" | jq -r '.reconcile["max-proposals-per-run"] // 10')
```
When `RULE_EXPIRY_DAYS` is empty, pass `ruleExpiryDays: undefined` to `runReconcile` so the engine uses its per-type TTL. Defaults when the `reconcile` block is absent or a field is missing:
- `rule-expiry-days`: empty → per-type TTL (`deriveExpiresAt`, default 60d). Preserves FA2 behaviour; matches the `null` resolver default.
- `confidence-floor`: 0.5
- `mode`: warn (enum `off` | `warn`)
- `min-rule-days`: 7 — floor window (days) applied to a proposed rule's `expires-at` so a
near-dead or already-elapsed natural expiry never produces a born-dead rule (issue #741.1).
- `min-insight-chars`: 24 — opt-in minimum insight length gating the eligibility
placeholder-insight check (issue #741.2).
- `max-proposals-per-run`: 10 — volume brake (issue #900 D); the engine sorts eligible
learnings by confidence DESC and proposes at most this many per run.
Note: `reconcile.enabled` is intentionally NOT checked — this on-demand command always runs.
### 1.3 Parse Arguments
Check `$ARGUMENTS` for `--dry-run`:
```bash
DRY_RUN=false
if echo "$ARGUMENTS" | grep -q -- '--dry-run'; then
DRY_RUN=true
fi
```
---
## Phase 2: Run the Reconciliation Engine
### 2.1 Resolve Plugin Root
Resolve `$PLUGIN_ROOT` per `skills/_shared/config-reading.md` (the standard resolution chain:
`$CLAUDE_PLUGIN_ROOT` → `$CODEX_PLUGIN_ROOT` → `$CURSOR_RULES_DIR` → common install locations).
### 2.2 Resolve the Effective Write-Targets
`reconcile.targets` says WHERE approved rules land. Resolve it BEFORE surfacing
the approval AUQ — the operator must never be asked to approve a write to a
destination that cannot exist:
```javascript
import { resolveEffectiveTargets } from '$PLUGIN_ROOT/scripts/lib/reconcile/engine.mjs';
const { targets, baselineRoot, dropped, reason } = resolveEffectiveTargets({
targets: CONFIG.reconcile?.targets, // ['repo-local'] | ['baseline'] | both
baselineRoot: CONFIG['plan-baseline-path'], // already 3-tier-resolved by config.mjs
});
```
| Target | Writes to | Root |
|---|---|---|
| `repo-local` (default) | `<repoRoot>/.claude/rules/<slug>.md` | `repoRoot` |
| `baseline` (#1099) | `<baselineRoot>/proposals/<slug>.md` | `plan-baseline-path`, resolved `SO_BASELINE_PATH` env > `owner.yaml` `paths.baseline-path` > committed |
`baseline` is DROPPED (with one stderr WARN, and `dropped: ['baseline']` in the
return) when the root is unresolvable on all three tiers, is still the committed
`OVERRIDE-IN-…` placeholder, or is not absolute. A dropped target means: do not
surface its proposals in the AUQ at all. If `targets` comes back EMPTY, stop
here and report the `reason` — there is nowhere to write.
Writing to `baseline` is still advisory and AUQ-gated exactly like `repo-local`:
files land under `proposals/` in the baseline checkout, nothing is committed
there, and no branch is touched. The operator reviews and commits in that repo
himself.
### 2.3 Invoke `runReconcile`
```javascript
import { runReconcile } from '$PLUGIN_ROOT/scripts/lib/reconcile/engine.mjs';
const { proposals, rejected, summary, error } = await runReconcile({
repoRoot, // absolute path from git rev-parse --show-toplevel
ruleExpiryDays: RULE_EXPIRY_DAYS, // empty → undefined → engine per-type TTL
minRuleDays: MIN_RULE_DAYS, // default 7 — floors a near-dead expires-at
minInsightChars: MIN_INSIGHT_CHARS, // default 24 — opt-in placeholder-insight length gate
maxProposalsPerRun: MAX_PROPOSALS_PER_RUN, // default 10 — volume brake (issue #900 D)
now: new Date(),
dryRun: DRY_RUN, // true → engine touches no disk (no idempotency sidecar write)
});
// The engine does NOT apply a confidence floor — it proposes every eligible
// learning and carries each one's `confidence` through. `confidence-floor` is a
// DELIVERY gate: filter proposals here before the sidecar + AUQ (mirrors
// session-end Phase 3.6.8). Use `surfaced` everywhere "proposals" appears below.
const surfaced = proposals.filter((p) => typeof p.confidence === 'number' && p.confidence >= CONFIDENCE_FLOOR);
```
`runReconcile` NEVER throws — a top-level error populates `result.error` instead.
If `error` is present, surface it to the user and abort:
> "Reconcile engine error: `<error>`. Check `.orchestrator/metrics/learnings.jsonl` and retry."
### 2.3 Handle Empty / Zero-Proposal Cases
If `summary.totalLearnings === 0`:
> "No learnings found in `.orchestrator/metrics/learnings.jsonl`. Run `/evolve analyze` first to extract session patterns."
Exit cleanly.
If `summary.eligible === 0` (learnings exist but none are eligible — already proposed,
or wrong learning type — eligibility is type/`file_paths`-based, NOT confidence-based):
> "No eligible learnings for rule proposals (total: `summary.totalLearnings`, already proposed or ineligible type: all). Run more sessions to accumulate evidence."
Exit cleanly. Optionally list the rejection reasons from `rejected[]` (field `reason`) as an
informational table.
If `summary.eligible > 0` but `surfaced.length === 0` (proposals exist but ALL fall below
`confidence-floor`):
> "No proposals above the confidence floor (`CONFIDENCE_FLOOR`). Lower `reconcile.confidence-floor` or run more sessions so the underlying learnings accrue confidence."
Exit cleanly.
If `summary.proposed === 0` but `summary.eligible > 0` (emit/render failures consumed all
candidates — unusual):
> "Engine produced 0 proposals from N eligible learnings. See rejection log."
List `rejected[].reason` and exit.
---
## Phase 3: Dry-Run Branch
**Only when `DRY_RUN=true`.**
Print the proposals in a readable table. Do NOT write the sidecar, do NOT render an AUQ, and
do NOT write candidates into `.orchestrator/runtime/reconcile-candidates.jsonl` — that store
is `mergeCandidates`' alone (see Posture Contract). A dry-run write-up belongs in
`docs/reconcile/`.
```
## Reconcile — Dry Run (N proposals, M rejected)
| # | Slug | Confidence | Learning Key | Rule Path |
|---|------|-----------|-------------|-----------|
| 1 | <slug> | 0.72 | <learningKey> | .claude/rules/<slug>.md |
| 2 | ... | ... | ... | ... |
Rejected (not eligible for proposal):
| Learning Key | Reason |
|-------------|--------|
| <key> | <reason> |
Re-run without --dry-run to enter the approval flow.
```
Exit after printing. Do not proceed to Phase 4.
---
## Phase 4: Write Pending Sidecar (Normal Mode Only)
Write the proposals to `.orchestrator/metrics/reconcile-pending.md` as a human-readable
record before presenting the AUQ. This sidecar is informational only — it lets the operator
see the full proposal set in an editor alongside the AUQ prompt.
```markdown
# Reconcile Pending — <ISO date>
Generated by `/reconcile` on <timestamp>. N proposals, M rejected.
## Proposals
| # | Slug | Confidence | Learning Key | Rendered Rule Path |
|---|------|-----------|-------------|-------------------|
| 1 | <slug> | 0.72 | <key> | .claude/rules/<slug>.md |
...
## Rejected
| Learning Key | Type | Reason |
|-------------|------|--------|
| <key> | <type> | <reason> |
...
```
Write via standard file write (not atomic, not lock-protected — this is a disposable sidecar,
not a critical artifact).
---
## Phase 5: AUQ Approval Flow (Normal Mode Only)
Present proposals to the operator in batches of 4. Mirror the session-end Phase 3.6.3 / 3.6.8
multiSelect pattern exactly.
For each batch (proposals sliced into groups of 4):
```
AskUserQuestion({
questions: [{
question: "Batch K of N — which rule proposals should be written into .claude/rules/?",
header: "Regeln",
options: [
{
label: "<slug>.md (confidence: 0.72)",
description: "From learning <learningKey>. Becomes a file under .claude/rules/ — where this repo keeps its rules. Text: <first 100 chars of rendered content>"
},
...up to 4 options per batch...
{
label: "Skip all in this batch",
description: "Decline all proposals in this batch — they are archived to the rejected log."
}
],
multiSelect: true
}]
})
```
Collect responses across all batches:
- Selected options (excluding "Skip all") → `approved[]`
- Unselected options + "Skip all" batches → `rejected_by_operator[]`
> **Codex CLI fallback:** AskUserQuestion is unavailable in subagents and on Codex CLI
> (AUQ-004). In those contexts, present proposals as a numbered Markdown list and ask
> the operator to reply with the numbers they wish to approve.
---
## Phase 6: Write Approved Rules
### 6.1 Invoke `writeApprovedRules`
```javascript
import { writeApprovedRules } from '$PLUGIN_ROOT/scripts/lib/reconcile/writer.mjs';
const { written, archived, errors } = await writeApprovedRules({
approved: approved, // proposals the operator approved
rejected: rejected_by_operator, // proposals the operator declined
repoRoot,
baselineRoot, // from Phase 2.2; omit/undefined ⇒ baseline is a no-op
targets, // from Phase 2.2; omitted ⇒ ['repo-local']
sessionId: currentSessionId, // informational; from STATE.md or 'manual'
});
```
`writeApprovedRules` NEVER throws — per-item failures are collected in `errors[]`.
`written` is a FILE count, not a proposal count: one proposal approved with both
targets in effect writes two files and counts 2, while stamping the idempotency
sidecar exactly once. A baseline root that does not exist on disk (the
fresh-clone / CI case) skips that target with an `errors[]` entry — it is NEVER
created, because a typo'd path that silently mints a directory tree looks
exactly like a successful write.
### 6.2 Handle Errors
If `errors.length > 0`, surface each error to the operator:
> "Warning: `N` rule(s) failed to write: `<error list>`. Successfully written: `written`. Archived: `archived`."
Log each error but do NOT abort — partial success is acceptable.
### 6.3 Report
```
## Reconcile Complete
- Written: <written> rule file(s) to .claude/rules/
- Archived: <archived> declined proposal(s) to .orchestrator/reconcile.rejected.log
- Errors: <errors.length> (see warnings above, if any)
New rules take effect immediately — they are loaded by the wave-executor's rule-loader
on the next wave dispatch.
```
If `written === 0` and `approved.length === 0`:
> "No proposals approved. No rules written."
---
## Critical Rules
- **NEVER** call `writeApprovedRules` before the operator has confirmed via AUQ — this is the
only write-protection gate for `.claude/rules/`.
- **NEVER** pass `dryRun: false` to `runReconcile` and then skip the AUQ — the idempotency
sidecar is written during the engine run; writing rules without AUQ confirmation would create
an inconsistency between the sidecar and the actual rule files.
- **ALWAYS** surface `errors[]` from `writeApprovedRules` — per-item isolation must not
silently swallow failures.
- **ALWAYS** present proposals in batches of ≤4 via AUQ multiSelect — mirrors session-end
3.6.3 / 3.6.8 and keeps the operator prompt readable.
- **ALWAYS** honour `confidence-floor`, `rule-expiry-days`, `min-rule-days`,
`min-insight-chars`, and `max-proposals-per-run` from Session Config `reconcile` block —
the engine reads these, but the skill must pass them explicitly.
## Anti-Patterns
- **DO NOT** write any file to `.claude/rules/` without AUQ operator confirmation.
- **DO NOT** check `reconcile.enabled` — that flag gates the automatic session-end phase, not
this on-demand command.
- **DO NOT** emit or approve a rule with `alwaysApply: true` — the engine structurally prevents
it, but the reviewer should reject any proposal that would produce an always-on rule.
- **DO NOT** skip the dry-run branch when `--dry-run` is passed — the entire AUQ + write flow
must be bypassed.
- **DO NOT** treat `runReconcile` failures as fatal — check the `error` field and surface it,
then exit cleanly.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!