一键导入
reconcile
Reconcile skill — detect wiki/backend drift and let the User resolve it via four moves
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Reconcile skill — detect wiki/backend drift and let the User resolve it via four moves
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | reconcile |
| description | Reconcile skill — detect wiki/backend drift and let the User resolve it via four moves |
| risk_class | hard |
Detect drift between the wiki and its linked backends, surface it to the User per ticket, and apply the User's chosen resolution.
This is the workflow that makes "the wiki is the source of truth" actually true. See ADR 0005. See the Rules section at the bottom for the locked invariants.
/good-morning and /wrap-up may invoke this skill as part of their flow.Invoke immediately — do not ask "would you like me to run /reconcile?" first.
The skill runs in two phases: read (drift detection across active tasks) and write (per-ticket resolution).
The CLI does not compute a 3-way diff. This skill assembles one by merging two pairwise diffs in TypeScript (decision Q9 of issue #111). Run these three in parallel:
rubber-ducky pull asana --active --dry-run --json
rubber-ducky pull jira --active --dry-run --json
rubber-ducky pull github --active --dry-run --json
The asana and jira runs supply the per-field deltas the merge algorithm consumes. The github run is purely a capability probe — pull github walks every active page with a gh_ref and returns one skipped: true, reason: "backend does not support pull (github)" record per page. The skill uses that list to identify github-only pages (see Step 1d); without the third call, pages whose only backend ref is gh_ref would be invisible, since selectPullTargets for asana/jira only enumerates pages that have the corresponding ref.
Each call emits a BulkPullResult JSON envelope:
{
"success": true,
"backend": "asana",
"dryRun": true,
"processed": 12,
"updated": 4,
"skipped": 1,
"errored": 0,
"results": [
{
"success": true,
"skipped": false,
"file": "wiki/tasks/foo.md",
"backend": "asana",
"updated": true,
"dryRun": true,
"deltas": [
{ "field": "status", "before": "in-progress", "after": "in-review" },
{ "field": "assignee", "before": "alice", "after": "bob" }
],
"changes": ["status: in-progress → in-review", "assignee: alice → bob"]
},
{
"success": true,
"skipped": true,
"file": "wiki/tasks/bar.md",
"backend": "asana",
"reason": "task page has no asana_ref"
}
],
"errors": []
}
deltas is a PullDelta[]. Each entry is one of:
{ field: <PullableField>, before: scalar|null, after: scalar|null } for the eight scalar fields (status, asana_status_raw, jira_status_raw, assignee, due, priority, closed, comment_count).{ field: "description", before: { length, hash }, after: { length, hash } } for description drift. The body is never in the envelope. Description drift is read-only (Q10).updated: true means at least one delta exists for that file — i.e. that backend disagrees with the wiki. The records in results are the ones to fold into the merge. The top-level success field is informational and is always true; use errored for the bulk-failure check (any non-zero value means one or more per-file pulls threw).
A note on what's not in each run: selectPullTargets only enumerates pages whose ref field for that backend is a string, so the asana run's results contain only pages with asana_ref, the jira run only pages with jira_ref, and the github run only pages with gh_ref. A page with only gh_ref is therefore invisible to the asana and jira runs entirely — it does not appear there as a skip, it just isn't there. That's why the github run is needed (Step 1d).
Keyed by file, merge the two backends' deltas into a single per-ticket record:
file: wiki/tasks/foo.md
asanaDeltas: [...] // from the asana run, or null if skipped (no asana_ref)
jiraDeltas: [...] // from the jira run, or null if skipped (no jira_ref)
For each field that appears in either delta list, the 3-way view is:
wiki value — the before value from whichever delta list has the field (both befores should agree; both came from the same wiki page).asana value — after from the asana delta, or wiki if asana had no delta on that field.jira value — after from the jira delta, or wiki if jira had no delta on that field.A ticket has drift if at least one field differs across {wiki, asana, jira}. Tickets where both backends agree with the wiki on every field are in sync — drop them from the report.
If zero tickets have drift after the merge, report:
No drift found across N active tasks.
Where N is the union of files seen in either run (i.e. every active task with at least one backend ref). Then stop. Do not proceed to phase 2.
Every record in the github run is skipped: true, reason: "backend does not support pull (github)" — that's the capability gate in pullOne short-circuiting before any network call. The set of files in the github run is exactly the set of active pages with a gh_ref.
To find the github-only subset, take the github run's file set and subtract every file that appears in either the asana run or the jira run. What remains is pages whose only backend ref is gh_ref — drift detection is structurally unavailable for them.
Surface these once at the bottom of the report as capability-skipped:
N page(s) capability-skipped — only backend is GitHub, which has no pull capability. Drift detection is unavailable for these.
Do not crash. Do not retry. Move on. (Pages that have a gh_ref alongside an asana_ref or jira_ref are reconciled through those backends — the github capability-skip on them carries no new information and is dropped from the report.)
For each drifted ticket, present a compact per-field 3-way view in plain English. Aim for something the User can scan, not a JSON dump. Group fields when the story is clean ("Asana and Jira agree, wiki is behind") and split them when it isn't.
Example for one ticket:
wiki/tasks/foo.md — "Refresh OAuth scopes"
- status: wiki says
in-progress, Asana saysin-progress, Jira saysin-review- assignee: wiki says
alice, Asana saysbob, Jira saysalice- description: drift detected against Asana (length 412 → 487). Manual merge needed — no automated resolution.
If many tickets drifted, summarise the shape first ("Six tickets drifted overnight. Two have Jira moved to in-review while wiki and Asana are still in-progress; three have a new assignee in Asana; one has description drift.") and then walk them one at a time.
For each drifted ticket, present the four moves and wait for the User to pick exactly one. Do not batch the choices — one ticket at a time.
(a) Accept Asana — Asana wins. Wiki takes its values. Jira pushed to match.
(j) Accept Jira — Jira wins. Wiki takes its values. Asana pushed to match.
(w) Accept Wiki — Wiki was right. Both backends pushed. No pull.
(s) Skip — No changes. Drift re-surfaces next ritual.
Two CLI calls, in order:
# 1. Pull Asana into wiki (writes the eight scalar fields; never description).
rubber-ducky pull asana <task-file>
# 2. Push wiki's new status to Jira so all three agree.
TOKEN=$(rubber-ducky --no-json confirm request \
--action jira.transition \
--preview "<preview-text>")
rubber-ducky --json transition <task-file> \
--backend jira \
--to <status-from-asana> \
--confirm-token "$TOKEN"
The <status-from-asana> is the wiki status after the pull — i.e. the Asana after value from the original delta. The transition verb is the hard-write path and is fully gated by the confirm-token flow (same as the /transition skill).
If the only drifted field was status, this is the full resolution. Only status is pushed to the other backend; non-status field drift in the other backend is left for a separate manual action — flag once and move on.
Mirror of Move 1, in the opposite direction:
rubber-ducky pull jira <task-file>
TOKEN=$(rubber-ducky --no-json confirm request \
--action asana.transition \
--preview "<preview-text>")
rubber-ducky --json transition <task-file> \
--backend asana \
--to <status-from-jira> \
--confirm-token "$TOKEN"
No pull. The wiki is already correct, by assumption. Push the wiki's status to both backends so they catch up:
# Asana
TOKEN_A=$(rubber-ducky --no-json confirm request \
--action asana.transition \
--preview "<preview-text>")
rubber-ducky --json transition <task-file> \
--backend asana \
--to <wiki-status> \
--confirm-token "$TOKEN_A"
# Jira
TOKEN_J=$(rubber-ducky --no-json confirm request \
--action jira.transition \
--preview "<preview-text>")
rubber-ducky --json transition <task-file> \
--backend jira \
--to <wiki-status> \
--confirm-token "$TOKEN_J"
If only one backend disagreed with the wiki, only run that backend's transition — there's nothing to push to the one that already matched.
Do nothing. Issue no CLI calls. Move to the next ticket.
The drift will re-surface on the next /reconcile invocation as long as the backends still disagree. There is no skip-with-memory — that's a locked decision in ADR 0005. Do not write a "skipped" note to the wiki, do not stash the decision anywhere, do not ask the User if they want to suppress this ticket in future runs.
Description drift surfaces as a description delta with {length, hash} envelopes. No resolution move writes the description body. This is locked by Q10 of issue #111.
When a ticket has description drift, surface it alongside the scalar drift but with an explicit manual-merge instruction:
Description drift detected on wiki/tasks/foo.md against asana (length 412 → 487, hash a1b2c3d4 → e5f6a7b8). The body has not been pulled. To resolve, open the Asana task and the wiki page side by side and merge by hand. No automated write is available.
The User can still pick one of the four moves for the scalar fields — the description drift is reported independently and the chosen move does not touch the body. After "Accept Asana" or "Accept Jira", the wiki body still does not change; only the eight scalar fields and the activity-log line are written.
After walking every drifted ticket, output a one-line summary:
Reconciled N drifted tickets: A accepted Asana, J accepted Jira, W accepted Wiki, S skipped, D have description drift remaining for manual merge.
transition call goes through confirm request → transition --confirm-token. The CLI refuses to write without a valid token; do not try to bypass.skipped: true for them; surface that fact and move on.pull already stamps updated and appends one activity-log line. Do not also call frontmatter set updated after a pull — that would double-stamp.A conversational walkthrough: one summary line up front, then per-ticket prompts. Use short bullets for the 3-way view. Do not dump JSON. Do not paste full backend responses. Keep each prompt small enough to scan in one breath.
Capability discovery — the canonical answer to 'what can you do?' and 'what do I say to ...?'
Close skill — draft resolution comment, sync backends, redirect to next task
Start skill — begin task with optional backend sync
End-of-day wrap-up skill — daily summary and status update
Morning brief skill — prioritized daily summary
Triage skill — walk untriaged Asana pages, push a new ticket or mark not-needed