feat(outcome): add follow-up branch to chase quiet applications (#198)

Adds Step 2b to /outcome: surfaces open applications gone quiet (default 10 days), drafts a brief channel-appropriate follow-up in the candidate's voice using only claims from the already-submitted materials, and logs it (followed up marker in notes + followup_YYYY-MM-DD.md in the archive). Reachable from the no-arg pipeline table (now showing days-quiet and follow-ups-sent) and via /outcome followup [N|company]. Draft-only never send, capped at two follow-ups terminating into Step 2's existing no_response path, and a thank-you note offered the moment Step 3 records a completed interview stage. Reads the contact_person column nothing previously consumed.

Folded into /outcome rather than a standalone command - dependency-free (unlike /gmail-sync) and reusing detection, the archive, the notes ledger, and the resolution path already there. The invited return of #46 (closed stale, not on merit). Guardrail tests mirror the /notion-sync pattern; 10-vs-30-day threshold contrast with /gmail-sync documented and test-pinned.

By @ayobamiseun.
This commit is contained in:
Ayobami Adegoke
2026-07-20 20:37:22 +02:00
committed by GitHub
parent b3b351605c
commit a0e81204da
3 changed files with 129 additions and 2 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Guards for /outcome's follow-up branch (Step 2b).
The branch is part of the /outcome markdown spec (the spec IS the
implementation), so these tests pin the invariants that would break
silently: the draft-only rule (the branch must never send anything on the
user's behalf), the no-new-claims rule that keeps follow-ups inside the
framework's never-fabricate boundary, the two-follow-up cap that terminates
into /outcome's own no_response flow, and the deliberate contrast with
/gmail-sync's 30-day staleness flag.
"""
import subprocess
import sys
import unittest
from pathlib import Path
try:
import yaml # noqa: F401 - only probing availability for the lint integration test
_HAVE_YAML = True
except ImportError:
_HAVE_YAML = False
REPO = Path(__file__).resolve().parent.parent
COMMAND = REPO / ".claude" / "commands" / "outcome.md"
class OutcomeFollowupBranchSpec(unittest.TestCase):
def test_followup_branch_exists(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"## Step 2b: Follow-Up Branch",
text,
"outcome.md lost its follow-up branch section",
)
def test_followup_argument_documented(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"`followup <N>`",
text,
"spec lost the /outcome followup argument forms",
)
def test_draft_only_rule_present(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"draft only, never send",
text,
"spec lost the rule that the follow-up branch never sends anything on the user's behalf",
)
def test_no_new_claims_rule_present(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"no new claims",
text,
"spec lost the rule that follow-ups only reuse claims from the archived submitted materials",
)
def test_two_followup_cap_present(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"Maximum two follow-ups per application",
text,
"spec lost the cap that stops the follow-up branch from nagging indefinitely",
)
def test_threshold_contrast_with_gmail_sync_documented(self):
text = COMMAND.read_text(encoding="utf-8")
self.assertIn(
"30-day staleness flag",
text,
"spec lost the rationale for the 10-day nudge vs /gmail-sync's 30-day alarm",
)
@unittest.skipUnless(
_HAVE_YAML,
"PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)",
)
def test_lint_skills_passes(self):
result = subprocess.run(
[sys.executable, str(REPO / "tools" / "lint_skills.py")],
cwd=REPO,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, f"lint_skills.py failed:\n{result.stdout}{result.stderr}")
if __name__ == "__main__":
unittest.main()