A Personal AI Employee system for Hackathon 0 Silver Tier. Multi-channel task ingestion → OpenAI-powered reasoning plans → LinkedIn HITL approval → cloud automation via GitHub Actions.
LinkedIn OAuth note: LinkedIn OAuth integration is prepared for future real mode, but Silver runs in simulated mode only. No live LinkedIn API calls are made during Silver tier operation.
Obsidian note: This repository can be opened as an Obsidian vault (
Dashboard.md,Company_Handbook.md,Welcome.mdare vault documents). The automation pipeline — watchers, agent, approve, post — runs entirely via Python scripts and GitHub Actions and does not require Obsidian to be installed or running.
┌────────────────────────────────────────────────────────────────────┐
│ INGESTION LAYER (5 watchers) │
│ │
│ Inbox/ manual_input.txt whatsapp_input.txt linkedin_input.txt │
│ │ │ │ │ │
│ [watcher_ [watcher_ [whatsapp_ [linkedin_ │
│ inbox] manual] watcher] watcher] │
│ \ \ / / │
│ ╰────────╴─────────────╴──────────────────╯ │
│ │ │
│ Gmail API ──────► [gmail_watcher] ──► Inbox/ │
│ (when GMAIL_OAUTH_ENABLED=true + credentials present) │
│ │ │
│ Needs_Action/ │
└──────────────────────────────┼─────────────────────────────────────┘
│
┌──────────────────────────────▼─────────────────────────────────────┐
│ AGENT LAYER (agent.py) │
│ │
│ skills/planning_skill.py → Plans/<task>_Plan.md │
│ skills/summarize_skill.py → Pending_Approval/<task>.md │
│ skills/linkedin_skill.py → Pending_Approval/linkedin_draft_* │
│ (business tasks only) │
│ │
│ MCP tools: │
│ mcp_file_ops.py list / read / write / move │
│ mcp_linkedin_ops.py LinkedIn UGC Post API + simulated │
│ mcp_email_ops.py SMTP email + simulated (bonus) │
│ mcp_calendar_ops.py calendar events, simulated (bonus) │
└──────────────────────────────┼─────────────────────────────────────┘
│
┌──────────────────────────────▼─────────────────────────────────────┐
│ HITL LAYER (Human-in-the-Loop) │
│ │
│ Pending_Approval/ │
│ │ │
│ [approve.py] ← MANUAL ONLY — human must invoke this │
│ │ │
│ Approved/ │
│ │ │
│ [post_approved.py] ← reads ONLY from Approved/ │
│ ├── LinkedIn UGC Post API (or writes simulated evidence) │
│ └── on success → moves file to Done/ │
└──────────────────────────────┼─────────────────────────────────────┘
│
┌──────────────────────────────▼─────────────────────────────────────┐
│ EVIDENCE LAYER │
│ │
│ run_log.md human-readable UTC audit log │
│ prompt_history.md full prompt audit trail │
│ Logs/events_<date>.jsonl structured JSONL event stream │
│ Logs/summary_<ts>.md per-run stats (fallback counts etc.) │
│ Logs/linkedin_simulated_*.json simulated post evidence │
│ Logs/posted_ids.json idempotency hash registry │
└────────────────────────────────────────────────────────────────────┘
The diagram below shows the full pipeline from ingestion to evidence output. Rendered directly from docs/architecture.svg — no external tooling required.
Key design signal for judges: The pipeline has four hard layers — Ingestion → Agent → HITL → Evidence. A task cannot skip the HITL layer;
post_approved.pyreads only fromApproved/, which is populated only by the manualapprove.pystep. LinkedIn is simulated by default; no real post can occur unless three env vars are simultaneously set.
AI_Employee_Vault_Silver/
│
├── Inbox/ # Raw file drops — watcher_inbox picks these up
├── Needs_Action/ # Tasks queued for agent processing
├── Pending_Approval/ # Agent output awaiting human approval
├── Approved/ # Human-approved items (ready to post)
├── Done/ # Completed tasks and moved source files
├── Plans/ # <task>_Plan.md reasoning plans
├── Logs/ # JSONL events, summaries, evidence files
├── prompts/ # Timestamped Claude run prompt logs
├── skills/ # Skill modules called by agent.py
│ ├── planning_skill.py # structured plan generation
│ ├── summarize_skill.py # task summarisation
│ └── linkedin_skill.py # LinkedIn post draft creation
├── specs/ # Requirement / spec documents
│
├── .github/workflows/
│ └── silver-agent.yml # GitHub Actions: every 10 min + workflow_dispatch
│
│── Obsidian vault documents ─────────────────────────────────────────
├── Dashboard.md # AI Employee dashboard (Obsidian view)
├── Company_Handbook.md # Company reference document
├── Welcome.md # Vault welcome note
│── Pipeline scripts ─────────────────────────────────────────────────
├── watcher_inbox.py # Watcher 1: Inbox/ → Needs_Action/
├── watcher_manual.py # Watcher 2: manual_input.txt → Needs_Action/
├── whatsapp_watcher.py # Watcher 3: whatsapp_input.txt (simulated)
├── linkedin_watcher.py # Watcher 4: linkedin_input.txt (simulated, bonus)
├── gmail_watcher.py # Watcher 5: Gmail API (exits cleanly if credentials absent)
│
├── agent.py # Core agent: plans + summaries + LinkedIn drafts
├── approve.py # HITL: Pending_Approval → Approved (manual only)
├── post_approved.py # LinkedIn poster: Approved → Done
│── MCP tools ────────────────────────────────────────────────────────
├── mcp_file_ops.py # Safe file helpers (list/read/write/move/log)
├── mcp_linkedin_ops.py # LinkedIn UGC Post API + simulated mode
├── mcp_email_ops.py # SMTP email + simulated mode (bonus)
├── mcp_calendar_ops.py # Calendar events, simulated (bonus)
├── mcp_server.py # Original MCP server (backward compatibility)
│── Utilities ────────────────────────────────────────────────────────
├── send_test_email.py # CLI helper: verify MCP email integration (bonus)
├── processor.py # Task processor helper
├── watcher.py # Base watcher helper
├── agent_queue.py # Queue-based agent variant
├── evidence_pack.py # Generates evidence ZIP for judges
│── Docs & audit ─────────────────────────────────────────────────────
├── JUDGE_PROOF.md # Detailed compliance evidence guide
├── run_log.md # Human-readable UTC audit log
├── prompt_history.md # Full prompt audit trail
│── Input files ──────────────────────────────────────────────────────
├── manual_input.txt # Drop manual tasks here (separator: ---)
├── whatsapp_input.txt # Simulated WhatsApp DM input
├── linkedin_input.txt # Simulated LinkedIn DM input
│── Config ───────────────────────────────────────────────────────────
├── .env.example # Template for all environment variables
├── requirements.txt
└── .gitignore # Excludes .env, credentials.json, token.json, __pycache__
Five watchers ingest tasks from different sources into Needs_Action/. Each watcher deduplicates and logs every event to run_log.md and Logs/events_<date>.jsonl.
| # | Script | Source | Mode |
|---|---|---|---|
| 1 | watcher_inbox.py |
Inbox/ — any .md file dropped here |
Live |
| 2 | watcher_manual.py |
manual_input.txt — entries separated by --- |
Live |
| 3 | whatsapp_watcher.py |
whatsapp_input.txt — simulated WhatsApp DMs |
Simulated |
| 4 | linkedin_watcher.py |
linkedin_input.txt — simulated LinkedIn DMs |
Simulated (bonus) |
| 5 | gmail_watcher.py |
Gmail API — unread inbox, domain-filtered, dedup by message ID | OAuth-guarded |
- Cloud: The Gmail step in
silver-agent.ymlonly runs whenGMAIL_OAUTH_ENABLED=trueis set and theGMAIL_CLIENT_SECRET_JSON/GMAIL_TOKEN_JSONsecrets are present. - Local: If the Google API libraries are not installed (
pip install -r requirements.txtnot yet run), the script prints"Gmail libraries missing."and exits cleanly with code 1. This is a dependency issue, not a system failure — install requirements and it works. - Domain allowlist: Only emails from allowed domains are ingested (see Gmail Domain Allowlist).
- Deduplication: Each email is identified by its Gmail message ID. Re-running the watcher never writes the same email twice.
agent.py reads every .md file in Needs_Action/ and routes each through three skill modules in sequence.
- Produces
Plans/<task>_Plan.mdwith: task analysis, step-by-step plan, risks, and a completion checklist. - Calls OpenAI (
gpt-4o-miniby default) ifOPENAI_API_KEYis set and quota is available. - Fallback: If the key is absent or the API returns any error (including
429 insufficient_quota), a deterministic structured plan is generated and the status is recorded asplan_fallback. The agent does not crash.
- Produces
Pending_Approval/<task>.mdwith: original content, AI summary, model name, and status. - Same OpenAI-or-fallback pattern. Fallback status:
fallback.
- Runs only when the task text is classified as a business or marketing task.
- Produces
Pending_Approval/linkedin_draft_<task>_<hash>.mdcontaining: generated post text, source task reference, SHA1 task hash, and a risk note requiring human approval before posting. - Same OpenAI-or-fallback pattern.
After all skills run, the source file is moved from Needs_Action/ to Done/_source_<task>.md.
| Condition | Behaviour | Logged status |
|---|---|---|
OPENAI_API_KEY set, quota available |
OpenAI call succeeds | openai_ok |
OPENAI_API_KEY missing |
Deterministic fallback used immediately | plan_fallback / fallback |
API returns 429 insufficient_quota or any HTTP error |
Deterministic fallback used, error caught | plan_fallback / fallback |
Fallback counts are reported in Logs/summary_<ts>.md and every prompt attempt is recorded in prompt_history.md (timestamp, model, status, file, prompt snippet — no secrets logged).
Cloud note:
OPENAI_REQUIRED=trueis set in the GitHub Actions workflow env. This causes the agent to exit with an error ifOPENAI_API_KEYis entirely absent in CI, ensuring cloud runs always use the real API.
| File | Contents |
|---|---|
run_log.md |
One UTC line per event (plan created, task processed, errors) |
prompt_history.md |
Timestamp, model, status, filename, prompt snippet |
Logs/events_<date>.jsonl |
Structured JSONL — one object per event |
Logs/summary_<ts>.md |
Counts: tasks, plans, drafts, OpenAI OK, fallbacks, errors |
approve.py is the only mechanism that promotes files from Pending_Approval/ to Approved/. It is never called automatically — not by the agent, not by the GitHub Actions workflow.
python approve.py # list all pending files
python approve.py linkedin_draft_foo.md # approve one specific file
python approve.py --all # approve all pending filesLinkedIn drafts are tagged [LINKEDIN] in the list view. Every approval is logged to run_log.md and Logs/events_<date>.jsonl.
post_approved.py enforces the HITL gate at runtime: before processing Approved/, it scans Pending_Approval/ for any unapproved linkedin_draft_* files and logs each one as blocked_without_approval. This produces auditable evidence that no draft was ever posted without human sign-off.
Silver Tier: LinkedIn posting is SIMULATED by default — no real posts are made. When simulated, the system writes evidence JSON to
Logs/linkedin_simulated_<timestamp>.jsonand logs the result torun_log.md. The repo is OAuth-ready (LINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRETconfigured), but real posting requires LinkedIn "Share on LinkedIn" product access/approval from LinkedIn's developer portal. Prepared for real OAuth, pending LinkedIn approval.
post_approved.py reads only from Approved/. Files in Pending_Approval/ are never touched by this script.
- Evidence JSON written to
Logs/linkedin_simulated_<ts>.json. - File stays in
Approved/(not moved toDone/). - Safe to run in any environment — no public posts are made.
- Requires LinkedIn "Share on LinkedIn" product access enabled on the Developer App — without this LinkedIn-gated approval, the UGC Post API rejects calls even with a valid token.
- Requires all three:
LINKEDIN_ACCESS_TOKEN+LINKEDIN_PERSON_URN+LINKEDIN_SIMULATED=false. - Calls the LinkedIn UGC Post API via
mcp_linkedin_ops.create_post(). - On success: file moves to
Done/, task hash saved toLogs/posted_ids.json.
Logs/posted_ids.json stores the SHA1 hash of every successfully posted task. Re-running post_approved.py checks this registry and skips already-posted items — no double-posting, even across workflow runs.
LinkedIn OAuth credentials have been registered and stored in GitHub Actions Secrets as proof of integration readiness. No live OAuth flow executes during Silver tier operation. Prepared for real OAuth, pending LinkedIn approval.
Note on LinkedIn product approval: Live posting via the UGC Post API requires the "Share on LinkedIn" product to be explicitly enabled on the LinkedIn Developer App. This is a LinkedIn-gated approval — it is not automatic and must be separately requested. Silver does not make live API calls, so this approval is not required for Silver evaluation; it is the sole remaining gate before Gold-tier real posting can be activated.
- A LinkedIn Developer App has been created and
LINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRETare stored as GitHub Repository Secrets. The client secret never appears in the repository or workflow logs. - Silver keeps
LINKEDIN_SIMULATED=trueby default. The mode gate inmcp_linkedin_ops.pychecks this flag before any HTTP request is made — if it istrue, the code writes a simulated evidence JSON file and returns without contacting the LinkedIn API. No public posts can occur in Silver. - Real OAuth + real posting is a Gold-tier switch. Activating live posting requires three explicit conditions: a valid
LINKEDIN_ACCESS_TOKEN(obtained via the OAuth Authorization Code flow outside of GitHub Actions), a correctLINKEDIN_PERSON_URN, andLINKEDIN_SIMULATED=false. Changing any one of these alone is insufficient — all three must be satisfied simultaneously.
This design proves future integration readiness while making accidental public posts structurally impossible during Silver evaluation.
This section summarises the layered guardrails that make it structurally impossible for Silver to post to LinkedIn without explicit human action and correct environment configuration.
| # | Guardrail | How it works |
|---|---|---|
| 1 | LINKEDIN_SIMULATED=true by default |
mcp_linkedin_ops.create_post() checks this flag before any HTTP request. If true, it writes a simulated evidence JSON and returns — the LinkedIn API is never contacted. No credential is required to run safely. |
| 2 | Pending_Approval gate | agent.py places LinkedIn drafts in Pending_Approval/. post_approved.py reads only from Approved/. A file can never jump between these two folders automatically. |
| 3 | Manual HITL (approve.py) |
approve.py is a CLI tool; it is never called by the GitHub Actions workflow. A human must explicitly run it to move a draft from Pending_Approval/ to Approved/. There is no timer, webhook, or automation that triggers it. |
| 4 | Triple env-var requirement for live mode | Live posting requires all three simultaneously: LINKEDIN_SIMULATED=false + LINKEDIN_ACCESS_TOKEN (valid, non-expired) + LINKEDIN_PERSON_URN. Missing any single one keeps the system in safe simulated mode. |
| 5 | Idempotency hash registry | Logs/posted_ids.json stores a SHA1 hash of every posted task. Re-running the workflow skips already-posted items — even if the workflow is triggered twice in the same minute. |
| 6 | No secrets logged | mcp_linkedin_ops.py logs only boolean flags (token_present, person_urn_present) — the token value is never written to run_log.md, JSONL, or any evidence file. |
These two demos can be run locally to verify safe degradation. No real credentials are needed.
# 1. Drop a task
echo "Draft a blog post about AI productivity tools." > Inbox/test_fallback.md
python watcher_inbox.py # moves to Needs_Action/
# 2. Run agent with no key (or an invalid/quota-exhausted key)
OPENAI_API_KEY="" python agent.py
# Expected output (run_log.md):
# Agent: processed | task=test_fallback.md | status=plan_fallback
# (or status=fallback / status=linkedin_fallback depending on task type)
# 3. Verify Plans/ was still created (deterministic fallback plan)
ls Plans/
# → test_fallback_Plan.md (fallback plan produced; no crash)
# 4. Check run_log.md for fallback evidence
grep "fallback" run_log.md | tail -5What to observe: The agent never raises an exception — it catches all OpenAI errors (no_api_key, HTTP 429 insufficient_quota, network timeout) and produces a structured fallback plan. The status field in run_log.md will be plan_fallback, fallback, or linkedin_fallback depending on skill. The pipeline continues to the next task.
# Run post_approved.py with no LinkedIn credentials set
# (LINKEDIN_SIMULATED defaults to true; no token needed)
echo "Test approved draft." > Approved/linkedin_draft_demo_test.md
python post_approved.py
# Expected output:
# Not posted (simulated_mode). File kept in Approved/
# Evidence written to: Logs/linkedin_simulated_<timestamp>.json
# Verify evidence JSON was created
ls Logs/linkedin_simulated_*.json | tail -1
# Inspect it
cat $(ls -t Logs/linkedin_simulated_*.json | head -1)
# → {"mode": "simulated", "token_present": false, "person_urn_present": false, ...}What to observe: With LINKEDIN_SIMULATED=true (the Silver default), post_approved.py never attempts an HTTP call. It writes structured evidence JSON and returns cleanly. No exception. No partial post. The file stays in Approved/ for re-processing when real credentials are eventually provided.
| Module | Responsibility |
|---|---|
mcp_file_ops.py |
list, read, write, move, copy files; log_event() helper |
mcp_linkedin_ops.py |
LinkedIn UGC Post API calls; simulated mode; evidence JSON |
mcp_email_ops.py |
SMTP email sending; simulated mode; evidence JSON (bonus) |
mcp_calendar_ops.py |
Local simulated calendar event store (bonus) |
mcp_server.py |
Original MCP server entry point (backward compatibility) |
All MCP tools return structured results and write evidence files when credentials are absent — they never raise unhandled exceptions.
Two additional MCP modules extend the core pipeline beyond the LinkedIn requirement. Both follow the same design contract as mcp_linkedin_ops.py: structured return values, full audit logging, and safe degradation when credentials are absent.
Exposes a single send_email(to, subject, body) function callable from any part of the pipeline.
Simulated mode (default):
- Triggered when any of
SMTP_HOST,SMTP_USER, orSMTP_PASSis absent. - Writes
Logs/email_simulated_<ts>.jsoncontaining recipient, subject, body, and the list of missing credentials. - Returns
{"ok": False, "reason": "not_configured", "evidence_path": "..."}.
Real SMTP mode:
- Activated when all three credentials are set (
SMTP_HOST,SMTP_USER,SMTP_PASS). - Connects via STARTTLS on port 587 (configurable via
SMTP_PORT). SMTP_FROMis optional; falls back toSMTP_USERif not set.- Passwords are never logged — only the sender address and recipient appear in audit records.
- Returns
{"ok": True}on success; catches and logs SMTP exceptions without crashing.
A companion script send_test_email.py provides a CLI to verify the integration end-to-end before using it in production.
Exposes create_event(title, start, end, description) and read_events() backed by a local JSON store at Logs/calendar_events.json.
- Operates entirely in simulated mode — no external API required.
- Each event is persisted with a timestamp-based
id,created_atUTC timestamp, and"mode": "simulated"field. read_events()returns the full event list with a count — usable by any downstream skill or agent step.- Every write and read is logged to
run_log.mdandLogs/events_<date>.jsonl.
| Criterion | Contribution |
|---|---|
| MCP tool breadth | Demonstrates the MCP abstraction pattern across three distinct action types (file, communication, scheduling) beyond the minimum LinkedIn requirement |
| Safe-by-default | Both modules write evidence files rather than failing silently — judges can verify every invocation without live credentials |
| Audit completeness | All email and calendar events appear in the same JSONL stream as the rest of the pipeline, giving a single queryable audit trail for the full run |
| Real-mode upgrade path | Email switches to live SMTP with three env vars; calendar is structured for a Google Calendar API drop-in — no code changes needed in callers |
| Feature | Default | Condition for real mode | Evidence when simulated |
|---|---|---|---|
| LinkedIn posting | Simulated | LINKEDIN_SIMULATED=false + LINKEDIN_ACCESS_TOKEN + LINKEDIN_PERSON_URN |
Logs/linkedin_simulated_<ts>.json |
| Email sending | Simulated | SMTP_HOST + SMTP_USER + SMTP_PASS set |
Logs/email_simulated_<ts>.json |
| OpenAI plans | Fallback if key missing or quota exceeded | OPENAI_API_KEY valid and within quota |
plan_fallback / fallback status in Plans/ and prompt_history.md |
| Gmail ingestion | Disabled unless enabled | GMAIL_OAUTH_ENABLED=true + credentials |
Clean exit logged to run_log.md |
| WhatsApp ingestion | Always simulated | n/a — reads local text file | whatsapp_input.txt cleared after ingestion |
| LinkedIn ingestion | Always simulated | n/a — reads local text file | linkedin_input.txt cleared after ingestion |
| Calendar ops | Always simulated | n/a | Logs/calendar_events.json |
File: .github/workflows/silver-agent.yml
Triggers:
- Scheduled every 10 minutes —
cron: "*/10 * * * *" - Manual —
workflow_dispatch - No push trigger (prevents recursive commit loops)
Concurrency: cancel-in-progress: true — a new scheduled run cancels any still-running previous run.
Step order:
| Step | Action |
|---|---|
| 1 | Watcher 1 — Inbox/ → Needs_Action/ |
| 2 | Watcher 2 — manual_input.txt → Needs_Action/ |
| 3 | Watcher 3 — whatsapp_input.txt → Needs_Action/ |
| 4 | Watcher 4 — linkedin_input.txt → Needs_Action/ |
| 5 | Gmail OAuth setup — only if GMAIL_OAUTH_ENABLED=true |
| 6 | Watcher 5 — Gmail → Inbox/ — only if GMAIL_OAUTH_ENABLED=true |
| 7 | Agent — reads Needs_Action/, writes Plans/ and Pending_Approval/ |
| 8 | Post — HITL block check + LinkedIn posting from Approved/ → Done/ |
| 9 | Commit and push — git pull --rebase before push |
| 10 | Print summary — task counts, plan counts, error counts |
| 11 | Upload artifact — run_log.md, Plans/, Logs/, Done/ etc. (30-day retention) |
Committed paths per run: Needs_Action/, Pending_Approval/, Approved/, Done/, Plans/, Logs/, run_log.md, prompt_history.md.
This repository is structured to be openable as an Obsidian vault. The following markdown files serve as vault documents for human navigation:
| File | Purpose |
|---|---|
Dashboard.md |
Live AI Employee dashboard |
Company_Handbook.md |
Company reference document |
Welcome.md |
Vault welcome and orientation note |
The automation pipeline does not depend on Obsidian. All watchers, the agent, approve, and post scripts are standard Python. The GitHub Actions workflow runs on ubuntu-latest with no Obsidian dependency. Opening the vault in Obsidian is optional and purely for human readability.
Add these as GitHub Repository Secrets (Settings → Secrets → Actions):
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes (cloud) | OpenAI key — workflow sets OPENAI_REQUIRED=true |
OPENAI_MODEL |
Optional | Model name; default gpt-4o-mini |
LINKEDIN_ACCESS_TOKEN |
Optional | LinkedIn OAuth token for live posting |
LINKEDIN_PERSON_URN |
Optional | e.g. urn:li:person:AbCdEfGh |
LINKEDIN_SIMULATED |
Optional | false = enable real posting; default true |
LINKEDIN_CLIENT_ID |
Optional | LinkedIn Developer App client ID — for future OAuth enable |
LINKEDIN_CLIENT_SECRET |
Optional | LinkedIn Developer App client secret — for future OAuth enable |
GMAIL_OAUTH_ENABLED |
Optional | true = run Gmail watcher step in cloud; default false |
GMAIL_CLIENT_SECRET_JSON |
Optional | Full JSON contents of credentials.json |
GMAIL_TOKEN_JSON |
Optional | Full JSON contents of token.json |
Silver does not run live OAuth;
LINKEDIN_CLIENT_IDandLINKEDIN_CLIENT_SECRETare stored only as future-ready proof. Real OAuth flow and live posting will be enabled in Gold tier by settingLINKEDIN_SIMULATED=falseand providing a valid access token.
Local development: cp .env.example .env — fill in values. .env is gitignored; never commit it.
gmail_watcher.py only ingests emails from senders whose domain matches this allowlist (exact match or subdomain):
google.comgithub.commicrosoft.comazure.comanthropic.com
All other senders are silently skipped. To add domains, edit ALLOWED_DOMAINS in gmail_watcher.py. Deduplication is by Gmail message ID — the same email is never written twice across runs.
All steps work in simulated mode — no credentials required.
# 0. Install dependencies (once)
pip install -r requirements.txt
# 1. Drop a business task into Inbox/
echo "Launch a LinkedIn campaign for our new AI product targeting enterprise." > Inbox/demo.md
# 2. Watcher 1 — moves Inbox/ → Needs_Action/
python watcher_inbox.py
ls Needs_Action/ # demo.md appears here
# 3. Agent — generates Plan + summary + LinkedIn draft
python agent.py
ls Plans/ # demo_Plan.md
ls Pending_Approval/ # demo.md AND linkedin_draft_demo_<hash>.md
cat prompt_history.md # shows model, status (openai_ok or plan_fallback), snippet
# 4. HITL hard block — post_approved refuses unapproved drafts
python post_approved.py
# Output: [BLOCKED] linkedin_draft_demo_*.md — requires human approval first.
grep "blocked_without_approval" run_log.md # auditable evidence
# 5. Human approval — Pending_Approval/ → Approved/
python approve.py # list files; linkedin_draft shown with [LINKEDIN] tag
python approve.py --all # approve all
ls Approved/ # linkedin_draft_demo_*.md now here
# 6. Post (simulated — no LinkedIn credentials needed)
python post_approved.py
# Output: "Not posted (simulated_mode). File kept in Approved/."
ls Logs/ # linkedin_simulated_*.json written as evidence
# 7. Review audit trail
cat run_log.md
cat prompt_history.md
cat "Logs/events_$(date +%Y-%m-%d).jsonl"
cat Logs/summary_*.md
# 8. Generate evidence ZIP for judges
python evidence_pack.py
# Creates: evidence_<timestamp>.zip| Evidence | Path |
|---|---|
| UTC audit log | run_log.md |
| Prompt audit trail (model, status, snippet) | prompt_history.md |
| Structured JSONL events | Logs/events_<YYYY-MM-DD>.jsonl |
| Per-run stats (tasks, plans, fallback count) | Logs/summary_<timestamp>.md |
| LinkedIn simulated post records | Logs/linkedin_simulated_<timestamp>.json |
| Idempotency registry | Logs/posted_ids.json |
| Reasoning plans | Plans/<taskname>_Plan.md |
| LinkedIn drafts awaiting approval | Pending_Approval/linkedin_draft_*.md |
| Approved items | Approved/ |
| Completed tasks | Done/ |
| Evidence ZIP | evidence_<timestamp>.zip |
| GitHub Actions artifacts | Actions tab → run number → Artifacts (30-day retention) |
| ID | Requirement | Implementation | Status |
|---|---|---|---|
| A1 | Watcher: Inbox → Needs_Action | watcher_inbox.py |
✅ |
| A2 | Watcher: manual_input.txt → Needs_Action | watcher_manual.py |
✅ |
| A3 | Watcher: simulated channel | whatsapp_watcher.py |
✅ |
| A4 | Watcher: Gmail API, domain filter, dedup | gmail_watcher.py |
✅ |
| A5 | 5th watcher: LinkedIn simulated | linkedin_watcher.py |
✅ BONUS |
| B | Plans/<task>_Plan.md per task |
skills/planning_skill.py → Plans/ |
✅ |
| B | OpenAI + deterministic fallback (missing key or 429) | skills/*.py — each wraps _call_openai with fallback |
✅ |
| B | prompt_history.md (timestamp, model, status, file, snippet) |
agent.py:_log_prompt_history() |
✅ |
| C1 | MCP file ops | mcp_file_ops.py |
✅ |
| C2 | MCP LinkedIn ops (live + simulated + evidence) | mcp_linkedin_ops.py |
✅ |
| C3 | MCP email ops (SMTP + simulated) | mcp_email_ops.py |
✅ BONUS |
| C4 | MCP calendar ops (simulated) | mcp_calendar_ops.py |
✅ BONUS |
| D | LinkedIn draft for business tasks | agent.py + skills/linkedin_skill.py |
✅ |
| D | Draft format: title, source, post, status, risk note, hash | agent.py:li_draft_md |
✅ |
| D | post_approved.py posts only from Approved/ |
post_approved.py |
✅ |
| D | Idempotency via Logs/posted_ids.json |
post_approved.py |
✅ BONUS |
| D | HITL hard block (blocked_without_approval logged) |
post_approved.py:_check_and_log_pending_blocks() |
✅ BONUS |
| E | approve.py: manual move Pending_Approval → Approved |
approve.py |
✅ |
| E | approve.py list / single file / --all |
approve.py |
✅ |
| F | GitHub Actions: every 10 min + workflow_dispatch |
.github/workflows/silver-agent.yml |
✅ |
| F | Workflow order: watchers → agent → post → commit | silver-agent.yml |
✅ |
| F | Gmail guard: step conditional on GMAIL_OAUTH_ENABLED |
silver-agent.yml |
✅ |
| F | Safe push: git pull --rebase before push |
silver-agent.yml |
✅ |
| F | Artifact upload per run (30-day retention) | silver-agent.yml upload-artifact step |
✅ BONUS |
| G | README: diagram, checklist, demo steps, secrets | README.md |
✅ |
| G | JUDGE_PROOF.md compliance mapping |
JUDGE_PROOF.md |
✅ |
| H | .gitignore: .env, credentials.json, token.json, __pycache__ |
.gitignore |
✅ |
| H | .env.example with all keys |
.env.example |
✅ |
| I1 | Structured event logging: Logs/events_<date>.jsonl |
All modules | ✅ BONUS |
| I2 | evidence_pack.py: ZIP for judges |
evidence_pack.py |
✅ BONUS |
| I3 | Gmail domain allowlist | gmail_watcher.py:ALLOWED_DOMAINS |
✅ BONUS |
| I4 | Stats summary with fallback counts: Logs/summary_<ts>.md |
agent.py |
✅ BONUS |
| I5 | Modular skill package (skills/) |
planning_skill.py, summarize_skill.py, linkedin_skill.py |
✅ BONUS |
- Silver Tier — Fully implemented including all bonus items
- Pipeline — End-to-end verified locally and via GitHub Actions
- MCP Layer — 4 modules: file, LinkedIn, email, calendar
- HITL — Architecturally enforced;
approve.pyrequired; never bypassed - LinkedIn — Idempotency active; simulated by default; real posting opt-in
- Cloud — GitHub Actions every 10 min; artifact upload per run
- Simulated mode — Full pipeline runs without any credentials
- Fallback mode — All OpenAI skills degrade gracefully on missing key or quota error
- Obsidian — Vault-compatible; automation independent of Obsidian
- Evidence —
python evidence_pack.py→evidence_<timestamp>.zip