plugins/dev/skills/briefing-followup/SKILL.md
--- name: briefing-followup description: Interactive walk-through of today's morning briefing. Loads the briefing markdown at thoughts/briefings/YYYY-MM-DD.md (built by [[morning-briefing]]), parses the structured decisions: frontmatter, walks the user through each open decision, and executes the selected action via supported handlers — schedule calendar entry, file Linear ticket, dispatch orchestrator, draft email, ADR-drift-specific actions (update ADR / file code-drift ticket / de
npx skillsauth add coalesce-labs/catalyst plugins/dev/skills/briefing-followupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Invoke as /catalyst-dev:briefing-followup after /catalyst-dev:morning-briefing has
produced today's briefing. The skill reads that briefing's decisions: block and walks
the user through each open decision in turn. Phase 2 wires the real action handlers
listed below; ADR-drift-specific actions ship in Phase 3 (CTL-464) and resolution
write-back to the briefing markdown ships in Phase 4 (CTL-465). See
[[2026-05-16-catalyst-phase-agent-architecture]] §Initiative 3 Phase 2.
| Flag | Meaning |
|---|---|
| --date YYYY-MM-DD | Target briefing date. Default: today (UTC). |
| --file PATH | Override path resolution entirely (test/dev usage). |
| --status STATUS | Decision-status filter: open (default) or all. |
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-plugins/dev}/scripts/briefing-followup"
SESSION_SCRIPT="${CLAUDE_PLUGIN_ROOT:-plugins/dev}/scripts/catalyst-session.sh"
CATALYST_SESSION_ID=$("$SESSION_SCRIPT" start --skill "briefing-followup" \
--ticket "" --workflow "${CATALYST_SESSION_ID:-}")
export CATALYST_SESSION_ID
# Resolve briefing path. Pass --date / --file straight through from the user.
BRIEFING_PATH=$(bash "$SCRIPT_DIR/parse-briefing.sh" path "$@")
DATE=$(basename "$BRIEFING_PATH" .md)
echo "Briefing date: $DATE"
echo "Briefing path: $BRIEFING_PATH"
# Load + validate frontmatter (exits 1 with a helpful suggestion if missing,
# exits 2 if frontmatter is malformed or absent).
if ! FRONTMATTER_JSON=$(bash "$SCRIPT_DIR/parse-briefing.sh" load "$@"); then
"$SESSION_SCRIPT" end "$CATALYST_SESSION_ID" --status failed \
--reason "briefing not found or malformed"
exit 1
fi
If the briefing doesn't exist, parse-briefing.sh prints the resolved path and a
suggestion to run /catalyst-dev:morning-briefing before failing. Surface that message
verbatim to the user.
Render the open decisions as a numbered list with summary + type:
echo
echo "─── Agenda for $DATE ───"
bash "$SCRIPT_DIR/parse-briefing.sh" agenda "$@"
echo "───────────────────────"
echo
DECISION_COUNT=$(bash "$SCRIPT_DIR/parse-briefing.sh" decisions "$@" | jq 'length')
if [[ "$DECISION_COUNT" -eq 0 ]]; then
echo "No open decisions in this briefing. Nothing to follow up on."
"$SESSION_SCRIPT" end "$CATALYST_SESSION_ID" --status done \
--reason "no open decisions"
exit 0
fi
echo "$DECISION_COUNT open decision(s) to walk through."
Resolve the log path before the loop. Inside an orchestrator dispatch
($CATALYST_ORCHESTRATOR_DIR is set), prefer the orchestrator's run directory so the
record lands next to other worker artifacts; otherwise fall back to /tmp for local
runs. Phase 4 (CTL-465) of the parent plan replaces the placeholder log with a real
resolutions: write-back to the briefing markdown; for now the recorder writes both
a TSV log (Phase 1 contract) and a structured JSON file (Phase 2 contract that Phase 4
will consume).
if [[ -n "${CATALYST_ORCHESTRATOR_DIR:-}" ]]; then
LOG_DIR="$CATALYST_ORCHESTRATOR_DIR"
else
LOG_DIR="/tmp"
fi
LOG_FILE="$LOG_DIR/briefing-followup-$DATE.log"
mkdir -p "$LOG_DIR"
: > "$LOG_FILE" # truncate any prior run from today
log_response() {
local id="$1" action="$2" note="${3:-}"
printf '%s\t%s\t%s\t%s\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$id" "$action" "$note" \
>> "$LOG_FILE"
}
# Structured resolution recorder — appends to a JSON array consumed by Phase 4.
# Call after a successful action with the action name and the action's JSON output.
record_resolution() {
local id="$1" action="$2" result_json="${3:-{\}}"
bash "$SCRIPT_DIR/record-resolution.sh" \
--log-dir "$LOG_DIR" --date "$DATE" \
--id "$id" --action "$action" --result "$result_json"
}
For each open decision, present the details and the action set filtered by decision
type. The action handlers live in sibling scripts and each emit a one-line JSON result
on stdout; the skill captures that JSON and feeds it to record_resolution.
DECISIONS_JSON=$(bash "$SCRIPT_DIR/parse-briefing.sh" decisions "$@")
# Iterate over the JSON array. `jq -c .[]` emits one decision per line.
INDEX=0
TOTAL="$DECISION_COUNT"
echo "$DECISIONS_JSON" | jq -c '.[]' | while IFS= read -r dec; do
INDEX=$((INDEX + 1))
ID=$(echo "$dec" | jq -r '.id')
TYPE=$(echo "$dec" | jq -r '.type')
SUMMARY=$(echo "$dec" | jq -r '.summary')
PR_URL=$(echo "$dec" | jq -r '.pr_url // empty')
TICKET=$(echo "$dec" | jq -r '.ticket // empty')
ADR=$(echo "$dec" | jq -r '.adr // empty')
PENDING=$(echo "$dec" | jq -r '.pending // empty')
echo
echo "═══ Decision $INDEX of $TOTAL ═══"
echo "id: $ID"
echo "type: $TYPE"
echo "summary: $SUMMARY"
[[ -n "$PR_URL" ]] && echo "pr: $PR_URL"
[[ -n "$TICKET" ]] && echo "ticket: $TICKET"
[[ -n "$ADR" ]] && echo "adr: $ADR"
[[ -n "$PENDING" ]] && echo "pending: $PENDING"
echo
# A compound-engineering ADR proposal is discriminated by a non-empty
# `pending:` field (morning-briefing emits these as type=judgment_call with a
# `pending:` path, because the frontmatter schema's type enum has no
# `compound_adr` value). Check it before the type switch.
if [[ -n "$PENDING" ]]; then
echo "Actions: [a]pply ADR proposal · [e]dit then apply · [D]efer · [r]eject · [s]kip · [q]uit"
else
case "$TYPE" in
blocked_pr)
echo "Actions: [a]pprove · [r]eject · [d]efer · [o]rchestrate · [s]kip · [q]uit"
;;
adr_drift)
echo "Actions: [u]pdate ADR · [t]icket (code drift) · [D]efer · [s]kip · [q]uit"
;;
*)
echo "Actions: [a]pprove · [r]eject · [d]efer · [c]alendar · [t]icket · [o]rchestrate · [e]mail · [s]kip · [q]uit"
;;
esac
fi
done
When this skill runs in an interactive Claude Code session, present each decision as above and use the model to interpret the user's natural-language response. Map intents to action handlers as follows:
| User intent | Handler | Captures resolution? |
|---|---|---|
| approve / accept / yes / ship it | log_response "$ID" approve "$NOTE" | TSV log only |
| reject / no / dismiss | log_response "$ID" reject "$NOTE" | TSV log only |
| defer / later / skip for today | log_response "$ID" defer "$NOTE" | TSV log only |
| schedule meeting / book time / put on calendar | action-schedule.sh → record_resolution "$ID" schedule_calendar "$JSON" | TSV + JSON |
| file a ticket / open Linear issue | action-ticket.sh → record_resolution "$ID" file_ticket "$JSON" | TSV + JSON |
| dispatch orchestrator / kick off the work / run oneshot | action-orchestrate.sh --bg → record_resolution "$ID" dispatch_orchestrator "$JSON" | TSV + JSON |
| draft email / send a note to X / message Y | action-email.sh → record_resolution "$ID" draft_email "$JSON" | TSV + JSON |
| edit / update the ADR (adr_drift only) | action-adr.sh --mode update --adr-file "$ADR" → record_resolution "$ID" adr_update "$JSON" | TSV + JSON |
| file code-drift ticket / fix the code (adr_drift only) | action-adr.sh --mode ticket --adr-file "$ADR" --team CTL --summary "$SUMMARY" --drift-status "$DRIFT_STATUS" → record_resolution "$ID" adr_ticket "$JSON" | TSV + JSON |
| defer / note as intentional (adr_drift only) | action-adr.sh --mode defer --adr-file "$ADR" --reason "$REASON" → record_resolution "$ID" adr_defer "$JSON" | TSV + JSON |
| apply / approve / accept the ADR proposal (decisions with a pending: path) | action-compound.sh --mode apply --pending "$PENDING" --ticket "$TICKET" → record_resolution "$ID" compound_apply "$JSON" | TSV + JSON |
| edit / tweak then apply the proposal (pending:) | action-compound.sh --mode edit --pending "$PENDING" --ticket "$TICKET" → record_resolution "$ID" compound_edit "$JSON" | TSV + JSON |
| defer / not yet (pending:) | action-compound.sh --mode defer --pending "$PENDING" --ticket "$TICKET" --reason "$REASON" → record_resolution "$ID" compound_defer "$JSON" | TSV + JSON |
| reject / decline the proposal (pending:) | action-compound.sh --mode reject --pending "$PENDING" --ticket "$TICKET" --reason "$REASON" → record_resolution "$ID" compound_reject "$JSON" | TSV + JSON |
| skip | move on without logging |
| quit / stop / done | break out of the loop |
When the user picks a real action handler, call the corresponding script and pass any
context the user supplied. Each handler emits one JSON line on stdout — capture it,
display the relevant field to the user, then call record_resolution so Phase 4 can
write it back to the briefing markdown.
# Example — schedule_calendar from a judgment_call decision:
RESULT=$(bash "$SCRIPT_DIR/action-schedule.sh" \
--title "$EVENT_TITLE" \
--start "$START_ISO8601" \
--end "$END_ISO8601" \
--description "$EVENT_DESCRIPTION")
STATUS=$(echo "$RESULT" | jq -r '.status')
case "$STATUS" in
scheduled) echo "Scheduled — $(echo "$RESULT" | jq -r '.html_link')" ;;
skipped) echo "Skipped: $(echo "$RESULT" | jq -r '.reason')" ;;
*) echo "Failed: $(echo "$RESULT" | jq -r '.reason // "unknown"')" ;;
esac
record_resolution "$ID" schedule_calendar "$RESULT"
log_response "$ID" schedule_calendar "$STATUS"
The same pattern applies to action-ticket.sh, action-orchestrate.sh,
action-email.sh, action-adr.sh, and action-compound.sh — only the script name and
the action label change. All handlers soft-skip cleanly when their underlying tool or
input is missing (e.g. action-compound.sh returns
{"status": "skipped", "reason": "pending proposal not found: ..."} when the proposal
has already been resolved on another machine); the returned skip JSON is captured the
same way as a success result so the resolution log faithfully records what happened.
For a compound-engineering ADR proposal (any decision carrying a pending: path —
morning-briefing emits these as type: judgment_call with a pending: field, since the
frontmatter schema's type enum has no compound_adr value), --pending is that path,
surfaced by morning-briefing from thoughts/shared/compound/pending/*.md:
# Example — apply an approved ADR proposal:
RESULT=$(bash "$SCRIPT_DIR/action-compound.sh" \
--mode apply --pending "$PENDING" --ticket "$TICKET")
STATUS=$(echo "$RESULT" | jq -r '.status')
case "$STATUS" in
applied) echo "Applied — $(echo "$RESULT" | jq -r '.target') $(echo "$RESULT" | jq -r '.adr_id') @ $(echo "$RESULT" | jq -r '.commit_sha')" ;;
deferred) echo "Deferred — proposal left pending" ;;
rejected) echo "Rejected — $(echo "$RESULT" | jq -r '.reason')" ;;
skipped) echo "Skipped: $(echo "$RESULT" | jq -r '.reason')" ;;
*) echo "Failed: $(echo "$RESULT" | jq -r '.reason // "unknown"')" ;;
esac
record_resolution "$ID" compound_apply "$RESULT"
log_response "$ID" compound_apply "$STATUS"
action-compound.sh --mode apply (and edit, which tweaks then applies) is the ONLY
writer of docs/adrs.md in the system — the ticket-compound curator only ever
proposes ADR changes; a human approves them here.
Per-decision the user may attach a one-line note (e.g., why they rejected, what the
calendar event is for). Pass it as the third argument to log_response and, when
relevant, as --description / --body to the action handler.
Before ending the session, persist the recorded resolutions into the briefing
markdown's frontmatter resolutions: block and append a "## Decisions Made
Today" section to the body. The script commits to the routine-scoped branch
(when running inside the morning-briefing routine's writable clone) and emits
a briefing.followup.complete.<date> event so the next morning's briefing
routine can surface yesterday's decisions as carryovers.
WRITEBACK_RESULT=$(bash "$SCRIPT_DIR/writeback.sh" \
--briefing "$BRIEFING_PATH" \
--resolutions "$LOG_DIR/briefing-followup-$DATE-resolutions.json" \
--date "$DATE" 2>&1)
WRITEBACK_STATUS=$(echo "$WRITEBACK_RESULT" | jq -r '.status // "failed"')
case "$WRITEBACK_STATUS" in
updated)
COMMIT_SHA=$(echo "$WRITEBACK_RESULT" | jq -r '.commit_sha // "none"')
echo "Wrote resolutions back to $BRIEFING_PATH (commit: $COMMIT_SHA)"
;;
skipped)
REASON=$(echo "$WRITEBACK_RESULT" | jq -r '.reason // "no resolutions"')
echo "Skipped write-back: $REASON"
;;
*)
echo "Write-back failed: $WRITEBACK_RESULT" >&2
;;
esac
Flags that callers may pass to writeback.sh:
| Flag | Meaning |
|---|---|
| --no-commit | Update the markdown in place but do not run git commit. |
| --no-push | Commit but do not push. Default in cloud routine mode is push. |
| --no-event | Skip emitting briefing.followup.complete.<date>. |
| --events-dir DIR | Override the event log dir (defaults to $CATALYST_DIR/events). |
The script is idempotent: re-running with the same resolutions file produces
the same markdown (the previous "## Decisions Made Today" block is stripped
before the new one is appended, and the resolutions: array is replaced
rather than amended).
echo
echo "Logged $(wc -l < "$LOG_FILE" | tr -d ' ') response(s) to $LOG_FILE"
"$SESSION_SCRIPT" end "$CATALYST_SESSION_ID" --status done \
--reason "briefing-followup completed for $DATE"
thoughts/briefings/YYYY-MM-DD.md produced by [[morning-briefing]],
validated against plugins/dev/templates/briefing-frontmatter.schema.json.$CATALYST_ORCHESTRATOR_DIR/briefing-followup-<date>.log (or /tmp/... outside
orchestrator mode), one TSV line per resolved decision:
<utc-timestamp>\t<id>\t<action>\t<note>.$LOG_DIR/briefing-followup-<date>-resolutions.json, one entry per resolution that
invoked an action handler. Each entry is
{decision_id, action, timestamp, result} where result is the JSON the handler
returned. Phase 4 (CTL-465) reads this file to write the resolutions: block back
to the briefing markdown frontmatter.Per [[2026-05-16-catalyst-phase-agent-architecture]] §Initiative 3 Phase 2, the
following sibling scripts implement the supported actions. Each emits one JSON line on
stdout and exits 0 on success or soft-skip ({"status": "skipped", "reason": "..."});
non-zero exit indicates a hard failure (handler reached but the underlying API
returned no usable result).
| Action | Script | Output JSON (success) | Soft-skip trigger |
|---|---|---|---|
| Schedule a calendar event | action-schedule.sh | {event_id, html_link, status: "scheduled"} | GOOGLE_OAUTH_ACCESS_TOKEN unset |
| File a Linear ticket | action-ticket.sh | {identifier, url, status: "filed"} | linearis not on PATH |
| Dispatch orchestrator | action-orchestrate.sh | {orchestrator_id, status: "dispatched"} | claude (or $CATALYST_DISPATCH_CLAUDE_BIN) not on PATH |
| Draft an email | action-email.sh | {draft_id, status: "drafted"} | GMAIL_OAUTH_ACCESS_TOKEN unset |
| Update ADR (adr_drift) | action-adr.sh --mode update | {adr_file, adr_id, commit_sha, status: "updated"} | $EDITOR unset, no save, or ADR not in a git repo |
| File code-drift ticket (adr_drift) | action-adr.sh --mode ticket | {identifier, url, adr_id, status: "filed"} | linearis not on PATH |
| Defer ADR drift (adr_drift) | action-adr.sh --mode defer | {adr_file, adr_id, commit_sha, status: "deferred"} | ADR not in a git repo |
| Apply ADR proposal (pending:) | action-compound.sh --mode apply | {adrs_file, adr_id, target, commit_sha, status: "applied"} | proposal missing, or not in a git repo |
| Edit + apply proposal (pending:) | action-compound.sh --mode edit | {adrs_file, adr_id, target, commit_sha, status: "applied"} | $EDITOR unset, proposal missing, or not in a git repo |
| Defer ADR proposal (pending:) | action-compound.sh --mode defer | {pending, ticket, status: "deferred"} | proposal missing |
| Reject ADR proposal (pending:) | action-compound.sh --mode reject | {pending, ticket, reason, status: "rejected"} | proposal missing |
See cma/mcp/google-calendar.md and cma/mcp/gmail.md for the OAuth setup required
to bypass the calendar / email soft-skip paths. Linear and orchestrator handlers
require no extra setup beyond having linearis / claude on PATH (the standard local
dev environment provides both).
Each handler accepts --help to print its flag set. The skill captures the JSON,
surfaces the relevant field to the user, then calls record_resolution "$ID" <action> "$JSON" so the result lands in the resolutions JSON file for Phase 4 write-back.
action-adr.sh
with --mode update|ticket|defer for the three options per the parent plan.resolutions: block via
writeback.sh. Also appends a "## Decisions Made Today" section to the body,
commits to the routine-scoped branch (routines/briefings in the cloud
routine), and emits briefing.followup.complete.<date> so the next morning's
briefing routine reads yesterday's resolutions as carryovers.pending: path surface the ticket-compound curator's queued ADR proposals
(thoughts/shared/compound/pending/*.md; emitted as type: judgment_call
since the schema enum has no compound_adr). action-compound.sh resolves
them — apply (the only writer of docs/adrs.md), edit (tweak then apply),
defer, reject — keeping ADR edits human-gated and off the critical path.testing
Phase-agent that fixes a failing verify verdict so the pipeline self-heals instead of stalling to needs-human (CTL-653). Reads `${ORCH_DIR}/workers/<ticket>/verify.json`, fixes the `findings[]` (every severity:"high" plus the regression_risk drivers) directly via Edit/Write, commits the remediation, and emits `phase.remediate.complete.<ticket>`. The scheduler's router then re-dispatches `verify` to re-check (the verify⇄remediate cycle, cap 3). Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
development
Phase agent for the verify step of the 9-phase orchestrator pipeline (CTL-450). NEW skill — has no canonical wrapper. Runs read-only adversarial verification against the implement-phase diff: tsc, tests, lint, security scan, reward-hacking scan, code review, test coverage, silent-failure hunt. Writes ${ORCH_DIR}/workers/<TICKET>/verify.json then emits phase.verify.complete.<ticket>. Reads phase-implement.json as its prior-phase artifact. NEVER writes application code — only test files allowed. Spawned via phase-agent-dispatch via slash command — hence `user-invocable: true`.
tools
--- name: phase-triage description: Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies dependencies, estimates scope, writes triage.json, and posts a triage analysis comment to Linear. Triage completion is signaled by that comment plus the local triage.json — there is no `triaged` label. Emits phase.triage.complete.<TICKET> on success and phase.triage.failed.<TICKET> on error. Dispatched by the phase-agent orchestrator (CTL-452)
testing
Phase agent for the review step of the 9-phase orchestrator pipeline (CTL-450). Wraps the /review skill (gstack) — explicitly skips /ultrareview per user decision. Reads verify.json from the prior phase, runs /review against the diff, writes ${ORCH_DIR}/workers/<TICKET>/review.json, and creates a remediation commit for any HIGH-severity finding that has a deterministic fix. Emits phase.review.complete.<ticket>. Spawned via phase-agent-dispatch via slash command — hence `user-invocable: true`.