Skip to content

feat(bin): enforce the cipher needs-decision hook and durably close answered decisions - #22

Merged
morris2spears merged 3 commits into
mainfrom
fm/firstmate-issue21-needs-decision-hook-enforcement
Aug 31, 2026
Merged

morris2spears merged 3 commits into
mainfrom
fm/firstmate-issue21-needs-decision-hook-enforcement

Conversation

@morris2spears

Copy link
Copy Markdown
Owner

Intent

Fix firstmate issue #21: route genuine needs-decision findings through Cipher before answering or filing follow-ups. Observed incident: task iinvy-issue292-resolve-transactionless-entity emitted 'needs-decision: [key=issue292-cross-role] ...' and the primary session settled it (kept kunchenguid#292 scope, filed follow-up iinvy issue kunchenguid#293, reported the choice to the captain) without ever running bin/fm-cipher-hook.sh needs-decision; the keyed status line then stayed open stale, and was only closed by a manual resolved append. Deliberate changes: (1) AGENTS.md needs-decision trigger and the cipher-hook skill now define answering to include filing a decision-bearing follow-up issue, keeping the current PR scoped around the question, and reporting the choice as settled - none may precede the hook. (2) New 'fm-cipher-hook.sh resolve-decision ' subcommand (shell + python resolve_decision_plan) durably appends 'resolved [key=]: Cipher decision accepted ' to the task status - it deliberately refuses (exit 1) unless the acknowledged needs-decision request AND the committed authenticated decision-comment receive record both exist, so a decision can never be marked Cipher-answered without the durable GitHub answer; it is idempotent (silent exit 0 when the keyed decision is already closed). The cipher-hook skill's durable-GitHub-answer procedure now ends with this command. (3) fm-classify-lib.sh keyed-decision fold now also honors a key token written at the start of the note ('needs-decision: [key=x] summary' - the placement the real incident used), so open/close events match their intended key instead of silently degrading to 'default'; a leading bracket that is not a valid key slug stays ordinary prose (falls back to default, deliberately NOT skipping the line). (4) bin/fm-brief.sh rule 6 (ship and scout variants) now shows the canonical placement 'needs-decision [key=]: {summary}' with the token between verb and colon. (5) Regression tests: tests/fm-cipher-hook.test.sh gains test_note_keyed_decision_single_hook_and_park (a note-keyed current decision emits exactly one authenticated HMAC-V2 hook with its intended key, dedupes on replay, and the hook never mutates the status file or touches GitHub - it parks) and test_resolve_decision_requires_and_follows_authenticated_answer (disabled decision route still exits 3 to the existing authority; resolve-decision refuses unacknowledged and comment-less requests; closes durably and idempotently after the authenticated comment). tests/fm-instruction-owners.test.sh pins the new instruction wording. Constraints honored: one sentence per line in tracked Markdown, plain dash only, shellcheck-clean via bin/fm-lint.sh, colocated tests extending existing suites, usage() sed range updated for the longer header (2,47). The disabled-route exit-3 fallback and held-delivery fail-closed/supersede behavior are intentionally unchanged. PR must close issue #21 and is NOT auto-merged even when green (standing note from issue #14): report checks green and stop; Cipher/the captain decide the merge.

What Changed

  • Added bin/fm-cipher-hook.sh resolve-decision <task-id> <request-id> (with a resolve_decision_plan in bin/fm-cipher-hook.py) that appends resolved [key=<decision-id>]: Cipher decision accepted <comment-url> to the task status, refuses with exit 1 unless both the acknowledged needs-decision request and the committed authenticated decision-comment receive record exist, and exits 0 silently when the keyed decision is already closed.
  • Taught the bin/fm-classify-lib.sh keyed-decision fold to honor a key token written at the start of the note (needs-decision: [key=x] summary), so open and close events match their intended key instead of degrading to default; a leading bracket that is not a valid key slug stays ordinary prose and still folds to default, and a bare resolved: line still closes a note-placed keyed decision for backward compatibility.
  • Tightened the instructions and brief scaffolds: AGENTS.md and the cipher-hook skill now define answering to include filing a decision-bearing follow-up issue, scoping the PR around the question, or reporting the choice as settled (none may precede the hook), bin/fm-brief.sh rule 6 shows the canonical needs-decision [key=<decision-slug>]: {summary} placement, and new cases in tests/fm-cipher-hook.test.sh, tests/fm-watch-triage.test.sh, and tests/fm-instruction-owners.test.sh cover the note-keyed single hook and park, the resolve-decision refusals and idempotent closure, and the new wording.

Closes #21.

Risk Assessment

