Skill v1.0.1
currentAutomated scan100/100+2 new
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]" metadata: version: "1.0.1"
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 or linking GitHub issues for global changes.
Calls are 4.0 tool calls; shapes live in `../_generated/call-shapes.md`. Every answer is the response envelope (ok, data or error). A write is {ops: [...]} of flat elements (one per to-call block here). Results come back as data.results[i] in request order with data.summary {ok, failed}; read ops of an ops tool (config get, validate, effective) answer there too. These calls are non-atomic: if any element fails, even in a one-element call, the answer is ok: false with top-level error.code: partial_failure (no detail, no fix) beside the full data, and the cause is that element's data.results[i].error (code, message, detail, fix). Any other top-level error.code is a call-level rejection (envelope, unknown field, access).
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 C2/C4/C5 contract referenced in Step 5.
Step 0 - Parse Arguments and Read Config
Parse $ARGUMENTS:
- A UUID: single-proposal mode. Skip Step 1 and go to Step 2 for that item.
- `--scope global` or `--scope project`: run only that scope's discovery query. Otherwise run both.
Read the config file directly (a file read, not an MCP call): the path on the SessionStart context's Config: line when there is one, otherwise the workspace .taskorchestrator/config.yaml. In personal scope that path is the user-level file. Below, "the config file" is whichever you read here. Read:
project.rootId/project.name: enables the project-scoped discovery query and the per-root push in Step 5. In personal scope the root is the anchor-only personal root: skip the project-scoped discovery query (proposals live in the global container), but a project-scoped acceptance still edits and pushes the user-level file with rootId = the personal root. Without a rootId, project-scoped discovery and acceptance are unavailable; proceed global-only.extensions.claude-code.retrospective.github_feedback.enabled: defaultfalsewhen the block or key is absent.extensions.claude-code.retrospective.github_feedback.repo: defaultjpicklyk/task-orchestrator.
A leftover top-level retrospective: block is not read by the plugin and is rejected by a strict push (see Step 5, Accept - project-scoped).
Step 1 - Discovery
Skip in single-proposal mode. Run both queries in parallel (unless --scope narrows to one).
Global. Find the "Improvement Proposals" container, then read its children:
{"tool": "items_read", "select": "search", "args": {"query": "Improvement Proposals", "kinds": ["item"], "limit": 5}}
A search hit carries no depth, parent or type: take the hit titled exactly Improvement Proposals and confirm it is a root (items_read get, no parentId); a per-project container of that title sits under a project root and is not the global one.
{"tool": "items_read", "select": "overview", "args": {"anchorId": "<container-uuid>"}}
Project-scoped (only with a known project.rootId):
{"tool": "items_read", "select": "list", "args": {"tags": ["improvement-proposal"], "ancestorId": "<rootId>", "limit": 50}}
Partition results
Classify each candidate by role and notes:
- Pending: role
queue, noadoption-decisionnote yet. The main triage set. - Deferred: role
queue, with anadoption-decisionnote whose body starts with "deferred". List separately; do not re-offer unless the user asks to revisit deferred proposals or the stated revisit condition plausibly holds now. - Stalled: role
work(accepted and advanced bystartbut never reachedreviewor terminal: an interrupted adoption). Flag as stalled adoption; offer to resume (fill remaining notes andcomplete) or cancel. - Awaiting verification: role
review(accepted and applied;outcome-verificationunfilled). List separately as `<short-id>- awaiting verification (accepted <date from its adoption-decision note>). No decision is offered:/session-retrospectiveStep 8a fillsoutcome-verification` once at least 2 later retrospectives exist. - Terminal: skip.
If nothing is pending (no queue-role item without an adoption-decision, and no stalled item), report No pending improvement proposals. and stop.
Step 2 - Load Proposal Detail
For each candidate (pending and stalled; cap 15, taking the 15 oldest by creation and noting the overflow), list its notes by key, then get the bodies that exist. A list carries no bodies.
{"tool": "notes_read", "select": "list", "args": {"itemId": "<uuid>", "keys": ["proposal", "adoption-decision"]}}
{"tool": "notes_read", "select": "get", "args": {"itemId": "<uuid>", "key": "proposal"}}
From the notes extract:
- Proposal body: the
proposalnote (or the item'ssummaryif the note is absent; an absent note is backfilled before any Accept, see Step 5). - Scope clause: the scope classification the note states (global or project-specific). If it cannot be parsed unambiguously, treat as global: never auto-edit a project's config on an ambiguous scope reading.
- Existing `github-issue:` line: the last line of the proposal body,
github-issue: <url>. Carry it forward so the global-accept path reuses it instead of filing a duplicate.
Step 3 - Present Triage Table
One compact table of all pending and stalled candidates; deferred proposals get a short list underneath.
| # | 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 and session count that graduated the proposal.
- Age: days since creation. Issue: the existing
github-issue:link, else—.
Below it list proposals awaiting verification (no decision offered), deferred proposals as ` <short-id> - deferred: <condition> (deferred <date>) and stalled ones as <short-id> - stalled in work: <hint> `.
Step 4 - Collect Decisions
For each pending or stalled proposal, show the proposed change (the exact YAML it names, or the file and section it targets) and ask via AskUserQuestion:
◆ "<proposal title>" [scope: project]Proposed change:<exact YAML or file+section from the proposal note>1. Accept - apply this change2. Reject - do not adopt, record why3. Defer - revisit later, leave in queue4. Skip - no decision this round
For Reject or Defer take a one-line reason from the follow-up (or an "Other" free-text option) so the rationale is recorded verbatim in the adoption-decision note. Skip leaves the item untouched: no note, no transition.
Step 5 - Carry Out Dispositions
Before any Accept - ensure the proposal note
The floor schema requires the proposal note in the queue phase, and start is gate-blocked without it. A proposal created before the retrospective filled it has none, so every Accept path (project, global, maintainer or community) first checks Step 2's list. When the proposal note is absent, upsert it from the item's summary, the Step 2 scope classification and a backfill marker, before any config edit, push or `start`:
{"tool": "notes_write", "op": "upsert", "args": {"itemId": "<uuid>", "key": "proposal", "role": "queue", "body": "Problem and evidence: <from the item summary>.\nChange: <the exact change the summary names>.\nExpected effect: <as stated in the summary, or not stated>.\nBlast radius: <as stated, or not stated>.\nScope: <global | project-specific; target rootId>.\nbackfilled by review-proposals <YYYY-MM-DD>"}}
Check data.results[0].ok. Never write a note that is only a URL.
Accept - project-scoped
- Extract the exact YAML from the proposal note. If it has none ready to apply, draft the edit, show it as a diff against the current config file, and confirm via
AskUserQuestionbefore applying anything. - A different `rootId`. If the proposal targets a different
rootIdthan this workspace'sproject.rootId, do not edit this config. Upsertadoption-decisionrecording that the change must be applied from the owning workspace, and treat the item as deferred (leave it in queue; do not advance). - Otherwise Edit the config file to apply the change (additive and targeted, not a rewrite).
- Validate the full file text, LF-normalized, before pushing. Act on
data.results[0].validanddata.results[0].errors(each{path, reason}); validate reports an invalid document there, it does not fail the element withconfig_invalid. Strict validation rejects an unknown key: a top-levelretrospective:ororchestration:block fails, so move it underextensions.claude-code. Rules: `current/docs/config-reference.md`.
{"tool": "config", "op": "validate", "args": {"rootId": "<rootId>", "configYaml": "<full file text>"}}
- Read the head version, then push with it as
expectedVersion:
{"tool": "config", "op": "get", "args": {"rootId": "<rootId>"}}
{"tool": "config", "op": "push", "args": {"rootId": "<rootId>", "configYaml": "<full file text>", "expectedVersion": "<head version from get>"}}
expectedVersion is the data.results[0].version that get returned (an integer, 1 or greater; 0 is invalid_request). When the root has no config yet, get's element fails with not_found: omit expectedVersion from the push. Then check ok and data.results[0].ok; on a failure read data.results[0].error.code. On version_conflict (error.detail {expected, actual}) never force blindly: get the head again. If its version differs from the one you sent, the head moved: diff its configYaml against the local file, show the user, and once they confirm the local file wins re-push with the new head version as expectedVersion. If it still equals what you sent, the local text matches an older version (superseded): re-push with force: true and the same expectedVersion only after the user confirms that revert. force never skips the compare-and-set. config_invalid carries error.detail.errors as {path, reason} pointers: fix and re-validate.
- Upsert the closure note:
{"tool": "notes_write", "op": "upsert", "args": {"itemId": "<uuid>", "key": "adoption-decision", "role": "work", "body": "accepted - <rationale>. Applied to <config file actually edited> (<section>), pushed per-root <rootId> on <YYYY-MM-DD>."}}
- Advance with
starttwice, as two sequential calls (queue to work, then work to review):
{"tool": "advance", "op": "transition", "args": {"itemId": "<uuid>", "trigger": "start"}}
Read ok and data.results[0].newRole; on a failure data.results[0].error.code (gate_blocked lists error.detail.missing). Do not use complete: it jumps to terminal and gate-checks required notes across all phases, so the intentionally unfilled outcome-verification note would block it. start checks only the current phase's notes and lands in review.
- Stop in `review`. Tell the user the item dwells in
reviewwithoutcome-verificationintentionally unfilled:/session-retrospectiveStep 8a verifies whether the change helped, fills it and completes the item once at least 2 later retrospectives exist. This is by design; the item shows under Awaiting verification until then. - Apply the Source-trend write-back below: the config change is applied, so the source trend retires now.
Accept - global
- Ensure the `proposal` note exists (see Before any Accept), then ensure a GitHub issue exists. If Step 2 found a
github-issue:line, reuse it. Else, ifgithub_feedback.enabledistrueand theghguard (C5) passes, file an issue per the C4 template and append the record-back line (C2) to the proposal note. Else proceed without an issue and say so plainly in theadoption-decisionnote. - Distinguish install type:
gh repo view <repo> --json viewerPermission -q .viewerPermission.
- ADMIN, MAINTAIN or WRITE: maintainer path. Upsert
adoption-decisionwithaccepted - will implement; tracked in <url>.and advancestarttwice (the same dwell-in-review pattern as project-scoped acceptance, with the samecompletecaveat). Outcome verification is filled by a later retrospective. - Anything else, or `gh` unavailable: community handoff (the default). Upsert both notes (one
notes_writecall, twoupsertelements; the first shown):
{"tool": "notes_write", "op": "upsert", "args": {"itemId": "<uuid>", "key": "adoption-decision", "role": "work", "body": "accepted - delegated upstream; tracked in <issue-url>."}}
The second element is key: "outcome-verification", role: "review", body delegated upstream - verification happens in <repo> issue #<N>, not this workspace. Verdict: n/a (handed off). Check data.results[i].ok for both. Then advance start, then complete, as two calls: start enters work, and complete goes straight to terminal because the all-phases note gate passes (proposal, adoption-decision and outcome-verification are filled). The item is now terminal; the GitHub issue is the living tracker.
- Finish with the Source-trend write-back: community handoff (terminal) retires the source trend now; the maintainer path leaves it active until a later retrospective verifies the landed change.
Reject
{"tool": "notes_write", "op": "upsert", "args": {"itemId": "<uuid>", "key": "adoption-decision", "role": "work", "body": "rejected - <reason>. Recorded so future retrospectives do not re-propose."}}
{"tool": "advance", "op": "transition", "args": {"itemId": "<uuid>", "trigger": "cancel", "summary": "rejected - <reason>"}}
Read ok and data.results[0].newRole; on a failure data.results[0].error.code. cancel is gate-free and goes straight to terminal from any non-terminal role. Do not fill outcome-verification for a rejection. Then apply the Source-trend write-back.
Defer
{"tool": "notes_write", "op": "upsert", "args": {"itemId": "<uuid>", "key": "adoption-decision", "role": "work", "body": "deferred - revisit when <condition>. Deferred on <YYYY-MM-DD>."}}
Check ok and data.results[0].ok. No transition: the item stays in queue. A later Accept overwrites the same note by upsert, so accepting a previously deferred proposal needs no special-casing.
Source-trend write-back
Most proposals graduated from a trend item (tag retrospective-trend) whose summary carries GRADUATED -> proposal <short-id>. After a disposition, close the loop at the source using only gate-free operations (items_write update, advance cancel), never start or complete on a trend item. Locate it:
{"tool": "items_read", "select": "search", "args": {"query": "<proposal-short-id>", "kinds": ["item"], "tags": ["retrospective-trend"]}}
Skip silently when no trend matches: a proposal can be created ad hoc.
- After Reject:
getthe trend for its full summary, thenitems_writeupdate {id, summary}with one line appended:proposal <short-id> rejected <YYYY-MM-DD> - do not re-graduate; see its adoption-decision note.The trend stays active, and every future retrospective sees the rejection inline in its Step 4.2 listing. - After Accept, once the change is applied or landed (config pushed, or a community handoff filed and the item taken terminal): retire the trend with
advancetransition,trigger: "cancel",summary: "archived: addressed via proposal <short-id>". While adoption is only tracked (maintainer path dwelling inreview), leave the trend active: the retrospective that fillsoutcome-verificationretires it once the change held. - After Defer: no write-back.
Step 6 - Report
## Proposal Review — <YYYY-MM-DD>| ID | Proposal | Disposition | Action ||----|----------|-------------|--------|| `a1b2c3d4` | Add maxLength guard | Accepted | pushed per-root config (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 /session-retrospective (Step 8a) once at least 2 retrospectives have run since the adoption and the change has had time to show effect.
Troubleshooting
`gh` unavailable or unauthenticated. gh auth status failed (the C5 guard in the shared reference). For global acceptances proceed without an issue and say so in the note (accepted - no GitHub issue filed (gh unavailable).). Filing is best-effort and never blocks the disposition; the user can file the issue later and record the URL on the proposal note.
`version_conflict` on a config push. The head moved since this workspace last read it (another session, manage-schemas or quick-start pushed). get the head, diff, show the user exactly what differs, and after explicit confirmation re-push with the new head version as expectedVersion. force: true bypasses only the superseded-content guard (local text equal to an older version), never the compare-and-set.
`config_invalid` on push. The element error carries error.detail.errors; validate reports the same problems as valid: false with errors. The usual cause is a section written at the top level that belongs under extensions.claude-code (retrospective, orchestration, actor_attribution).
Proposal has no exact YAML. The body describes the change in prose only. Draft the edit, show it as a diff, and get explicit confirmation via AskUserQuestion before applying. Do not guess silently and push.
Proposal targets a different workspace's `rootId`. It was likely created in another project sharing the database. Do not edit this config. Record in adoption-decision that the change must be applied from the owning workspace (name the rootId if known), and leave the proposal in queue.