@nsicsm/cfed
v0.4.5
Published
CLI for the cfed federal construction proposal AI workspace dogfood loop.
Readme
@nsicsm/cfed
CLI for the cfed federal construction proposal AI workspace. Built for Ahama (BD Lead operator) and Adam (NSI exec demo target) to drive the platform without per-tick curl boilerplate.
Install
npm i -g @nsicsm/cfed
cfed auth login --email [email protected]
# Check your email for the 6-digit code, paste it into the prompt.That's it — zero-config against the production cfed deployment. No env vars required.
Optional env (dev/staging overrides)
The following default to production values and only need to be set when pointing the CLI at a non-prod cfed:
CFED_APP_URL=http://localhost:3000 # default: https://cfed.nsicsm.com
CFED_SUPABASE_URL=https://<project_ref>.supabase.co # default: https://zatknvlpeatcxjewpwlc.supabase.co
CFED_SUPABASE_PUBLISHABLE_KEY=<publishable-key> # default: prod publishable keyThese are PUBLIC values (URLs aren't secrets; Supabase publishable keys are designed to be embedded in client apps and are wire-visible to any anonymous browser hitting the prod web app). The defaults make npm i -g @nsicsm/cfed && cfed auth login zero-config.
Agent/dogfood-mode flags (also optional):
CFED_AGENT=true
CFED_AGENT_NAME=ahama
[email protected]Optional automation override (Ahama agent only)
CFED_SUPABASE_SERVICE_ROLE_KEY=<service-role-jwt>End users NEVER set this. It exists only for non-interactive automation that cannot read an inbox (e.g. the BD-Lead Ahama agent). When set, cfed auth login falls back to the legacy admin/generate_link flow: it mints an OTP server-side using the service-role JWT and verifies it inline, with no interactive prompt and no email round-trip. When unset (the default), cfed auth login POSTs to the cfed server route /api/auth/cli-login, which (server-side) mints an OTP via Supabase admin and delivers it via Resend — the user reads the 6-digit code from their inbox and pastes it back into the CLI prompt.
Changelog
0.4.4
- End-user OTP flow now uses NSI Platform's Resend-backed email delivery (was Supabase built-in SMTP in 0.4.3, which was never wired up — end users never received codes).
- CLI POSTs to
${CFED_APP_URL}/api/auth/cli-loginwhich server-side mints an OTP via Supabase admin and sends it via Resend. - Verify type unified to
'magiclink'for both end-user and agent paths (since both OTPs now come fromadmin.generateLink). - Legacy service-role admin/generate_link path still works when
CFED_SUPABASE_SERVICE_ROLE_KEYis set (Ahama agent workflow); byte-identical request semantics.
0.4.3
- End-user zero-config:
cfed auth login --email <addr>no longer requiresCFED_SUPABASE_SERVICE_ROLE_KEY. - New OTP-code flow: enter the 6-digit code from your email when prompted.
- Agent/automation users with
CFED_SUPABASE_SERVICE_ROLE_KEYset keep the existing admin/generate_link path (no behavior change).
0.4.2
- Fix:
cfed --versionnow reports the package version correctly (0.4.1 shipped with a stale hardcoded version string incli.ts).
0.4.1
- Zero-config:
CFED_APP_URL/CFED_SUPABASE_URL/CFED_SUPABASE_PUBLISHABLE_KEYnow default to production values; env vars remain as optional dev/staging overrides. CFED_SUPABASE_SERVICE_ROLE_KEY: retained as required.cfed auth logincalls Supabase Authadmin/generate_linkdirectly to mint a magic-link OTP — there is no /api/* route substitute because the operator has no user session yet at login time. The service-role key is a secret and is intentionally NOT defaulted.
Commands
cfed auth login [--email <addr>] [--json]
Mints a Supabase session for the given email and persists it to ~/.config/cfed/session.json (mode 600). Required for all subsequent commands.
cfed auth login --email [email protected]
# Check your email for a 6-digit code; paste it into the prompt.By default (end-user flow, zero-config) the command calls Supabase Auth POST /auth/v1/otp with the publishable key, which triggers an email containing a 6-digit OTP code; the CLI then prompts you to paste the code and verifies it via POST /auth/v1/verify. No secrets required.
If CFED_SUPABASE_SERVICE_ROLE_KEY is set, the command instead calls POST /auth/v1/admin/generate_link (which returns the OTP in the response body) and verifies inline — the non-interactive automation path used by the Ahama BD-Lead agent. End users never need this.
Email template note: the Supabase project's "Magic Link" email template must include the {{ .Token }} variable for the 6-digit code to appear in the user's inbox. Default Supabase templates include both {{ .ConfirmationURL }} and {{ .Token }}. If your email contains only a clickable link with no code, ask your admin to edit the template at Authentication → Email Templates → Magic Link to include {{ .Token }}.
cfed exec <opp-id> [--repair-buckets] [--json]
Runs the executive_summary skill for an opportunity. Refreshes the session in-band if expired (emits a one-line stderr hint on actual refresh).
cfed exec 11111111-1111-1111-1111-111111111111
cfed exec 11111111-1111-1111-1111-111111111111 --repair-bucketsWith --repair-buckets, the CLI first verifies the project for that opportunity has all 4 canonical bucket kinds (solicitation, competitive-advantage, deliverables, vendor-bids); any missing kinds trigger the create_project_buckets RPC before the exec call. Orphan buckets are ignored.
cfed worker drain [--max-ticks N] [--until-empty] [--json]
Fires POST /api/work/queue-poll to drain the background worker queue. Reuses the same session-cookie auth as cfed exec — no separate token dance.
cfed worker drain # one tick
cfed worker drain --max-ticks 5 # five consecutive ticks, 2s between
cfed worker drain --until-empty # tick until processed=0 (capped at 200)Each tick prints processed, succeeded, failed, and runtime_ms. After the run, a one-line cumulative summary is printed. With --json, a single JSON object is emitted to stdout containing all ticks plus totals and a stop_reason (max_ticks, empty_queue, or until_empty_cap).
cfed ingest sam-gov [--since YYYY-MM-DD] [--json]
Fires POST /api/ingest/sam-gov to trigger the Layer 1 SAM.gov ingest pull. Reuses the same session-cookie auth as cfed exec / cfed worker drain — no separate base64 cookie dance.
cfed ingest sam-gov # use the route's default window
cfed ingest sam-gov --since 2026-05-01 # override the window start dateHuman output is a single compact line: inserted_count, updated_count, amendments_count, parse_queue_count, attachment_count, errors (count). With --json, a single JSON object with the same fields (errors_count) is emitted to stdout. Non-2xx responses surface the upstream error body and exit non-zero.
cfed audit <opp-id> [--json]
Multi-source client-side join over an opportunity: opp basics + recent skill runs + approvals. Three fetches fire in parallel (Promise.all) against existing surfaces — no new server routes were added.
cfed audit 11111111-1111-1111-1111-111111111111
cfed --json audit 11111111-1111-1111-1111-111111111111Sources:
GET /api/opportunities/{id}— opp basics (title, agency, estimated_value, pipeline_stage, deadline_response). 404 → exit 2 withaudit <opp-id> not_found.GET /rest/v1/agent_runs?opportunity_id=eq.{id}— recent skill runs (id, agent_type, status, started_at, completed_at, error, result_summary). Direct PostgREST — no dedicated/api/*route exists at v1.GET /rest/v1/approvals?opportunity_id=eq.{id}— approvals (stage, final_status, egm/ogm decisions). Direct PostgREST — no dedicated/api/*route exists at v1.
Human output is a 3-section layout (opportunity / agent_runs / approvals) with bullet rows per entry. With --json, a single object {opportunity, agent_runs, approvals} is emitted to stdout — easy to pipe through jq.
executive_summaries and audit_log joins are deferred to cfed audit v2.1.
cfed pipeline [snapshot] [--near-deadline-days N] [--source X] [--json]
BD-Lead morning snapshot: "what's the state of the world?" One PostgREST round-trip pulls active+terminal-stage opportunities (auto_eliminated rows excluded server-side) and aggregates client-side into per-stage counts, total estimated value, and near-deadline counts.
cfed pipeline # 14-day near-deadline window (default)
cfed pipeline snapshot # explicit form, same behavior as `cfed pipeline`
cfed pipeline --near-deadline-days 30 # widen the deadline window
cfed pipeline --source sam.gov # filter to a single ingest source
cfed --json pipeline # structured outputHuman output:
== pipeline (199 active opps, $127.1M) ==
forecast 12 $78.8M 5 near deadline
pending-triage 123 $12.0M 87 near deadline
pre_rfp 6 $0.7M 4 near deadline
rfp_released 55 $9.8M 27 near deadline
proposal_in_progress 2 $1.4M 2 near deadline
matoc 1 $25.0M 0 near deadline
== terminal ==
submitted 1
awarded 1
lost 1The 6 active stages render in lifecycle order: forecast, pending-triage, pre_rfp, rfp_released, proposal_in_progress, matoc. The 3 terminal stages render in submitted, awarded, lost. Money is formatted as $X.XM / $X.XK / $N for sub-thousand. With --json, the payload is {active: [...], terminal: [...], totals: {active_count, active_value}}. auto_eliminated=true rows are excluded from all counts (matches the active-pipeline kanban filter from 2026-05-12).
cfed pipeline move <opp-id> --stage <target> --reason "..." [--allow-backwards] [--json]
Move an opportunity to a target pipeline_stage. Direct PostgREST writes: PATCH /rest/v1/opportunities (sets pipeline_stage only) followed by POST /rest/v1/audit_log (entry with action='pipeline_move', before_state, after_state). Pipeline move is the BD-Lead's stage-edit primitive — it does NOT touch auto_eliminated (that's cfed triage eliminate's territory) and does NOT trigger any agent runs.
cfed pipeline move 11111111-1111-1111-1111-111111111111 --stage rfp_released --reason "RFP dropped on SAM."
cfed pipeline move 11111111-1111-1111-1111-111111111111 --stage awarded --reason "Award notice received."
cfed pipeline move 11111111-1111-1111-1111-111111111111 --stage forecast --reason "Operator override." --allow-backwards
cfed --json pipeline move 11111111-1111-1111-1111-111111111111 --stage submitted --reason "Proposal submitted."--stage accepts one of forecast, pending-triage, pre_rfp, rfp_released, proposal_in_progress, matoc, submitted, awarded, lost, cancelled. Any other value exits 2 before any network call. --reason is REQUIRED (missing or empty exits 2). Bad <opp-id> (non-UUID) also exits 2 before any network call.
Backwards-transition guard. Lifecycle order is forecast → pending-triage → pre_rfp → rfp_released → proposal_in_progress → matoc → submitted → awarded. Terminal stages (awarded, lost, cancelled) are end-states. If the requested transition is backwards in this order, OR moves OUT of a terminal stage, the command exits 2 with backwards transition <prev> → <target> requires --allow-backwards. Pass --allow-backwards to override (operator action recorded in audit_log.reason). Same-stage no-op moves are not gated and emit an idempotent audit row.
Opp-not-found exits 2 with pipeline move <id> not_found. PATCH or audit_log non-2xx surface the upstream error body and exit Server (3). No rollback on partial failure — the audit gap is intentional (matches cfed triage accept/eliminate semantics) and surfaces in the next dogfood pass.
Human output: pipeline move <opp-id> → pipeline_stage=<prev>→<target> (reason: <tagged reason>). With --json, the payload is {opportunity_id, action: 'pipeline_move', prev_state, new_state, audit_log_id, reason}. When CFED_AGENT=true, --reason is auto-tagged with [dogfood-<CFED_AGENT_NAME>] (idempotent — preserves an existing prefix).
cfed opportunities list [--stage X] [--source X] [--naics 123456] [--deadline-within 14d] [--value-min 5M] [--limit 20] [--sort deadline|value|created] [--json]
BD-Lead workhorse list view: "show me what's on the board." One PostgREST round-trip pulls non-auto-eliminated opportunities and renders a column-aligned table.
cfed opportunities list # default: deadline asc, limit 20
cfed opportunities list --stage rfp_released --limit 50
cfed opportunities list --source sam.gov --deadline-within 14d
cfed opportunities list --naics 236220 --value-min 5M
cfed opportunities list --sort value # highest estimated_value first
cfed --json opportunities list --stage pending-triage # structured outputDefault sort is deadline (asc, nulls last). value and created are desc. Limit defaults to 20 and caps at 200. --value-min accepts 5M, 500k, or a raw integer. --deadline-within Nd restricts to opportunities whose deadline_response falls between now and now+N days (future-only — past deadlines are excluded). --stage is single-value only; multi-stage filtering is intentionally out of scope.
Human output is a column-aligned table:
== opportunities (3) ==
id value agency title naics source
11111111 $25.0M AFGSC Hangar reroof, Barksdale AFB 236220 sam.gov
22222222 $5.0M USACE Levee repair, Tulsa District 237990 sam.gov
33333333 $700.0K - Small project missing agency - -Columns: id (8-char prefix of UUID) | value ($X.XM / $X.XK / $N) | agency (leaf segment of the hierarchical SAM path — splits on . or /, falls back to full string) | title (truncated to 50 chars with …) | naics | source. With --json, the payload is {opportunities: [...], count, applied_filters} — applied_filters echoes the parsed inputs so downstream tooling can verify it parsed 5M → 5000000 etc.
Subsequent list commands (approvals list, triage queue, eliminated list) will copy this command's shape — idiomatic copy-then-refactor is fine.
cfed approvals list [--pending] [--decided] [--as egm|ogm] [--stage pre_rfp|post_rfp|matoc] [--limit 20] [--json]
Pending-approvals view per BD-Lead E2E findings 2026-05-18, Gap R2. One PostgREST round-trip JOINs approvals with opportunities via embedded-resource syntax, then sorts client-side by parent-opp deadline_response ASC nulls-last and truncates to --limit.
cfed approvals list # default: pending, sorted by parent-opp deadline asc, limit 20
cfed approvals list --decided --limit 50 # show decided rows (approved | disapproved)
cfed approvals list --as egm # rows EGM uniquely owns (decision still null)
cfed approvals list --as ogm # rows OGM uniquely owns (parallel + decision null)
cfed approvals list --stage post_rfp # filter on approval.stage (NOT opp.pipeline_stage)
cfed --json approvals list --as ogm # structured outputDefault is --pending (matches final_status IS NULL OR final_status='pending' — both forms exist in current DB rows). --decided overrides --pending and matches final_status IN ('approved','disapproved'). --as <role> scopes to rows that role uniquely owns:
egm:egm_decision IS NULLANDapproval_type IN ('single','parallel')—auto_gorows are already pre-approved.ogm:ogm_decision IS NULLANDapproval_type='parallel'— single approvals are EGM-only by convention;auto_gois already done.
--stage filters on the approval row's own stage column (pre_rfp | post_rfp | matoc), not the parent opportunity's pipeline_stage. Limit defaults to 20 and caps at 200. The fetch over-pulls 2x the limit so the client-side deadline sort honors --limit even though PostgREST embedded-resource ORDER syntax has inconsistent support across Supabase versions.
Human output is a column-aligned table:
== approvals (2) ==
approval_id stage type egm ogm opp_id value title deadline
aaaaaaaa pre_rfp single - - bbbbbbbb $25.0M Hangar reroof, Barksdale AFB 2026-06-01
cccccccc post_rfp dual - - dddddddd $5.0M Levee repair, Tulsa District 2026-06-14Columns: approval_id (8-char) | stage | type (single/dual/auto — DB approval_type single/parallel/auto_go mapped to display labels) | egm decision | ogm decision | opp_id (8-char) | value ($X.XM) | title (truncated 40 chars) | deadline (YYYY-MM-DD). With --json, the payload is {approvals: [...], count, applied_filters} — the embedded opportunities object is preserved on each approval row so downstream tooling can read title/value/deadline/pipeline_stage without a second fetch.
cfed triage queue [--state recommended|untriaged|all] [--decision keep|reject|review] [--confidence-min 0..1] [--rule H1] [--flag <name>] [--limit 20] [--sort value|confidence] [--json]
"Review Required" surface per BD-Lead E2E findings 2026-05-18, Gap R4. Two PostgREST round-trips — one for pending-triage opportunities, one for completed opportunity_triage agent_runs — then a client-side anti-join + filter + sort. Mirrors the cfed pipeline / cfed approvals list fetch-then-aggregate pattern.
cfed triage queue # default: recommended, value DESC, limit 20
cfed triage queue --state untriaged --limit 50 # the 123 pre-trigger backlog
cfed triage queue --state all # both buckets, value DESC, limit 20
cfed triage queue --decision keep --confidence-min 0.8
cfed triage queue --rule H1 # opps the skill flagged on rule H1
cfed triage queue --flag set_aside_8a # opps with the 8(a) set-aside flag
cfed triage queue --sort confidence # highest-confidence first
cfed --json triage queue --state all # structured outputThree states:
recommended(default): pending-triage opps that have a completedopportunity_triageagent_run. The "ready for human review" cohort.untriaged: pending-triage opps that have NO completedopportunity_triageagent_run — the pre-trigger backlog (123 opps at 2026-05-18).all: union of the two buckets.
Filters (all apply on the parsed result_summary jsonb on the latest completed triage run):
--decision keep|reject|reviewmatchesresult_summary.decision = 'accept' | 'reject' | 'human-review'. Combining--decisionwith--state untriagedis a Validation error (untriaged rows have no triage run).--confidence-min Nkeeps rows withresult_summary.confidence >= N.--rule <code>keeps rows whoseresult_summary.rules_appliedarray contains<code>(e.g.H1).--flag <name>keeps rows whoseresult_summary.flagsarray contains<name>.
Default sort is value (DESC, nulls last). confidence sorts by result_summary.confidence DESC nulls-last. Limit defaults to 20 and caps at 200.
Human output is a column-aligned table:
== triage queue (2) [recommended=2 untriaged=123] ==
id decision confidence value agency title rules flags
11111111 KEEP 0.95 $25.0M AFGSC Hangar reroof, Barksdale AFB H1,H2,H4 -
22222222 REJECT 0.62 $5.0M USACE Levee repair, Tulsa District H3 set_aside_8aColumns: id (8-char opp UUID prefix) | decision (KEEP for accept, REJECT for reject, REVIEW for human-review) | confidence (two decimals, e.g. 0.95; - if null) | value ($X.XM / $X.XK / $N) | agency (leaf segment of SAM path) | title (truncated 40 chars) | rules (comma-joined, max 3 with +N remainder) | flags (comma-joined, first match with +N remainder).
With --json, the payload is {opportunities: [...], count, applied_filters, state_summary: {recommended, untriaged}}. state_summary reflects total counts in BOTH buckets regardless of which --state is filtered, so the operator always sees how many rows live in the other bucket. Each opportunities[] row exposes both opportunity (id, title, agency, estimated_value, pipeline_stage, source) and agent_run (id, status, started_at, completed_at, result_summary) — agent_run is null for untriaged rows.
cfed triage accept <opp-id> --reason "..." [--target-stage forecast|pre_rfp] [--json]
Accept a pending-triage opportunity into the active pipeline. Direct PostgREST writes: PATCH /rest/v1/opportunities (sets pipeline_stage + auto_eliminated=false) followed by POST /rest/v1/audit_log (entry with action='triage_accept', before_state, after_state). Both refuse to act unless the opportunity's current pipeline_stage is pending-triage — this is the human-in-the-loop guard on the Review Required surface.
cfed triage accept 11111111-1111-1111-1111-111111111111 --reason "Strong fit; advance to forecast."
cfed triage accept 11111111-1111-1111-1111-111111111111 --reason "Skip forecast; push to pre_rfp." --target-stage pre_rfp
cfed --json triage accept 11111111-1111-1111-1111-111111111111 --reason "..." # structured output--reason is REQUIRED (missing or empty exits 2). --target-stage accepts forecast (default) or pre_rfp; any other value exits 2 before any network call. Bad <opp-id> (non-UUID) also exits 2 before any network call.
Stage guard: if the opportunity is not in pending-triage, exits 2 with opp <id> stage=<stage>, not pending-triage — triage accept only valid on Review Required queue. Opp-not-found exits 2 with triage accept <id> not_found.
Human output: triage accept <opp-id> → pipeline_stage=<target> (reason: <reason>). With --json, the payload is {opportunity_id, action: 'triage_accept', prev_state, new_state, audit_log_id, reason}. audit_log_id is null if the audit insert returns no body row (defensive — PostgREST Prefer: return=representation normally returns the inserted row).
cfed triage eliminate <opp-id> --reason "..." [--json]
Manually eliminate a pending-triage opportunity. Sets opportunities.auto_eliminated=true and writes an audit_log entry with action='triage_eliminate'. Does NOT change pipeline_stage — the opp stays in pending-triage but falls off the active pipeline via the auto_eliminated=true filter (matches the existing Fort Worth seed pattern). Same stage guard as accept.
cfed triage eliminate 11111111-1111-1111-1111-111111111111 --reason "8(a) set-aside; NSI not 8(a)-certified."
cfed --json triage eliminate 11111111-1111-1111-1111-111111111111 --reason "..."--reason is REQUIRED. Validation errors mirror triage accept. Human output: triage eliminate <opp-id> → auto_eliminated=true (reason: <reason>). With --json, the payload is {opportunity_id, action: 'triage_eliminate', prev_state, new_state, audit_log_id, reason}; new_state.pipeline_stage is always pending-triage.
When CFED_AGENT=true, --reason is auto-tagged with [dogfood-<CFED_AGENT_NAME>] for both accept and eliminate (idempotent — preserves an existing prefix). Mirrors cfed decide.
PR #4 ships cfed triage accept and cfed triage eliminate — the first action commands under cfed triage. The CLI is bidirectional at 0.4.0. Bulk-action and non-triage-stage variants (cfed eliminate, cfed pipeline move) ship in follow-up PRs.
cfed eliminated list [--since 30d] [--method auto|manual|terminal] [--naics 236220] [--limit 20] [--json]
"Show me what fell off the board" view (v2.1.4). One PostgREST round-trip pulls opportunities that match the eliminated criteria within the --since window, then derives method + eliminated_at client-side.
cfed eliminated list # default: last 30d, sort updated_at DESC, limit 20
cfed eliminated list --since 7d # last 7 days only
cfed eliminated list --method auto # only auto_eliminated=true rows
cfed eliminated list --method terminal # only lost / cancelled rows
cfed eliminated list --naics 236220 --limit 50
cfed --json eliminated list --since 90d # structured outputEliminated criteria (server-side OR-filter):
auto_eliminated = true, ORpipeline_stage IN ('eliminated', 'lost', 'cancelled')
Time window filters on updated_at (best available signal for "when did this opp flip to eliminated" without joining audit_log). eliminated_at is rendered as YYYY-MM-DD from updated_at, falling back to created_at if updated_at is null.
method is derived client-side (auto_eliminated takes precedence over stage):
auto—auto_eliminated=trueterminal—pipeline_stage IN (lost, cancelled)AND not auto-eliminatedmanual— anything else (e.g.pipeline_stage='eliminated'from an operator action)
Validation: --since must match ^\d+d$ (e.g. 7d, 30d, 90d); --method must be one of auto|manual|terminal; --limit defaults to 20, caps at 200.
Human output is a column-aligned table:
== eliminated (2) ==
id eliminated_at method reason value agency title
11111111 2026-05-10 auto - $25.0M AFGSC Hangar reroof, Barksdale AFB
22222222 2026-05-12 terminal - $5.0M USACE Levee repair, Tulsa DistrictColumns: id (8-char opp UUID prefix) | eliminated_at (YYYY-MM-DD) | method (auto/manual/terminal) | reason | value ($X.XM / $X.XK / $N) | agency (leaf segment of SAM path) | title (truncated 40 chars).
reason is hard-coded to - in v2.1.4. v2.2.x action commands (cfed eliminated restore, cfed triage eliminate, etc.) will populate audit_log and a follow-up will JOIN that for richer reason text. The cfed eliminated namespace is reserved for these future siblings.
With --json, the payload is {opportunities: [...], count, applied_filters}. Each opportunities[] row exposes opportunity (full DB row), eliminated_at, method, and reason.
cfed eliminate <opp-id> --reason "..." [--json]
Non-triage eliminate path (v2.2.2). Kills an opportunity at ANY pipeline_stage — post-triage, in-progress, awarded, lost, anything. Distinct from cfed triage eliminate, which is the human-in-the-loop guard on the pending-triage Review Required queue. Same DB effect (auto_eliminated=true, pipeline_stage preserved), different audit_log.action value (eliminate vs triage_eliminate) so dashboards can distinguish the two cohorts.
cfed eliminate 11111111-1111-1111-1111-111111111111 --reason "Customer cancelled the project."
cfed --json eliminate 11111111-1111-1111-1111-111111111111 --reason "..."--reason is REQUIRED (missing or empty exits 2). Bad <opp-id> (non-UUID) exits 2 before any network call. Opp-not-found exits 2 with eliminate <id> not_found. No stage validation — works at every stage by design. auto_eliminated=true is the falls-off-active-pipeline mechanism; pipeline_stage is left untouched so historical context survives.
Human output: eliminate <opp-id> → auto_eliminated=true (reason: <tagged reason>). With --json, the payload is {opportunity_id, action: 'eliminate', prev_state, new_state, audit_log_id, reason}; new_state.pipeline_stage always equals prev_state.pipeline_stage. When CFED_AGENT=true, --reason is auto-tagged with [dogfood-<CFED_AGENT_NAME>] (idempotent — preserves an existing prefix). Restore-from-eliminated is a separate ticket (Gap #5) — this command is one-way at v2.2.2.
cfed decide <approval-id> --as <egm|ogm> --decision <approved|disapproved> [--reason "..."] [--json]
Records a go/no-go decision on an approval row. After the decide call succeeds, the CLI does a best-effort read-back against the opportunity to surface the updated pipeline_stage and auto_eliminated flag — this exposes stage-transition / zombie-state side-effects inline. Read-back failures emit a stderr warning but do not gate exit 0 (the decide call already succeeded).
cfed decide 5dc14d7d-570e-457c-8f2a-caf151ec1f7c --as egm --decision approved --reason "Strong fit, approve to advance."
cfed decide 5dc14d7d-570e-457c-8f2a-caf151ec1f7c --as egm --decision disapproved --reason "8(a) set-aside, NSI not 8(a)-certified."When CFED_AGENT=true, --reason is auto-tagged with [dogfood-<CFED_AGENT_NAME>] (idempotent — preserves an existing prefix).
PR #3 ships decide. The CLI is feature-complete at 0.3.0.
Exit codes
0success1env / session error2validation error (bad arg)3server error (API / RPC failure)4unexpected
Spec
See ../.k2so/specs/cli-cfed-v1.md for the full v1 spec and acceptance criteria. Working HTTP reference at ../.k2so/specs/cli-cfed-v1-curl-reference.md.
