| name | review-proposals |
| description | Triages pending improvement-proposal MCP items — presents each with its scope and evidence, collects an accept/reject/defer decision per proposal, and carries out the disposition: project-scoped acceptances get their exact YAML applied to .taskorchestrator/config.yaml and pushed per-root; global acceptances get a tracked GitHub issue (or dwell in review for the maintainer); rejections are recorded and cancelled; deferrals are recorded and left in queue. Use when a user says: review proposals, triage proposals, pending proposals, what proposals are open, adopt proposals, process improvement proposals, accept or reject proposals. |
| argument-hint | [optional: proposal UUID | --scope global|project] |
Review Proposals
Triages improvement-proposal MCP items created by /session-retrospective when a cross-session
trend graduates. Presents pending proposals, collects a per-proposal decision, and carries out the
disposition — including applying project-scoped config changes and filing/linking GitHub issues for
global changes.
Shared GitHub conventions (issue template, scrub rule, gh guard, dedup procedure) are not
duplicated here — see
<skill-base-dir>/../session-retrospective/references/github-feedback.md for the full C2/C4/C5
contract and dedup steps referenced throughout Step 5.
Step 0 — Parse Arguments and Read Config
Parse $ARGUMENTS:
- A UUID → single-proposal mode: skip Step 1 discovery entirely: go straight to Step 2 for
that one item.
--scope global or --scope project → filter Step 1 discovery to only that scope's query.
Absent → run both discovery queries.
- Neither → full discovery, both scopes.
Read the workspace .taskorchestrator/config.yaml directly (file read, not MCP) for:
project.rootId / project.name — enables the project-scoped discovery query and per-root push
in Step 5. If absent, project-scoped discovery and project-scoped acceptance are unavailable —
proceed with global-only discovery.
retrospective.github_feedback.enabled — default false if the block or key is absent.
retrospective.github_feedback.repo — default jpicklyk/task-orchestrator if absent.
Step 1 — Discovery
Skip this step entirely in single-proposal mode (UUID argument supplied).
Run both queries in parallel (unless --scope narrows to one):
Global:
query_items(operation="search", query="Improvement Proposals", limit=5)
Find the "Improvement Proposals" container from the result, then:
query_items(operation="overview", anchorId="<container-uuid>", includeChildren=true)
Project-scoped (only if project.rootId is known):
query_items(operation="search", tags="improvement-proposal", ancestorId="<rootId>", limit=50)
Partition results
For each candidate item, classify by role and notes:
- Pending — role
queue, no adoption-decision note yet. The main triage set.
- Deferred — role
queue, has an adoption-decision note whose body starts with "deferred".
List in a separate section; do not re-offer for a decision unless the user explicitly asks to
revisit deferred proposals, or the stated revisit condition in the note plausibly holds now.
- Stalled — role
work (item was accepted and advanced to work via start but never reached
review/terminal — an interrupted or stuck adoption). Flag as stalled adoption; offer to
resume (fill remaining notes and complete) or cancel.
- Terminal — skip; already resolved (accepted-and-verified, rejected, or fully handed off).
If nothing is pending (no queue-role items without an adoption-decision, and no stalled items),
report:
No pending improvement proposals.
and stop — do not proceed to later steps.
Step 2 — Load Proposal Detail
For each candidate (pending + stalled; cap at 15 — if more, take the 15 oldest by creation and note
the overflow count), fetch notes:
query_notes(operation="list", itemId="<uuid>", includeBody=true)
From the returned notes, extract:
- Proposal body — the
proposal note (or the item's summary if the note is absent).
- Scope clause — read the scope classification the note states (global vs project-specific).
If it cannot be parsed unambiguously, treat as global — this is conservative: never auto-edit
a project's
config.yaml on an ambiguous scope reading.
- Existing
github-issue: line — the last line of the proposal note body, if present, in the
form github-issue: <url>. Carry this forward so Step 5's global-accept path can reuse it instead
of filing a duplicate.
Step 3 — Present Triage Table
Render one compact table covering all pending + stalled candidates (deferred proposals get their
own short list underneath, not full rows):
| # | ID | Proposal | Scope | Evidence | Age | Issue |
|---|-----|----------|-------|----------|-----|-------|
| 1 | `a1b2c3d4` | Add `session-tracking` maxLength guard | project | 3 sessions, sparse-note trend | 4d | — |
| 2 | `e5f6a7b8` | Nudge cooldown too short for solo dev | global | 2 sessions, friction theme | 1d | #142 |
- Evidence — a one-line summary of the trend/session count that graduated this proposal (from
the proposal body or summary).
- Age — days since creation.
- Issue — existing
github-issue: link if present, else —.
Below the table, list deferred proposals as a short reminder line each: `<short-id>` — deferred: <condition> (deferred <date>) and stalled proposals as `<short-id>` — stalled in work: <hint>.
Step 4 — Collect Decisions
For each pending/stalled proposal, show the proposed change (the exact YAML the proposal names, or
the file + section it targets) and ask via AskUserQuestion:
◆ "<proposal title>" [scope: project]
Proposed change:
<exact YAML or file+section from the proposal note>
What would you like to do?
1. Accept — apply this change
2. Reject — do not adopt, record why
3. Defer — revisit later, leave in queue
4. Skip — no decision this round
If the user picks Reject or Defer, either take a one-line reason from their follow-up or offer an
"Other" free-text option so the rationale can be recorded verbatim in the adoption-decision note.
Skip leaves the item untouched — no note upsert, no advance.
Step 5 — Carry Out Dispositions
Accept — project-scoped
- Extract the exact YAML from the proposal note. If the proposal doesn't include ready-to-apply
YAML, draft the edit yourself, show it to the user as a diff against the current
.taskorchestrator/config.yaml, and confirm via AskUserQuestion before applying anything.
- If the proposal targets a different
rootId than this workspace's project.rootId: do not
edit this workspace's config. Instead, upsert adoption-decision recording that the change must
be applied from the owning workspace, and treat the item as deferred (leave it in queue) —
do not advance it.
- Otherwise, Edit the workspace
.taskorchestrator/config.yaml to apply the change.
- Validate before pushing — same bar as
manage-schemas' config-format rules (see
<skill-base-dir>/../manage-schemas/references/config-format.md):
- File still parses as valid YAML.
- Any note
role values are limited to queue | work | review.
- Existing sections are untouched — the edit is additive/targeted, not a rewrite.
- Push:
manage_project_config(operation="push", rootId="<rootId>", configYaml="<full file content>").
- On
CONFLICT_ERROR: never force blindly. Call manage_project_config(operation="get", rootId="<rootId>"),
diff the server's stored config against the local file, and surface the divergence to the user.
Only re-push with force: true after explicit user confirmation.
- A response listing
ignoredSections containing retrospective and/or project is expected,
not an error — those sections are client-side-only and never resolved server-side.
- Upsert the closure note:
manage_notes(operation="upsert", notes=[{
itemId: "<uuid>",
key: "adoption-decision",
role: "work",
body: "accepted — <rationale>. Applied to .taskorchestrator/config.yaml (<section>), pushed per-root <rootId> on <YYYY-MM-DD>."
}])
- Advance with
start twice — queue→work, then work→review:
advance_item(transitions=[{itemId: "<uuid>", trigger: "start"}])
advance_item(transitions=[{itemId: "<uuid>", trigger: "start"}])
Do use here — jumps straight to terminal and gate-checks required
notes across phases, so it would be blocked by the intentionally-unfilled
note. checks only the current phase's notes (,
just filled) and lands in .
Accept — global
- Ensure a GitHub issue exists for the change:
- If Step 2 found an existing
github-issue: line, reuse that URL — no new issue.
- Else, if
retrospective.github_feedback.enabled is true and the gh guard (C5 in the
shared reference) passes, file an issue per the C4 template and append the record-back line
(C2) to the proposal note.
- Else, proceed without an issue — say so plainly in the
adoption-decision note.
- Distinguish install type via:
gh repo view <repo> --json viewerPermission -q .viewerPermission
- ADMIN / MAINTAIN / WRITE ⇒ maintainer path. Upsert
adoption-decision:
"accepted — will implement; tracked in <url>." Advance start twice (queue→work,
work→review) — the same dwell-in-review pattern as project-scoped acceptance, with the same
complete-would-gate-block caveat; outcome-verification is filled by a later retrospective.
- Anything else, or
gh unavailable ⇒ community handoff (default). Upsert both notes:
manage_notes(operation="upsert", notes=[
{
itemId: "<uuid>",
key: "adoption-decision",
role: "work",
body: "accepted — delegated upstream; tracked in <issue-url>."
},
{
itemId: "<uuid>",
key: "outcome-verification",
role: "review",
body: "delegated upstream — verification happens in <repo> issue #<N>, not this workspace. Verdict: n/a (handed off)."
}
])
Then advance to terminal in two calls:
advance_item(transitions=[{itemId: "<uuid>", trigger: "start"}])
advance_item(transitions=[{itemId: "<uuid>", trigger: "complete"}])
start enters work; complete then goes straight to terminal — its all-phases note gate
passes because proposal, adoption-decision, AND outcome-verification are all filled.
The item is now fully terminal — the GitHub issue is the living tracker, not this workspace.
Either way, finish with the Source-trend write-back below: community handoff (terminal)
retires the source trend now; the maintainer path leaves it active until a later retrospective
verifies the landed change.
Reject
manage_notes(operation="upsert", notes=[{
itemId: "<uuid>",
key: "adoption-decision",
role: "work",
body: "rejected — <reason>. Recorded so future retrospectives do not re-propose."
}])
advance_item(transitions=[{itemId: "<uuid>", trigger: "cancel"}])
cancel is a single gate-free call — it goes straight to terminal from any non-terminal role, no
note gate involved. Do not fill outcome-verification for a rejection; there is no change to
verify. Then apply the Source-trend write-back below.
Defer
manage_notes(operation="upsert", notes=[{
itemId: "<uuid>",
key: "adoption-decision",
role: "work",
body: "deferred — revisit when <condition>. Deferred on <YYYY-MM-DD>."
}])
No advance_item call — the item stays in queue. A later Accept decision overwrites this same
note via upsert (last-writer-wins), so re-running this skill and accepting a previously-deferred
proposal works without any special-casing.
Source-trend write-back
Most proposals graduated from a trend item (tags retrospective-trend, one item per cross-session
pattern) whose summary carries GRADUATED -> proposal <short-id>. After carrying out a disposition,
close the loop at the source. Everything here uses only gate-free operations (manage_items update,
advance_item with cancel) — never start or complete on a trend item — so it is safe under
any schema config. Locate the source trend:
query_items(operation="search", query="<proposal-short-id>", scope={tags: ["retrospective-trend"]})
Skip this write-back silently when no trend matches — proposals can also be created ad hoc without
a graduating trend.
- After Reject: append one line to the trend item's summary via
manage_items(operation="update"):
proposal <short-id> rejected <YYYY-MM-DD> — do not re-graduate; see its adoption-decision note.
The trend stays active and keeps accumulating evidence, but every future retrospective now sees
the rejection inline in the Step 4.2 listing instead of having to discover the cancelled proposal.
- After Accept, once the change is actually applied or landed (project-scoped config pushed, or
a community-handoff issue filed and the item taken terminal): retire the trend —
advance_item(itemId="<trend-uuid>", trigger="cancel", summary="archived: addressed via proposal <short-id>").
When adoption is only tracked so far (maintainer path dwelling in review awaiting
implementation), leave the trend active — the later retrospective that fills
outcome-verification retires it once the change demonstrably held.
- After Defer: no trend write-back.
Step 6 — Report
Render a summary table of the run:
## Proposal Review — <YYYY-MM-DD>
| ID | Proposal | Disposition | Action |
|----|----------|-------------|--------|
| `a1b2c3d4` | Add maxLength guard | Accepted | pushed .taskorchestrator/config.yaml (work_item_schemas), dwelling in review |
| `e5f6a7b8` | Nudge cooldown | Accepted (global) | filed https://github.com/jpicklyk/task-orchestrator/issues/143, terminal |
| `c9d0e1f2` | Rename note key | Rejected | cancelled — duplicate of existing key |
| `f3a4b5c6` | Widen skill pointer | Deferred | revisit when trait usage grows |
Close with a reminder: accepted items dwelling in review are not stuck — their
outcome-verification note is filled by a future /session-retrospective run once the applied
change has had a chance to show effect.
Troubleshooting
gh unavailable or unauthenticated
Cause: gh auth status failed (not installed, not logged in, or network issue) — see the C5 guard
in <skill-base-dir>/../session-retrospective/references/github-feedback.md.
Solution: For global acceptances, proceed without filing an issue and say so in the
adoption-decision note ("accepted — no GitHub issue filed (gh unavailable)."). This never blocks
the disposition — filing is best-effort. The user can file the issue manually later and record the
URL back onto the proposal note themselves.
CONFLICT_ERROR on manage_project_config push
Cause: The stored per-root config's fingerprint has moved since this workspace last read it —
someone else (another session, manage-schemas, or quick-start) pushed a newer version.
Solution: Never force blindly. Call manage_project_config(operation="get", rootId="<rootId>"),
diff the returned configYaml against the local file, and show the user exactly what differs.
Only retry with force: true after the user explicitly confirms which version should win — a blind
force can silently revert someone else's concurrent change.
Proposal has no exact YAML to apply
Cause: The proposal body describes the change in prose only (common for proposals that predate a
YAML-inclusion convention, or for changes that aren't schema edits — e.g., a skill wording tweak).
Solution: Draft the edit yourself from the proposal's description, show it to the user as a diff
before touching any file, and get explicit confirmation via AskUserQuestion before applying. Do
not guess silently and push.
Proposal targets a different workspace's rootId
Cause: The proposal's stated scope names a project root UUID that doesn't match this workspace's
project.rootId — the proposal was likely created while working in a different project sharing the
same MCP database.
Solution: Do not edit this workspace's .taskorchestrator/config.yaml. Record in the
adoption-decision note that the change must be applied from the owning workspace (name the
rootId if known), and treat the proposal as deferred — leave it in queue rather than advancing
it, since no action was actually taken here.