✅ Low: The fold change is now backward compatible with every existing status file (verified by hand-tracing legacy note-placed, explicit-keyed, reopened, and activity-phase cases plus the pre-existing keys.status case), the resolve-decision subcommand is fail-closed on both the acknowledged request and the authenticated comment record, and the only remaining issue is a non-behavioral duplication cleanup.

Testing

Ran the targeted suites for every touched surface (cipher hook, classify-lib keyed-decision fold, instruction owners, generated briefs, and the fleet-snapshot and decision-nudge consumers of the fold) - all green - and, because passing units alone would not show the incident being prevented, drove the real scripts end-to-end against a live HMAC-verifying localhost gateway using the exact status line from the iinvy-issue292 incident. That transcript shows the note-placed key folding to issue292-cross-role instead of the old default, one authenticated HMAC-V2 delivery with that key that dedupes on replay and neither mutates the status file nor touches GitHub, resolve-decision refusing (exit 1, "no authenticated Cipher decision comment is recorded") before the answer exists, and a single durable resolved [key=...]: Cipher decision accepted &lt;comment-url&gt; line after the authenticated comment arrives, with the replay silently doing nothing. The change is CLI/instruction-facing with no rendered UI surface, so the reviewer-visible evidence is CLI transcripts and the generated brief text rather than screenshots. The worktree is clean of test artifacts.

Evidence: End-to-end incident replay (before/after fold, hook delivery, refusal, durable closure)

=== the worker's status file (real incident placement) === working: reviewing transactionless entity scope needs-decision: [key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here === BEFORE this change: which decision key does the fold see? === key=default verb=needs-decision === AFTER this change: the fold honors the intended key === key=issue292-cross-role verb=needs-decision === firstmate routes the finding through Cipher (never answers it) === exit=0 -- replay (dedupes, no second delivery) -- exit=0 -- authenticated deliveries seen by the gateway -- decision_id=issue292-cross-role event=needs-decision hmac_v2_valid=True timestamp_fresh=True -- status file untouched by the hook (worker still parked, finding unanswered) -- working: reviewing transactionless entity scope needs-decision: [key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here -- github activity attempted by the hook -- (none) === firstmate tries to settle the decision BEFORE Cipher's durable GitHub answer === $ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf4139... error: Cipher hook refused: no authenticated Cipher decision comment is recorded for this request exit=1 -- status file still shows the decision open -- issue292-cross-role needs-decision [key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here === Cipher answers with an authenticated decision comment on GitHub === queued: fmcr-v1-00daebf9... $ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf4139... resolved iinvy-issue292-resolve-transactionless-entity issue292-cross-role exit=0 -- durable closure appended to the task status -- resolved [key=issue292-cross-role]: Cipher decision accepted https://github.com/example/iinvy/issues/292#issuecomment-9921&#10;-- open decisions now -- (none - the keyed decision is durably closed) -- replay is idempotent -- $ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf4139... exit=0 (silent, no second line) resolved-lines=1

=== the worker's status file (real incident placement) ===
working: reviewing transactionless entity scope
needs-decision: [key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here

=== BEFORE this change: which decision key does the fold see? ===
key=default  verb=needs-decision

=== AFTER this change: the fold honors the intended key ===
key=issue292-cross-role  verb=needs-decision

=== firstmate routes the finding through Cipher (never answers it) ===
exit=0
-- replay (dedupes, no second delivery) --
exit=0
-- authenticated deliveries seen by the gateway --
decision_id=issue292-cross-role event=needs-decision hmac_v2_valid=True timestamp_fresh=True
-- status file untouched by the hook (worker still parked, finding unanswered) --
working: reviewing transactionless entity scope
needs-decision: [key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here
-- github activity attempted by the hook --
(none)

=== firstmate tries to settle the decision BEFORE Cipher's durable GitHub answer ===
$ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf413984bb62319670cc3a351258ad87c52d376f623caa0d9387793946f31d
error: Cipher hook refused: no authenticated Cipher decision comment is recorded for this request
exit=1
-- status file still shows the decision open --
issue292-cross-role	needs-decision	[key=issue292-cross-role] keep #292 scoped to the entity, or absorb the cross-role history change here

=== Cipher answers with an authenticated decision comment on GitHub ===
queued: fmcr-v1-00daebf9278730ef7ad8b564c32da1732c8016b728f4b1b8767045e79e357cd8
$ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf413984bb62319670cc3a351258ad87c52d376f623caa0d9387793946f31d
resolved iinvy-issue292-resolve-transactionless-entity issue292-cross-role
exit=0
-- durable closure appended to the task status --
resolved [key=issue292-cross-role]: Cipher decision accepted https://github.com/example/iinvy/issues/292#issuecomment-9921
-- open decisions now --
(none - the keyed decision is durably closed)
-- replay is idempotent --
$ bin/fm-cipher-hook.sh resolve-decision iinvy-issue292-resolve-transactionless-entity fmch-v1-41cf413984bb62319670cc3a351258ad87c52d376f623caa0d9387793946f31d
exit=0 (silent, no second line)
resolved-lines=1
Evidence: Generated worker brief rule 6 (ship + scout) and fm-cipher-hook.sh usage

=== ship-variant brief rule 6 (generated by bin/fm-brief.sh) === 6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings), append needs-decision [key=&lt;decision-slug&gt;]: {summary of options} - keep the [key=...] token between the verb and the colon - and stop. Firstmate will apply the configured authority and reply with the decision. === scout-variant brief rule 6 === 6. If a decision belongs to a human (product choices, destructive actions), append needs-decision [key=&lt;decision-slug&gt;]: {summary of options} - keep the [key=...] token between the verb and the colon - and stop. Firstmate will reply with the decision. === bin/fm-cipher-hook.sh usage (sed range 2,47 renders the new block) === resolve-decision durably closes the answered keyed status decision for an acknowledged needs-decision event. It refuses unless the authenticated decision-comment receive record for that exact request exists, then appends one idempotent "resolved [key=<decision-id>]: Cipher decision accepted <comment-url>" status line while the keyed decision is still open. Usage: fm-cipher-hook.sh needs-decision <task-id> [decision-id] fm-cipher-hook.sh pr-ready <task-id> <pr-url> fm-cipher-hook.sh retry-held fm-cipher-hook.sh resolve-decision <task-id> <request-id> ...

=== ship-variant brief rule 6 (generated by bin/fm-brief.sh) ===
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
   append `needs-decision [key=<decision-slug>]: {summary of options}` - keep the `[key=...]`
   token between the verb and the colon - and stop. Firstmate will apply the configured authority and reply with the decision.
   When firstmate replies or a blocker clears and you resume, append `resolved: {how it was decided or unblocked}` (add the same `[key=<slug>]` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing.

=== scout-variant brief rule 6 ===
6. If a decision belongs to a human (product choices, destructive actions),
   append `needs-decision [key=<decision-slug>]: {summary of options}` - keep the `[key=...]`
   token between the verb and the colon - and stop. Firstmate will reply with the decision.
   When firstmate replies or a blocker clears and you resume, append `resolved: {how it was decided or unblocked}` (add the same `[key=<slug>]` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing.

=== bin/fm-cipher-hook.sh usage ===
Deliver the two versioned, authenticated Cipher/Hermes transitions and provide
Cipher's narrow exact-head entrypoint into the guarded iinvy merge path.

`needs-decision` first proves that the named keyed decision remains open and
current; `pr-ready` first proves that current-state reconciliation reports a
checks-green PR. The Python module receives only validated identity fields,
never worker prose. It owns strict payload/config validation, HMAC-SHA256 over
the exact request bytes, stable request IDs, private request/sent/ack/hold
records under state/cipher-hooks/, bounded retry, and localhost transport.
Production sends Hermes generic HMAC V2 over `<timestamp>.<exact-body>` in
X-Webhook-Signature-V2 plus X-Webhook-Timestamp, with the stable identity in
X-Request-ID. The body-only V1 header exists only when both test-only
FM_CIPHER_SIGNATURE_VERSION=legacy-v1 and FM_CIPHER_ALLOW_LEGACY_V1_TEST=1.

An absent or explicitly disabled decision route exits 3 without changing the
existing decision authority. A gated PR that is not currently green exits 4
without delivery so ordinary PR registration can continue waiting for checks.
The iinvy PR-ready route otherwise refuses on every
missing, disabled, malformed, unavailable, timed-out, or invalid-response case.
Only Cipher invokes `merge`; firstmate's ordinary merge command is separately
guarded and accepts an iinvy merge only with this event's acknowledged request
identity, while GitHub's exact-head condition prevents a later head from riding
an earlier inspection.

`retry-held` is the watcher's recovery sweep for transiently held deliveries
(gateway unavailable, timeout, transient HTTP). Each still-current held event
re-enters its own preflighted trigger path, which adopts the recorded request
so the exact body and request ID are retried with no duplicate delivery. It
prints one line per delivered or superseded event and nothing while an event
simply stays held; configuration-class holds are never auto-retried.

`resolve-decision` durably closes the answered keyed status decision for an
acknowledged needs-decision event. It refuses unless the authenticated
decision-comment receive record for that exact request exists, then appends
one idempotent "resolved [key=<decision-id>]: Cipher decision accepted
<comment-url>" status line while the keyed decision is still open, so an
answered decision cannot linger stale or keep held duplicates alive.

Usage:
  fm-cipher-hook.sh needs-decision <task-id> [decision-id]
  fm-cipher-hook.sh pr-ready <task-id> <pr-url>
  fm-cipher-hook.sh retry-held
  fm-cipher-hook.sh resolve-decision <task-id> <request-id>
  fm-cipher-hook.sh merge <task-id> <pr-url> <request-id> [-- <extra merge args>]
  fm-cipher-hook.sh verify-merge <task-id> <pr-url> <request-id>
  fm-cipher-hook.sh repo-gated <owner/repo>
Evidence: tests/fm-cipher-hook.test.sh output
ok - Cipher events use replay-protected HMAC V2 and dedupe by decision gate and exact PR head
ok - a note-keyed current decision emits exactly one authenticated hook and stays parked
ok - durable keyed closure requires the authenticated Cipher answer and is idempotent
ok - legacy body-only HMAC is isolated behind an explicit tested compatibility seam
ok - real Hermes HTTP 200 duplicate response is accepted exactly
ok - malformed inputs are rejected and unavailable delivery holds once without secret leakage
ok - gateway timeout is a durable fail-safe hold
ok - only iinvy repositories emit at checks-green and every route failure holds them
ok - iinvy merges require Cipher's actor path and the exact inspected PR head
ok - authenticated receive routes by task identity when a self-repo worker pane is present
ok - a transiently held event is retried and acknowledged once after gateway recovery
ok - obsolete transient holds are durably superseded instead of retried
ok - normal supervision retries a held delivery after gateway recovery and wakes firstmate once
Evidence: tests/fm-watch-triage.test.sh output
ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
ok - stale_is_terminal: terminal status surfaces, non-terminal and no-status are benign
ok - scan_captain_relevant_statuses lists only captain-relevant statuses
ok - classifier primitives: keyed decisions and activity phases, captain relevance, window-to-task, and overrides
ok - crew_is_provably_working: only working+run-step/pane is provable; idle/finished/parked/failed/unknown surface
ok - status_is_paused: only the leading paused verb matches, and paused is not captain-relevant
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
ok - signal_crew_provably_working: benign only when every referenced crew is provably working
ok - a no-verb signal whose crew is provably working is absorbed (no exit, no queue, suppressor advanced, beacon present)
ok - a bare turn-end whose crew is provably working (busy pane) is absorbed
ok - a bare turn-end whose crew is not provably working is surfaced (the swallowed-finish fix)
ok - a no-verb working: note whose crew is idle with no running pipeline is surfaced
ok - captain-relevant signal is surfaced (queue + exit) and marked surfaced
ok - a stale pane sitting on a terminal status is surfaced (queue + exit)
ok - a stale terminal-looking status is overridden and absorbed while a run is actively working, then wedge-escalated
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - a declared pause is absorbed on first sight, then re-surfaced as a recheck past the threshold, never wedge-escalated
ok - exited declared-pause and captain-held panes use bounded pause cadence while a live decision gate still surfaces once
ok - a declared paused secondmate re-surfaces on the bounded normal-mode cadence
ok - a non-paused secondmate retains normal stale suppression
ok - a resumed secondmate clears pause and stale tracking before stale exemption
ok - unchanged stale hashes reclassify when a crew enters or leaves pause
ok - a declared pause is periodically rechecked against authoritative active-run state
ok - a paused status overridden by authoritative working preserves its wedge timer and escalates
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - triage log capping handles wc byte counts with leading spaces
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
ok - AFK changed paused panes hand off plain stale identities for daemon-owned pause triage
Evidence: tests/fm-instruction-owners.test.sh output
ok - new internal skills have one precise AGENTS.md trigger each
ok - diagnostic-reasoning owns the approved evidence procedure
ok - project-management owns registry, delivery posture, consent, initialization, and removal safety
ok - generic effort fallback applies only below captain and standing configuration
ok - firstmate directly compares every quota candidate with authoritative model discovery
ok - firstmate-coding-guidelines owns compatibility review and deterministic enforcement
ok - secondmate registry guidance keeps concise routes and points to the charter
ok - state, startup, and ordinary recovery have focused owners and triggers
ok - compressed AGENTS.md records the approved one-owner map
ok - intake reuses evidence, reserves scouts for uncertainty, and parallelizes safe work
ok - compressed AGENTS.md retains authority, supervision, AFK, and X safety
ok - cipher-hook owns every answering form and the durable resolution step

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-classify-lib.sh:192 - The new note-placement key fallback changes the reading of status files that already exist on disk. A historical line 'needs-decision: [key=x] ...' previously folded to key 'default' and was closed by a bare 'resolved:' line; it now folds to key 'x', so that bare closure no longer matches and the decision re-opens on the next fold. Reachable consequences with no new writes: bin/fm-decision-hold.sh:359 fails with 'open structured decision <task>/x has no captain-held inventory entry' because the captain-hold inventory for that origin was recorded under the old 'default' key, and bin/fm-afk-return.sh:76 reports the same decision as a live blocker again. The incident task described in the intent is precisely this shape (note-placed key, closed by a manual 'resolved' append). Either accept the re-open as intended migration cost, or make a bare resolved/captain-held event also close a note-placed key opened before this change.

🔧 Fix: keep bare resolved closing note-placed keyed decisions
1 info still open:

  • ℹ️ bin/fm-classify-lib.sh:211 - _fm_decision_key_placement re-implements the whole bracket-parsing body of _fm_decision_key (prefix scan, note fallback, slug validation) just to report where the token sat, and both folds now call it once per status line in addition to _fm_decision_key - so every line costs two extra subshells (the placement helper plus its own status_line_note call) in a hot per-task fold used by fm-fleet-snapshot, fm-decision-hold, and fm-afk-return. Having _fm_decision_key emit '<key>\t<placement>' (or a paired _fm_decision_key_parse) and splitting once at the call sites in status_open_decisions:281 and _fm_status_open_activities_stream:325 removes the duplicate grammar - which otherwise has to be kept in sync by hand - and halves the fork count. Behavior is correct as written; this is cleanup only.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-cipher-hook.test.sh (includes the two new tests: note-keyed single hook and park, resolve-decision requires the authenticated answer)
  • bash tests/fm-watch-triage.test.sh (keyed-decision fold, note-placed keys, bare-close backward compatibility)
  • bash tests/fm-instruction-owners.test.sh (new AGENTS.md / cipher-hook / brief wording pins)
  • bash tests/fm-brief.test.sh (brief scaffolds after the rule 6 change)
  • bash tests/fm-decision-hold-lifecycle.test.sh, bash tests/fm-fleet-snapshot-view.test.sh, bash tests/fm-decision-nudge.test.sh (consumers of the open-decision fold)
  • Manual end-to-end CLI replay of the incident against a live localhost HMAC-verifying Cipher gateway: base-vs-target fold comparison, bin/fm-cipher-hook.sh needs-decision (delivery + replay dedupe + park + no GitHub writes), bin/fm-cipher-hook.sh resolve-decision refusal before the answer, bin/fm-cipher-receive.sh decision-comment, then durable and idempotent closure
  • Manual render of the generated worker brief (ship and scout variants) to confirm the canonical needs-decision [key=&lt;decision-slug&gt;]: placement, and bin/fm-cipher-hook.sh --help to confirm the widened usage() sed range shows the new subcommand
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…needs-decision hook bypasses

A genuine keyed needs-decision must enter the Cipher hook before firstmate
answers it in any form; the iinvy#292 event was settled by filing a follow-up
issue and reporting the scope choice without the hook ever running, and its
keyed status line stayed open afterwards (firstmate#21).

- cipher-hook skill and AGENTS.md now name filing a decision-bearing
  follow-up, keeping the current PR scoped, and reporting the choice as
  settled as forms of answering that may not precede the hook.
- new `fm-cipher-hook.sh resolve-decision <task-id> <request-id>` appends
  the durable keyed `resolved` closure once the authenticated Cipher
  decision comment is recorded, refuses without it, and is idempotent;
  the skill's durable-answer procedure now ends with it.
- the keyed-decision fold honors the key token written at the start of the
  note (the observed real-world placement), so open and close events match
  their intended key instead of degrading to "default", and the generated
  brief now shows the canonical verb-adjacent placement.
- regression tests prove a current keyed decision emits exactly one
  authenticated hook and parks unanswered, the disabled-route fallback stays
  exit 3, an unacknowledged or comment-less request refuses durable closure,
  and instruction owners keep the follow-up branch wording.

Closes #21

Claude-Session: https://claude.ai/code/session_01W5thFUb7qQqQPpNc9Uyupv
@morris2spears
morris2spears merged commit f21df51 into main Aug 31, 2026
10 checks passed
@morris2spears
morris2spears deleted the fm/firstmate-issue21-needs-decision-hook-enforcement branch August 31, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Route genuine needs-decision findings through Cipher before answering or filing follow-ups

1 participant